From b8666ff5e0e9699f2b3dbd6d9204e0e4c253d56e Mon Sep 17 00:00:00 2001 From: Ramon Gutierrez Date: Tue, 22 Oct 2024 11:58:35 +0800 Subject: [PATCH] Add loyalty points earning support, add points to successful subscription and FCM notification #799 --- config/services.yaml | 1 + .../CustomerAppAPI/SubscriptionController.php | 86 ++++++++++++++++--- src/Controller/PayMongoController.php | 81 ++++++++++++++--- src/Ramcar/FirebaseNotificationType.php | 19 ++++ src/Service/FCMSender.php | 17 +++- src/Service/LoyaltyConnector.php | 9 ++ src/Service/PayMongoConnector.php | 30 +++++-- translations/messages.en.yaml | 4 + 8 files changed, 216 insertions(+), 31 deletions(-) create mode 100644 src/Ramcar/FirebaseNotificationType.php diff --git a/config/services.yaml b/config/services.yaml index 268c0bf4..dfbe0c01 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -24,6 +24,7 @@ parameters: subscription_paymongo_secret_key: "%env(SUBSCRIPTION_PAYMONGO_SECRET_KEY)%" subscription_paymongo_webhook_id: "%env(SUBSCRIPTION_PAYMONGO_WEBHOOK_ID)%" subscription_months: "%env(SUBSCRIPTION_MONTHS)%" + loyalty_php_point_multiplier: "%env(LOYALTY_PHP_POINT_MULTIPLIER)%" services: # default configuration for services in *this* file diff --git a/src/Controller/CustomerAppAPI/SubscriptionController.php b/src/Controller/CustomerAppAPI/SubscriptionController.php index 2a22961d..cce780df 100644 --- a/src/Controller/CustomerAppAPI/SubscriptionController.php +++ b/src/Controller/CustomerAppAPI/SubscriptionController.php @@ -11,7 +11,10 @@ use App\Entity\Vehicle; use App\Entity\Subscription; use App\Entity\CustomerVehicle; use App\Ramcar\SubscriptionStatus; - +use App\Entity\GatewayTransaction; +use App\Ramcar\TransactionStatus; +use App\Service\FCMSender; +use App\Service\LoyaltyConnector; use DateTime; class SubscriptionController extends ApiController @@ -43,8 +46,6 @@ class SubscriptionController extends ApiController $plan = $pm->getPlanByBatterySize($batts[0]->getSize()); } - error_log("FOUND PLAN FOR $vid: " . print_r($plan, true)); - // response return new ApiResponse(true, '', [ 'plan' => $plan, @@ -76,6 +77,7 @@ class SubscriptionController extends ApiController 'plan_id', 'cv_id', 'email', + 'remember_email', ]); if (!$validity['is_valid']) { @@ -119,6 +121,13 @@ class SubscriptionController extends ApiController // initialize paymongo connector $this->initializeSubscriptionPayMongoConnector($pm); + // get the paymongo plan by ID + $plan_id = $req->request->get('plan_id'); + $plan = $pm->getPlan($plan_id); + if (empty($plan['response']['data']['id'])) { + return new ApiResponse(false, 'No subscription plans found for this vehicle.'); + } + // get paymongo customer $pm_cust = $pm->findOrCreateCustomer($email, $cust); if (empty($pm_cust)) { @@ -127,11 +136,12 @@ class SubscriptionController extends ApiController // create subscription // NOTE: for now we save ourselves the extra API call and assume the plan_id is valid since this won't change often anyway - $pm_sub = $pm->createSubscription($pm_cust['id'], $req->request->get('plan_id')); + $pm_sub = $pm->createSubscription($pm_cust['id'], $plan_id); $sub_pi = $pm_sub['response']['data']['attributes']['latest_invoice']['payment_intent'] ?? null; + $sub_invoice = $pm_sub['response']['data']['attributes']['latest_invoice'] ?? null; // not the response we expected - if (empty($sub_pi)) { + if (empty($sub_pi) || empty($sub_invoice)) { return new ApiResponse(false, 'Error creating subscription. Please try again later.'); } @@ -160,8 +170,29 @@ class SubscriptionController extends ApiController if (!empty($req->request->get('remember_email'))) { $cust->setEmail($email); } + + // create new gateway transaction + $gt = new GatewayTransaction(); + $gt->setCustomer($cust); + $gt->setDateCreate(new DateTime()); + $gt->setAmount($plan['response']['data']['attributes']['amount']); + $gt->setStatus(TransactionStatus::PENDING); + $gt->setGateway('paymongo'); // TODO: define values elsewhere + $gt->setType('subscription'); // TODO: define values elsewhere + $gt->setExtTransactionId($sub_invoice['id']); + $gt->setMetadata([ + 'invoice_id' => $sub_invoice['id'], + 'subscription_id' => $pm_sub['response']['data']['id'], + 'payment_intent_id' => $sub_pi['id'], + ]); + + // if we set it to remember email, update customer email with this + if (!empty($req->request->get('remember_email'))) { + $cust->setEmail($email); + } // save stuff to db + $this->em->persist($gt); $this->em->persist($obj); $this->em->flush(); @@ -174,7 +205,7 @@ class SubscriptionController extends ApiController ]); } - public function finalizeSubscription(Request $req, $id, PayMongoConnector $pm) + public function finalizeSubscription(Request $req, $id, PayMongoConnector $pm, LoyaltyConnector $lc, FCMSender $fcmclient) { // check requirements $validity = $this->validateRequest($req); @@ -186,11 +217,14 @@ class SubscriptionController extends ApiController // initialize paymongo connector $this->initializeSubscriptionPayMongoConnector($pm); + // get customer + $cust = $this->session->getCustomer(); + // get subscription $sub_obj = $this->em->getRepository(Subscription::class)->findOneBy([ 'id' => $id, 'status' => SubscriptionStatus::PENDING, - 'customer' => $this->session->getCustomer(), + 'customer' => $cust, ]); if (empty($sub_obj)) { @@ -222,7 +256,9 @@ class SubscriptionController extends ApiController $sub_obj->getStatus() === SubscriptionStatus::PENDING && $pi['response']['data']['attributes']['status'] === 'succeeded' ) { - $sub_start_date = new DateTime(); + $now = new DateTime(); + + $sub_start_date = $now; $sub_end_date = clone $sub_start_date; $sub_end_date->modify('+' . $this->getParameter('subscription_months') . ' month'); @@ -230,13 +266,41 @@ class SubscriptionController extends ApiController ->setDateStart($sub_start_date) ->setDateEnd($sub_end_date); + // update the gateway transaction record as well + $gt = $this->em->getRepository(GatewayTransaction::class)->findOneBy([ + 'status' => TransactionStatus::PENDING, + 'ext_transaction_id' => $pm_sub['response']['data']['attributes']['latest_invoice']['id'], + ]); + if (empty($gt)) { + return new ApiResponse(false, 'Error retrieving transaction. Please try again later.'); + } + + $gt->setStatus(TransactionStatus::PAID) + ->setDatePay($now); + $this->em->flush(); } + + // compute loyalty points to be added + // TODO: get a proper matrix for this. right now we are using a flat multiplier for demo purposes + $points_amount = ($gt->getAmount() / 100) * $this->getParameter('loyalty_php_point_multiplier'); - error_log("PI STATUS: " . $pi['response']['data']['attributes']['status']); + // add loyalty points + $points_res = $lc->updatePoints($cust, $points_amount); + if ($points_res['success']) { + // notify the customer that points were added + $fcmclient->sendLoyaltyEvent( + $cust, + "loyalty_fcm_title_add_points", + "loyalty_fcm_body_add_points", + [], + [], + ['points' => $points_amount], + ); + } - // response - return new ApiResponse(true, '', [ + // response + return new ApiResponse(true, '', [ 'payment_intent' => $pi['response']['data'], ]); } diff --git a/src/Controller/PayMongoController.php b/src/Controller/PayMongoController.php index af698acd..6ecb509a 100644 --- a/src/Controller/PayMongoController.php +++ b/src/Controller/PayMongoController.php @@ -4,6 +4,8 @@ namespace App\Controller; use App\Entity\GatewayTransaction; use App\Ramcar\TransactionStatus; +use App\Service\FCMSender; +use App\Service\LoyaltyConnector; use App\Service\PayMongoConnector; use Doctrine\ORM\EntityManagerInterface; @@ -17,11 +19,15 @@ class PayMongoController extends Controller { protected $pm; protected $em; + protected $lc; + protected $fcmclient; - public function __construct(PayMongoConnector $pm, EntityManagerInterface $em) + public function __construct(PayMongoConnector $pm, EntityManagerInterface $em, LoyaltyConnector $lc, FCMSender $fcmclient) { $this->pm = $pm; $this->em = $em; + $this->lc = $lc; + $this->fcmclient = $fcmclient; } public function listen(Request $req) @@ -63,14 +69,75 @@ class PayMongoController extends Controller protected function handlePaymentPaid($event) { - $metadata = $event['attributes']['metadata']; - $obj = $this->getTransaction($metadata['transaction_id']); + // TODO: work with paymongo to figure out a better way to standardize callbacks. For now we rely on the callback description + $description = $event['attributes']['description']; + + // define if we earn loyalty points or not + $add_loyalty_points = false; + + // set initial criteria + $criteria = [ + 'status' => TransactionStatus::PENDING, + ]; + + // figure out transaction type by ID + switch (true) { + // subscription payment + case strpos($description, 'Payment for subs') !== false: + // retrieve sub and invoice ID from description + $desc_parts = explode(" - ", $description); + + // add to criteria + $criteria['ext_transaction_id'] = $desc_parts[1]; + + // we earn loyalty points + $add_loyalty_points = true; + + break; + + // insurance premium + // TODO: retest this later so we don't use a default clause + default: + $metadata = $event['attributes']['metadata']; + + // add to criteria + $criteria['id'] = $metadata['transaction_id']; + + break; + } + + // get transaction + $obj = $this->em->getRepository(GatewayTransaction::class)->findOneBy($criteria); if (!empty($obj)) { // mark as paid $obj->setStatus(TransactionStatus::PAID); $obj->setDatePay(new DateTime()); $this->em->flush(); + + // add loyalty points if applicable + if ($add_loyalty_points) { + // get the customer + $cust = $obj->getCustomer(); + + // compute loyalty points to be added + // TODO: get a proper matrix for this. right now we are using a flat multiplier for demo purposes + $points_amount = ($obj->getAmount() / 100) * $this->getParameter('loyalty_php_point_multiplier'); + + // add loyalty points + $points_res = $this->lc->updatePoints($cust, $points_amount); + if ($points_res['success']) { + // notify the customer that points were added + $this->fcmclient->sendLoyaltyEvent( + $cust, + "loyalty_fcm_title_add_points", + "loyalty_fcm_body_add_points", + [], + [], + ['points' => $points_amount], + ); + } + } } return $this->json([ @@ -86,14 +153,6 @@ class PayMongoController extends Controller ]); } - protected function getTransaction($id) - { - //$class_name = 'App\\Entity\\' . $type; - //$instance = new $class_name; - - return $this->em->getRepository(GatewayTransaction::class)->find($id); - } - public function paymentSuccess(Request $req) { return $this->render('paymongo/success.html.twig'); diff --git a/src/Ramcar/FirebaseNotificationType.php b/src/Ramcar/FirebaseNotificationType.php new file mode 100644 index 00000000..326c665d --- /dev/null +++ b/src/Ramcar/FirebaseNotificationType.php @@ -0,0 +1,19 @@ + 'Job Order', + 'loyalty' => 'Loyalty', + 'subscription' => 'Subscription', + 'insurance' => 'Insurance', + ]; +} diff --git a/src/Service/FCMSender.php b/src/Service/FCMSender.php index 4b2c646d..a9b7dbd7 100644 --- a/src/Service/FCMSender.php +++ b/src/Service/FCMSender.php @@ -3,6 +3,7 @@ namespace App\Service; use App\Entity\Customer; +use App\Ramcar\FirebaseNotificationType; use Symfony\Contracts\Translation\TranslatorInterface; use Fcm\FcmClient; use Fcm\Push\Notification; @@ -52,7 +53,7 @@ class FCMSender return $this->client->send($notification); } - public function sendJoEvent(JobOrder $job_order, $title, $body, $data = []) + public function sendJoEvent(JobOrder $job_order, $title, $body, $data = [], $title_params = [], $body_params = []) { // get customer object $cust = $job_order->getCustomer(); @@ -60,12 +61,22 @@ class FCMSender // attach jo info $data['jo_id'] = $job_order->getID(); $data['jo_status'] = $job_order->getStatus(); + $data['notification_type'] = FirebaseNotificationType::JOB_ORDER; // send the event - return $this->sendEvent($cust, $title, $body, $data); + return $this->sendEvent($cust, $title, $body, $data, $title_params, $body_params); } - public function sendEvent(Customer $cust, $title, $body, $data = []) + public function sendLoyaltyEvent(Customer $cust, $title, $body, $data = [], $title_params = [], $body_params = []) + { + // attach type info + $data['notification_type'] = FirebaseNotificationType::LOYALTY; + + // send the event + return $this->sendEvent($cust, $title, $body, $data, $title_params, $body_params); + } + + public function sendEvent(Customer $cust, $title, $body, $data = [], $title_params = [], $body_params = []) { // get all v2 devices $devices = $this->getDevices($cust); diff --git a/src/Service/LoyaltyConnector.php b/src/Service/LoyaltyConnector.php index 6281ed55..a8f623c8 100644 --- a/src/Service/LoyaltyConnector.php +++ b/src/Service/LoyaltyConnector.php @@ -31,6 +31,15 @@ class LoyaltyConnector ]); } + public function updatePoints(Customer $cust, int $amount) + { + return $this->doRequest('/api/wallet/update', 'POST', [ + 'external_id' => $cust->getPhoneMobile(), + 'currency' => 'points', + 'amount' => $amount, + ]); + } + protected function generateSignature(string $path, string $method, string $date_string) { $elements = [ diff --git a/src/Service/PayMongoConnector.php b/src/Service/PayMongoConnector.php index 90748ac8..39a0bcda 100644 --- a/src/Service/PayMongoConnector.php +++ b/src/Service/PayMongoConnector.php @@ -125,6 +125,11 @@ class PayMongoConnector return $found_plan; } + public function getPlan($plan_id) + { + return $this->doRequest('/v1/subscriptions/plans/'. $plan_id, 'GET'); + } + public function createPlan($plan_data) { $body = [ @@ -288,7 +293,19 @@ class PayMongoConnector ], ]; - return $this->doRequest('/v1/subscriptions', 'POST', $body); + $url = '/v1/subscriptions'; + $method = 'POST'; + $result = $this->doRequest($url, $method, $body); + + // log if we don't get the expected response + if ( + empty($result['response']['data']['attributes']['latest_invoice']['payment_intent']) || + empty($result['response']['data']['attributes']['latest_invoice']) + ) { + $this->log($method . " " . $url, $body, $result, "error"); + } + + return $result; } public function getSubscription($sub_id) @@ -358,7 +375,7 @@ class PayMongoConnector error_log(Psr7\Message::toString($e->getRequest())); // log this error - $this->log($url, Psr7\Message::toString($e->getRequest()), Psr7\Message::toString($e->getResponse()), 'error'); + $this->log($method . " " . $url, Psr7\Message::toString($e->getRequest()), Psr7\Message::toString($e->getResponse()), 'error'); if ($e->hasResponse()) { $error['response'] = Psr7\Message::toString($e->getResponse()); @@ -373,7 +390,7 @@ class PayMongoConnector $result_body = $response->getBody(); // log response - $this->log($url, json_encode($request_body), $result_body); + $this->log($method . " " . $url, json_encode($request_body), $result_body); return [ 'success' => true, @@ -382,19 +399,20 @@ class PayMongoConnector } // TODO: make this more elegant - public function log($title, $request_body = "[]", $result_body = "[]", $type = 'api') + public function log($title, $request_body = "[]", $result_body = "[]", $type = 'api', $custom_message = null) { $filename = '/../../var/log/paymongo_' . $type . '.log'; $date = date("Y-m-d H:i:s"); // build log entry - $entry = implode("\r\n", [ + $entry = implode("\r\n", array_filter([ $date, $title, + (!empty($custom_message) ? "MESSAGE: " . $custom_message : ""), "REQUEST:\r\n" . $request_body, "RESPONSE:\r\n" . $result_body, "\r\n----------------------------------------\r\n\r\n", - ]); + ])); @file_put_contents(__DIR__ . $filename, $entry, FILE_APPEND); } diff --git a/translations/messages.en.yaml b/translations/messages.en.yaml index 6fde186c..01512e6f 100644 --- a/translations/messages.en.yaml +++ b/translations/messages.en.yaml @@ -180,3 +180,7 @@ insurance_fcm_title_updated: 'Application updated' insurance_fcm_title_completed: 'Application completed' insurance_fcm_body_updated: 'Some details on your insurance application have been updated.' insurance_fcm_body_completed: 'Your insurance application has been processed!' + +# fcm loyalty +loyalty_fcm_title_add_points: 'Hooray!' +loyalty_fcm_body_add_points: '{1} You have earned %points% point!|]1,Inf] You have earned %points% points!' \ No newline at end of file