Compare commits
53 commits
master
...
799-subscr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ef626729c9 | ||
|
|
00697414b0 | ||
|
|
97ebaa05a0 | ||
|
|
0800fc1066 | ||
|
|
5f8133f02c | ||
|
|
0ea93622be | ||
|
|
7f1b35ad29 | ||
|
|
beb1a63577 | ||
|
|
e4e031f0a9 | ||
|
|
d1104b7416 | ||
|
|
f554658c7f | ||
|
|
74c45b6d18 | ||
|
|
874c35bfff | ||
|
|
8c83393b0c | ||
|
|
3ed65e7fc6 | ||
|
|
f7ba91892b | ||
|
|
33f48647b6 | ||
|
|
b8666ff5e0 | ||
|
|
857c573ae5 | ||
|
|
a724b00ce7 | ||
|
|
0a4ea563d9 | ||
|
|
d1059797a5 | ||
|
|
bd655a459a | ||
|
|
b79f2f2dfb | ||
|
|
d2a0638ffa | ||
|
|
5056637b66 | ||
|
|
1fd883b07b | ||
|
|
40c629eee3 | ||
|
|
aa85198b7a | ||
|
|
62f11c9ef5 | ||
|
|
9dbaf92698 | ||
|
|
d7cc0fc3de | ||
|
|
8c61a27376 | ||
|
|
0d9da221a7 | ||
|
|
919b56688d | ||
|
|
7af20f3d69 | ||
|
|
b3548fcc50 | ||
|
|
5a2f57492d | ||
|
|
debb399e96 | ||
|
|
e3649c3d2d | ||
|
|
4f5560f6f7 | ||
|
|
b67f960055 | ||
|
|
219d5c09d3 | ||
|
|
96a7cc929e | ||
|
|
fe4806f41a | ||
|
|
17e583e11a | ||
|
|
a911b8c6c1 | ||
|
|
ab64161afb | ||
|
|
627b3da748 | ||
|
|
48d87ae119 | ||
|
|
2ccd1e0e2d | ||
|
|
c9cb6e8b53 | ||
|
|
d9d4ffbecf |
56 changed files with 2193 additions and 317 deletions
|
|
@ -33,6 +33,7 @@
|
|||
"doctrine/doctrine-migrations-bundle": "^2",
|
||||
"doctrine/orm": "^2",
|
||||
"edwinhoksberg/php-fcm": "dev-notif-priority-hotfix",
|
||||
"firebase/php-jwt": "^6.10",
|
||||
"guzzlehttp/guzzle": "^6.3",
|
||||
"hashids/hashids": "^4.1",
|
||||
"jankstudio/catalyst-api-bundle": "dev-master",
|
||||
|
|
|
|||
65
composer.lock
generated
65
composer.lock
generated
|
|
@ -4,7 +4,7 @@
|
|||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "653f8558c75614dd65421cb3eb48c29b",
|
||||
"content-hash": "4676209ee947dbcf3cfcf937c838c6f2",
|
||||
"packages": [
|
||||
{
|
||||
"name": "composer/package-versions-deprecated",
|
||||
|
|
@ -1827,6 +1827,69 @@
|
|||
},
|
||||
"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",
|
||||
"version": "v1.0.5",
|
||||
|
|
|
|||
|
|
@ -313,3 +313,50 @@ apiv2_insurance_body_types:
|
|||
path: /apiv2/insurance/body_types
|
||||
controller: App\Controller\CustomerAppAPI\InsuranceController::getBodyTypes
|
||||
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]
|
||||
|
|
@ -17,6 +17,14 @@ parameters:
|
|||
ios_app_version: "%env(IOS_APP_VERSION)%"
|
||||
insurance_premiums_banner_url: "%env(INSURANCE_PREMIUMS_BANNER_URL)%"
|
||||
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:
|
||||
# default configuration for services in *this* file
|
||||
|
|
@ -114,7 +122,6 @@ services:
|
|||
arguments:
|
||||
$em: "@doctrine.orm.entity_manager"
|
||||
$paymongo: "@App\\Service\\PayMongoConnector"
|
||||
$webhook_id: "%env(PAYMONGO_WEBHOOK_ID)%"
|
||||
|
||||
# rider tracker service
|
||||
App\Service\RiderTracker:
|
||||
|
|
@ -229,6 +236,10 @@ services:
|
|||
arguments:
|
||||
$em: "@doctrine.orm.entity_manager"
|
||||
$ic: "@App\\Service\\InsuranceConnector"
|
||||
$fcmclient: "@App\\Service\\FCMSender"
|
||||
$lc: "@App\\Service\\LoyaltyConnector"
|
||||
$sub_months: "%subscription_months%"
|
||||
$loyalty_point_multiplier: "%loyalty_php_point_multiplier%"
|
||||
tags:
|
||||
- name: doctrine.orm.entity_listener
|
||||
event: 'postUpdate'
|
||||
|
|
@ -238,8 +249,13 @@ services:
|
|||
App\Service\PayMongoConnector:
|
||||
arguments:
|
||||
$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
|
||||
App\EntityListener\CustomerVehicleSerialListener:
|
||||
|
|
@ -316,8 +332,9 @@ services:
|
|||
# FCM sender
|
||||
App\Service\FCMSender:
|
||||
arguments:
|
||||
$server_key: "%env(FCM_SERVER_KEY)%"
|
||||
$sender_id: "%env(FCM_SENDER_ID)%"
|
||||
$project_id: "%env(FCM_PROJECT_ID)%"
|
||||
$base_uri: "%env(FCM_BASE_URI)%"
|
||||
$creds_file: "%env(FCM_CREDENTIALS_PATH)%"
|
||||
|
||||
# price tier manager
|
||||
App\Service\PriceTierManager:
|
||||
|
|
|
|||
|
|
@ -383,3 +383,19 @@ span.has-danger,
|
|||
.map-info .m-badge {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.form-group-subscription {
|
||||
> * {
|
||||
background-color: #f1edd5;
|
||||
padding-top: 15px;
|
||||
padding-bottom: 15px;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
border-color: #666;
|
||||
}
|
||||
|
||||
label {
|
||||
color: #000;
|
||||
}
|
||||
}
|
||||
BIN
public/assets/images/logo-subscription.png
Executable file
BIN
public/assets/images/logo-subscription.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 32 KiB |
|
|
@ -6,6 +6,7 @@ use Symfony\Component\Console\Command\Command;
|
|||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
|
||||
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
|
||||
|
|
@ -19,14 +20,18 @@ class ProcessLatePaymongoTransactionsCommand extends Command
|
|||
{
|
||||
protected $em;
|
||||
protected $paymongo;
|
||||
|
||||
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->webhook_id = $params->get('insurance_paymongo_webhook_id');
|
||||
|
||||
$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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1057,6 +1057,9 @@ class APIController extends Controller implements LoggedController
|
|||
$hub_criteria = new HubCriteria();
|
||||
$hub_criteria->setPoint($jo->getCoordinates());
|
||||
|
||||
// set subscription flag
|
||||
$hub_criteria->setSubscription($jo->getSubscription() !== null);
|
||||
|
||||
// get distance limit for mobile from env
|
||||
$dotenv = new Dotenv();
|
||||
$dotenv->loadEnv(__DIR__.'/../../.env');
|
||||
|
|
@ -3002,6 +3005,9 @@ class APIController extends Controller implements LoggedController
|
|||
$hub_criteria = new HubCriteria();
|
||||
$hub_criteria->setPoint($jo->getCoordinates());
|
||||
|
||||
// set subscription flag
|
||||
$hub_criteria->setSubscription($jo->getSubscription() !== null);
|
||||
|
||||
// get distance limit for mobile from env
|
||||
// get value of hub_filter_enable from env
|
||||
$dotenv = new Dotenv();
|
||||
|
|
|
|||
|
|
@ -118,6 +118,7 @@ class BatteryController extends Controller
|
|||
$row['total_height'] = $orow[0]->getTotalHeight();
|
||||
$row['image_file'] = $orow[0]->getImageFile();
|
||||
$row['flag_active'] = $orow[0]->isActive();
|
||||
$row['flag_subscription'] = $orow[0]->isSubscription();
|
||||
|
||||
// add row metadata
|
||||
$row['meta'] = [
|
||||
|
|
@ -184,7 +185,8 @@ class BatteryController extends Controller
|
|||
->setTotalHeight($req->request->get('total_height'))
|
||||
->setSellingPrice($req->request->get('sell_price'))
|
||||
->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
|
||||
$error_array = [];
|
||||
|
|
@ -311,6 +313,7 @@ class BatteryController extends Controller
|
|||
->setSellingPrice($req->request->get('sell_price'))
|
||||
->setImageFile($req->request->get('image_file'))
|
||||
->setActive($req->request->get('flag_active', false))
|
||||
->setSubscription($req->request->get('flag_subscription', false))
|
||||
->clearVehicles();
|
||||
|
||||
// initialize error list
|
||||
|
|
|
|||
|
|
@ -9,11 +9,24 @@ use Symfony\Component\HttpFoundation\Request;
|
|||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Validator\Validator\ValidatorInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
|
||||
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
|
||||
use App\Service\PayMongoConnector;
|
||||
|
||||
use Catalyst\MenuBundle\Annotation\Menu;
|
||||
|
||||
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")
|
||||
*/
|
||||
|
|
@ -130,7 +143,8 @@ class BatterySizeController extends Controller
|
|||
->setTIPriceMotolite($req->request->get('tip_motolite'))
|
||||
->setTIPricePremium($req->request->get('tip_premium'))
|
||||
->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)
|
||||
|
|
@ -167,6 +181,9 @@ class BatterySizeController extends Controller
|
|||
$em->persist($row);
|
||||
$em->flush();
|
||||
|
||||
// create new paymongo subscription plan
|
||||
$this->pm->createOrUpdateSubPlan($row);
|
||||
|
||||
// return successful response
|
||||
return $this->json([
|
||||
'success' => 'Changes have been saved!'
|
||||
|
|
@ -234,6 +251,9 @@ class BatterySizeController extends Controller
|
|||
// validated! save the entity
|
||||
$em->flush();
|
||||
|
||||
// find if paymongo subscription plan exists, then update accordingly
|
||||
$this->pm->createOrUpdateSubPlan($row);
|
||||
|
||||
// return successful response
|
||||
return $this->json([
|
||||
'success' => 'Changes have been saved!'
|
||||
|
|
|
|||
|
|
@ -386,7 +386,6 @@ class RiderAppController extends ApiController
|
|||
'flag_coolant' => $jo->hasCoolant(),
|
||||
'has_motolite' => $cv->hasMotoliteBattery(),
|
||||
'delivery_status' => $jo->getDeliveryStatus(),
|
||||
'flag_sealant' => $jo->hasSealant(),
|
||||
]
|
||||
];
|
||||
}
|
||||
|
|
@ -1325,7 +1324,7 @@ class RiderAppController extends ApiController
|
|||
return new APIResponse(false, 'Invalid promo id - ' . $promo_id);
|
||||
}
|
||||
|
||||
// get other parameters, if any: has motolite battery, has warranty doc, with coolant, payment method, with sealant
|
||||
// get other parameters, if any: has motolite battery, has warranty doc, with coolant, payment method
|
||||
if (isset($items['flag_motolite_battery']))
|
||||
{
|
||||
// get customer vehicle from jo
|
||||
|
|
@ -1360,15 +1359,6 @@ class RiderAppController extends ApiController
|
|||
$jo->setModeOfPayment($payment_method);
|
||||
}
|
||||
|
||||
if (isset($items['flag_sealant']))
|
||||
{
|
||||
$has_sealant = $items['flag_sealant'];
|
||||
if ($has_sealant == 'true')
|
||||
$jo->setHasSealant(true);
|
||||
else
|
||||
$jo->setHasSealant(false);
|
||||
}
|
||||
|
||||
// get capi user
|
||||
$capi_user = $this->getUser();
|
||||
if ($capi_user == null)
|
||||
|
|
@ -1453,13 +1443,6 @@ class RiderAppController extends ApiController
|
|||
else
|
||||
$jo->setHasCoolant(false);
|
||||
|
||||
// sealant
|
||||
$flag_sealant = $req->request->get('flag_sealant', 'false');
|
||||
if ($flag_sealant == 'true')
|
||||
$jo->setHasSealant(true);
|
||||
else
|
||||
$jo->setHasSealant(false);
|
||||
|
||||
// has motolite battery
|
||||
$cv = $jo->getCustomerVehicle();
|
||||
$has_motolite = $req->request->get('has_motolite', 'false');
|
||||
|
|
@ -1560,9 +1543,6 @@ class RiderAppController extends ApiController
|
|||
// get coolant if any
|
||||
$flag_coolant = $jo->hasCoolant();
|
||||
|
||||
// get sealant if any
|
||||
$flag_sealant = $jo->hasSealant();
|
||||
|
||||
// check if new promo is null
|
||||
if ($promo == null)
|
||||
{
|
||||
|
|
@ -1581,7 +1561,6 @@ class RiderAppController extends ApiController
|
|||
->setCustomerVehicle($cv)
|
||||
->setSource($source)
|
||||
->setHasCoolant($flag_coolant)
|
||||
->setHasSealant($flag_sealant)
|
||||
->setIsTaxable();
|
||||
|
||||
// set price tier
|
||||
|
|
|
|||
|
|
@ -13,6 +13,8 @@ use App\Ramcar\JOStatus;
|
|||
use App\Entity\Warranty;
|
||||
use App\Entity\JobOrder;
|
||||
use App\Entity\CustomerSession;
|
||||
use App\Service\PayMongoConnector;
|
||||
use DateTime;
|
||||
|
||||
class ApiController extends BaseApiController
|
||||
{
|
||||
|
|
@ -63,6 +65,9 @@ class ApiController extends BaseApiController
|
|||
}
|
||||
}
|
||||
|
||||
// update session timestamp
|
||||
$this->updateSessionTimestamp();
|
||||
|
||||
return [
|
||||
'is_valid' => !$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!';
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ use App\Ramcar\CustomerSource;
|
|||
use App\Entity\Customer;
|
||||
use App\Entity\PrivacyPolicy;
|
||||
use App\Service\HashGenerator;
|
||||
use App\Service\PayMongoConnector;
|
||||
|
||||
class CustomerController extends ApiController
|
||||
{
|
||||
|
|
@ -27,6 +28,7 @@ class CustomerController extends ApiController
|
|||
return new ApiResponse(true, '', [
|
||||
'first_name' => '',
|
||||
'last_name' => '',
|
||||
'email' => '',
|
||||
'priv_third_party' => (bool) false,
|
||||
'priv_promo' => (bool) false,
|
||||
]);
|
||||
|
|
@ -36,18 +38,16 @@ class CustomerController extends ApiController
|
|||
return new ApiResponse(true, '', [
|
||||
'first_name' => $cust->getFirstName(),
|
||||
'last_name' => $cust->getLastName(),
|
||||
'email' => $cust->getEmail(),
|
||||
'priv_third_party' => (bool) $cust->getPrivacyThirdParty(),
|
||||
'priv_promo' => (bool) $cust->getPrivacyPromo(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function updateInfo(Request $req)
|
||||
public function updateInfo(Request $req, PayMongoConnector $pm)
|
||||
{
|
||||
// validate params
|
||||
$validity = $this->validateRequest($req, [
|
||||
'first_name',
|
||||
'last_name',
|
||||
]);
|
||||
$validity = $this->validateRequest($req);
|
||||
|
||||
if (!$validity['is_valid']) {
|
||||
return new ApiResponse(false, $validity['error']);
|
||||
|
|
@ -65,6 +65,12 @@ class CustomerController extends ApiController
|
|||
|
||||
$this->em->flush();
|
||||
|
||||
// initialize paymongo connector
|
||||
$this->initializeSubscriptionPayMongoConnector($pm);
|
||||
|
||||
// update customer paymongo record if exists
|
||||
$pm->updateCustomerIfExists($cust);
|
||||
|
||||
// response
|
||||
return new ApiResponse();
|
||||
}
|
||||
|
|
@ -127,10 +133,21 @@ class CustomerController extends ApiController
|
|||
$this->session->setCustomer($cust);
|
||||
}
|
||||
|
||||
$cust->setFirstName($req->request->get('first_name'))
|
||||
->setLastName($req->request->get('last_name'))
|
||||
->setEmail($req->request->get('email', ''))
|
||||
->setConfirmed($this->session->isConfirmed());
|
||||
if (!is_null($req->request->get('first_name'))) {
|
||||
$cust->setFirstName($req->request->get('first_name'));
|
||||
}
|
||||
|
||||
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 ($cust->getCustomerUser() == null) {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
namespace App\Controller\CustomerAppAPI;
|
||||
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\HttpKernel\KernelInterface;
|
||||
use Catalyst\ApiBundle\Component\Response as ApiResponse;
|
||||
|
|
@ -33,7 +34,7 @@ class InsuranceController extends ApiController
|
|||
$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
|
||||
$validity = $this->validateRequest($req, [
|
||||
|
|
@ -162,6 +163,12 @@ class InsuranceController extends ApiController
|
|||
$this->em->persist($gt);
|
||||
$this->em->flush();
|
||||
|
||||
// initialize paymongo connector
|
||||
$paymongo->initialize(
|
||||
$params->get('insurance_paymongo_public_key'),
|
||||
$params->get('insurance_paymongo_secret_key')
|
||||
);
|
||||
|
||||
// create paymongo checkout resource
|
||||
$checkout = $paymongo->createCheckout(
|
||||
$cust,
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ use App\Entity\Battery;
|
|||
use App\Entity\BatterySize;
|
||||
use App\Entity\Customer;
|
||||
use App\Entity\CustomerMetadata;
|
||||
use App\Ramcar\ServiceType;
|
||||
|
||||
class InvoiceController extends ApiController
|
||||
{
|
||||
|
|
@ -55,7 +56,16 @@ class InvoiceController extends ApiController
|
|||
|
||||
// make invoice criteria
|
||||
$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
|
||||
$promo_id = $req->request->get('promo_id');
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ use App\Entity\JOEvent;
|
|||
use App\Entity\Warranty;
|
||||
use App\Entity\JobOrder;
|
||||
use App\Entity\CustomerVehicle;
|
||||
|
||||
use App\Ramcar\SubscriptionStatus;
|
||||
use DateTime;
|
||||
|
||||
class JobOrderController extends ApiController
|
||||
|
|
@ -616,9 +616,16 @@ class JobOrderController extends ApiController
|
|||
|
||||
// validate 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)) {
|
||||
return new ApiResponse(false, 'Invalid service type.');
|
||||
}
|
||||
|
||||
$jo->setServiceType($stype);
|
||||
|
||||
// validate warranty
|
||||
|
|
@ -712,12 +719,24 @@ class JobOrderController extends ApiController
|
|||
$pt_id = $pt_manager->getPriceTier($jo->getCoordinates());
|
||||
$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
|
||||
$invoice = $ic->generateInvoice($icrit);
|
||||
$jo->setInvoice($invoice);
|
||||
|
||||
//error_log("GENERATED INVOICE");
|
||||
|
||||
// save here first so we have a JO ID which is required for the hub selector
|
||||
$this->em->persist($invoice);
|
||||
$this->em->persist($jo);
|
||||
|
|
@ -738,7 +757,8 @@ class JobOrderController extends ApiController
|
|||
->setJoOrigin($jo->getSource())
|
||||
->setCustomerClass($cust->getCustomerClassification())
|
||||
->setOrderDate($jo->getDateCreate())
|
||||
->setServiceType($jo->getServiceType());
|
||||
->setServiceType($jo->getServiceType())
|
||||
->setSubscription($jo->getSubscription() !== null);
|
||||
|
||||
// get distance limit for mobile from env
|
||||
// get value of hub_filter_enable from env
|
||||
|
|
@ -1182,6 +1202,9 @@ class JobOrderController extends ApiController
|
|||
$hub_criteria = new HubCriteria();
|
||||
$hub_criteria->setPoint($jo->getCoordinates());
|
||||
|
||||
// set subscription flag
|
||||
$hub_criteria->setSubscription($jo->getSubscription() !== null);
|
||||
|
||||
// get distance limit for mobile from env
|
||||
$limit_distance = $_ENV['CUST_DISTANCE_LIMIT'];
|
||||
|
||||
|
|
@ -1406,6 +1429,8 @@ class JobOrderController extends ApiController
|
|||
|
||||
$dest = $jo->getCoordinates();
|
||||
|
||||
$sub = $jo->getSubscription();
|
||||
|
||||
$jo_data = [
|
||||
'id' => $jo->getID(),
|
||||
'date_create' => $jo->getDateCreate()->format('M d, Y'),
|
||||
|
|
@ -1420,6 +1445,7 @@ class JobOrderController extends ApiController
|
|||
'landmark' => $jo->getLandmark(),
|
||||
'jo_status' => $status,
|
||||
'status' => $this->generateAPIRiderStatus($status),
|
||||
'subscription' => !empty($sub) ? $sub->getID() : null,
|
||||
];
|
||||
|
||||
// customer vehicle and warranty
|
||||
|
|
|
|||
29
src/Controller/CustomerAppAPI/LoyaltyController.php
Normal file
29
src/Controller/CustomerAppAPI/LoyaltyController.php
Normal 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']);
|
||||
}
|
||||
}
|
||||
34
src/Controller/CustomerAppAPI/StaticContentController.php
Normal file
34
src/Controller/CustomerAppAPI/StaticContentController.php
Normal 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(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
388
src/Controller/CustomerAppAPI/SubscriptionController.php
Normal file
388
src/Controller/CustomerAppAPI/SubscriptionController.php
Normal 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,
|
||||
]);
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@
|
|||
namespace App\Controller\CustomerAppAPI;
|
||||
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
|
||||
use Catalyst\ApiBundle\Component\Response as ApiResponse;
|
||||
use CrEOF\Spatial\PHP\Types\Geometry\Point;
|
||||
|
||||
|
|
@ -15,6 +16,7 @@ use App\Ramcar\JOStatus;
|
|||
use App\Ramcar\ServiceType;
|
||||
use App\Ramcar\TradeInType;
|
||||
use App\Ramcar\InsuranceApplicationStatus;
|
||||
use App\Ramcar\SubscriptionStatus;
|
||||
use App\Service\PayMongoConnector;
|
||||
use App\Service\PriceTierManager;
|
||||
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
|
||||
$validity = $this->validateRequest($req);
|
||||
|
|
@ -139,7 +141,7 @@ class VehicleController extends ApiController
|
|||
|
||||
// response
|
||||
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
|
||||
$validity = $this->validateRequest($req);
|
||||
|
|
@ -231,7 +233,7 @@ class VehicleController extends ApiController
|
|||
// only get the customer's vehicles whose flag_active is true
|
||||
$cvs = $this->em->getRepository(CustomerVehicle::class)->findBy(['flag_active' => true, 'customer' => $cust]);
|
||||
foreach ($cvs as $cv) {
|
||||
$cv_list[] = $this->generateVehicleInfo($cv, true, $paymongo);
|
||||
$cv_list[] = $this->generateVehicleInfo($paymongo, $params, $cv, true, true);
|
||||
}
|
||||
|
||||
// response
|
||||
|
|
@ -259,7 +261,10 @@ class VehicleController extends ApiController
|
|||
$lng = $req->query->get('longitude', '');
|
||||
$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;
|
||||
if ((!(empty($lng))) && (!(empty($lat))))
|
||||
{
|
||||
|
|
@ -393,7 +398,7 @@ class VehicleController extends ApiController
|
|||
// TODO: possibly refactor this bit
|
||||
// if no valid previous JO is found, base the trade-in value on recommended batteries
|
||||
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
|
||||
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;
|
||||
if ($cv->getCurrentBattery() != null)
|
||||
|
|
@ -433,10 +438,14 @@ class VehicleController extends ApiController
|
|||
if ($cv->getName() != null)
|
||||
$cv_name = $cv->getName();
|
||||
|
||||
$vehicle = $cv->getVehicle();
|
||||
|
||||
$row = [
|
||||
'cv_id' => $cv->getID(),
|
||||
'mfg_id' => $cv->getVehicle()->getManufacturer()->getID(),
|
||||
'make_id' => $cv->getVehicle()->getID(),
|
||||
'mfg_id' => $vehicle->getManufacturer()->getID(),
|
||||
'make_id' => $vehicle->getID(),
|
||||
'mfg_name' => $vehicle->getManufacturer()->getName(),
|
||||
'make_name' => $vehicle->getMake(),
|
||||
'name' => $cv_name,
|
||||
'plate_num' => $cv->getPlateNumber(),
|
||||
'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
|
||||
switch ($status) {
|
||||
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
|
||||
$checkout = $paymongo->getCheckout($gt->getExtTransactionId());
|
||||
|
||||
|
|
@ -486,6 +501,7 @@ class VehicleController extends ApiController
|
|||
break;
|
||||
}
|
||||
|
||||
// build insurance row
|
||||
$insurance = [
|
||||
'id' => $iobj->getID(),
|
||||
'ext_transaction_id' => $iobj->getExtTransactionId(),
|
||||
|
|
@ -495,17 +511,35 @@ class VehicleController extends ApiController
|
|||
'transaction_status' => $gt->getStatus(),
|
||||
'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_complete' => $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_complete' => !empty($date_complete) ? $date_complete->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;
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -754,8 +754,6 @@ class JobOrderController extends Controller
|
|||
$promo_id = $req->request->get('promo');
|
||||
$cvid = $req->request->get('cvid');
|
||||
$service_charges = $req->request->get('service_charges', []);
|
||||
$flag_coolant = $req->request->get('flag_coolant', false);
|
||||
$flag_sealant = $req->request->get('flag_sealant', false);
|
||||
|
||||
// coordinates
|
||||
// need to check if lng and lat are set
|
||||
|
|
@ -786,9 +784,7 @@ class JobOrderController extends Controller
|
|||
->setCustomerVehicle($cv)
|
||||
->setIsTaxable()
|
||||
->setSource(TransactionOrigin::CALL)
|
||||
->setPriceTier($price_tier)
|
||||
->setHasCoolant($flag_coolant)
|
||||
->setHasSealant($flag_sealant);
|
||||
->setPriceTier($price_tier);
|
||||
|
||||
/*
|
||||
// if it's a jumpstart or troubleshoot only, we know what to charge already
|
||||
|
|
|
|||
|
|
@ -4,11 +4,12 @@ namespace App\Controller;
|
|||
|
||||
use App\Entity\GatewayTransaction;
|
||||
use App\Ramcar\TransactionStatus;
|
||||
use App\Service\FCMSender;
|
||||
use App\Service\LoyaltyConnector;
|
||||
use App\Service\PayMongoConnector;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
|
||||
|
||||
use DateTime;
|
||||
|
|
@ -17,11 +18,15 @@ class PayMongoController extends Controller
|
|||
{
|
||||
protected $pm;
|
||||
protected $em;
|
||||
protected $lc;
|
||||
protected $fcmclient;
|
||||
|
||||
public function __construct(PayMongoConnector $pm, EntityManagerInterface $em)
|
||||
public function __construct(PayMongoConnector $pm, EntityManagerInterface $em, LoyaltyConnector $lc, FCMSender $fcmclient)
|
||||
{
|
||||
$this->pm = $pm;
|
||||
$this->em = $em;
|
||||
$this->lc = $lc;
|
||||
$this->fcmclient = $fcmclient;
|
||||
}
|
||||
|
||||
public function listen(Request $req)
|
||||
|
|
@ -63,13 +68,45 @@ class PayMongoController extends Controller
|
|||
|
||||
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'];
|
||||
$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)) {
|
||||
// mark as paid
|
||||
$obj->setStatus(TransactionStatus::PAID);
|
||||
$obj->setDatePay(new DateTime());
|
||||
|
||||
$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)
|
||||
{
|
||||
return $this->render('paymongo/success.html.twig');
|
||||
|
|
|
|||
|
|
@ -212,8 +212,7 @@ class StaticContentController extends Controller
|
|||
throw $this->createNotFoundException('The item does not exist');
|
||||
|
||||
// set and save values
|
||||
$row->setID($req->request->get('id'))
|
||||
->setContent($req->request->get('content'));
|
||||
$row->setContent($req->request->get('content'));
|
||||
|
||||
// validate
|
||||
$errors = $validator->validate($row);
|
||||
|
|
@ -221,13 +220,6 @@ class StaticContentController extends Controller
|
|||
// initialize error list
|
||||
$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
|
||||
foreach ($errors as $error) {
|
||||
$error_array[$error->getPropertyPath()] = $error->getMessage();
|
||||
|
|
|
|||
|
|
@ -192,6 +192,9 @@ class JobOrderController extends ApiController
|
|||
$hub_criteria = new HubCriteria();
|
||||
$hub_criteria->setPoint($jo->getCoordinates());
|
||||
|
||||
// set subscription flag
|
||||
$hub_criteria->setSubscription($jo->getSubscription() !== null);
|
||||
|
||||
// get distance limit for mobile from env
|
||||
// get value of hub_filter_enable from env
|
||||
$dotenv = new Dotenv();
|
||||
|
|
|
|||
|
|
@ -153,6 +153,12 @@ class Battery
|
|||
*/
|
||||
protected $flag_active;
|
||||
|
||||
// flag if battery is used for subscriptions
|
||||
/**
|
||||
* @ORM\Column(type="boolean", options={"default": false})
|
||||
*/
|
||||
protected $flag_subscription;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->vehicles = new ArrayCollection();
|
||||
|
|
@ -167,6 +173,7 @@ class Battery
|
|||
$this->date_create = new DateTime();
|
||||
|
||||
$this->flag_active = true;
|
||||
$this->flag_subscription = false;
|
||||
}
|
||||
|
||||
public function getID()
|
||||
|
|
@ -396,9 +403,20 @@ class Battery
|
|||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -57,6 +57,12 @@ class BatterySize
|
|||
*/
|
||||
protected $tip_lazada;
|
||||
|
||||
// subscription msrp
|
||||
/**
|
||||
* @ORM\Column(type="decimal", precision=7, scale=2, nullable=true)
|
||||
*/
|
||||
protected $sub_recurring_fee;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->batteries = new ArrayCollection();
|
||||
|
|
@ -64,6 +70,7 @@ class BatterySize
|
|||
$this->tip_premium = 0;
|
||||
$this->tip_other = 0;
|
||||
$this->tip_lazada = 0;
|
||||
$this->sub_recurring_fee = 0;
|
||||
}
|
||||
|
||||
public function getID()
|
||||
|
|
@ -149,4 +156,14 @@ class BatterySize
|
|||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -100,6 +100,12 @@ class CustomerSession
|
|||
*/
|
||||
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
|
||||
/**
|
||||
* @ORM\OneToMany(targetEntity="Review", mappedBy="mobile_session")
|
||||
|
|
@ -115,6 +121,7 @@ class CustomerSession
|
|||
$this->confirm_flag = false;
|
||||
$this->date_confirmed = null;
|
||||
$this->date_code_sent = null;
|
||||
$this->date_latest_activity = null;
|
||||
$this->reviews = new ArrayCollection();
|
||||
}
|
||||
|
||||
|
|
@ -270,4 +277,15 @@ class CustomerSession
|
|||
{
|
||||
return $this->reviews;
|
||||
}
|
||||
|
||||
public function setDateLatestActivity(DateTime $date)
|
||||
{
|
||||
$this->date_latest_activity = $date;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getDateLatestActivity()
|
||||
{
|
||||
return $this->date_latest_activity;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
namespace App\Entity;
|
||||
|
||||
use App\Ramcar\InsuranceApplicationStatus;
|
||||
use App\Ramcar\SubscriptionStatus;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
|
||||
|
|
@ -12,8 +13,12 @@ use Doctrine\Common\Collections\Criteria;
|
|||
|
||||
/**
|
||||
* @ORM\Entity
|
||||
* @ORM\Table(name="customer_vehicle", indexes={@ORM\Index(columns={"plate_number"}, flags={"fulltext"}),
|
||||
@ORM\Index(name="plate_number_idx", columns={"plate_number"})})
|
||||
* @ORM\Table(
|
||||
* name="customer_vehicle",
|
||||
* indexes={
|
||||
* @ORM\Index(name="plate_number_idx", columns={"plate_number"}, flags={"fulltext"})
|
||||
* }
|
||||
* )
|
||||
*/
|
||||
class CustomerVehicle
|
||||
{
|
||||
|
|
@ -122,6 +127,12 @@ class CustomerVehicle
|
|||
*/
|
||||
protected $insurance_applications;
|
||||
|
||||
// link to subscription
|
||||
/**
|
||||
* @ORM\OneToMany(targetEntity="Subscription", mappedBy="customer_vehicle")
|
||||
*/
|
||||
protected $subscriptions;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->flag_active = true;
|
||||
|
|
@ -306,4 +317,26 @@ class CustomerVehicle
|
|||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -441,11 +441,12 @@ class JobOrder
|
|||
*/
|
||||
protected $flag_cust_new;
|
||||
|
||||
// only for tire service, if it requires sealant or not
|
||||
// get the subscription this job order is associated with
|
||||
/**
|
||||
* @ORM\Column(type="boolean")
|
||||
* @ORM\ManyToOne(targetEntity="Subscription", inversedBy="claims")
|
||||
* @ORM\JoinColumn(name="subscription_id", referencedColumnName="id", nullable=true, onDelete="SET NULL")
|
||||
*/
|
||||
protected $flag_sealant;
|
||||
protected $subscription;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
|
|
@ -464,7 +465,6 @@ class JobOrder
|
|||
$this->trade_in_type = null;
|
||||
$this->flag_rider_rating = false;
|
||||
$this->flag_coolant = false;
|
||||
$this->flag_sealant = false;
|
||||
|
||||
$this->priority = 0;
|
||||
$this->meta = [];
|
||||
|
|
@ -1263,15 +1263,15 @@ class JobOrder
|
|||
return $this->flag_cust_new;
|
||||
}
|
||||
|
||||
public function setHasSealant($flag = true)
|
||||
public function getSubscription()
|
||||
{
|
||||
$this->flag_sealant = $flag;
|
||||
return $this->subscription;
|
||||
}
|
||||
|
||||
public function setSubscription($subscription)
|
||||
{
|
||||
$this->subscription = $subscription;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function hasSealant()
|
||||
{
|
||||
return $this->flag_sealant;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
226
src/Entity/Subscription.php
Normal file
226
src/Entity/Subscription.php
Normal 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
|
||||
}
|
||||
}
|
||||
|
|
@ -180,11 +180,20 @@ class Vehicle
|
|||
return $this->cust_vehicles;
|
||||
}
|
||||
|
||||
public function getActiveBatteries()
|
||||
public function getActiveBatteries($is_subscription = false)
|
||||
{
|
||||
$crit = Criteria::create();
|
||||
$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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,22 +5,34 @@ namespace App\EntityListener;
|
|||
use Doctrine\ORM\Event\LifecycleEventArgs;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
|
||||
use App\Entity\Subscription;
|
||||
use App\Entity\GatewayTransaction;
|
||||
use App\Entity\InsuranceApplication;
|
||||
use App\Service\InsuranceConnector;
|
||||
use App\Ramcar\InsuranceApplicationStatus;
|
||||
use App\Ramcar\TransactionStatus;
|
||||
use App\Ramcar\SubscriptionStatus;
|
||||
use App\Service\FCMSender;
|
||||
use App\Service\LoyaltyConnector;
|
||||
use DateTime;
|
||||
|
||||
class GatewayTransactionListener
|
||||
{
|
||||
protected $ic;
|
||||
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->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)
|
||||
|
|
@ -36,14 +48,20 @@ class GatewayTransactionListener
|
|||
$prev_value = $field_changes[0] ?? null;
|
||||
$new_value = $field_changes[1] ?? null;
|
||||
|
||||
error_log($prev_value . " vs " . $new_value);
|
||||
|
||||
// only do something if the status has changed to paid
|
||||
if ($prev_value !== $new_value && $new_value === TransactionStatus::PAID) {
|
||||
// determine if we will add loyalty points for this transaction
|
||||
// handle based on type
|
||||
// TODO: add types here as we go. there's probably a better way to do this.
|
||||
switch ($gt_obj->getType()) {
|
||||
case 'insurance_premium':
|
||||
return $this->handleInsurancePremium($gt_obj);
|
||||
$this->handleInsurancePremium($gt_obj);
|
||||
break;
|
||||
case 'subscription':
|
||||
$this->handleSubscription($gt_obj);
|
||||
$this->addLoyaltyPoints($gt_obj);
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
|
@ -51,7 +69,7 @@ class GatewayTransactionListener
|
|||
}
|
||||
}
|
||||
|
||||
protected function handleInsurancePremium($gt_obj)
|
||||
protected function handleInsurancePremium(GatewayTransaction $gt_obj)
|
||||
{
|
||||
// get insurance application object
|
||||
$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)],
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
69
src/InvoiceRule/IsSubscription.php
Normal file
69
src/InvoiceRule/IsSubscription.php
Normal 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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -31,7 +31,6 @@ class TireRepair implements InvoiceRuleInterface
|
|||
public function compute($criteria, &$total)
|
||||
{
|
||||
$stype = $criteria->getServiceType();
|
||||
$has_sealant = $criteria->hasSealant();
|
||||
$pt_id = $criteria->getPriceTier();
|
||||
|
||||
$items = [];
|
||||
|
|
@ -57,28 +56,8 @@ class TireRepair implements InvoiceRuleInterface
|
|||
'price' => $price,
|
||||
];
|
||||
|
||||
$qty_fee = bcmul($qty, $price, 2);
|
||||
$total_price = $qty_fee;
|
||||
|
||||
if ($has_sealant)
|
||||
{
|
||||
$sealant_fee_data = $this->getSealantFeeData();
|
||||
|
||||
$sealant_fee = $sealant_fee_data['fee'];
|
||||
$sealant_title = $sealant_fee_data['title'];
|
||||
|
||||
$items[] = [
|
||||
'service_type' => $this->getID(),
|
||||
'qty' => $qty,
|
||||
'title' => $sealant_title,
|
||||
'price' => $sealant_fee,
|
||||
];
|
||||
|
||||
$qty_price = bcmul($sealant_fee, $qty, 2);
|
||||
$total_price = bcadd($total_price, $qty_price, 2);
|
||||
}
|
||||
|
||||
$total['total_price'] = bcadd($total['total_price'], $total_price, 2);
|
||||
$qty_price = bcmul($price, $qty, 2);
|
||||
$total['total_price'] = bcadd($total['total_price'], $qty_price, 2);
|
||||
}
|
||||
|
||||
return $items;
|
||||
|
|
@ -152,28 +131,4 @@ class TireRepair implements InvoiceRuleInterface
|
|||
|
||||
return $title;
|
||||
}
|
||||
|
||||
public function getSealantFeeData()
|
||||
{
|
||||
$data = [
|
||||
'fee' => 0.00,
|
||||
'title' => '',
|
||||
];
|
||||
|
||||
$code = 'tire_sealant_fee';
|
||||
|
||||
// find the service fee using the code
|
||||
// if we can't find the fee, return 0
|
||||
$fee = $this->em->getRepository(ServiceOffering::class)->findOneBy(['code' => $code]);
|
||||
|
||||
if ($fee != null)
|
||||
{
|
||||
$data = [
|
||||
'fee' => $fee->getFee(),
|
||||
'title' => $fee->getName(),
|
||||
];
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
19
src/Ramcar/FirebaseNotificationType.php
Normal file
19
src/Ramcar/FirebaseNotificationType.php
Normal 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',
|
||||
];
|
||||
}
|
||||
|
|
@ -25,6 +25,7 @@ class HubCriteria
|
|||
protected $order_date; // date JO was created
|
||||
protected $service_type; // service type of JO
|
||||
protected $jo_origin; // origin of JO
|
||||
protected $flag_subscription; // flag if subscription or not
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
|
|
@ -45,6 +46,7 @@ class HubCriteria
|
|||
$this->order_date = new DateTime();
|
||||
$this->service_type = null;
|
||||
$this->jo_origin = null;
|
||||
$this->flag_subscription = false;
|
||||
}
|
||||
|
||||
public function setPoint(Point $point)
|
||||
|
|
@ -235,5 +237,16 @@ class HubCriteria
|
|||
{
|
||||
return $this->jo_origin;
|
||||
}
|
||||
|
||||
public function setSubscription($flag_subscription = true)
|
||||
{
|
||||
$this->flag_subscription = $flag_subscription;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isSubscription()
|
||||
{
|
||||
return $this->flag_subscription;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ class InvoiceCriteria
|
|||
protected $flag_taxable;
|
||||
protected $source; // use Ramcar's TransactionOrigin
|
||||
protected $price_tier;
|
||||
protected $flag_sealant;
|
||||
protected $is_subscription;
|
||||
|
||||
// entries are battery and trade-in combos
|
||||
protected $entries;
|
||||
|
|
@ -35,7 +35,7 @@ class InvoiceCriteria
|
|||
$this->flag_taxable = false;
|
||||
$this->source = '';
|
||||
$this->price_tier = 0; // set to default
|
||||
$this->flag_sealant = false;
|
||||
$this->is_subscription = false;
|
||||
}
|
||||
|
||||
public function setServiceType($stype)
|
||||
|
|
@ -205,14 +205,14 @@ class InvoiceCriteria
|
|||
return $this->price_tier;
|
||||
}
|
||||
|
||||
public function setHasSealant($flag = true)
|
||||
public function setSubscription($is_subscription)
|
||||
{
|
||||
$this->flag_sealant = $flag;
|
||||
$this->is_subscription = $is_subscription;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function hasSealant()
|
||||
public function isSubscription()
|
||||
{
|
||||
return $this->flag_sealant;
|
||||
return $this->is_subscription;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ class ModeOfPayment extends NameValue
|
|||
const INSTALLMENT = 'installment';
|
||||
const GCASH = 'gcash';
|
||||
const CREDIT_CARD_AMEX = 'credit_card_amex';
|
||||
const SUBSCRIPTION = 'subscription';
|
||||
|
||||
const COLLECTION = [
|
||||
'cash' => 'Cash',
|
||||
|
|
@ -18,5 +19,6 @@ class ModeOfPayment extends NameValue
|
|||
'installment' => 'Installment - BDO',
|
||||
'gcash' => 'GCash',
|
||||
'credit_card_amex' => 'Credit Card - AMEX',
|
||||
'subscription' => 'Subscription',
|
||||
];
|
||||
}
|
||||
|
|
|
|||
20
src/Ramcar/SubscriptionStatus.php
Normal file
20
src/Ramcar/SubscriptionStatus.php
Normal 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',
|
||||
];
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@ namespace App\Service\CustomerHandler;
|
|||
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
|
||||
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\Validator\Validator\ValidatorInterface;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
|
|
@ -24,7 +25,7 @@ use App\Entity\Battery;
|
|||
use App\Entity\VehicleManufacturer;
|
||||
use App\Entity\BatteryManufacturer;
|
||||
use App\Entity\CustomerTag;
|
||||
|
||||
use App\Service\PayMongoConnector;
|
||||
use DateTime;
|
||||
|
||||
class ResqCustomerHandler implements CustomerHandlerInterface
|
||||
|
|
@ -34,14 +35,22 @@ class ResqCustomerHandler implements CustomerHandlerInterface
|
|||
protected $country_code;
|
||||
protected $security;
|
||||
protected $template_hash;
|
||||
protected $pm;
|
||||
|
||||
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->validator = $validator;
|
||||
$this->country_code = $country_code;
|
||||
$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();
|
||||
}
|
||||
|
|
@ -391,6 +400,9 @@ class ResqCustomerHandler implements CustomerHandlerInterface
|
|||
$em->persist($cust);
|
||||
$em->flush();
|
||||
|
||||
// update customer paymongo record if exists
|
||||
$this->pm->updateCustomerIfExists($cust);
|
||||
|
||||
$result = [
|
||||
'id' => $cust->getID(),
|
||||
];
|
||||
|
|
|
|||
|
|
@ -3,53 +3,179 @@
|
|||
namespace App\Service;
|
||||
|
||||
use App\Entity\Customer;
|
||||
use App\Ramcar\FirebaseNotificationType;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
use Fcm\FcmClient;
|
||||
use Fcm\Push\Notification;
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\Exception\RequestException;
|
||||
use Firebase\JWT\JWT;
|
||||
use App\Entity\JobOrder;
|
||||
use App\Entity\Subscription;
|
||||
use Exception;
|
||||
use RuntimeException;
|
||||
use DateTime;
|
||||
use DateInterval;
|
||||
|
||||
class FCMSender
|
||||
{
|
||||
protected $client;
|
||||
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->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();
|
||||
$notification->setTitle($title)
|
||||
->setBody($body);
|
||||
// build the access token parts
|
||||
$private_key = $this->credentials['private_key'];
|
||||
$now = time();
|
||||
|
||||
foreach ($recipients as $recipient) {
|
||||
$notification->addRecipient($recipient);
|
||||
$jwt_payload = [
|
||||
'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)) {
|
||||
$notification->setColor($color);
|
||||
}
|
||||
|
||||
if (!empty($sound)) {
|
||||
$notification->setSound($sound);
|
||||
}
|
||||
|
||||
if (!empty($color)) {
|
||||
$notification->setColor($color);
|
||||
}
|
||||
|
||||
if (!empty($badge)) {
|
||||
$notification->setBadge($badge);
|
||||
public function send($recipients, $title, $body, $data = [], $color = null, $sound = null, $image = null)
|
||||
{
|
||||
// set URL for sending
|
||||
$url = "projects/{$this->project_id}/messages:send";
|
||||
$access_token = $this->generateAccessToken();
|
||||
|
||||
// build payload structure
|
||||
$payload = [
|
||||
'message' => [
|
||||
'notification' => [
|
||||
'title' => $title,
|
||||
'body' => $body,
|
||||
],
|
||||
'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)) {
|
||||
$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 = [])
|
||||
|
|
@ -60,12 +186,34 @@ class FCMSender
|
|||
// attach jo info
|
||||
$data['jo_id'] = $job_order->getID();
|
||||
$data['jo_status'] = $job_order->getStatus();
|
||||
$data['notification_type'] = FirebaseNotificationType::JOB_ORDER;
|
||||
|
||||
// send the event
|
||||
return $this->sendEvent($cust, $title, $body, $data);
|
||||
}
|
||||
|
||||
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
|
||||
$devices = $this->getDevices($cust);
|
||||
|
|
@ -75,7 +223,12 @@ class FCMSender
|
|||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
|
@ -95,11 +248,23 @@ class FCMSender
|
|||
return false;
|
||||
}
|
||||
|
||||
// get device timestamp cutoff
|
||||
$oldest_timestamp = (new DateTime())->sub(new DateInterval('P1M'));
|
||||
|
||||
// send to every customer session
|
||||
foreach ($sessions as $sess) {
|
||||
$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
|
||||
$device_ids[$device_id] = true;
|
||||
}
|
||||
|
|
@ -112,4 +277,24 @@ class FCMSender
|
|||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ class JoTypeHubFilter extends BaseHubFilter implements HubFilterInterface
|
|||
public function getRequestedParams() : array
|
||||
{
|
||||
return [
|
||||
'flag_subscription',
|
||||
'flag_emergency',
|
||||
'jo_type',
|
||||
];
|
||||
|
|
@ -22,6 +23,9 @@ class JoTypeHubFilter extends BaseHubFilter implements HubFilterInterface
|
|||
if ($params['flag_emergency'])
|
||||
return $hubs;
|
||||
|
||||
if ($params['flag_subscription'])
|
||||
return $hubs;
|
||||
|
||||
if (empty($params['jo_type']))
|
||||
return $hubs;
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ class PaymentMethodHubFilter extends BaseHubFilter implements HubFilterInterface
|
|||
public function getRequestedParams() : array
|
||||
{
|
||||
return [
|
||||
'flag_subscription',
|
||||
'flag_emergency',
|
||||
'payment_method',
|
||||
];
|
||||
|
|
@ -22,6 +23,9 @@ class PaymentMethodHubFilter extends BaseHubFilter implements HubFilterInterface
|
|||
if ($params['flag_emergency'])
|
||||
return $hubs;
|
||||
|
||||
if ($params['flag_subscription'])
|
||||
return $hubs;
|
||||
|
||||
if (empty($params['payment_method']))
|
||||
return $hubs;
|
||||
|
||||
|
|
|
|||
|
|
@ -68,6 +68,7 @@ class HubSelector
|
|||
$jo_origin = $criteria->getJoOrigin();
|
||||
$customer_id = $criteria->getCustomerId();
|
||||
$customer_class = $criteria->getCustomerClass();
|
||||
$flag_subscription = $criteria->isSubscription();
|
||||
|
||||
// needed for JORejection records and SMS notifs
|
||||
$order_date = $criteria->getOrderDate();
|
||||
|
|
@ -95,6 +96,7 @@ class HubSelector
|
|||
'payment_method' => $payment_method,
|
||||
'flag_riders_check' => $flag_riders_check,
|
||||
'flag_round_robin' => $flag_round_robin,
|
||||
'flag_subscription' => $flag_subscription,
|
||||
];
|
||||
|
||||
// loop through all enabled filters
|
||||
|
|
|
|||
|
|
@ -55,6 +55,7 @@ class InvoiceManager implements InvoiceGeneratorInterface
|
|||
new InvoiceRule\Fuel($this->em, $this->pt_manager),
|
||||
new InvoiceRule\TireRepair($this->em, $this->pt_manager),
|
||||
new InvoiceRule\DiscountType($this->em),
|
||||
new InvoiceRule\IsSubscription($this->em),
|
||||
new InvoiceRule\TradeIn($this->em),
|
||||
new InvoiceRule\Tax($this->em, $this->pt_manager),
|
||||
];
|
||||
|
|
@ -69,14 +70,6 @@ class InvoiceManager implements InvoiceGeneratorInterface
|
|||
->setCustomerVehicle($jo->getCustomerVehicle())
|
||||
->setPriceTier($price_tier);
|
||||
|
||||
if (($jo->getServiceType() == ServiceType::OVERHEAT_ASSISTANCE) &&
|
||||
($jo->hasCoolant()))
|
||||
$criteria->setHasCoolant(true);
|
||||
|
||||
if (($jo->getServiceType() == ServiceType::TIRE_REPAIR) &&
|
||||
($jo->hasSealant()))
|
||||
$criteria->setHasSealant(true);
|
||||
|
||||
// set if taxable
|
||||
// NOTE: ideally, this should be a parameter when calling generateInvoiceCriteria. But that
|
||||
// would mean adding it as a parameter to the call, impacting all calls
|
||||
|
|
|
|||
|
|
@ -2576,6 +2576,9 @@ class ResqJobOrderHandler implements JobOrderHandlerInterface
|
|||
$long = $obj->getCoordinates()->getLongitude();
|
||||
$lat = $obj->getCoordinates()->getLatitude();
|
||||
|
||||
// set subscription flag
|
||||
$hub_criteria->setSubscription($obj->getSubscription() !== null);
|
||||
|
||||
// set result limit and location and date_time
|
||||
$hub_criteria->setPoint($obj->getCoordinates())
|
||||
->setDateTime($obj->getDateSchedule())
|
||||
|
|
@ -2953,6 +2956,9 @@ class ResqJobOrderHandler implements JobOrderHandlerInterface
|
|||
$long = $obj->getCoordinates()->getLongitude();
|
||||
$lat = $obj->getCoordinates()->getLatitude();
|
||||
|
||||
// set subscription flag
|
||||
$hub_criteria->setSubscription($obj->getSubscription() !== null);
|
||||
|
||||
$hub_criteria->setPoint($obj->getCoordinates())
|
||||
->setDateTime($obj->getDateSchedule())
|
||||
->setLimitResults(50);
|
||||
|
|
|
|||
134
src/Service/LoyaltyConnector.php
Normal file
134
src/Service/LoyaltyConnector.php
Normal 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -2,25 +2,39 @@
|
|||
|
||||
namespace App\Service;
|
||||
|
||||
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
|
||||
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\Psr7;
|
||||
use GuzzleHttp\Exception\RequestException;
|
||||
|
||||
use App\Entity\Customer;
|
||||
use App\Entity\BatterySize;
|
||||
use Doctrine\ORM\Query\Parameter;
|
||||
|
||||
class PayMongoConnector
|
||||
{
|
||||
protected $base_url;
|
||||
protected $public_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->sub_months = $params->get('subscription_months');
|
||||
}
|
||||
|
||||
public function initialize($public_key, $secret_key)
|
||||
{
|
||||
$this->public_key = $public_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 = [])
|
||||
|
|
@ -84,25 +98,268 @@ class PayMongoConnector
|
|||
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();
|
||||
$headers = [
|
||||
'Content-Type' => 'application/json',
|
||||
'accept' => 'application/json',
|
||||
'authorization' => 'Basic '. $this->hash,
|
||||
// get all plans
|
||||
$plans = $this->getPlans();
|
||||
|
||||
// find the plan with the matching metadata for plan ID
|
||||
$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 {
|
||||
$response = $client->request($method, $this->base_url . '/' . $url, [
|
||||
'json' => $request_body,
|
||||
'headers' => $headers,
|
||||
return $this->doRequest('/v1/subscriptions/plans', 'POST', $body);
|
||||
}
|
||||
|
||||
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) {
|
||||
$error = ['message' => $e->getMessage()];
|
||||
|
||||
|
|
@ -118,7 +375,7 @@ class PayMongoConnector
|
|||
error_log(Psr7\Message::toString($e->getRequest()));
|
||||
|
||||
// log this error
|
||||
$this->log($url, Psr7\Message::toString($e->getRequest()), Psr7\Message::toString($e->getResponse()), 'error');
|
||||
$this->log($method . " " . $url, Psr7\Message::toString($e->getRequest()), Psr7\Message::toString($e->getResponse()), 'error');
|
||||
|
||||
if ($e->hasResponse()) {
|
||||
$error['response'] = Psr7\Message::toString($e->getResponse());
|
||||
|
|
@ -133,7 +390,7 @@ class PayMongoConnector
|
|||
$result_body = $response->getBody();
|
||||
|
||||
// log response
|
||||
$this->log($url, json_encode($request_body), $result_body);
|
||||
$this->log($method . " " . $url, json_encode($request_body), $result_body);
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
|
|
@ -142,19 +399,20 @@ class PayMongoConnector
|
|||
}
|
||||
|
||||
// TODO: make this more elegant
|
||||
public function log($title, $request_body = "[]", $result_body = "[]", $type = 'api')
|
||||
public function log($title, $request_body = "[]", $result_body = "[]", $type = 'api', $custom_message = null)
|
||||
{
|
||||
$filename = '/../../var/log/paymongo_' . $type . '.log';
|
||||
$date = date("Y-m-d H:i:s");
|
||||
|
||||
// build log entry
|
||||
$entry = implode("\r\n", [
|
||||
$entry = implode("\r\n", array_filter([
|
||||
$date,
|
||||
$title,
|
||||
(!empty($custom_message) ? "MESSAGE: " . $custom_message : ""),
|
||||
"REQUEST:\r\n" . $request_body,
|
||||
"RESPONSE:\r\n" . $result_body,
|
||||
"\r\n----------------------------------------\r\n\r\n",
|
||||
]);
|
||||
]));
|
||||
|
||||
@file_put_contents(__DIR__ . $filename, $entry, FILE_APPEND);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -89,6 +89,9 @@
|
|||
"edwinhoksberg/php-fcm": {
|
||||
"version": "1.0.0"
|
||||
},
|
||||
"firebase/php-jwt": {
|
||||
"version": "v6.10.1"
|
||||
},
|
||||
"guzzlehttp/guzzle": {
|
||||
"version": "6.3.0"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -80,6 +80,15 @@
|
|||
<div class="form-control-feedback hide" data-field="tip_lazada"></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 class="m-portlet__foot m-portlet__foot--fit">
|
||||
<div class="m-form__actions m-form__actions--solid m-form__actions--right">
|
||||
|
|
|
|||
|
|
@ -115,6 +115,20 @@
|
|||
<div class="col-lg-8">
|
||||
</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="col-lg-6">
|
||||
<label data-field="image_file">
|
||||
|
|
|
|||
|
|
@ -126,6 +126,21 @@
|
|||
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',
|
||||
|
|
|
|||
|
|
@ -191,7 +191,7 @@
|
|||
<div class="form-control-feedback hide" data-field="flag_research_sms"></div>
|
||||
</label>
|
||||
<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
|
||||
<span></span>
|
||||
<div class="form-control-feedback hide" data-field="flag_research_email"></div>
|
||||
|
|
@ -233,34 +233,48 @@
|
|||
Vehicle Details
|
||||
</h3>
|
||||
</div>
|
||||
<div class="form-group m-form__group row">
|
||||
<div class="col-lg-3">
|
||||
<div class="form-group m-form__group row {{ obj.getSubscription is not null ? 'form-group-subscription' }}">
|
||||
<div class="col-lg-9">
|
||||
<div class="row form-group">
|
||||
<div class="col-lg-4">
|
||||
<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>
|
||||
<div class="form-control-feedback hide" data-field="vmfg"></div>
|
||||
</div>
|
||||
<div class="col-lg-3">
|
||||
<div class="col-lg-4">
|
||||
<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>
|
||||
<div class="form-control-feedback hide" data-field="vehicle_make"></div>
|
||||
</div>
|
||||
<div class="col-lg-3">
|
||||
<div class="col-lg-4">
|
||||
<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>
|
||||
<div class="form-control-feedback hide" data-field="vehicle_year"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group m-form__group row">
|
||||
<div class="col-lg-3">
|
||||
<div class="row form-group">
|
||||
<div class="col-lg-4">
|
||||
<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>
|
||||
<div class="form-control-feedback hide" data-field="vehicle_color"></div>
|
||||
</div>
|
||||
<div class="col-lg-3">
|
||||
<div class="col-lg-4">
|
||||
<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>
|
||||
<div class="form-control-feedback hide" data-field="vehicle_color"></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 class="m-form__section">
|
||||
|
|
@ -1207,8 +1221,6 @@
|
|||
|
||||
<script>
|
||||
var invoiceItems = [];
|
||||
var hasCoolant = 0;
|
||||
var hasSealant = 0;
|
||||
|
||||
// location search autocomplete
|
||||
var input = document.getElementById('m_gmap_address');
|
||||
|
|
@ -1272,19 +1284,6 @@ $(function() {
|
|||
{% endif %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
// need to check if jo has coolant or sealant
|
||||
{% if obj.getServiceType == 'overheat' %}
|
||||
{% if obj.hasCoolant == 1 %}
|
||||
hasCoolant = 1;
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if obj.getServiceType == 'tire' %}
|
||||
{% if obj.hasSealant == 1 %}
|
||||
hasSealant = 1;
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
}
|
||||
|
||||
|
|
@ -1845,8 +1844,6 @@ $(function() {
|
|||
'cvid': cvid,
|
||||
'coord_lng': lng,
|
||||
'coord_lat': lat,
|
||||
'flag_coolant': hasCoolant,
|
||||
'flag_sealant': hasSealant,
|
||||
}
|
||||
}).done(function(response) {
|
||||
// mark as invoice changed
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@
|
|||
<label data-field="id">
|
||||
ID:
|
||||
</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>
|
||||
</div>
|
||||
|
|
@ -48,7 +48,7 @@
|
|||
<label data-field="content">
|
||||
Content
|
||||
</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>
|
||||
</div>
|
||||
|
|
@ -70,8 +70,19 @@
|
|||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block stylesheets %}
|
||||
<link rel="stylesheet" href="https://unpkg.com/easymde/dist/easymde.min.css">
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script src="https://unpkg.com/easymde/dist/easymde.min.js"></script>
|
||||
<script>
|
||||
// load markdown editor
|
||||
const mde = new EasyMDE({
|
||||
element: document.getElementById('content'),
|
||||
spellChecker: false,
|
||||
});
|
||||
|
||||
$(function() {
|
||||
$("#row-form").submit(function(e) {
|
||||
var form = $(this);
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ battery_size_tradein_brand: Trade-in Motolite
|
|||
battery_size_tradein_premium: Trade-in Premium
|
||||
battery_size_tradein_other: Trade-in Other
|
||||
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
|
||||
jo_title_pdf: Motolite Res-Q Job Order
|
||||
country_code_prefix: '+63'
|
||||
|
|
@ -179,3 +180,11 @@ insurance_fcm_title_updated: 'Application updated'
|
|||
insurance_fcm_title_completed: 'Application completed'
|
||||
insurance_fcm_body_updated: 'Some details on your insurance application have been updated.'
|
||||
insurance_fcm_body_completed: 'Your insurance application has been processed!'
|
||||
|
||||
# fcm loyalty
|
||||
loyalty_fcm_title_add_points: 'Hooray!'
|
||||
loyalty_fcm_body_add_points: '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!'
|
||||
|
|
@ -1 +0,0 @@
|
|||
INSERT INTO service_offering (name, code, fee) VALUES ('Tire Sealant Fee', 'tire_sealant_fee', '200.00');
|
||||
Loading…
Reference in a new issue