Add loyalty points earning support, add points to successful subscription and FCM notification #799

This commit is contained in:
Ramon Gutierrez 2024-10-22 11:58:35 +08:00
parent 857c573ae5
commit b8666ff5e0
8 changed files with 216 additions and 31 deletions

View file

@ -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

View file

@ -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.');
}
@ -161,7 +171,28 @@ class SubscriptionController extends ApiController
$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();
}
error_log("PI STATUS: " . $pi['response']['data']['attributes']['status']);
// 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');
// response
return new ApiResponse(true, '', [
// 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, '', [
'payment_intent' => $pi['response']['data'],
]);
}

View file

@ -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');

View file

@ -0,0 +1,19 @@
<?php
namespace App\Ramcar;
class FirebaseNotificationType extends NameValue
{
const JOB_ORDER = 'job_order';
const LOYALTY = 'loyalty';
const SUBSCRIPTION = 'subscription';
const INSURANCE = 'insurance';
const COLLECTION = [
'job_order' => 'Job Order',
'loyalty' => 'Loyalty',
'subscription' => 'Subscription',
'insurance' => 'Insurance',
];
}

View file

@ -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);

View file

@ -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 = [

View file

@ -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);
}

View file

@ -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!'