Compare commits

...

53 commits

Author SHA1 Message Date
Ramon Gutierrez
ef626729c9 Fix API output for active subscriptions #799 2024-10-30 04:41:41 +08:00
Ramon Gutierrez
00697414b0 Remove debug output for session timestamp updates #799 2024-10-30 01:27:01 +08:00
Ramon Gutierrez
97ebaa05a0 Only include recently active sessions when sending FCM notifications #799 2024-10-29 17:22:51 +08:00
Ramon Gutierrez
0800fc1066 Update session timestamp on every action #799 2024-10-29 17:22:29 +08:00
Ramon Gutierrez
5f8133f02c Add timestamp for latest session activity #799 2024-10-29 17:05:48 +08:00
Ramon Gutierrez
0ea93622be Fix annotation syntax error on customer vehicle #799 2024-10-28 16:44:08 +08:00
Ramon Gutierrez
7f1b35ad29 Flag job orders associated with job orders as emergency orders #799 2024-10-27 06:29:37 +08:00
Ramon Gutierrez
beb1a63577 Add style for subscription on job order form #799 2024-10-27 06:28:32 +08:00
Ramon Gutierrez
e4e031f0a9 Add support for subscription related job orders #799 2024-10-26 08:28:17 +08:00
Ramon Gutierrez
d1104b7416 Change unfulfilled subscriptions endpoint to retrieve vehicle data and not just a count #799 2024-10-26 03:40:32 +08:00
Ramon Gutierrez
f554658c7f Add subscription as mode of payment #799 2024-10-26 03:39:50 +08:00
Ramon Gutierrez
74c45b6d18 Add support for retrieving subscription specific compatible battery #799 2024-10-26 03:39:30 +08:00
Ramon Gutierrez
874c35bfff Fix issue with subscription not being marked as active due to listener conflict, add endpoint for unfulfilled sub count #799 2024-10-24 06:07:36 +08:00
Ramon Gutierrez
8c83393b0c Add relationship between job order and subscription for tagging #799 2024-10-24 06:07:03 +08:00
Ramon Gutierrez
3ed65e7fc6 Move all points logic from paymongo listener to gateway transaction listener #799 2024-10-24 04:05:20 +08:00
Ramon Gutierrez
f7ba91892b Add handling of post payment for subscriptions #799 2024-10-23 20:46:31 +08:00
Ramon Gutierrez
33f48647b6 Create new FCM sender service to replace outdated library #799 2024-10-23 20:46:01 +08:00
Ramon Gutierrez
b8666ff5e0 Add loyalty points earning support, add points to successful subscription and FCM notification #799 2024-10-22 11:58:35 +08:00
Ramon Gutierrez
857c573ae5 Merge branch '799-subscription-support' into 809-loyalty-system-support 2024-10-14 07:22:24 +08:00
Ramon Gutierrez
a724b00ce7 Include email in customer info endpoint #809 2024-10-10 04:03:30 +08:00
Ramon Gutierrez
0a4ea563d9 Fix response format of loyalty registration to remove unnecessary array #809 2024-10-07 08:11:29 +08:00
Ramon Gutierrez
d1059797a5 Simplify loyalty register result format #809 2024-10-07 07:01:47 +08:00
Ramon Gutierrez
bd655a459a Add loyalty connector and register endpoint #809 2024-10-07 06:28:57 +08:00
Ramon Gutierrez
b79f2f2dfb Merge branch 'master' into 799-subscription-support 2024-10-02 14:01:32 +08:00
Ramon Gutierrez
d2a0638ffa Fix customer retrieval from paymongo #799 2024-10-02 13:28:27 +08:00
Ramon Gutierrez
5056637b66 Add email field to subscription entity #799 2024-10-02 13:27:37 +08:00
Ramon Gutierrez
1fd883b07b Consolidate subscription setup payment intent checking and initial activation when applicable #799 2024-08-26 08:01:00 +08:00
Ramon Gutierrez
40c629eee3 Add endpoint for activating a subscription #799 2024-08-24 18:00:38 +08:00
Ramon Gutierrez
aa85198b7a Add subscription entity #799 2024-08-24 07:50:50 +08:00
Ramon Gutierrez
62f11c9ef5 Fix json payload structure for payment intent checking #799 2024-08-23 07:51:53 +08:00
Ramon Gutierrez
9dbaf92698 Add missing basic auth to payment intent endpoint #799 2024-08-23 05:07:11 +08:00
Ramon Gutierrez
d7cc0fc3de Add endpoint for re-checking payment intent status #799 2024-08-22 06:19:42 +08:00
Ramon Gutierrez
8c61a27376 Fix handling of payment intent values when returning created sub details #799 2024-08-21 07:44:08 +08:00
Ramon Gutierrez
0d9da221a7 Fix expected format from paymongo subscription endpoints #799 2024-08-21 01:16:43 +08:00
Ramon Gutierrez
919b56688d Add endpoint for creating subscriptions, sync updating of customers with paymongo API if record exists #799 2024-08-13 05:45:46 +08:00
Ramon Gutierrez
7af20f3d69 Replace sub fee endpoint with complete plan details #799 2024-08-12 16:30:49 +08:00
Ramon Gutierrez
b3548fcc50 Add null check for subscription fee on paymongo connector #799 2024-08-12 06:52:13 +08:00
Ramon Gutierrez
5a2f57492d Tie battery sizes to subscription plans #799 2024-08-12 06:50:01 +08:00
Ramon Gutierrez
debb399e96 Add support for handling multiple paymongo accounts #799 2024-08-12 05:09:58 +08:00
Ramon Gutierrez
e3649c3d2d Fix URL format of subscription fee endpoint #799 2024-08-11 15:26:48 +08:00
Ramon Gutierrez
4f5560f6f7 Update subscription fee endpoint to be specific to each vehicle model #799 2024-08-11 15:05:20 +08:00
Ramon Gutierrez
b67f960055 Add subscription MSRP field to battery sizes #799 2024-08-10 05:38:58 +08:00
Ramon Gutierrez
219d5c09d3 Add endpoint for subscription paymongo public key #799 2024-08-10 05:26:04 +08:00
Ramon Gutierrez
96a7cc929e Add email to customer info endpoint #799 2024-08-03 06:31:35 +08:00
Ramon Gutierrez
fe4806f41a Add endpoint for retrieving subscription fee #799 2024-08-03 06:31:12 +08:00
Ramon Gutierrez
17e583e11a Merge branch 'master' into 799-subscription-support 2024-07-31 18:08:45 +08:00
Ramon Gutierrez
a911b8c6c1 Merge branch 'master' into 799-subscription-support 2024-07-24 15:02:58 +08:00
Ramon Gutierrez
ab64161afb Add placeholder customer vehicle sub getter #799 2024-05-10 14:42:58 +08:00
Ramon Gutierrez
627b3da748 Disallow editing of IDs of static content #799 2024-05-05 07:14:26 +08:00
Ramon Gutierrez
48d87ae119 Retrieve subscription data with customer vehicle info endpoint #799 2024-05-05 07:13:07 +08:00
Ramon Gutierrez
2ccd1e0e2d Use proper getter for static content API #799 2024-04-29 18:07:00 +08:00
Ramon Gutierrez
c9cb6e8b53 Add markdown support to static content form view #799 2024-04-27 01:45:25 +08:00
Ramon Gutierrez
d9d4ffbecf Add static content endpoint for customer app #799 2024-04-27 01:44:39 +08:00
52 changed files with 2202 additions and 199 deletions

View file

@ -33,6 +33,7 @@
"doctrine/doctrine-migrations-bundle": "^2", "doctrine/doctrine-migrations-bundle": "^2",
"doctrine/orm": "^2", "doctrine/orm": "^2",
"edwinhoksberg/php-fcm": "dev-notif-priority-hotfix", "edwinhoksberg/php-fcm": "dev-notif-priority-hotfix",
"firebase/php-jwt": "^6.10",
"guzzlehttp/guzzle": "^6.3", "guzzlehttp/guzzle": "^6.3",
"hashids/hashids": "^4.1", "hashids/hashids": "^4.1",
"jankstudio/catalyst-api-bundle": "dev-master", "jankstudio/catalyst-api-bundle": "dev-master",

65
composer.lock generated
View file

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "653f8558c75614dd65421cb3eb48c29b", "content-hash": "4676209ee947dbcf3cfcf937c838c6f2",
"packages": [ "packages": [
{ {
"name": "composer/package-versions-deprecated", "name": "composer/package-versions-deprecated",
@ -1827,6 +1827,69 @@
}, },
"time": "2023-07-19T09:04:27+00:00" "time": "2023-07-19T09:04:27+00:00"
}, },
{
"name": "firebase/php-jwt",
"version": "v6.10.1",
"source": {
"type": "git",
"url": "https://github.com/firebase/php-jwt.git",
"reference": "500501c2ce893c824c801da135d02661199f60c5"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/firebase/php-jwt/zipball/500501c2ce893c824c801da135d02661199f60c5",
"reference": "500501c2ce893c824c801da135d02661199f60c5",
"shasum": ""
},
"require": {
"php": "^8.0"
},
"require-dev": {
"guzzlehttp/guzzle": "^7.4",
"phpspec/prophecy-phpunit": "^2.0",
"phpunit/phpunit": "^9.5",
"psr/cache": "^2.0||^3.0",
"psr/http-client": "^1.0",
"psr/http-factory": "^1.0"
},
"suggest": {
"ext-sodium": "Support EdDSA (Ed25519) signatures",
"paragonie/sodium_compat": "Support EdDSA (Ed25519) signatures when libsodium is not present"
},
"type": "library",
"autoload": {
"psr-4": {
"Firebase\\JWT\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Neuman Vong",
"email": "neuman+pear@twilio.com",
"role": "Developer"
},
{
"name": "Anant Narayanan",
"email": "anant@php.net",
"role": "Developer"
}
],
"description": "A simple library to encode and decode JSON Web Tokens (JWT) in PHP. Should conform to the current spec.",
"homepage": "https://github.com/firebase/php-jwt",
"keywords": [
"jwt",
"php"
],
"support": {
"issues": "https://github.com/firebase/php-jwt/issues",
"source": "https://github.com/firebase/php-jwt/tree/v6.10.1"
},
"time": "2024-05-18T18:05:11+00:00"
},
{ {
"name": "friendsofphp/proxy-manager-lts", "name": "friendsofphp/proxy-manager-lts",
"version": "v1.0.5", "version": "v1.0.5",

View file

@ -313,3 +313,50 @@ apiv2_insurance_body_types:
path: /apiv2/insurance/body_types path: /apiv2/insurance/body_types
controller: App\Controller\CustomerAppAPI\InsuranceController::getBodyTypes controller: App\Controller\CustomerAppAPI\InsuranceController::getBodyTypes
methods: [GET] methods: [GET]
apiv2_loyalty_register:
path: /apiv2/loyalty/register
controller: App\Controller\CustomerAppAPI\LoyaltyController::register
methods: [POST]
# static content
apiv2_static_content:
path: /apiv2/static_content/{id}
controller: App\Controller\CustomerAppAPI\StaticContentController::getContent
methods: [GET]
# subscription
apiv2_subscription_plan_details:
path: /apiv2/subscription/vehicle/{vid}/plan
controller: App\Controller\CustomerAppAPI\SubscriptionController::getPlanDetails
methods: [GET]
#apiv2_subscription_paymongo_public_key:
# path: /apiv2/subscription/ppk
# controller: App\Controller\CustomerAppAPI\SubscriptionController::getPaymongoPublicKey
# methods: [GET]
apiv2_subscription_create:
path: /apiv2/subscription
controller: App\Controller\CustomerAppAPI\SubscriptionController::createSubscription
methods: [POST]
apiv2_subscription_unfulfilled_list:
path: /apiv2/subscription/unfulfilled
controller: App\Controller\CustomerAppAPI\SubscriptionController::getUnfulfilledSubs
methods: [GET]
apiv2_subscription_finalize:
path: /apiv2/subscription/{id}/finalize
controller: App\Controller\CustomerAppAPI\SubscriptionController::finalizeSubscription
methods: [GET]
#apiv2_subscription_payment_intent:
# path: /apiv2/subscription/payment_intent/{pi_id}
# controller: App\Controller\CustomerAppAPI\SubscriptionController::getPaymentIntent
# methods: [GET]
#apiv2_subscription_activate:
# path: /apiv2/subscription/{id}/activate
# controller: App\Controller\CustomerAppAPI\SubscriptionController::activateSubscription
# methods: [POST]

View file

@ -17,6 +17,14 @@ parameters:
ios_app_version: "%env(IOS_APP_VERSION)%" ios_app_version: "%env(IOS_APP_VERSION)%"
insurance_premiums_banner_url: "%env(INSURANCE_PREMIUMS_BANNER_URL)%" insurance_premiums_banner_url: "%env(INSURANCE_PREMIUMS_BANNER_URL)%"
enabled_hub_filters: "%env(ENABLED_HUB_FILTERS)%" enabled_hub_filters: "%env(ENABLED_HUB_FILTERS)%"
insurance_paymongo_public_key: "%env(INSURANCE_PAYMONGO_PUBLIC_KEY)%"
insurance_paymongo_secret_key: "%env(INSURANCE_PAYMONGO_SECRET_KEY)%"
insurance_paymongo_webhook_id: "%env(INSURANCE_PAYMONGO_WEBHOOK_ID)%"
subscription_paymongo_public_key: "%env(SUBSCRIPTION_PAYMONGO_PUBLIC_KEY)%"
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: services:
# default configuration for services in *this* file # default configuration for services in *this* file
@ -114,7 +122,6 @@ services:
arguments: arguments:
$em: "@doctrine.orm.entity_manager" $em: "@doctrine.orm.entity_manager"
$paymongo: "@App\\Service\\PayMongoConnector" $paymongo: "@App\\Service\\PayMongoConnector"
$webhook_id: "%env(PAYMONGO_WEBHOOK_ID)%"
# rider tracker service # rider tracker service
App\Service\RiderTracker: App\Service\RiderTracker:
@ -229,6 +236,10 @@ services:
arguments: arguments:
$em: "@doctrine.orm.entity_manager" $em: "@doctrine.orm.entity_manager"
$ic: "@App\\Service\\InsuranceConnector" $ic: "@App\\Service\\InsuranceConnector"
$fcmclient: "@App\\Service\\FCMSender"
$lc: "@App\\Service\\LoyaltyConnector"
$sub_months: "%subscription_months%"
$loyalty_point_multiplier: "%loyalty_php_point_multiplier%"
tags: tags:
- name: doctrine.orm.entity_listener - name: doctrine.orm.entity_listener
event: 'postUpdate' event: 'postUpdate'
@ -238,8 +249,13 @@ services:
App\Service\PayMongoConnector: App\Service\PayMongoConnector:
arguments: arguments:
$base_url: "%env(PAYMONGO_BASE_URL)%" $base_url: "%env(PAYMONGO_BASE_URL)%"
$public_key: "%env(PAYMONGO_PUBLIC_KEY)%"
$secret_key: "%env(PAYMONGO_SECRET_KEY)%" # loyalty system connector
App\Service\LoyaltyConnector:
arguments:
$base_url: "%env(LOYALTY_BASE_URL)%"
$api_key: "%env(LOYALTY_API_KEY)%"
$secret_key: "%env(LOYALTY_SECRET_KEY)%"
# entity listener for customer vehicle warranty code history # entity listener for customer vehicle warranty code history
App\EntityListener\CustomerVehicleSerialListener: App\EntityListener\CustomerVehicleSerialListener:
@ -316,8 +332,9 @@ services:
# FCM sender # FCM sender
App\Service\FCMSender: App\Service\FCMSender:
arguments: arguments:
$server_key: "%env(FCM_SERVER_KEY)%" $project_id: "%env(FCM_PROJECT_ID)%"
$sender_id: "%env(FCM_SENDER_ID)%" $base_uri: "%env(FCM_BASE_URI)%"
$creds_file: "%env(FCM_CREDENTIALS_PATH)%"
# price tier manager # price tier manager
App\Service\PriceTierManager: App\Service\PriceTierManager:

View file

@ -383,3 +383,19 @@ span.has-danger,
.map-info .m-badge { .map-info .m-badge {
border-radius: 0; border-radius: 0;
} }
.form-group-subscription {
> * {
background-color: #f1edd5;
padding-top: 15px;
padding-bottom: 15px;
}
.form-control {
border-color: #666;
}
label {
color: #000;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

View file

@ -6,6 +6,7 @@ use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
@ -19,14 +20,18 @@ class ProcessLatePaymongoTransactionsCommand extends Command
{ {
protected $em; protected $em;
protected $paymongo; protected $paymongo;
protected $webhook_id; protected $webhook_id;
public function __construct(EntityManagerInterface $em, PayMongoConnector $paymongo, $webhook_id) public function __construct(EntityManagerInterface $em, PayMongoConnector $paymongo, ParameterBagInterface $params)
{ {
$this->em = $em; $this->em = $em;
$this->webhook_id = $params->get('insurance_paymongo_webhook_id');
$this->paymongo = $paymongo; $this->paymongo = $paymongo;
$this->webhook_id = $webhook_id; $this->paymongo->initialize(
$params->get('insurance_paymongo_public_key'),
$params->get('insurance_paymongo_secret_key')
);
parent::__construct(); parent::__construct();
} }

View file

@ -1057,6 +1057,9 @@ class APIController extends Controller implements LoggedController
$hub_criteria = new HubCriteria(); $hub_criteria = new HubCriteria();
$hub_criteria->setPoint($jo->getCoordinates()); $hub_criteria->setPoint($jo->getCoordinates());
// set subscription flag
$hub_criteria->setSubscription($jo->getSubscription() !== null);
// get distance limit for mobile from env // get distance limit for mobile from env
$dotenv = new Dotenv(); $dotenv = new Dotenv();
$dotenv->loadEnv(__DIR__.'/../../.env'); $dotenv->loadEnv(__DIR__.'/../../.env');
@ -3002,6 +3005,9 @@ class APIController extends Controller implements LoggedController
$hub_criteria = new HubCriteria(); $hub_criteria = new HubCriteria();
$hub_criteria->setPoint($jo->getCoordinates()); $hub_criteria->setPoint($jo->getCoordinates());
// set subscription flag
$hub_criteria->setSubscription($jo->getSubscription() !== null);
// get distance limit for mobile from env // get distance limit for mobile from env
// get value of hub_filter_enable from env // get value of hub_filter_enable from env
$dotenv = new Dotenv(); $dotenv = new Dotenv();

View file

@ -118,6 +118,7 @@ class BatteryController extends Controller
$row['total_height'] = $orow[0]->getTotalHeight(); $row['total_height'] = $orow[0]->getTotalHeight();
$row['image_file'] = $orow[0]->getImageFile(); $row['image_file'] = $orow[0]->getImageFile();
$row['flag_active'] = $orow[0]->isActive(); $row['flag_active'] = $orow[0]->isActive();
$row['flag_subscription'] = $orow[0]->isSubscription();
// add row metadata // add row metadata
$row['meta'] = [ $row['meta'] = [
@ -184,7 +185,8 @@ class BatteryController extends Controller
->setTotalHeight($req->request->get('total_height')) ->setTotalHeight($req->request->get('total_height'))
->setSellingPrice($req->request->get('sell_price')) ->setSellingPrice($req->request->get('sell_price'))
->setImageFile($req->request->get('image_file')) ->setImageFile($req->request->get('image_file'))
->setActive($req->request->get('flag_active', false)); ->setActive($req->request->get('flag_active', false))
->setSubscription($req->request->get('flag_subscription', false));
// initialize error list // initialize error list
$error_array = []; $error_array = [];
@ -311,6 +313,7 @@ class BatteryController extends Controller
->setSellingPrice($req->request->get('sell_price')) ->setSellingPrice($req->request->get('sell_price'))
->setImageFile($req->request->get('image_file')) ->setImageFile($req->request->get('image_file'))
->setActive($req->request->get('flag_active', false)) ->setActive($req->request->get('flag_active', false))
->setSubscription($req->request->get('flag_subscription', false))
->clearVehicles(); ->clearVehicles();
// initialize error list // initialize error list

View file

@ -9,11 +9,24 @@ use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Validator\Validator\ValidatorInterface; use Symfony\Component\Validator\Validator\ValidatorInterface;
use Symfony\Bundle\FrameworkBundle\Controller\Controller; use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use App\Service\PayMongoConnector;
use Catalyst\MenuBundle\Annotation\Menu; use Catalyst\MenuBundle\Annotation\Menu;
class BatterySizeController extends Controller class BatterySizeController extends Controller
{ {
protected $pm;
public function __construct(PayMongoConnector $pm, ParameterBagInterface $params)
{
$this->pm = $pm;
$this->pm->initialize(
$params->get('subscription_paymongo_public_key'),
$params->get('subscription_paymongo_secret_key'),
);
}
/** /**
* @Menu(selected="bsize_list") * @Menu(selected="bsize_list")
*/ */
@ -130,7 +143,8 @@ class BatterySizeController extends Controller
->setTIPriceMotolite($req->request->get('tip_motolite')) ->setTIPriceMotolite($req->request->get('tip_motolite'))
->setTIPricePremium($req->request->get('tip_premium')) ->setTIPricePremium($req->request->get('tip_premium'))
->setTIPriceOther($req->request->get('tip_other')) ->setTIPriceOther($req->request->get('tip_other'))
->setTIPriceLazada($req->request->get('tip_lazada')); ->setTIPriceLazada($req->request->get('tip_lazada'))
->setSubRecurringFee($req->request->get('sub_recurring_fee'));
} }
public function addSubmit(Request $req, ValidatorInterface $validator) public function addSubmit(Request $req, ValidatorInterface $validator)
@ -167,6 +181,9 @@ class BatterySizeController extends Controller
$em->persist($row); $em->persist($row);
$em->flush(); $em->flush();
// create new paymongo subscription plan
$this->pm->createOrUpdateSubPlan($row);
// return successful response // return successful response
return $this->json([ return $this->json([
'success' => 'Changes have been saved!' 'success' => 'Changes have been saved!'
@ -234,6 +251,9 @@ class BatterySizeController extends Controller
// validated! save the entity // validated! save the entity
$em->flush(); $em->flush();
// find if paymongo subscription plan exists, then update accordingly
$this->pm->createOrUpdateSubPlan($row);
// return successful response // return successful response
return $this->json([ return $this->json([
'success' => 'Changes have been saved!' 'success' => 'Changes have been saved!'

View file

@ -13,6 +13,8 @@ use App\Ramcar\JOStatus;
use App\Entity\Warranty; use App\Entity\Warranty;
use App\Entity\JobOrder; use App\Entity\JobOrder;
use App\Entity\CustomerSession; use App\Entity\CustomerSession;
use App\Service\PayMongoConnector;
use DateTime;
class ApiController extends BaseApiController class ApiController extends BaseApiController
{ {
@ -63,6 +65,9 @@ class ApiController extends BaseApiController
} }
} }
// update session timestamp
$this->updateSessionTimestamp();
return [ return [
'is_valid' => !$error, 'is_valid' => !$error,
'error' => $error, 'error' => $error,
@ -164,4 +169,18 @@ class ApiController extends BaseApiController
{ {
return 'Our services are currently limited to some areas in Metro Manila, Baguio, Batangas, Laguna, Cavite, Pampanga, and Palawan. We will update you as soon as we are available in your area. Thank you for understanding. Keep safe!'; return 'Our services are currently limited to some areas in Metro Manila, Baguio, Batangas, Laguna, Cavite, Pampanga, and Palawan. We will update you as soon as we are available in your area. Thank you for understanding. Keep safe!';
} }
protected function initializeSubscriptionPayMongoConnector(PayMongoConnector &$pm)
{
$pm->initialize(
$this->getParameter('subscription_paymongo_public_key'),
$this->getParameter('subscription_paymongo_secret_key'),
);
}
protected function updateSessionTimestamp()
{
$this->session->setDateLatestActivity(new DateTime());
$this->em->flush();
}
} }

View file

@ -9,6 +9,7 @@ use App\Ramcar\CustomerSource;
use App\Entity\Customer; use App\Entity\Customer;
use App\Entity\PrivacyPolicy; use App\Entity\PrivacyPolicy;
use App\Service\HashGenerator; use App\Service\HashGenerator;
use App\Service\PayMongoConnector;
class CustomerController extends ApiController class CustomerController extends ApiController
{ {
@ -27,6 +28,7 @@ class CustomerController extends ApiController
return new ApiResponse(true, '', [ return new ApiResponse(true, '', [
'first_name' => '', 'first_name' => '',
'last_name' => '', 'last_name' => '',
'email' => '',
'priv_third_party' => (bool) false, 'priv_third_party' => (bool) false,
'priv_promo' => (bool) false, 'priv_promo' => (bool) false,
]); ]);
@ -36,18 +38,16 @@ class CustomerController extends ApiController
return new ApiResponse(true, '', [ return new ApiResponse(true, '', [
'first_name' => $cust->getFirstName(), 'first_name' => $cust->getFirstName(),
'last_name' => $cust->getLastName(), 'last_name' => $cust->getLastName(),
'email' => $cust->getEmail(),
'priv_third_party' => (bool) $cust->getPrivacyThirdParty(), 'priv_third_party' => (bool) $cust->getPrivacyThirdParty(),
'priv_promo' => (bool) $cust->getPrivacyPromo(), 'priv_promo' => (bool) $cust->getPrivacyPromo(),
]); ]);
} }
public function updateInfo(Request $req) public function updateInfo(Request $req, PayMongoConnector $pm)
{ {
// validate params // validate params
$validity = $this->validateRequest($req, [ $validity = $this->validateRequest($req);
'first_name',
'last_name',
]);
if (!$validity['is_valid']) { if (!$validity['is_valid']) {
return new ApiResponse(false, $validity['error']); return new ApiResponse(false, $validity['error']);
@ -65,6 +65,12 @@ class CustomerController extends ApiController
$this->em->flush(); $this->em->flush();
// initialize paymongo connector
$this->initializeSubscriptionPayMongoConnector($pm);
// update customer paymongo record if exists
$pm->updateCustomerIfExists($cust);
// response // response
return new ApiResponse(); return new ApiResponse();
} }
@ -127,10 +133,21 @@ class CustomerController extends ApiController
$this->session->setCustomer($cust); $this->session->setCustomer($cust);
} }
$cust->setFirstName($req->request->get('first_name')) if (!is_null($req->request->get('first_name'))) {
->setLastName($req->request->get('last_name')) $cust->setFirstName($req->request->get('first_name'));
->setEmail($req->request->get('email', '')) }
->setConfirmed($this->session->isConfirmed());
if (!is_null($req->request->get('last_name'))) {
$cust->setLastName($req->request->get('last_name'));
}
if (!is_null($req->request->get('email'))) {
$cust->setEmail($req->request->get('email'));
}
if (!is_null($this->session->isConfirmed())) {
$cust->setConfirmed($this->session->isConfirmed());
}
// if customer user isn't set, set it now // if customer user isn't set, set it now
if ($cust->getCustomerUser() == null) { if ($cust->getCustomerUser() == null) {

View file

@ -3,6 +3,7 @@
namespace App\Controller\CustomerAppAPI; namespace App\Controller\CustomerAppAPI;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpKernel\KernelInterface; use Symfony\Component\HttpKernel\KernelInterface;
use Catalyst\ApiBundle\Component\Response as ApiResponse; use Catalyst\ApiBundle\Component\Response as ApiResponse;
@ -33,7 +34,7 @@ class InsuranceController extends ApiController
$this->client = $client; $this->client = $client;
} }
public function createApplication(Request $req, PayMongoConnector $paymongo, UrlGeneratorInterface $router) public function createApplication(Request $req, PayMongoConnector $paymongo, UrlGeneratorInterface $router, ParameterBagInterface $params)
{ {
// validate params // validate params
$validity = $this->validateRequest($req, [ $validity = $this->validateRequest($req, [
@ -162,6 +163,12 @@ class InsuranceController extends ApiController
$this->em->persist($gt); $this->em->persist($gt);
$this->em->flush(); $this->em->flush();
// initialize paymongo connector
$paymongo->initialize(
$params->get('insurance_paymongo_public_key'),
$params->get('insurance_paymongo_secret_key')
);
// create paymongo checkout resource // create paymongo checkout resource
$checkout = $paymongo->createCheckout( $checkout = $paymongo->createCheckout(
$cust, $cust,

View file

@ -17,6 +17,7 @@ use App\Entity\Battery;
use App\Entity\BatterySize; use App\Entity\BatterySize;
use App\Entity\Customer; use App\Entity\Customer;
use App\Entity\CustomerMetadata; use App\Entity\CustomerMetadata;
use App\Ramcar\ServiceType;
class InvoiceController extends ApiController class InvoiceController extends ApiController
{ {
@ -55,7 +56,16 @@ class InvoiceController extends ApiController
// make invoice criteria // make invoice criteria
$icrit = new InvoiceCriteria(); $icrit = new InvoiceCriteria();
$icrit->setServiceType($req->request->get('service_type'));
// if service type is subscription, set to battery replacement but add extra criteria
$stype = $req->request->get('service_type');
if ($stype === 'subscription') {
$stype = ServiceType::BATTERY_REPLACEMENT_NEW;
$icrit->setSubscription(true);
}
// set the service type
$icrit->setServiceType($stype);
// check promo // check promo
$promo_id = $req->request->get('promo_id'); $promo_id = $req->request->get('promo_id');

View file

@ -42,7 +42,7 @@ use App\Entity\JOEvent;
use App\Entity\Warranty; use App\Entity\Warranty;
use App\Entity\JobOrder; use App\Entity\JobOrder;
use App\Entity\CustomerVehicle; use App\Entity\CustomerVehicle;
use App\Ramcar\SubscriptionStatus;
use DateTime; use DateTime;
class JobOrderController extends ApiController class JobOrderController extends ApiController
@ -616,9 +616,16 @@ class JobOrderController extends ApiController
// validate service type // validate service type
$stype = $req->request->get('service_type'); $stype = $req->request->get('service_type');
// if this is a subscription, change to battery replacement so we follow invoice rules for this
if ($stype === 'subscription') {
$stype = ServiceType::BATTERY_REPLACEMENT_NEW;
}
if (!ServiceType::validate($stype)) { if (!ServiceType::validate($stype)) {
return new ApiResponse(false, 'Invalid service type.'); return new ApiResponse(false, 'Invalid service type.');
} }
$jo->setServiceType($stype); $jo->setServiceType($stype);
// validate warranty // validate warranty
@ -712,12 +719,24 @@ class JobOrderController extends ApiController
$pt_id = $pt_manager->getPriceTier($jo->getCoordinates()); $pt_id = $pt_manager->getPriceTier($jo->getCoordinates());
$icrit->setPriceTier($pt_id); $icrit->setPriceTier($pt_id);
// if subscription, set subscription on invoice criteria and find the subscription row to associate with the JO
if ($req->request->get('service_type') === 'subscription') {
$icrit->setSubscription(true);
// get subscription row and check state
$sub = $cv->getLatestSubscription();
if (empty($sub) || $sub->getStatus() !== SubscriptionStatus::ACTIVE) {
return new ApiResponse(false, 'Invalid subscription state for this vehicle.');
}
// associate subscription with JO
$jo->setSubscription($sub);
}
// send to invoice generator // send to invoice generator
$invoice = $ic->generateInvoice($icrit); $invoice = $ic->generateInvoice($icrit);
$jo->setInvoice($invoice); $jo->setInvoice($invoice);
//error_log("GENERATED INVOICE");
// save here first so we have a JO ID which is required for the hub selector // save here first so we have a JO ID which is required for the hub selector
$this->em->persist($invoice); $this->em->persist($invoice);
$this->em->persist($jo); $this->em->persist($jo);
@ -738,7 +757,8 @@ class JobOrderController extends ApiController
->setJoOrigin($jo->getSource()) ->setJoOrigin($jo->getSource())
->setCustomerClass($cust->getCustomerClassification()) ->setCustomerClass($cust->getCustomerClassification())
->setOrderDate($jo->getDateCreate()) ->setOrderDate($jo->getDateCreate())
->setServiceType($jo->getServiceType()); ->setServiceType($jo->getServiceType())
->setSubscription($jo->getSubscription() !== null);
// get distance limit for mobile from env // get distance limit for mobile from env
// get value of hub_filter_enable from env // get value of hub_filter_enable from env
@ -1182,6 +1202,9 @@ class JobOrderController extends ApiController
$hub_criteria = new HubCriteria(); $hub_criteria = new HubCriteria();
$hub_criteria->setPoint($jo->getCoordinates()); $hub_criteria->setPoint($jo->getCoordinates());
// set subscription flag
$hub_criteria->setSubscription($jo->getSubscription() !== null);
// get distance limit for mobile from env // get distance limit for mobile from env
$limit_distance = $_ENV['CUST_DISTANCE_LIMIT']; $limit_distance = $_ENV['CUST_DISTANCE_LIMIT'];
@ -1406,6 +1429,8 @@ class JobOrderController extends ApiController
$dest = $jo->getCoordinates(); $dest = $jo->getCoordinates();
$sub = $jo->getSubscription();
$jo_data = [ $jo_data = [
'id' => $jo->getID(), 'id' => $jo->getID(),
'date_create' => $jo->getDateCreate()->format('M d, Y'), 'date_create' => $jo->getDateCreate()->format('M d, Y'),
@ -1420,6 +1445,7 @@ class JobOrderController extends ApiController
'landmark' => $jo->getLandmark(), 'landmark' => $jo->getLandmark(),
'jo_status' => $status, 'jo_status' => $status,
'status' => $this->generateAPIRiderStatus($status), 'status' => $this->generateAPIRiderStatus($status),
'subscription' => !empty($sub) ? $sub->getID() : null,
]; ];
// customer vehicle and warranty // customer vehicle and warranty

View file

@ -0,0 +1,29 @@
<?php
namespace App\Controller\CustomerAppAPI;
use App\Service\LoyaltyConnector;
use Symfony\Component\HttpFoundation\Request;
use Catalyst\ApiBundle\Component\Response as ApiResponse;
class LoyaltyController extends ApiController
{
public function register(Request $req, LoyaltyConnector $lc)
{
// validate params
$validity = $this->validateRequest($req);
if (!$validity['is_valid']) {
return new ApiResponse(false, $validity['error']);
}
// register customer or retrieve existing record
$result = $lc->register($this->session->getCustomer());
if (!$result['success']) {
return new ApiResponse(false, $result['error']);
}
// response
return new ApiResponse(true, '', $result['response']['data']);
}
}

View file

@ -0,0 +1,34 @@
<?php
namespace App\Controller\CustomerAppAPI;
use Symfony\Component\HttpFoundation\Request;
use Catalyst\ApiBundle\Component\Response as ApiResponse;
use App\Entity\StaticContent;
class StaticContentController extends ApiController
{
public function getContent(Request $req, $id)
{
// check requirements
$validity = $this->validateRequest($req);
if (!$validity['is_valid']) {
return new ApiResponse(false, $validity['error']);
}
// get content
$content = $this->em->getRepository(Staticcontent::class)->find($id);
// check if it exists
if ($content == null) {
return new ApiResponse(false, 'Requested content does not exist.');
}
// response
return new ApiResponse(true, '', [
'content' => $content->getContent(),
]);
}
}

View file

@ -0,0 +1,388 @@
<?php
namespace App\Controller\CustomerAppAPI;
use Symfony\Component\HttpFoundation\Request;
use Catalyst\ApiBundle\Component\Response as ApiResponse;
use App\Service\PayMongoConnector;
use App\Entity\Customer;
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 DateTime;
class SubscriptionController extends ApiController
{
public function getPlanDetails(Request $req, $vid, PayMongoConnector $pm)
{
// check requirements
$validity = $this->validateRequest($req);
if (!$validity['is_valid']) {
return new ApiResponse(false, $validity['error']);
}
// get vehicle
$vehicle = $this->em->getRepository(Vehicle::class)->find($vid);
if ($vehicle == null) {
return new ApiResponse(false, 'Invalid vehicle.');
}
$plan = null;
// get compatible batteries
$batts = $vehicle->getActiveBatteries();
if (!empty($batts)) {
// initialize paymongo connector
$this->initializeSubscriptionPayMongoConnector($pm);
$plan = $pm->getPlanByBatterySize($batts[0]->getSize());
}
// response
return new ApiResponse(true, '', [
'plan' => $plan,
]);
}
// NOTE: disabling this since we can just include the public key with the subscription creation endpoint
/*
public function getPayMongoPublicKey(Request $req)
{
// check requirements
$validity = $this->validateRequest($req);
if (!$validity['is_valid']) {
return new ApiResponse(false, $validity['error']);
}
// response
return new ApiResponse(true, '', [
'key' => $this->getParameter('subscription_paymongo_public_key'),
]);
}
*/
public function createSubscription(Request $req, PayMongoConnector $pm)
{
// check requirements
$validity = $this->validateRequest($req, [
'plan_id',
'cv_id',
'email',
'remember_email',
]);
if (!$validity['is_valid']) {
return new ApiResponse(false, $validity['error']);
}
// get customer
$cust = $this->session->getCustomer();
if ($cust == null) {
return new ApiResponse(false, 'No customer information found.');
}
// verify email does not belong to someone else
$email = $req->request->get('email');
$qb = $this->em->getRepository(Customer::class)
->createQueryBuilder('c')
->select('c')
->where('c.email = :email')
->andWhere('c.id != :cust_id')
->setParameter('email', $email)
->setParameter('cust_id', $cust->getID());
$email_exists = $qb->getQuery()->getOneOrNullResult();
if (!empty($email_exists)) {
return new ApiResponse(false, 'Email is already in use. Please use a different email address.');
}
// get customer vehicle
$cv = $this->em->getRepository(CustomerVehicle::class)->find($req->request->get('cv_id'));
// check if it exists
if ($cv == null) {
return new ApiResponse(false, 'Vehicle does not exist.');
}
// check if it's owned by customer
if ($cv->getCustomer()->getID() != $cust->getID()) {
return new ApiResponse(false, 'Invalid vehicle.');
}
// 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)) {
return new ApiResponse(false, 'Error retrieving customer record. Please try again later.');
}
// 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'], $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) || empty($sub_invoice)) {
return new ApiResponse(false, 'Error creating subscription. Please try again later.');
}
// the payment intent must still be in a pending state
// TODO: log this somewhere
if ($sub_pi['status'] !== 'awaiting_payment_method') {
return new ApiResponse(false, 'Error creating subscription invoice. Please try again later.');
}
// fetch payment intent details for client key
$pi = $pm->getPaymentIntent($sub_pi['id']);
if (empty($pi['response']['data']['id'])) {
return new ApiResponse(false, 'Error retrieving payment intent. Please try again later.');
}
// create subscription entity
$obj = new Subscription();
$obj->setCustomer($cust)
->setCustomerVehicle($cv)
->setEmail($email)
->setStatus(SubscriptionStatus::PENDING)
->setExtApiId($pm_sub['response']['data']['id'])
->setMetadata($pm_sub['response']['data']);
// if requested to save email, save it
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();
// response
return new ApiResponse(true, '', [
'subscription_id' => $obj->getID(),
'payment_intent_id' => $pi['response']['data']['id'],
'payment_intent_client_key' => $pi['response']['data']['attributes']['client_key'],
'paymongo_public_key' => $this->getParameter('subscription_paymongo_public_key'),
]);
}
public function finalizeSubscription(Request $req, $id, PayMongoConnector $pm)
{
// check requirements
$validity = $this->validateRequest($req);
if (!$validity['is_valid']) {
return new ApiResponse(false, $validity['error']);
}
// 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' => $cust,
]);
if (empty($sub_obj)) {
return new ApiResponse(false, 'Invalid subscription provided.');
}
// get paymongo subscription so we can verify if the latest invoice is paid or not
$pm_sub = $pm->getSubscription($sub_obj->getExtApiId());
if (empty($pm_sub['response']['data']['id'])) {
return new ApiResponse(false, 'Error retrieving subscription. Please try again later.');
}
// make sure the latest invoice has been paid
// NOTE: ignore this, this is unreliable due to race condition
/*
if ($pm_sub['response']['data']['attributes']['latest_invoice']['status'] !== 'paid') {
return new ApiResponse(false, 'Latest invoice for subscription is not yet paid.');
}
*/
// get payment intent
$pi = $pm->getPaymentIntent($pm_sub['response']['data']['attributes']['latest_invoice']['payment_intent']['id']);
if (empty($pi['response']['data']['id'])) {
return new ApiResponse(false, 'Error retrieving payment intent. Please try again later.');
}
// if the paymongo sub is active, and the payment was successful, update the gateway transaction record, which will also activate the sub via listener
if (
$sub_obj->getStatus() === SubscriptionStatus::PENDING &&
$pi['response']['data']['attributes']['status'] === 'succeeded'
) {
$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(new DateTime());
$this->em->flush();
}
// response
return new ApiResponse(true, '', [
'payment_intent' => $pi['response']['data'],
]);
}
public function getUnfulfilledSubs(Request $req)
{
// check requirements
$validity = $this->validateRequest($req);
if (!$validity['is_valid']) {
return new ApiResponse(false, $validity['error']);
}
// get customer
$cust = $this->session->getCustomer();
if ($cust == null) {
return new ApiResponse(false, 'No customer information found.');
}
// NOTE: this functions like an outer join as far as DQL is concerned
// get all customer vehicles for the current customer that do not have a JO
$sql = 'SELECT cv.id, cv.name, cv.model_year, cv.plate_number, v.id AS make_id, v.make AS make, vm.name AS manufacturer
FROM App\Entity\CustomerVehicle cv
JOIN App\Entity\Vehicle v WITH cv.vehicle = v
JOIN App\Entity\VehicleManufacturer vm WITH v.manufacturer = vm
JOIN App\Entity\Subscription s WITH s.customer_vehicle = cv AND s.status = :status
LEFT JOIN App\Entity\JobOrder jo WITH jo.subscription = s
WHERE jo.id IS NULL
AND cv.customer = :customer';
$query = $this->em->createQuery($sql)
->setParameters([
'status' => SubscriptionStatus::ACTIVE,
'customer' => $cust,
]);
$vehicles = $query->getResult();
// response
return new ApiResponse(true, '', [
'vehicles' => $vehicles,
]);
}
/*
public function getPaymentIntent(Request $req, $pid, PayMongoConnector $pm)
{
// check requirements
$validity = $this->validateRequest($req);
if (!$validity['is_valid']) {
return new ApiResponse(false, $validity['error']);
}
// initialize paymongo connector
$this->initializeSubscriptionPayMongoConnector($pm);
// get payment intent
$pi = $pm->getPaymentIntent($pid);
if (empty($pi['response']['data']['id'])) {
return new ApiResponse(false, 'Error retrieving payment intent. Please try again later.');
}
// response
return new ApiResponse(true, '', [
'payment_intent' => $pi['response']['data'],
]);
}
public function activateSubscription(Request $req, $id, PayMongoConnector $pm)
{
// check requirements
$validity = $this->validateRequest($req);
if (!$validity['is_valid']) {
return new ApiResponse(false, $validity['error']);
}
// initialize paymongo connector
$this->initializeSubscriptionPayMongoConnector($pm);
// get subscription
$obj = $this->em->getRepository(Subscription::class)->findOneBy([
'id' => $id,
'status' => SubscriptionStatus::PENDING,
'customer' => $this->session->getCustomer(),
]);
if (empty($obj)) {
return new ApiResponse(false, 'Invalid subscription provided.');
}
// get paymongo subscription so we can verify if the latest invoice is paid or not
$pm_sub = $pm->getSubscription($obj->getExtApiId());
if (empty($pm_sub['response']['data']['id'])) {
return new ApiResponse(false, 'Error retrieving subscription. Please try again later.');
}
// make sure the latest invoice has been paid
if ($pm_sub['response']['data']['attributes']['latest_invoice']['status'] !== 'succeeded') {
return new ApiResponse(false, 'Latest invoice for subscription is not yet paid.');
}
// mark subscription as paid
$obj->setStatus(SubscriptionStatus::ACTIVE)
->setDateStart(new DateTime());
$this->em->flush();
// response
return new ApiResponse(true, '', [
'success' => true,
]);
}
*/
}

View file

@ -3,6 +3,7 @@
namespace App\Controller\CustomerAppAPI; namespace App\Controller\CustomerAppAPI;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Catalyst\ApiBundle\Component\Response as ApiResponse; use Catalyst\ApiBundle\Component\Response as ApiResponse;
use CrEOF\Spatial\PHP\Types\Geometry\Point; use CrEOF\Spatial\PHP\Types\Geometry\Point;
@ -15,6 +16,7 @@ use App\Ramcar\JOStatus;
use App\Ramcar\ServiceType; use App\Ramcar\ServiceType;
use App\Ramcar\TradeInType; use App\Ramcar\TradeInType;
use App\Ramcar\InsuranceApplicationStatus; use App\Ramcar\InsuranceApplicationStatus;
use App\Ramcar\SubscriptionStatus;
use App\Service\PayMongoConnector; use App\Service\PayMongoConnector;
use App\Service\PriceTierManager; use App\Service\PriceTierManager;
use DateTime; use DateTime;
@ -115,7 +117,7 @@ class VehicleController extends ApiController
} }
public function getVehicle(Request $req, $id, PayMongoConnector $paymongo) public function getVehicle(Request $req, $id, PayMongoConnector $paymongo, ParameterBagInterface $params)
{ {
// check requirements // check requirements
$validity = $this->validateRequest($req); $validity = $this->validateRequest($req);
@ -139,7 +141,7 @@ class VehicleController extends ApiController
// response // response
return new ApiResponse(true, '', [ return new ApiResponse(true, '', [
'vehicle' => $this->generateVehicleInfo($cv, true, $paymongo), 'vehicle' => $this->generateVehicleInfo($paymongo, $params, $cv, true, true),
]); ]);
} }
@ -210,7 +212,7 @@ class VehicleController extends ApiController
]); ]);
} }
public function listVehicles(Request $req, PayMongoConnector $paymongo) public function listVehicles(Request $req, PayMongoConnector $paymongo, ParameterBagInterface $params)
{ {
// validate params // validate params
$validity = $this->validateRequest($req); $validity = $this->validateRequest($req);
@ -231,7 +233,7 @@ class VehicleController extends ApiController
// only get the customer's vehicles whose flag_active is true // only get the customer's vehicles whose flag_active is true
$cvs = $this->em->getRepository(CustomerVehicle::class)->findBy(['flag_active' => true, 'customer' => $cust]); $cvs = $this->em->getRepository(CustomerVehicle::class)->findBy(['flag_active' => true, 'customer' => $cust]);
foreach ($cvs as $cv) { foreach ($cvs as $cv) {
$cv_list[] = $this->generateVehicleInfo($cv, true, $paymongo); $cv_list[] = $this->generateVehicleInfo($paymongo, $params, $cv, true, true);
} }
// response // response
@ -259,7 +261,10 @@ class VehicleController extends ApiController
$lng = $req->query->get('longitude', ''); $lng = $req->query->get('longitude', '');
$lat = $req->query->get('latitude', ''); $lat = $req->query->get('latitude', '');
$batts = $vehicle->getActiveBatteries(); // if for subscription purposes, get only the most qualified model either by tag or price
$is_subscription = $req->query->get('is_subscription', false);
$batts = $vehicle->getActiveBatteries($is_subscription);
$pt_id = 0; $pt_id = 0;
if ((!(empty($lng))) && (!(empty($lat)))) if ((!(empty($lng))) && (!(empty($lat))))
{ {
@ -393,7 +398,7 @@ class VehicleController extends ApiController
// TODO: possibly refactor this bit // TODO: possibly refactor this bit
// if no valid previous JO is found, base the trade-in value on recommended batteries // if no valid previous JO is found, base the trade-in value on recommended batteries
if (!$previous_jo_found) { if (!$previous_jo_found) {
$comp_batteries = $cv->getVehicle()->getBatteries(); $comp_batteries = $cv->getVehicle()->getActiveBatteries();
// get the lowest trade-in value from the list of batteries // get the lowest trade-in value from the list of batteries
if (!empty($comp_batteries)) { if (!empty($comp_batteries)) {
@ -417,7 +422,7 @@ class VehicleController extends ApiController
]; ];
} }
protected function generateVehicleInfo(CustomerVehicle $cv, $include_insurance = false, PayMongoConnector $paymongo) protected function generateVehicleInfo(PayMongoConnector $paymongo, ParameterBagInterface $params, CustomerVehicle $cv, $include_insurance = false, $include_active_sub = false)
{ {
$battery_id = null; $battery_id = null;
if ($cv->getCurrentBattery() != null) if ($cv->getCurrentBattery() != null)
@ -433,10 +438,14 @@ class VehicleController extends ApiController
if ($cv->getName() != null) if ($cv->getName() != null)
$cv_name = $cv->getName(); $cv_name = $cv->getName();
$vehicle = $cv->getVehicle();
$row = [ $row = [
'cv_id' => $cv->getID(), 'cv_id' => $cv->getID(),
'mfg_id' => $cv->getVehicle()->getManufacturer()->getID(), 'mfg_id' => $vehicle->getManufacturer()->getID(),
'make_id' => $cv->getVehicle()->getID(), 'make_id' => $vehicle->getID(),
'mfg_name' => $vehicle->getManufacturer()->getName(),
'make_name' => $vehicle->getMake(),
'name' => $cv_name, 'name' => $cv_name,
'plate_num' => $cv->getPlateNumber(), 'plate_num' => $cv->getPlateNumber(),
'model_year' => $cv->getModelYear(), 'model_year' => $cv->getModelYear(),
@ -467,6 +476,12 @@ class VehicleController extends ApiController
// TODO: maybe handle this more elegantly. issue is not sure it is a good idea to update the db for this very transient status as the webhook listener also updates this status right away // TODO: maybe handle this more elegantly. issue is not sure it is a good idea to update the db for this very transient status as the webhook listener also updates this status right away
switch ($status) { switch ($status) {
case InsuranceApplicationStatus::CREATED: case InsuranceApplicationStatus::CREATED:
// initialize paymongo connector
$paymongo->initialize(
$params->get('insurance_paymongo_public_key'),
$params->get('insurance_paymongo_secret_key')
);
// get latest status on this checkout from paymongo // get latest status on this checkout from paymongo
$checkout = $paymongo->getCheckout($gt->getExtTransactionId()); $checkout = $paymongo->getCheckout($gt->getExtTransactionId());
@ -486,6 +501,7 @@ class VehicleController extends ApiController
break; break;
} }
// build insurance row
$insurance = [ $insurance = [
'id' => $iobj->getID(), 'id' => $iobj->getID(),
'ext_transaction_id' => $iobj->getExtTransactionId(), 'ext_transaction_id' => $iobj->getExtTransactionId(),
@ -495,17 +511,35 @@ class VehicleController extends ApiController
'transaction_status' => $gt->getStatus(), 'transaction_status' => $gt->getStatus(),
'premium_amount' => (string)bcdiv($gt->getAmount(), 100), // NOTE: hard expressing as string so it's consistent 'premium_amount' => (string)bcdiv($gt->getAmount(), 100), // NOTE: hard expressing as string so it's consistent
'date_submit' => $iobj->getDateSubmit()->format('Y-m-d H:i:s'), 'date_submit' => $iobj->getDateSubmit()->format('Y-m-d H:i:s'),
'date_complete' => $date_complete ? $date_complete->format('Y-m-d H:i:s') : null, 'date_complete' => !empty($date_complete) ? $date_complete->format('Y-m-d H:i:s') : null,
'date_expire' => $date_expire ? $date_expire->format('Y-m-d H:i:s') : null, 'date_expire' => !empty($date_expire) ? $date_expire->format('Y-m-d H:i:s') : null,
'changelog' => $iobj->getMetadata()['changes'] ?? [],
]; ];
// get information changelog
$insurance['changelog'] = $iobj->getMetadata()['changes'] ?? [];
} }
$row['latest_insurance'] = $insurance; $row['latest_insurance'] = $insurance;
} }
// get active subscription row
if ($include_active_sub) {
$active_sub = null;
$sobj = $cv->getLatestSubscription();
if (!empty($sobj)) {
$date_cancel = $sobj->getDateCancel();
// build subscription row
$active_sub = [
'email' => $sobj->getEmail(),
'date_start' => $sobj->getDateStart()->format('Y-m-d H:i:s'),
'date_end' => $sobj->getDateEnd()->format('Y-m-d H:i:s'),
'date_cancel' => !empty($date_cancel) ? $date_cancel->format('Y-m-d H:i:s') : null,
'status' => $sobj->getStatus(),
];
}
$row['active_subscription'] = $active_sub;
}
return $row; return $row;
} }

View file

@ -4,11 +4,12 @@ namespace App\Controller;
use App\Entity\GatewayTransaction; use App\Entity\GatewayTransaction;
use App\Ramcar\TransactionStatus; use App\Ramcar\TransactionStatus;
use App\Service\FCMSender;
use App\Service\LoyaltyConnector;
use App\Service\PayMongoConnector; use App\Service\PayMongoConnector;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Bundle\FrameworkBundle\Controller\Controller; use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use DateTime; use DateTime;
@ -17,11 +18,15 @@ class PayMongoController extends Controller
{ {
protected $pm; protected $pm;
protected $em; 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->pm = $pm;
$this->em = $em; $this->em = $em;
$this->lc = $lc;
$this->fcmclient = $fcmclient;
} }
public function listen(Request $req) public function listen(Request $req)
@ -63,13 +68,45 @@ class PayMongoController extends Controller
protected function handlePaymentPaid($event) protected function handlePaymentPaid($event)
{ {
// 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'];
// 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];
break;
// insurance premium
// TODO: retest this later so we don't use a default clause
default:
$metadata = $event['attributes']['metadata']; $metadata = $event['attributes']['metadata'];
$obj = $this->getTransaction($metadata['transaction_id']);
// add to criteria
$criteria['id'] = $metadata['transaction_id'];
break;
}
// get transaction
$obj = $this->em->getRepository(GatewayTransaction::class)->findOneBy($criteria);
if (!empty($obj)) { if (!empty($obj)) {
// mark as paid // mark as paid
$obj->setStatus(TransactionStatus::PAID); $obj->setStatus(TransactionStatus::PAID);
$obj->setDatePay(new DateTime()); $obj->setDatePay(new DateTime());
$this->em->flush(); $this->em->flush();
} }
@ -86,14 +123,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) public function paymentSuccess(Request $req)
{ {
return $this->render('paymongo/success.html.twig'); return $this->render('paymongo/success.html.twig');

View file

@ -212,8 +212,7 @@ class StaticContentController extends Controller
throw $this->createNotFoundException('The item does not exist'); throw $this->createNotFoundException('The item does not exist');
// set and save values // set and save values
$row->setID($req->request->get('id')) $row->setContent($req->request->get('content'));
->setContent($req->request->get('content'));
// validate // validate
$errors = $validator->validate($row); $errors = $validator->validate($row);
@ -221,13 +220,6 @@ class StaticContentController extends Controller
// initialize error list // initialize error list
$error_array = []; $error_array = [];
// check for duplicate ID
$result = $em->getRepository(StaticContent::class)->find($id);
if ($result != null)
{
$error_array['id'] = 'Duplicate ID exists.';
}
// add errors to list // add errors to list
foreach ($errors as $error) { foreach ($errors as $error) {
$error_array[$error->getPropertyPath()] = $error->getMessage(); $error_array[$error->getPropertyPath()] = $error->getMessage();

View file

@ -192,6 +192,9 @@ class JobOrderController extends ApiController
$hub_criteria = new HubCriteria(); $hub_criteria = new HubCriteria();
$hub_criteria->setPoint($jo->getCoordinates()); $hub_criteria->setPoint($jo->getCoordinates());
// set subscription flag
$hub_criteria->setSubscription($jo->getSubscription() !== null);
// get distance limit for mobile from env // get distance limit for mobile from env
// get value of hub_filter_enable from env // get value of hub_filter_enable from env
$dotenv = new Dotenv(); $dotenv = new Dotenv();

View file

@ -153,6 +153,12 @@ class Battery
*/ */
protected $flag_active; protected $flag_active;
// flag if battery is used for subscriptions
/**
* @ORM\Column(type="boolean", options={"default": false})
*/
protected $flag_subscription;
public function __construct() public function __construct()
{ {
$this->vehicles = new ArrayCollection(); $this->vehicles = new ArrayCollection();
@ -167,6 +173,7 @@ class Battery
$this->date_create = new DateTime(); $this->date_create = new DateTime();
$this->flag_active = true; $this->flag_active = true;
$this->flag_subscription = false;
} }
public function getID() public function getID()
@ -396,9 +403,20 @@ class Battery
return $this->flag_active; return $this->flag_active;
} }
public function setActive($flag_active = true) public function setActive($flag_subscription = true)
{ {
$this->flag_active = $flag_active; $this->flag_subscription = $flag_subscription;
return $this;
}
public function isSubscription()
{
return $this->flag_subscription;
}
public function setSubscription($flag_subscription = true)
{
$this->flag_subscription = $flag_subscription;
return $this; return $this;
} }
} }

View file

@ -57,6 +57,12 @@ class BatterySize
*/ */
protected $tip_lazada; protected $tip_lazada;
// subscription msrp
/**
* @ORM\Column(type="decimal", precision=7, scale=2, nullable=true)
*/
protected $sub_recurring_fee;
public function __construct() public function __construct()
{ {
$this->batteries = new ArrayCollection(); $this->batteries = new ArrayCollection();
@ -64,6 +70,7 @@ class BatterySize
$this->tip_premium = 0; $this->tip_premium = 0;
$this->tip_other = 0; $this->tip_other = 0;
$this->tip_lazada = 0; $this->tip_lazada = 0;
$this->sub_recurring_fee = 0;
} }
public function getID() public function getID()
@ -149,4 +156,14 @@ class BatterySize
return $this->tip_lazada; return $this->tip_lazada;
} }
public function setSubRecurringFee($sub_recurring_fee)
{
$this->sub_recurring_fee = $sub_recurring_fee;
return $this;
}
public function getSubRecurringFee()
{
return $this->sub_recurring_fee;
}
} }

View file

@ -100,6 +100,12 @@ class CustomerSession
*/ */
protected $date_code_sent; protected $date_code_sent;
// date and time this session was last used
/**
* @ORM\Column(type="datetime", nullable=true)
*/
protected $date_latest_activity;
// reviews made by mobile session // reviews made by mobile session
/** /**
* @ORM\OneToMany(targetEntity="Review", mappedBy="mobile_session") * @ORM\OneToMany(targetEntity="Review", mappedBy="mobile_session")
@ -115,6 +121,7 @@ class CustomerSession
$this->confirm_flag = false; $this->confirm_flag = false;
$this->date_confirmed = null; $this->date_confirmed = null;
$this->date_code_sent = null; $this->date_code_sent = null;
$this->date_latest_activity = null;
$this->reviews = new ArrayCollection(); $this->reviews = new ArrayCollection();
} }
@ -270,4 +277,15 @@ class CustomerSession
{ {
return $this->reviews; return $this->reviews;
} }
public function setDateLatestActivity(DateTime $date)
{
$this->date_latest_activity = $date;
return $this;
}
public function getDateLatestActivity()
{
return $this->date_latest_activity;
}
} }

View file

@ -3,6 +3,7 @@
namespace App\Entity; namespace App\Entity;
use App\Ramcar\InsuranceApplicationStatus; use App\Ramcar\InsuranceApplicationStatus;
use App\Ramcar\SubscriptionStatus;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\ArrayCollection;
@ -12,8 +13,12 @@ use Doctrine\Common\Collections\Criteria;
/** /**
* @ORM\Entity * @ORM\Entity
* @ORM\Table(name="customer_vehicle", indexes={@ORM\Index(columns={"plate_number"}, flags={"fulltext"}), * @ORM\Table(
@ORM\Index(name="plate_number_idx", columns={"plate_number"})}) * name="customer_vehicle",
* indexes={
* @ORM\Index(name="plate_number_idx", columns={"plate_number"}, flags={"fulltext"})
* }
* )
*/ */
class CustomerVehicle class CustomerVehicle
{ {
@ -122,6 +127,12 @@ class CustomerVehicle
*/ */
protected $insurance_applications; protected $insurance_applications;
// link to subscription
/**
* @ORM\OneToMany(targetEntity="Subscription", mappedBy="customer_vehicle")
*/
protected $subscriptions;
public function __construct() public function __construct()
{ {
$this->flag_active = true; $this->flag_active = true;
@ -306,4 +317,26 @@ class CustomerVehicle
return !empty($result) ? $result[0] : null; return !empty($result) ? $result[0] : null;
} }
public function getSubscriptions()
{
return $this->subscriptions;
}
public function getLatestSubscription()
{
// we get the latest subscription that actually started
$criteria = Criteria::create()
->where(Criteria::expr()->notIn('status', [
SubscriptionStatus::CANCELLED,
SubscriptionStatus::PENDING,
]))
->where(Criteria::expr()->neq('date_start', null))
->orderBy(['date_create' => Criteria::DESC])
->setMaxResults(1);
$result = $this->subscriptions->matching($criteria);
return !empty($result) ? $result[0] : null;
}
} }

View file

@ -441,6 +441,13 @@ class JobOrder
*/ */
protected $flag_cust_new; protected $flag_cust_new;
// get the subscription this job order is associated with
/**
* @ORM\ManyToOne(targetEntity="Subscription", inversedBy="claims")
* @ORM\JoinColumn(name="subscription_id", referencedColumnName="id", nullable=true, onDelete="SET NULL")
*/
protected $subscription;
public function __construct() public function __construct()
{ {
$this->date_create = new DateTime(); $this->date_create = new DateTime();
@ -1256,4 +1263,15 @@ class JobOrder
return $this->flag_cust_new; return $this->flag_cust_new;
} }
public function getSubscription()
{
return $this->subscription;
}
public function setSubscription($subscription)
{
$this->subscription = $subscription;
return $this;
}
} }

226
src/Entity/Subscription.php Normal file
View file

@ -0,0 +1,226 @@
<?php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
use Symfony\Component\Validator\Constraints as Assert;
use DateTime;
/**
* @ORM\Entity
* @ORM\Table(name="subscription")
*/
class Subscription
{
/**
* @ORM\Id
* @ORM\Column(type="integer")
* @ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
// link to customer
/**
* @ORM\ManyToOne(targetEntity="Customer")
* @ORM\JoinColumn(name="customer_id", referencedColumnName="id")
*/
protected $customer;
/**
* @ORM\ManyToOne(targetEntity="CustomerVehicle", inversedBy="subscriptions")
* @ORM\JoinColumn(name="customer_vehicle_id", referencedColumnName="id")
*/
protected $customer_vehicle;
// job orders associated with subscription
/**
* @ORM\OneToMany(targetEntity="JobOrder", mappedBy="subscription")
*/
protected $job_orders;
// email address
/**
* @ORM\Column(type="string", length=255, nullable=true)
*/
protected $email;
// date subscription was created
/**
* @ORM\Column(type="datetime")
*/
protected $date_create;
// date subscription starts
/**
* @ORM\Column(type="datetime", nullable=true)
*/
protected $date_start;
// date subscription ends
/**
* @ORM\Column(type="datetime", nullable=true)
*/
protected $date_end;
// date subscription was cancelled
/**
* @ORM\Column(type="datetime", nullable=true)
*/
protected $date_cancel;
// external api id (paymongo)
/**
* @ORM\Column(type="string", length=255, nullable=true)
*/
protected $ext_api_id;
// status of the subscription
/**
* @ORM\Column(type="string", length=50)
* @Assert\NotBlank()
*/
protected $status;
// other data related to the transaction
/**
* @ORM\Column(type="json")
*/
protected $metadata;
public function __construct()
{
$this->date_create = new DateTime();
$this->date_start = null;
$this->date_end = null;
$this->date_cancel = null;
$this->job_orders = new ArrayCollection();
$this->metadata = [];
}
public function getID()
{
return $this->id;
}
public function setCustomer(Customer $cust = null)
{
$this->customer = $cust;
return $this;
}
public function getCustomer()
{
return $this->customer;
}
public function setCustomerVehicle(CustomerVehicle $customer_vehicle)
{
$this->customer_vehicle = $customer_vehicle;
return $this;
}
public function getCustomerVehicle()
{
return $this->customer_vehicle;
}
public function setDateCreate(DateTime $date)
{
$this->date_create = $date;
return $this;
}
public function getDateCreate()
{
return $this->date_create;
}
public function setDateStart(DateTime $date)
{
$this->date_start = $date;
return $this;
}
public function getDateStart()
{
return $this->date_start;
}
public function setDateEnd(DateTime $date)
{
$this->date_end = $date;
return $this;
}
public function getDateEnd()
{
return $this->date_end;
}
public function setDateCancel(DateTime $date)
{
$this->date_cancel = $date;
return $this;
}
public function getDateCancel()
{
return $this->date_cancel;
}
public function setStatus($status)
{
$this->status = $status;
return $this;
}
public function getStatus()
{
return $this->status;
}
public function setEmail($email)
{
$this->email = $email;
return $this;
}
public function getEmail()
{
return $this->email;
}
public function setExtApiId($ext_api_id)
{
$this->ext_api_id = $ext_api_id;
return $this;
}
public function getExtApiId()
{
return $this->ext_api_id;
}
public function setMetadata($metadata)
{
$this->metadata = $metadata;
return $this;
}
public function getMetadata()
{
return $this->metadata;
}
public function getJobOrders()
{
return $this->job_orders;
}
public function getGatewayTransactions()
{
// TODO: get gateway transactions here via type and metadata
}
}

View file

@ -180,11 +180,20 @@ class Vehicle
return $this->cust_vehicles; return $this->cust_vehicles;
} }
public function getActiveBatteries() public function getActiveBatteries($is_subscription = false)
{ {
$crit = Criteria::create(); $crit = Criteria::create();
$crit->where(Criteria::expr()->eq('flag_active', true)); $crit->where(Criteria::expr()->eq('flag_active', true));
// if by subscrpiption, order first by if it is a subscription battery, then by descending price
if ($is_subscription) {
$crit->orderBy([
'flag_subscription' => 'desc',
'sell_price' => 'desc',
])
->setMaxResults(1);
}
return $this->batteries->matching($crit); return $this->batteries->matching($crit);
} }
} }

View file

@ -5,22 +5,34 @@ namespace App\EntityListener;
use Doctrine\ORM\Event\LifecycleEventArgs; use Doctrine\ORM\Event\LifecycleEventArgs;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use App\Entity\Subscription;
use App\Entity\GatewayTransaction; use App\Entity\GatewayTransaction;
use App\Entity\InsuranceApplication; use App\Entity\InsuranceApplication;
use App\Service\InsuranceConnector; use App\Service\InsuranceConnector;
use App\Ramcar\InsuranceApplicationStatus; use App\Ramcar\InsuranceApplicationStatus;
use App\Ramcar\TransactionStatus; use App\Ramcar\TransactionStatus;
use App\Ramcar\SubscriptionStatus;
use App\Service\FCMSender;
use App\Service\LoyaltyConnector;
use DateTime; use DateTime;
class GatewayTransactionListener class GatewayTransactionListener
{ {
protected $ic; protected $ic;
protected $em; protected $em;
protected $fcmclient;
protected $lc;
protected $sub_months;
protected $loyalty_point_multiplier;
public function __construct(EntityManagerInterface $em, InsuranceConnector $ic) public function __construct(EntityManagerInterface $em, InsuranceConnector $ic, FCMSender $fcmclient, LoyaltyConnector $lc, $sub_months, $loyalty_point_multiplier)
{ {
$this->em = $em; $this->em = $em;
$this->ic = $ic; $this->ic = $ic;
$this->fcmclient = $fcmclient;
$this->lc = $lc;
$this->sub_months = $sub_months;
$this->loyalty_point_multiplier = $loyalty_point_multiplier;
} }
public function postUpdate(GatewayTransaction $gt_obj, LifecycleEventArgs $args) public function postUpdate(GatewayTransaction $gt_obj, LifecycleEventArgs $args)
@ -36,14 +48,20 @@ class GatewayTransactionListener
$prev_value = $field_changes[0] ?? null; $prev_value = $field_changes[0] ?? null;
$new_value = $field_changes[1] ?? null; $new_value = $field_changes[1] ?? null;
error_log($prev_value . " vs " . $new_value);
// only do something if the status has changed to paid // only do something if the status has changed to paid
if ($prev_value !== $new_value && $new_value === TransactionStatus::PAID) { if ($prev_value !== $new_value && $new_value === TransactionStatus::PAID) {
// determine if we will add loyalty points for this transaction
// handle based on type // handle based on type
// TODO: add types here as we go. there's probably a better way to do this. // TODO: add types here as we go. there's probably a better way to do this.
switch ($gt_obj->getType()) { switch ($gt_obj->getType()) {
case 'insurance_premium': case 'insurance_premium':
return $this->handleInsurancePremium($gt_obj); $this->handleInsurancePremium($gt_obj);
break; break;
case 'subscription':
$this->handleSubscription($gt_obj);
$this->addLoyaltyPoints($gt_obj);
default: default:
break; break;
} }
@ -51,7 +69,7 @@ class GatewayTransactionListener
} }
} }
protected function handleInsurancePremium($gt_obj) protected function handleInsurancePremium(GatewayTransaction $gt_obj)
{ {
// get insurance application object // get insurance application object
$obj = $this->em->getRepository(InsuranceApplication::class)->findOneBy([ $obj = $this->em->getRepository(InsuranceApplication::class)->findOneBy([
@ -74,5 +92,68 @@ class GatewayTransactionListener
} }
} }
} }
protected function handleSubscription(GatewayTransaction $gt_obj)
{
$sub_id = $gt_obj->getMetadata()['subscription_id'];
// activate the sub
// TODO: put subscription management into a service
$sub = $this->em->getRepository(Subscription::class)->findOneBy([
'ext_api_id' => $sub_id,
'status' => SubscriptionStatus::PENDING,
]);
if (empty($sub)) {
// NOTE: this isn't supposed to happen
error_log("Subscription not found for ID: ". $sub_id);
} else {
// set sub date parameters
// TODO: this really needs to be in a service
$sub_start_date = new DateTime();
$sub_end_date = clone $sub_start_date;
$sub_end_date->modify('+' . $this->sub_months . ' month');
$sub->setStatus(SubscriptionStatus::ACTIVE)
->setDateStart($sub_start_date)
->setDateEnd($sub_end_date);
$this->em->flush();
error_log("Subscription has been set to active via listener");
error_log("SUB ID: " . $sub->getID());
error_log($sub->getStatus());
// send notification about subscription
$this->fcmclient->sendSubscriptionEvent(
$sub,
"sub_fcm_title_active",
"sub_fcm_body_active",
);
}
}
protected function addLoyaltyPoints(GatewayTransaction $gt_obj)
{
$cust = $gt_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 = ($gt_obj->getAmount() / 100) * $this->loyalty_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%' => number_format($points_amount)],
);
}
}
} }

View file

@ -0,0 +1,69 @@
<?php
namespace App\InvoiceRule;
use Doctrine\ORM\EntityManagerInterface;
use App\InvoiceRuleInterface;
use App\Ramcar\ServiceType;
class IsSubscription implements InvoiceRuleInterface
{
protected $em;
public function __construct(EntityManagerInterface $em)
{
$this->em = $em;
}
public function getID()
{
return 'discount';
}
public function compute($criteria, &$total)
{
$items = [];
// set the discount to the total selling price
$discount = $total['sell_price'];
$qty = 1;
$price = bcmul(-1, $discount, 2);
$items[] = [
'title' => $this->getTitle(),
'qty' => $qty,
'price' => $price,
];
$total['discount'] = $discount;
$total['total_price'] = bcsub($total['total_price'], $discount, 2);
return $items;
}
public function validatePromo($criteria, $promo_id)
{
// only applies to battery sales
if ($criteria->getServiceType() != ServiceType::BATTERY_REPLACEMENT_NEW)
return null;
// only applies if this is a subscription order
if ($criteria->isSubscription() === false)
return null;
return false;
}
public function validateInvoiceItems($criteria, $invoice_items)
{
return null;
}
protected function getTitle()
{
$title = 'Waived for subscription';
return $title;
}
}

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

@ -25,6 +25,7 @@ class HubCriteria
protected $order_date; // date JO was created protected $order_date; // date JO was created
protected $service_type; // service type of JO protected $service_type; // service type of JO
protected $jo_origin; // origin of JO protected $jo_origin; // origin of JO
protected $flag_subscription; // flag if subscription or not
public function __construct() public function __construct()
{ {
@ -45,6 +46,7 @@ class HubCriteria
$this->order_date = new DateTime(); $this->order_date = new DateTime();
$this->service_type = null; $this->service_type = null;
$this->jo_origin = null; $this->jo_origin = null;
$this->flag_subscription = false;
} }
public function setPoint(Point $point) public function setPoint(Point $point)
@ -235,5 +237,16 @@ class HubCriteria
{ {
return $this->jo_origin; return $this->jo_origin;
} }
public function setSubscription($flag_subscription = true)
{
$this->flag_subscription = $flag_subscription;
return $this;
}
public function isSubscription()
{
return $this->flag_subscription;
}
} }

View file

@ -18,6 +18,7 @@ class InvoiceCriteria
protected $flag_taxable; protected $flag_taxable;
protected $source; // use Ramcar's TransactionOrigin protected $source; // use Ramcar's TransactionOrigin
protected $price_tier; protected $price_tier;
protected $is_subscription;
// entries are battery and trade-in combos // entries are battery and trade-in combos
protected $entries; protected $entries;
@ -34,6 +35,7 @@ class InvoiceCriteria
$this->flag_taxable = false; $this->flag_taxable = false;
$this->source = ''; $this->source = '';
$this->price_tier = 0; // set to default $this->price_tier = 0; // set to default
$this->is_subscription = false;
} }
public function setServiceType($stype) public function setServiceType($stype)
@ -202,4 +204,15 @@ class InvoiceCriteria
{ {
return $this->price_tier; return $this->price_tier;
} }
public function setSubscription($is_subscription)
{
$this->is_subscription = $is_subscription;
return $this;
}
public function isSubscription()
{
return $this->is_subscription;
}
} }

View file

@ -10,6 +10,7 @@ class ModeOfPayment extends NameValue
const INSTALLMENT = 'installment'; const INSTALLMENT = 'installment';
const GCASH = 'gcash'; const GCASH = 'gcash';
const CREDIT_CARD_AMEX = 'credit_card_amex'; const CREDIT_CARD_AMEX = 'credit_card_amex';
const SUBSCRIPTION = 'subscription';
const COLLECTION = [ const COLLECTION = [
'cash' => 'Cash', 'cash' => 'Cash',
@ -18,5 +19,6 @@ class ModeOfPayment extends NameValue
'installment' => 'Installment - BDO', 'installment' => 'Installment - BDO',
'gcash' => 'GCash', 'gcash' => 'GCash',
'credit_card_amex' => 'Credit Card - AMEX', 'credit_card_amex' => 'Credit Card - AMEX',
'subscription' => 'Subscription',
]; ];
} }

View file

@ -0,0 +1,20 @@
<?php
namespace App\Ramcar;
class SubscriptionStatus extends NameValue
{
const PENDING = 'pending';
const ACTIVE = 'active';
const ENDED = 'ended';
const CANCELLED = 'cancelled';
const REPOSSESSED = 'reposessed';
const COLLECTION = [
'pending' => 'Pending',
'active' => 'Active',
'ended' => 'Ended',
'cancelled' => 'Cancelled',
'repossessed' => 'Reposessed',
];
}

View file

@ -4,6 +4,7 @@ namespace App\Service\CustomerHandler;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Validator\Validator\ValidatorInterface; use Symfony\Component\Validator\Validator\ValidatorInterface;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
@ -24,7 +25,7 @@ use App\Entity\Battery;
use App\Entity\VehicleManufacturer; use App\Entity\VehicleManufacturer;
use App\Entity\BatteryManufacturer; use App\Entity\BatteryManufacturer;
use App\Entity\CustomerTag; use App\Entity\CustomerTag;
use App\Service\PayMongoConnector;
use DateTime; use DateTime;
class ResqCustomerHandler implements CustomerHandlerInterface class ResqCustomerHandler implements CustomerHandlerInterface
@ -34,14 +35,22 @@ class ResqCustomerHandler implements CustomerHandlerInterface
protected $country_code; protected $country_code;
protected $security; protected $security;
protected $template_hash; protected $template_hash;
protected $pm;
public function __construct(EntityManagerInterface $em, ValidatorInterface $validator, public function __construct(EntityManagerInterface $em, ValidatorInterface $validator,
string $country_code, Security $security) string $country_code, Security $security, PayMongoConnector $pm, ParameterBagInterface $params)
{ {
$this->em = $em; $this->em = $em;
$this->validator = $validator; $this->validator = $validator;
$this->country_code = $country_code; $this->country_code = $country_code;
$this->security = $security; $this->security = $security;
$this->pm = $pm;
// initialize paymongo connector
$this->pm->initialize(
$params->get('subscription_paymongo_public_key'),
$params->get('subscription_paymongo_secret_key'),
);
$this->loadTemplates(); $this->loadTemplates();
} }
@ -391,6 +400,9 @@ class ResqCustomerHandler implements CustomerHandlerInterface
$em->persist($cust); $em->persist($cust);
$em->flush(); $em->flush();
// update customer paymongo record if exists
$this->pm->updateCustomerIfExists($cust);
$result = [ $result = [
'id' => $cust->getID(), 'id' => $cust->getID(),
]; ];

View file

@ -3,53 +3,179 @@
namespace App\Service; namespace App\Service;
use App\Entity\Customer; use App\Entity\Customer;
use App\Ramcar\FirebaseNotificationType;
use Symfony\Contracts\Translation\TranslatorInterface; use Symfony\Contracts\Translation\TranslatorInterface;
use Fcm\FcmClient; use GuzzleHttp\Client;
use Fcm\Push\Notification; use GuzzleHttp\Exception\RequestException;
use Firebase\JWT\JWT;
use App\Entity\JobOrder; use App\Entity\JobOrder;
use App\Entity\Subscription;
use Exception;
use RuntimeException;
use DateTime;
use DateInterval;
class FCMSender class FCMSender
{ {
protected $client; protected $client;
protected $translator; protected $translator;
protected $project_id;
protected $base_uri;
protected $credentials;
public function __construct(TranslatorInterface $translator, $server_key, $sender_id) public function __construct(TranslatorInterface $translator, string $creds_file, string $project_id, string $base_uri)
{ {
$this->client = new FcmClient($server_key, $sender_id);
$this->translator = $translator; $this->translator = $translator;
$this->project_id = $project_id;
$this->base_uri = $base_uri;
// check credentials file
if (!file_exists($creds_file)) {
throw new RuntimeException("Service account JSON file not found: $creds_file");
} }
public function send($recipients, $title, $body, $data = [], $color = null, $sound = null, $badge = null) // set credentials from file
$this->credentials = json_decode(file_get_contents($creds_file), true);
// instantiate client
$this->client = new Client([
'base_uri' => $base_uri . 'v1/',
'timeout' => 5.0,
]);
}
private function generateAccessToken()
{ {
$notification = new Notification(); // build the access token parts
$notification->setTitle($title) $private_key = $this->credentials['private_key'];
->setBody($body); $now = time();
foreach ($recipients as $recipient) { $jwt_payload = [
$notification->addRecipient($recipient); 'iss' => $this->credentials['client_email'],
'sub' => $this->credentials['client_email'],
'aud' => $this->base_uri,
'iat' => $now,
'exp' => $now + 3600,
];
// encode into JWT
return JWT::encode($jwt_payload, $private_key, 'RS256');
} }
if (!empty($color)) { public function send($recipients, $title, $body, $data = [], $color = null, $sound = null, $image = null)
$notification->setColor($color); {
} // set URL for sending
$url = "projects/{$this->project_id}/messages:send";
if (!empty($sound)) { $access_token = $this->generateAccessToken();
$notification->setSound($sound);
} // build payload structure
$payload = [
if (!empty($color)) { 'message' => [
$notification->setColor($color); 'notification' => [
} 'title' => $title,
'body' => $body,
if (!empty($badge)) { ],
$notification->setBadge($badge); 'android' => [
'priority' => 'high',
'notification' => [
'sound' => $sound ?? 'default',
],
],
'apns' => [
'headers' => [
'apns-priority' => '10',
],
'payload' => [
'aps' => [
'alert' => [
'title' => $title,
'body' => $body,
],
'sound' => $sound ?? 'default',
],
],
],
],
];
// if image is provided, apply params
if (!empty($image)) {
$payload['message']['notification']['image'] = $image;
$payload['message']['android']['notification']['image'] = $image;
$payload['message']['apns']['payload']['aps']['mutable-content'] = 1;
} }
// if data is provided, attach it
if (!empty($data)) { if (!empty($data)) {
$notification->addDataArray($data); $payload['message']['data'] = $data;
} }
return $this->client->send($notification); // build headers for request
$headers = [
'Authorization' => 'Bearer ' . $access_token,
'Content-Type' => 'application/json',
];
// send the message to each recipient
foreach ($recipients as $recipient) {
$payload['message']['token'] = $recipient;
try {
$response = $this->client->post($url, [
'headers' => $headers,
'json' => $payload,
]);
$result = $response->getBody();
$json = json_decode($result, true);
// log response
$this->log(
$title,
$body,
'Success',
'success',
json_encode($payload),
$result,
);
// message sent!
return [
'success' => true,
'response' => $json,
];
} catch (RequestException $e) {
$error_msg = $e->hasResponse() ? $e->getResponse()->getBody()->getContents() : $e->getMessage();
// something went wrong
$this->log(
$title,
$body,
$error_msg,
'error',
json_encode($payload),
);
return [
'success' => false,
'error' => "Request error: " . $error_msg,
];
} catch (Exception $e) {
// something went wrong
$this->log(
$title,
$body,
$e->getMessage(),
'error',
json_encode($payload),
);
return [
'success' => false,
'error' => "Unexpected error: " . $e->getMessage(),
];
}
}
} }
public function sendJoEvent(JobOrder $job_order, $title, $body, $data = []) public function sendJoEvent(JobOrder $job_order, $title, $body, $data = [])
@ -60,12 +186,34 @@ class FCMSender
// attach jo info // attach jo info
$data['jo_id'] = $job_order->getID(); $data['jo_id'] = $job_order->getID();
$data['jo_status'] = $job_order->getStatus(); $data['jo_status'] = $job_order->getStatus();
$data['notification_type'] = FirebaseNotificationType::JOB_ORDER;
// send the event // send the event
return $this->sendEvent($cust, $title, $body, $data); return $this->sendEvent($cust, $title, $body, $data);
} }
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 sendSubscriptionEvent(Subscription $sub, $title, $body, $data = [], $title_params = [], $body_params = [])
{
// get customer object
$cust = $sub->getCustomer();
// attach type info
$data['notification_type'] = FirebaseNotificationType::SUBSCRIPTION;
// 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 // get all v2 devices
$devices = $this->getDevices($cust); $devices = $this->getDevices($cust);
@ -75,7 +223,12 @@ class FCMSender
} }
// send fcm notification // send fcm notification
$result = $this->send(array_keys($devices), $this->translator->trans($title), $this->translator->trans($body), $data); $result = $this->send(
array_keys($devices),
$this->translator->trans($title, $title_params),
$this->translator->trans($body, $body_params),
$data,
);
return $result; return $result;
} }
@ -95,11 +248,23 @@ class FCMSender
return false; return false;
} }
// get device timestamp cutoff
$oldest_timestamp = (new DateTime())->sub(new DateInterval('P1M'));
// send to every customer session // send to every customer session
foreach ($sessions as $sess) { foreach ($sessions as $sess) {
$device_id = $sess->getDevicePushID(); $device_id = $sess->getDevicePushID();
if (!empty($device_id) && !isset($device_ids[$device_id])) { // ignore duplicates and empty device IDs
if (empty($device_id) || isset($device_ids[$device_id])) {
continue;
}
// get latest device timestamp
$latest_timestamp = $sess->getDateLatestActivity();
// make sure we only send to devices that have been active on or after the cutoff
if ($latest_timestamp >= $oldest_timestamp) {
// send to this device // send to this device
$device_ids[$device_id] = true; $device_ids[$device_id] = true;
} }
@ -112,4 +277,24 @@ class FCMSender
return $device_ids; return $device_ids;
} }
// TODO: make this more elegant
public function log($title, $body, $message, $type, $data = "[]", $result = "[]")
{
$filename = '/../../var/log/fcm_' . $type . '.log';
$date = date("Y-m-d H:i:s");
// build log entry
$entry = implode("\r\n", array_filter([
$date,
$title,
$body,
$message,
"DATA:\r\n" . $data,
"RESULT:\r\n" . $result,
"\r\n----------------------------------------\r\n\r\n",
]));
file_put_contents(__DIR__ . $filename, $entry, FILE_APPEND);
}
} }

View file

@ -12,6 +12,7 @@ class JoTypeHubFilter extends BaseHubFilter implements HubFilterInterface
public function getRequestedParams() : array public function getRequestedParams() : array
{ {
return [ return [
'flag_subscription',
'flag_emergency', 'flag_emergency',
'jo_type', 'jo_type',
]; ];
@ -22,6 +23,9 @@ class JoTypeHubFilter extends BaseHubFilter implements HubFilterInterface
if ($params['flag_emergency']) if ($params['flag_emergency'])
return $hubs; return $hubs;
if ($params['flag_subscription'])
return $hubs;
if (empty($params['jo_type'])) if (empty($params['jo_type']))
return $hubs; return $hubs;

View file

@ -12,6 +12,7 @@ class PaymentMethodHubFilter extends BaseHubFilter implements HubFilterInterface
public function getRequestedParams() : array public function getRequestedParams() : array
{ {
return [ return [
'flag_subscription',
'flag_emergency', 'flag_emergency',
'payment_method', 'payment_method',
]; ];
@ -22,6 +23,9 @@ class PaymentMethodHubFilter extends BaseHubFilter implements HubFilterInterface
if ($params['flag_emergency']) if ($params['flag_emergency'])
return $hubs; return $hubs;
if ($params['flag_subscription'])
return $hubs;
if (empty($params['payment_method'])) if (empty($params['payment_method']))
return $hubs; return $hubs;

View file

@ -68,6 +68,7 @@ class HubSelector
$jo_origin = $criteria->getJoOrigin(); $jo_origin = $criteria->getJoOrigin();
$customer_id = $criteria->getCustomerId(); $customer_id = $criteria->getCustomerId();
$customer_class = $criteria->getCustomerClass(); $customer_class = $criteria->getCustomerClass();
$flag_subscription = $criteria->isSubscription();
// needed for JORejection records and SMS notifs // needed for JORejection records and SMS notifs
$order_date = $criteria->getOrderDate(); $order_date = $criteria->getOrderDate();
@ -95,6 +96,7 @@ class HubSelector
'payment_method' => $payment_method, 'payment_method' => $payment_method,
'flag_riders_check' => $flag_riders_check, 'flag_riders_check' => $flag_riders_check,
'flag_round_robin' => $flag_round_robin, 'flag_round_robin' => $flag_round_robin,
'flag_subscription' => $flag_subscription,
]; ];
// loop through all enabled filters // loop through all enabled filters

View file

@ -55,6 +55,7 @@ class InvoiceManager implements InvoiceGeneratorInterface
new InvoiceRule\Fuel($this->em, $this->pt_manager), new InvoiceRule\Fuel($this->em, $this->pt_manager),
new InvoiceRule\TireRepair($this->em, $this->pt_manager), new InvoiceRule\TireRepair($this->em, $this->pt_manager),
new InvoiceRule\DiscountType($this->em), new InvoiceRule\DiscountType($this->em),
new InvoiceRule\IsSubscription($this->em),
new InvoiceRule\TradeIn($this->em), new InvoiceRule\TradeIn($this->em),
new InvoiceRule\Tax($this->em, $this->pt_manager), new InvoiceRule\Tax($this->em, $this->pt_manager),
]; ];

View file

@ -2576,6 +2576,9 @@ class ResqJobOrderHandler implements JobOrderHandlerInterface
$long = $obj->getCoordinates()->getLongitude(); $long = $obj->getCoordinates()->getLongitude();
$lat = $obj->getCoordinates()->getLatitude(); $lat = $obj->getCoordinates()->getLatitude();
// set subscription flag
$hub_criteria->setSubscription($obj->getSubscription() !== null);
// set result limit and location and date_time // set result limit and location and date_time
$hub_criteria->setPoint($obj->getCoordinates()) $hub_criteria->setPoint($obj->getCoordinates())
->setDateTime($obj->getDateSchedule()) ->setDateTime($obj->getDateSchedule())
@ -2953,6 +2956,9 @@ class ResqJobOrderHandler implements JobOrderHandlerInterface
$long = $obj->getCoordinates()->getLongitude(); $long = $obj->getCoordinates()->getLongitude();
$lat = $obj->getCoordinates()->getLatitude(); $lat = $obj->getCoordinates()->getLatitude();
// set subscription flag
$hub_criteria->setSubscription($obj->getSubscription() !== null);
$hub_criteria->setPoint($obj->getCoordinates()) $hub_criteria->setPoint($obj->getCoordinates())
->setDateTime($obj->getDateSchedule()) ->setDateTime($obj->getDateSchedule())
->setLimitResults(50); ->setLimitResults(50);

View file

@ -0,0 +1,134 @@
<?php
namespace App\Service;
use GuzzleHttp\Client;
use GuzzleHttp\Psr7;
use GuzzleHttp\Exception\RequestException;
use App\Entity\Customer;
use \DateTime;
use \DateTimeZone;
class LoyaltyConnector
{
protected $base_url;
protected $api_key;
protected $secret_key;
public function __construct($base_url, $api_key, $secret_key)
{
$this->base_url = $base_url;
$this->api_key = $api_key;
$this->secret_key = $secret_key;
}
public function register(Customer $cust)
{
return $this->doRequest('/api/customer/register', 'POST', [
'external_id' => $cust->getPhoneMobile(),
]);
}
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 = [
$method,
$path,
$date_string,
$this->secret_key,
];
// generate raw signature
$sig_src = implode("|", $elements);
$raw_sig = hash_hmac('sha1', $sig_src, $this->secret_key, true);
// return encoded signature
return base64_encode($raw_sig);
}
protected function doRequest($url, $method, $request_body = [])
{
// format current date and time
$now = new DateTime('now', new DateTimeZone('UTC'));
$date_string = $now->format('D, d M Y H:i:s T');
// prepare request
$client = new Client();
$headers = [
'X-Cata-API-Key' => $this->api_key,
'X-Cata-Signature' => $this->generateSignature($url, $method, $date_string),
'X-Cata-Date' => $date_string,
];
try {
$response = $client->request($method, $this->base_url . $url, [
'form_params' => $request_body,
'headers' => $headers,
]);
} catch (RequestException $e) {
$error = ['message' => $e->getMessage()];
ob_start();
//var_dump($request_body);
$varres = ob_get_clean();
error_log($varres);
error_log("--------------------------------------");
error_log($e->getResponse()->getBody()->getContents());
error_log("Loyalty API Error: " . $error['message']);
error_log(Psr7\Message::toString($e->getRequest()));
// log this error
$this->log($url, Psr7\Message::toString($e->getRequest()), Psr7\Message::toString($e->getResponse()), 'error');
if ($e->hasResponse()) {
$error['response'] = Psr7\Message::toString($e->getResponse());
}
return [
'success' => false,
'error' => $error,
];
}
$result_body = $response->getBody();
// log response
$this->log($url, json_encode($request_body), $result_body);
return [
'success' => true,
'response' => json_decode($response->getBody(), true)
];
}
// TODO: make this more elegant
public function log($title, $request_body = "[]", $result_body = "[]", $type = 'api')
{
$filename = '/../../var/log/loyalty_' . $type . '.log';
$date = date("Y-m-d H:i:s");
// build log entry
$entry = implode("\r\n", [
$date,
$title,
"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

@ -2,25 +2,39 @@
namespace App\Service; namespace App\Service;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use GuzzleHttp\Client; use GuzzleHttp\Client;
use GuzzleHttp\Psr7; use GuzzleHttp\Psr7;
use GuzzleHttp\Exception\RequestException; use GuzzleHttp\Exception\RequestException;
use App\Entity\Customer; use App\Entity\Customer;
use App\Entity\BatterySize;
use Doctrine\ORM\Query\Parameter;
class PayMongoConnector class PayMongoConnector
{ {
protected $base_url; protected $base_url;
protected $public_key; protected $public_key;
protected $secret_key; protected $secret_key;
protected $hash;
public function __construct($base_url, $public_key, $secret_key) protected $public_hash;
protected $secret_hash;
protected $sub_months;
public function __construct($base_url, ParameterBagInterface $params)
{ {
$this->base_url = $base_url; $this->base_url = $base_url;
$this->sub_months = $params->get('subscription_months');
}
public function initialize($public_key, $secret_key)
{
$this->public_key = $public_key; $this->public_key = $public_key;
$this->secret_key = $secret_key; $this->secret_key = $secret_key;
$this->hash = $this->generateHash(); $this->public_hash = $this->generateHash($this->public_key);
$this->secret_hash = $this->generateHash($this->secret_key);
} }
public function createCheckout(Customer $cust, $items, $ref_no = null, $description = null, $success_url = null, $cancel_url = null, $metadata = []) public function createCheckout(Customer $cust, $items, $ref_no = null, $description = null, $success_url = null, $cancel_url = null, $metadata = [])
@ -84,25 +98,268 @@ class PayMongoConnector
return $this->doRequest('/v1/webhooks/' . $id . '/enable', 'POST'); return $this->doRequest('/v1/webhooks/' . $id . '/enable', 'POST');
} }
protected function generateHash() public function getPlans()
{ {
return base64_encode($this->secret_key); return $this->doRequest('/v1/subscriptions/plans?limit=100', 'GET');
} }
protected function doRequest($url, $method, $request_body = []) public function getPlanByBatterySize(BatterySize $bsize)
{ {
$client = new Client(); // get all plans
$headers = [ $plans = $this->getPlans();
'Content-Type' => 'application/json',
'accept' => 'application/json', // find the plan with the matching metadata for plan ID
'authorization' => 'Basic '. $this->hash, $found_plan = null;
if ($plans['success'] && !empty($plans['response']['data'])) {
foreach ($plans['response']['data'] as $plan) {
$plan_bsize_id = $plan['attributes']['metadata']['battery_size_id'] ?? null;
if ($plan_bsize_id == $bsize->getID()) {
$found_plan = $plan;
break;
}
}
}
return $found_plan;
}
public function getPlan($plan_id)
{
return $this->doRequest('/v1/subscriptions/plans/'. $plan_id, 'GET');
}
public function createPlan($plan_data)
{
$body = [
'data' => [
'attributes' => [
'amount' => $plan_data['amount'],
'currency' => 'PHP',
'description' => $plan_data['description'],
'interval' => $plan_data['interval'],
'interval_count' => $plan_data['interval_count'],
//'cycle_count' => $plan_data['cycle_count'],
'name' => $plan_data['name'],
'metadata' => $plan_data['metadata'],
],
],
]; ];
try { return $this->doRequest('/v1/subscriptions/plans', 'POST', $body);
$response = $client->request($method, $this->base_url . '/' . $url, [ }
'json' => $request_body,
'headers' => $headers, public function updatePlan($plan_id, $plan_data)
{
$body = [
'data' => [
'attributes' => [
'amount' => $plan_data['amount'],
],
],
];
return $this->doRequest('/v1/subscriptions/plans/' . $plan_id, 'PUT', $body);
}
public function createOrUpdateSubPlan(BatterySize $bsize)
{
$found_plan = $this->getPlanByBatterySize($bsize);
if (!empty($found_plan)) {
// update existing plan
$result = $this->updatePlan($found_plan['id'], [
'amount' => (int)bcmul($bsize->getSubRecurringFee(), 100),
]); ]);
} else {
// create new plan
$result = $this->createPlan([
'name' => "RESQ Subscription",
'amount' => (int)bcmul($bsize->getSubRecurringFee() ?? 0, 100),
'description' => "Motolite Battery Subscription Plan",
'interval' => 'monthly',
'interval_count' => 1,
//'cycle_count' => $this->sub_months,
'metadata' => [
'battery_size_id' => (string)$bsize->getID(),
],
]);
}
return $result;
}
public function findCustomerByEmail($email)
{
return $this->doRequest('/v1/customers?email=' . $email, 'GET');
}
public function createCustomer(Customer $cust, $email_override = "")
{
$body = [
'data' => [
'attributes' => [
'first_name' => $cust->getFirstName(),
'last_name' => $cust->getLastName(),
'phone' => "+63" . $cust->getPhoneMobile(),
'email' => $email_override ?? $cust->getEmail(),
'default_device' => 'email',
],
],
];
return $this->doRequest('/v1/customers', 'POST', $body);
}
public function updateCustomer(Customer $cust, $ext_cust_id)
{
$body = [
'data' => [
'attributes' => [
'first_name' => $cust->getFirstName(),
'last_name' => $cust->getLastName(),
'phone' => $cust->getPhoneMobile(),
'email' => $cust->getEmail(),
],
],
];
return $this->doRequest('/v1/customers/' . $ext_cust_id, 'PUT', $body);
}
public function updateCustomerIfExists(Customer $cust)
{
$email = $cust->getEmail();
// if no email, then we don't need to update
if (empty($email)) {
return false;
}
// check if we have an existing paymongo customer with this email
$found_cust = $this->findCustomerByEmail($email);
if (isset($found_cust['data']['id'])) {
// update existing customer record
return $this->updateCustomer($cust, $found_cust['data']);
}
return false;
}
public function findOrCreateCustomer($email, Customer $cust)
{
error_log("FINDING RECORD FOR $email");
// check if we have an existing paymongo customer with this email
$found_cust = $this->findCustomerByEmail($email);
$pm_cust = null;
error_log("FOUND CUSTOMER?");
error_log(print_r($found_cust, true));
if (isset($found_cust['response']['data'][0]['id'])) {
// we found a customer record
$pm_cust = $found_cust['response']['data'][0];
} else {
error_log("CREATING CUSTOMER");
// we create a new customer record
$new_cust = $this->createCustomer($cust, $email);
error_log("NEW CUST RESPONSE");
error_log(print_r($new_cust, true));
if (isset($new_cust['response']['data']['id'])) {
// customer record was created successfully
$pm_cust = $new_cust['response']['data'];
}
}
// NOTE: if $pm_cust is null at this point, an error occurred during customer creation and we check the request logs for more details
return $pm_cust;
}
public function createSubscription($ext_cust_id, $plan_id)
{
$body = [
'data' => [
'attributes' => [
'customer_id' => $ext_cust_id,
'plan_id' => $plan_id,
],
],
];
$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)
{
return $this->doRequest('/v1/subscriptions/'. $sub_id, 'GET');
}
public function getPaymentIntent($pi_id)
{
return $this->doRequest('/v1/payment_intents/' . $pi_id, 'GET');
}
public function attachPaymentIntent($pm_id, $pi_id)
{
$body = [
'data' => [
'attributes' => [
'payment_method' => $pm_id,
],
],
];
return $this->doRequest('/v1/payment_intents/' . $pi_id . '/attach', 'POST', $body);
}
protected function generateHash($key)
{
return base64_encode($key);
}
protected function buildHeaders($use_public_key = false)
{
$hash = $use_public_key ? $this->public_hash : $this->secret_hash;
return [
'Content-Type' => 'application/json',
'accept' => 'application/json',
'authorization' => 'Basic '. $hash,
];
}
protected function doRequest($url, $method, $request_body = [], $use_public_key = false)
{
$client = new Client();
$headers = $this->buildHeaders($use_public_key);
$request_params = ['headers' => $headers];
if (!empty($request_body)) {
$request_params['json'] = $request_body;
}
try {
$response = $client->request($method, $this->base_url . $url, $request_params);
} catch (RequestException $e) { } catch (RequestException $e) {
$error = ['message' => $e->getMessage()]; $error = ['message' => $e->getMessage()];
@ -118,7 +375,7 @@ class PayMongoConnector
error_log(Psr7\Message::toString($e->getRequest())); error_log(Psr7\Message::toString($e->getRequest()));
// log this error // 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()) { if ($e->hasResponse()) {
$error['response'] = Psr7\Message::toString($e->getResponse()); $error['response'] = Psr7\Message::toString($e->getResponse());
@ -133,7 +390,7 @@ class PayMongoConnector
$result_body = $response->getBody(); $result_body = $response->getBody();
// log response // log response
$this->log($url, json_encode($request_body), $result_body); $this->log($method . " " . $url, json_encode($request_body), $result_body);
return [ return [
'success' => true, 'success' => true,
@ -142,19 +399,20 @@ class PayMongoConnector
} }
// TODO: make this more elegant // 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'; $filename = '/../../var/log/paymongo_' . $type . '.log';
$date = date("Y-m-d H:i:s"); $date = date("Y-m-d H:i:s");
// build log entry // build log entry
$entry = implode("\r\n", [ $entry = implode("\r\n", array_filter([
$date, $date,
$title, $title,
(!empty($custom_message) ? "MESSAGE: " . $custom_message : ""),
"REQUEST:\r\n" . $request_body, "REQUEST:\r\n" . $request_body,
"RESPONSE:\r\n" . $result_body, "RESPONSE:\r\n" . $result_body,
"\r\n----------------------------------------\r\n\r\n", "\r\n----------------------------------------\r\n\r\n",
]); ]));
@file_put_contents(__DIR__ . $filename, $entry, FILE_APPEND); @file_put_contents(__DIR__ . $filename, $entry, FILE_APPEND);
} }

View file

@ -89,6 +89,9 @@
"edwinhoksberg/php-fcm": { "edwinhoksberg/php-fcm": {
"version": "1.0.0" "version": "1.0.0"
}, },
"firebase/php-jwt": {
"version": "v6.10.1"
},
"guzzlehttp/guzzle": { "guzzlehttp/guzzle": {
"version": "6.3.0" "version": "6.3.0"
}, },

View file

@ -80,6 +80,15 @@
<div class="form-control-feedback hide" data-field="tip_lazada"></div> <div class="form-control-feedback hide" data-field="tip_lazada"></div>
</div> </div>
</div> </div>
<div class="form-group m-form__group row no-border">
<label class="col-lg-3 col-form-label" data-field="sub_recurring_fee">
{% trans %}battery_Size_sub_recurring_fee{% endtrans %}
</label>
<div class="col-lg-9">
<input type="text" name="sub_recurring_fee" class="form-control m-input" value="{{ obj.getSubRecurringFee }}">
<div class="form-control-feedback hide" data-field="sub_recurring_fee"></div>
</div>
</div>
</div> </div>
<div class="m-portlet__foot m-portlet__foot--fit"> <div class="m-portlet__foot m-portlet__foot--fit">
<div class="m-form__actions m-form__actions--solid m-form__actions--right"> <div class="m-form__actions m-form__actions--solid m-form__actions--right">

View file

@ -115,6 +115,20 @@
<div class="col-lg-8"> <div class="col-lg-8">
</div> </div>
</div> </div>
<div class="form-group m-form__group row">
<div class="col-lg-4">
<span class="m-switch m-switch--icon block-switch">
<label>
<input type="checkbox" name="flag_subscription" id="flag_subscription" value="1"{{ obj.isSubscription() ? ' checked' }}>
<label class="switch-label">Used for subscriptions</label>
<span></span>
</label>
</span>
<div class="form-control-feedback hide" data-field="flag_subscription"></div>
</div>
<div class="col-lg-8">
</div>
</div>
<div class="form-group m-form__group row"> <div class="form-group m-form__group row">
<div class="col-lg-6"> <div class="col-lg-6">
<label data-field="image_file"> <label data-field="image_file">

View file

@ -126,6 +126,21 @@
return tag; return tag;
} }
}, },
{
field: 'flag_subscription',
title: 'Subscription',
template: function (row, index, datatable) {
var tag = '';
if (row.flag_subscription === true) {
tag = '<span class="m-badge m-badge--success m-badge--wide">Yes</span>';
} else {
tag = '<span class="m-badge m-badge--danger m-badge--wide">No</span>';
}
return tag;
}
},
/* /*
{ {
field: 'prod_code', field: 'prod_code',

View file

@ -191,7 +191,7 @@
<div class="form-control-feedback hide" data-field="flag_research_sms"></div> <div class="form-control-feedback hide" data-field="flag_research_sms"></div>
</label> </label>
<label class="m-checkbox"> <label class="m-checkbox">
<input type="checkbox" name="flag_research_email" id="flag-research-email" "value="1"{{ obj.getCustomer ? obj.getCustomer.isResearchEmail ? ' checked' }} > <input type="checkbox" name="flag_research_email" id="flag-research-email" value="1"{{ obj.getCustomer ? obj.getCustomer.isResearchEmail ? ' checked' }} >
Email Email
<span></span> <span></span>
<div class="form-control-feedback hide" data-field="flag_research_email"></div> <div class="form-control-feedback hide" data-field="flag_research_email"></div>
@ -233,34 +233,48 @@
Vehicle Details Vehicle Details
</h3> </h3>
</div> </div>
<div class="form-group m-form__group row"> <div class="form-group m-form__group row {{ obj.getSubscription is not null ? 'form-group-subscription' }}">
<div class="col-lg-3"> <div class="col-lg-9">
<div class="row form-group">
<div class="col-lg-4">
<label data-field="vmfg">Manufacturer</label> <label data-field="vmfg">Manufacturer</label>
<input type="text" name="vmfg" id="vmfg" class="form-control m-input" value="{{ obj.getCustomerVehicle ? obj.getCustomerVehicle.getVehicle.getManufacturer.getName }}" data-vehicle-field="1" disabled> <input type="text" name="vmfg" id="vmfg" class="form-control m-input" value="{{ obj.getCustomerVehicle ? obj.getCustomerVehicle.getVehicle.getManufacturer.getName }}" data-vehicle-field="1" disabled>
<div class="form-control-feedback hide" data-field="vmfg"></div> <div class="form-control-feedback hide" data-field="vmfg"></div>
</div> </div>
<div class="col-lg-3"> <div class="col-lg-4">
<label data-field="vehicle_make">Make</label> <label data-field="vehicle_make">Make</label>
<input type="text" name="vehicle_make" id="vehicle-make" class="form-control m-input" value="{{ obj.getCustomerVehicle ? obj.getCustomerVehicle.getVehicle.getMake }}" data-vehicle-field="1" disabled> <input type="text" name="vehicle_make" id="vehicle-make" class="form-control m-input" value="{{ obj.getCustomerVehicle ? obj.getCustomerVehicle.getVehicle.getMake }}" data-vehicle-field="1" disabled>
<div class="form-control-feedback hide" data-field="vehicle_make"></div> <div class="form-control-feedback hide" data-field="vehicle_make"></div>
</div> </div>
<div class="col-lg-3"> <div class="col-lg-4">
<label data-field="vehicle_year">Model Year</label> <label data-field="vehicle_year">Model Year</label>
<input type="text" name="vehicle_year" id="vehicle-year" class="form-control m-input" value="{{ obj.getCustomerVehicle ? obj.getCustomerVehicle.getModelYear }}" data-vehicle-field="1" disabled> <input type="text" name="vehicle_year" id="vehicle-year" class="form-control m-input" value="{{ obj.getCustomerVehicle ? obj.getCustomerVehicle.getModelYear }}" data-vehicle-field="1" disabled>
<div class="form-control-feedback hide" data-field="vehicle_year"></div> <div class="form-control-feedback hide" data-field="vehicle_year"></div>
</div> </div>
</div> </div>
<div class="form-group m-form__group row"> <div class="row form-group">
<div class="col-lg-3"> <div class="col-lg-4">
<label data-field="vehicle_plate">Plate #</label> <label data-field="vehicle_plate">Plate #</label>
<input type="text" name="vehicle_plate" id="vehicle-plate" class="form-control m-input" value="{{ obj.getCustomerVehicle.getPlateNumber|default('') }}" data-vehicle-field="1" disabled> <input type="text" name="vehicle_plate" id="vehicle-plate" class="form-control m-input" value="{{ obj.getCustomerVehicle.getPlateNumber|default('') }}" data-vehicle-field="1" disabled>
<div class="form-control-feedback hide" data-field="vehicle_color"></div> <div class="form-control-feedback hide" data-field="vehicle_color"></div>
</div> </div>
<div class="col-lg-3"> <div class="col-lg-4">
<label data-field="vehicle_color">Color</label> <label data-field="vehicle_color">Color</label>
<input type="text" name="vehicle_color" id="vehicle-color" class="form-control m-input" value="{{ obj.getCustomerVehicle ? obj.getCustomerVehicle.getColor }}" data-vehicle-field="1" disabled> <input type="text" name="vehicle_color" id="vehicle-color" class="form-control m-input" value="{{ obj.getCustomerVehicle ? obj.getCustomerVehicle.getColor }}" data-vehicle-field="1" disabled>
<div class="form-control-feedback hide" data-field="vehicle_color"></div> <div class="form-control-feedback hide" data-field="vehicle_color"></div>
</div> </div>
<div class="col-lg-4">
<label data-field="subscription">Subscription</label>
<input type="text" name="subscription" id="subscription" class="form-control m-input" value="{{ obj.getSubscription ? "Yes" : "No" }}" data-vehicle-field="1" disabled>
<div class="form-control-feedback hide" data-field="vehicle_color"></div>
</div>
</div>
</div>
<div class="col-lg-3 p-5 d-flex align-items-center justify-content-center">
{% if obj.getSubscription is not null %}
<img class="img-fluid" src="{{ asset('assets/images/logo-subscription.png') }}" alt="">
{% endif %}
</div>
</div> </div>
</div> </div>
<div class="m-form__section"> <div class="m-form__section">

View file

@ -39,7 +39,7 @@
<label data-field="id"> <label data-field="id">
ID: ID:
</label> </label>
<input type="text" name="id" class="form-control m-input" value="{{ obj.getID() }}"> <input type="text" name="id" class="form-control m-input" value="{{ obj.getID() }}" {{ mode == 'update' ? 'disabled' }}>
<div class="form-control-feedback hide" data-field="id"></div> <div class="form-control-feedback hide" data-field="id"></div>
</div> </div>
</div> </div>
@ -48,7 +48,7 @@
<label data-field="content"> <label data-field="content">
Content Content
</label> </label>
<textarea name="content" class="form-control m-input" data-name="content" rows="50">{{ obj.getContent() }}</textarea> <textarea id="content" name="content" class="form-control m-input" data-name="content" rows="50">{{ obj.getContent() }}</textarea>
<div class="form-control-feedback hide" data-field="content"></div> <div class="form-control-feedback hide" data-field="content"></div>
</div> </div>
</div> </div>
@ -70,8 +70,19 @@
</div> </div>
{% endblock %} {% endblock %}
{% block stylesheets %}
<link rel="stylesheet" href="https://unpkg.com/easymde/dist/easymde.min.css">
{% endblock %}
{% block scripts %} {% block scripts %}
<script src="https://unpkg.com/easymde/dist/easymde.min.js"></script>
<script> <script>
// load markdown editor
const mde = new EasyMDE({
element: document.getElementById('content'),
spellChecker: false,
});
$(function() { $(function() {
$("#row-form").submit(function(e) { $("#row-form").submit(function(e) {
var form = $(this); var form = $(this);

View file

@ -9,6 +9,7 @@ battery_size_tradein_brand: Trade-in Motolite
battery_size_tradein_premium: Trade-in Premium battery_size_tradein_premium: Trade-in Premium
battery_size_tradein_other: Trade-in Other battery_size_tradein_other: Trade-in Other
battery_size_tradein_lazada: Trade-in Lazada battery_size_tradein_lazada: Trade-in Lazada
battery_Size_sub_recurring_fee: Subscription Recurring Fee
add_cust_vehicle_battery_info: This vehicle is using a Motolite battery add_cust_vehicle_battery_info: This vehicle is using a Motolite battery
jo_title_pdf: Motolite Res-Q Job Order jo_title_pdf: Motolite Res-Q Job Order
country_code_prefix: '+63' country_code_prefix: '+63'
@ -179,3 +180,11 @@ insurance_fcm_title_updated: 'Application updated'
insurance_fcm_title_completed: 'Application completed' insurance_fcm_title_completed: 'Application completed'
insurance_fcm_body_updated: 'Some details on your insurance application have been updated.' insurance_fcm_body_updated: 'Some details on your insurance application have been updated.'
insurance_fcm_body_completed: 'Your insurance application has been processed!' insurance_fcm_body_completed: 'Your insurance application has been processed!'
# fcm loyalty
loyalty_fcm_title_add_points: 'Hooray!'
loyalty_fcm_body_add_points: 'You have earned %points% points! Check out our rewards catalog!'
# fcm subscription
sub_fcm_title_active: 'Subscription active!'
sub_fcm_body_active: 'Your Motolite PLATINUM subscription is now active. Have your new battery installed now!'