Compare commits

..

35 commits

Author SHA1 Message Date
Ramon Gutierrez
857c573ae5 Merge branch '799-subscription-support' into 809-loyalty-system-support 2024-10-14 07:22:24 +08:00
Ramon Gutierrez
a724b00ce7 Include email in customer info endpoint #809 2024-10-10 04:03:30 +08:00
Ramon Gutierrez
0a4ea563d9 Fix response format of loyalty registration to remove unnecessary array #809 2024-10-07 08:11:29 +08:00
Ramon Gutierrez
d1059797a5 Simplify loyalty register result format #809 2024-10-07 07:01:47 +08:00
Ramon Gutierrez
bd655a459a Add loyalty connector and register endpoint #809 2024-10-07 06:28:57 +08:00
Ramon Gutierrez
b79f2f2dfb Merge branch 'master' into 799-subscription-support 2024-10-02 14:01:32 +08:00
Ramon Gutierrez
d2a0638ffa Fix customer retrieval from paymongo #799 2024-10-02 13:28:27 +08:00
Ramon Gutierrez
5056637b66 Add email field to subscription entity #799 2024-10-02 13:27:37 +08:00
Ramon Gutierrez
1fd883b07b Consolidate subscription setup payment intent checking and initial activation when applicable #799 2024-08-26 08:01:00 +08:00
Ramon Gutierrez
40c629eee3 Add endpoint for activating a subscription #799 2024-08-24 18:00:38 +08:00
Ramon Gutierrez
aa85198b7a Add subscription entity #799 2024-08-24 07:50:50 +08:00
Ramon Gutierrez
62f11c9ef5 Fix json payload structure for payment intent checking #799 2024-08-23 07:51:53 +08:00
Ramon Gutierrez
9dbaf92698 Add missing basic auth to payment intent endpoint #799 2024-08-23 05:07:11 +08:00
Ramon Gutierrez
d7cc0fc3de Add endpoint for re-checking payment intent status #799 2024-08-22 06:19:42 +08:00
Ramon Gutierrez
8c61a27376 Fix handling of payment intent values when returning created sub details #799 2024-08-21 07:44:08 +08:00
Ramon Gutierrez
0d9da221a7 Fix expected format from paymongo subscription endpoints #799 2024-08-21 01:16:43 +08:00
Ramon Gutierrez
919b56688d Add endpoint for creating subscriptions, sync updating of customers with paymongo API if record exists #799 2024-08-13 05:45:46 +08:00
Ramon Gutierrez
7af20f3d69 Replace sub fee endpoint with complete plan details #799 2024-08-12 16:30:49 +08:00
Ramon Gutierrez
b3548fcc50 Add null check for subscription fee on paymongo connector #799 2024-08-12 06:52:13 +08:00
Ramon Gutierrez
5a2f57492d Tie battery sizes to subscription plans #799 2024-08-12 06:50:01 +08:00
Ramon Gutierrez
debb399e96 Add support for handling multiple paymongo accounts #799 2024-08-12 05:09:58 +08:00
Ramon Gutierrez
e3649c3d2d Fix URL format of subscription fee endpoint #799 2024-08-11 15:26:48 +08:00
Ramon Gutierrez
4f5560f6f7 Update subscription fee endpoint to be specific to each vehicle model #799 2024-08-11 15:05:20 +08:00
Ramon Gutierrez
b67f960055 Add subscription MSRP field to battery sizes #799 2024-08-10 05:38:58 +08:00
Ramon Gutierrez
219d5c09d3 Add endpoint for subscription paymongo public key #799 2024-08-10 05:26:04 +08:00
Ramon Gutierrez
96a7cc929e Add email to customer info endpoint #799 2024-08-03 06:31:35 +08:00
Ramon Gutierrez
fe4806f41a Add endpoint for retrieving subscription fee #799 2024-08-03 06:31:12 +08:00
Ramon Gutierrez
17e583e11a Merge branch 'master' into 799-subscription-support 2024-07-31 18:08:45 +08:00
Ramon Gutierrez
a911b8c6c1 Merge branch 'master' into 799-subscription-support 2024-07-24 15:02:58 +08:00
Ramon Gutierrez
ab64161afb Add placeholder customer vehicle sub getter #799 2024-05-10 14:42:58 +08:00
Ramon Gutierrez
627b3da748 Disallow editing of IDs of static content #799 2024-05-05 07:14:26 +08:00
Ramon Gutierrez
48d87ae119 Retrieve subscription data with customer vehicle info endpoint #799 2024-05-05 07:13:07 +08:00
Ramon Gutierrez
2ccd1e0e2d Use proper getter for static content API #799 2024-04-29 18:07:00 +08:00
Ramon Gutierrez
c9cb6e8b53 Add markdown support to static content form view #799 2024-04-27 01:45:25 +08:00
Ramon Gutierrez
d9d4ffbecf Add static content endpoint for customer app #799 2024-04-27 01:44:39 +08:00
30 changed files with 1245 additions and 192 deletions

View file

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

View file

@ -17,6 +17,13 @@ parameters:
ios_app_version: "%env(IOS_APP_VERSION)%" ios_app_version: "%env(IOS_APP_VERSION)%"
insurance_premiums_banner_url: "%env(INSURANCE_PREMIUMS_BANNER_URL)%" insurance_premiums_banner_url: "%env(INSURANCE_PREMIUMS_BANNER_URL)%"
enabled_hub_filters: "%env(ENABLED_HUB_FILTERS)%" enabled_hub_filters: "%env(ENABLED_HUB_FILTERS)%"
insurance_paymongo_public_key: "%env(INSURANCE_PAYMONGO_PUBLIC_KEY)%"
insurance_paymongo_secret_key: "%env(INSURANCE_PAYMONGO_SECRET_KEY)%"
insurance_paymongo_webhook_id: "%env(INSURANCE_PAYMONGO_WEBHOOK_ID)%"
subscription_paymongo_public_key: "%env(SUBSCRIPTION_PAYMONGO_PUBLIC_KEY)%"
subscription_paymongo_secret_key: "%env(SUBSCRIPTION_PAYMONGO_SECRET_KEY)%"
subscription_paymongo_webhook_id: "%env(SUBSCRIPTION_PAYMONGO_WEBHOOK_ID)%"
subscription_months: "%env(SUBSCRIPTION_MONTHS)%"
services: services:
# default configuration for services in *this* file # default configuration for services in *this* file
@ -114,7 +121,6 @@ services:
arguments: arguments:
$em: "@doctrine.orm.entity_manager" $em: "@doctrine.orm.entity_manager"
$paymongo: "@App\\Service\\PayMongoConnector" $paymongo: "@App\\Service\\PayMongoConnector"
$webhook_id: "%env(PAYMONGO_WEBHOOK_ID)%"
# rider tracker service # rider tracker service
App\Service\RiderTracker: App\Service\RiderTracker:
@ -238,8 +244,13 @@ services:
App\Service\PayMongoConnector: App\Service\PayMongoConnector:
arguments: arguments:
$base_url: "%env(PAYMONGO_BASE_URL)%" $base_url: "%env(PAYMONGO_BASE_URL)%"
$public_key: "%env(PAYMONGO_PUBLIC_KEY)%"
$secret_key: "%env(PAYMONGO_SECRET_KEY)%" # loyalty system connector
App\Service\LoyaltyConnector:
arguments:
$base_url: "%env(LOYALTY_BASE_URL)%"
$api_key: "%env(LOYALTY_API_KEY)%"
$secret_key: "%env(LOYALTY_SECRET_KEY)%"
# entity listener for customer vehicle warranty code history # entity listener for customer vehicle warranty code history
App\EntityListener\CustomerVehicleSerialListener: App\EntityListener\CustomerVehicleSerialListener:

View file

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

View file

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

View file

@ -386,7 +386,6 @@ class RiderAppController extends ApiController
'flag_coolant' => $jo->hasCoolant(), 'flag_coolant' => $jo->hasCoolant(),
'has_motolite' => $cv->hasMotoliteBattery(), 'has_motolite' => $cv->hasMotoliteBattery(),
'delivery_status' => $jo->getDeliveryStatus(), '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); 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'])) if (isset($items['flag_motolite_battery']))
{ {
// get customer vehicle from jo // get customer vehicle from jo
@ -1360,15 +1359,6 @@ class RiderAppController extends ApiController
$jo->setModeOfPayment($payment_method); $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 // get capi user
$capi_user = $this->getUser(); $capi_user = $this->getUser();
if ($capi_user == null) if ($capi_user == null)
@ -1453,13 +1443,6 @@ class RiderAppController extends ApiController
else else
$jo->setHasCoolant(false); $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 // has motolite battery
$cv = $jo->getCustomerVehicle(); $cv = $jo->getCustomerVehicle();
$has_motolite = $req->request->get('has_motolite', 'false'); $has_motolite = $req->request->get('has_motolite', 'false');
@ -1560,9 +1543,6 @@ class RiderAppController extends ApiController
// get coolant if any // get coolant if any
$flag_coolant = $jo->hasCoolant(); $flag_coolant = $jo->hasCoolant();
// get sealant if any
$flag_sealant = $jo->hasSealant();
// check if new promo is null // check if new promo is null
if ($promo == null) if ($promo == null)
{ {
@ -1581,7 +1561,6 @@ class RiderAppController extends ApiController
->setCustomerVehicle($cv) ->setCustomerVehicle($cv)
->setSource($source) ->setSource($source)
->setHasCoolant($flag_coolant) ->setHasCoolant($flag_coolant)
->setHasSealant($flag_sealant)
->setIsTaxable(); ->setIsTaxable();
// set price tier // set price tier

View file

@ -13,6 +13,7 @@ use App\Ramcar\JOStatus;
use App\Entity\Warranty; use App\Entity\Warranty;
use App\Entity\JobOrder; use App\Entity\JobOrder;
use App\Entity\CustomerSession; use App\Entity\CustomerSession;
use App\Service\PayMongoConnector;
class ApiController extends BaseApiController class ApiController extends BaseApiController
{ {
@ -164,4 +165,12 @@ class ApiController extends BaseApiController
{ {
return 'Our services are currently limited to some areas in Metro Manila, Baguio, Batangas, Laguna, Cavite, Pampanga, and Palawan. We will update you as soon as we are available in your area. Thank you for understanding. Keep safe!'; return 'Our services are currently limited to some areas in Metro Manila, Baguio, Batangas, Laguna, Cavite, Pampanga, and Palawan. We will update you as soon as we are available in your area. Thank you for understanding. Keep safe!';
} }
protected function initializeSubscriptionPayMongoConnector(PayMongoConnector &$pm)
{
$pm->initialize(
$this->getParameter('subscription_paymongo_public_key'),
$this->getParameter('subscription_paymongo_secret_key'),
);
}
} }

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,315 @@
<?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 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());
}
error_log("FOUND PLAN FOR $vid: " . print_r($plan, true));
// 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',
]);
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 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'], $req->request->get('plan_id'));
$sub_pi = $pm_sub['response']['data']['attributes']['latest_invoice']['payment_intent'] ?? null;
// not the response we expected
if (empty($sub_pi)) {
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);
}
// save stuff to db
$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 subscription
$sub_obj = $this->em->getRepository(Subscription::class)->findOneBy([
'id' => $id,
'status' => SubscriptionStatus::PENDING,
'customer' => $this->session->getCustomer(),
]);
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 internal subscription status
if (
$sub_obj->getStatus() === SubscriptionStatus::PENDING &&
$pi['response']['data']['attributes']['status'] === 'succeeded'
) {
$sub_start_date = new DateTime();
$sub_end_date = clone $sub_start_date;
$sub_end_date->modify('+' . $this->getParameter('subscription_months') . ' month');
$sub_obj->setStatus(SubscriptionStatus::ACTIVE)
->setDateStart($sub_start_date)
->setDateEnd($sub_end_date);
$this->em->flush();
}
error_log("PI STATUS: " . $pi['response']['data']['attributes']['status']);
// response
return new ApiResponse(true, '', [
'payment_intent' => $pi['response']['data'],
]);
}
/*
public function getPaymentIntent(Request $req, $pid, PayMongoConnector $pm)
{
// check requirements
$validity = $this->validateRequest($req);
if (!$validity['is_valid']) {
return new ApiResponse(false, $validity['error']);
}
// initialize paymongo connector
$this->initializeSubscriptionPayMongoConnector($pm);
// get payment intent
$pi = $pm->getPaymentIntent($pid);
if (empty($pi['response']['data']['id'])) {
return new ApiResponse(false, 'Error retrieving payment intent. Please try again later.');
}
// response
return new ApiResponse(true, '', [
'payment_intent' => $pi['response']['data'],
]);
}
public function activateSubscription(Request $req, $id, PayMongoConnector $pm)
{
// check requirements
$validity = $this->validateRequest($req);
if (!$validity['is_valid']) {
return new ApiResponse(false, $validity['error']);
}
// initialize paymongo connector
$this->initializeSubscriptionPayMongoConnector($pm);
// get subscription
$obj = $this->em->getRepository(Subscription::class)->findOneBy([
'id' => $id,
'status' => SubscriptionStatus::PENDING,
'customer' => $this->session->getCustomer(),
]);
if (empty($obj)) {
return new ApiResponse(false, 'Invalid subscription provided.');
}
// get paymongo subscription so we can verify if the latest invoice is paid or not
$pm_sub = $pm->getSubscription($obj->getExtApiId());
if (empty($pm_sub['response']['data']['id'])) {
return new ApiResponse(false, 'Error retrieving subscription. Please try again later.');
}
// make sure the latest invoice has been paid
if ($pm_sub['response']['data']['attributes']['latest_invoice']['status'] !== 'succeeded') {
return new ApiResponse(false, 'Latest invoice for subscription is not yet paid.');
}
// mark subscription as paid
$obj->setStatus(SubscriptionStatus::ACTIVE)
->setDateStart(new DateTime());
$this->em->flush();
// response
return new ApiResponse(true, '', [
'success' => true,
]);
}
*/
}

View file

@ -3,6 +3,7 @@
namespace App\Controller\CustomerAppAPI; namespace App\Controller\CustomerAppAPI;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Catalyst\ApiBundle\Component\Response as ApiResponse; use Catalyst\ApiBundle\Component\Response as ApiResponse;
use CrEOF\Spatial\PHP\Types\Geometry\Point; use CrEOF\Spatial\PHP\Types\Geometry\Point;
@ -15,6 +16,7 @@ use App\Ramcar\JOStatus;
use App\Ramcar\ServiceType; use App\Ramcar\ServiceType;
use App\Ramcar\TradeInType; use App\Ramcar\TradeInType;
use App\Ramcar\InsuranceApplicationStatus; use App\Ramcar\InsuranceApplicationStatus;
use App\Ramcar\SubscriptionStatus;
use App\Service\PayMongoConnector; use App\Service\PayMongoConnector;
use App\Service\PriceTierManager; use App\Service\PriceTierManager;
use DateTime; use DateTime;
@ -115,7 +117,7 @@ class VehicleController extends ApiController
} }
public function getVehicle(Request $req, $id, PayMongoConnector $paymongo) public function getVehicle(Request $req, $id, PayMongoConnector $paymongo, ParameterBagInterface $params)
{ {
// check requirements // check requirements
$validity = $this->validateRequest($req); $validity = $this->validateRequest($req);
@ -139,7 +141,7 @@ class VehicleController extends ApiController
// response // response
return new ApiResponse(true, '', [ return new ApiResponse(true, '', [
'vehicle' => $this->generateVehicleInfo($cv, true, $paymongo), 'vehicle' => $this->generateVehicleInfo($cv, true, true, $paymongo, $params),
]); ]);
} }
@ -210,7 +212,7 @@ class VehicleController extends ApiController
]); ]);
} }
public function listVehicles(Request $req, PayMongoConnector $paymongo) public function listVehicles(Request $req, PayMongoConnector $paymongo, ParameterBagInterface $params)
{ {
// validate params // validate params
$validity = $this->validateRequest($req); $validity = $this->validateRequest($req);
@ -231,7 +233,7 @@ class VehicleController extends ApiController
// only get the customer's vehicles whose flag_active is true // only get the customer's vehicles whose flag_active is true
$cvs = $this->em->getRepository(CustomerVehicle::class)->findBy(['flag_active' => true, 'customer' => $cust]); $cvs = $this->em->getRepository(CustomerVehicle::class)->findBy(['flag_active' => true, 'customer' => $cust]);
foreach ($cvs as $cv) { foreach ($cvs as $cv) {
$cv_list[] = $this->generateVehicleInfo($cv, true, $paymongo); $cv_list[] = $this->generateVehicleInfo($cv, true, true, $paymongo, $params);
} }
// response // response
@ -393,7 +395,7 @@ class VehicleController extends ApiController
// TODO: possibly refactor this bit // TODO: possibly refactor this bit
// if no valid previous JO is found, base the trade-in value on recommended batteries // if no valid previous JO is found, base the trade-in value on recommended batteries
if (!$previous_jo_found) { if (!$previous_jo_found) {
$comp_batteries = $cv->getVehicle()->getBatteries(); $comp_batteries = $cv->getVehicle()->getActiveBatteries();
// get the lowest trade-in value from the list of batteries // get the lowest trade-in value from the list of batteries
if (!empty($comp_batteries)) { if (!empty($comp_batteries)) {
@ -417,7 +419,7 @@ class VehicleController extends ApiController
]; ];
} }
protected function generateVehicleInfo(CustomerVehicle $cv, $include_insurance = false, PayMongoConnector $paymongo) protected function generateVehicleInfo(CustomerVehicle $cv, $include_insurance = false, $include_active_sub = false, PayMongoConnector $paymongo, ParameterBagInterface $params)
{ {
$battery_id = null; $battery_id = null;
if ($cv->getCurrentBattery() != null) if ($cv->getCurrentBattery() != null)
@ -437,6 +439,8 @@ class VehicleController extends ApiController
'cv_id' => $cv->getID(), 'cv_id' => $cv->getID(),
'mfg_id' => $cv->getVehicle()->getManufacturer()->getID(), 'mfg_id' => $cv->getVehicle()->getManufacturer()->getID(),
'make_id' => $cv->getVehicle()->getID(), 'make_id' => $cv->getVehicle()->getID(),
'mfg_name' => $cv->getVehicle()->getManufacturer()->getName(),
'make_name' => $cv->getVehicle()->getMake(),
'name' => $cv_name, 'name' => $cv_name,
'plate_num' => $cv->getPlateNumber(), 'plate_num' => $cv->getPlateNumber(),
'model_year' => $cv->getModelYear(), 'model_year' => $cv->getModelYear(),
@ -467,6 +471,12 @@ class VehicleController extends ApiController
// TODO: maybe handle this more elegantly. issue is not sure it is a good idea to update the db for this very transient status as the webhook listener also updates this status right away // TODO: maybe handle this more elegantly. issue is not sure it is a good idea to update the db for this very transient status as the webhook listener also updates this status right away
switch ($status) { switch ($status) {
case InsuranceApplicationStatus::CREATED: case InsuranceApplicationStatus::CREATED:
// initialize paymongo connector
$paymongo->initialize(
$params->get('insurance_paymongo_public_key'),
$params->get('insurance_paymongo_secret_key')
);
// get latest status on this checkout from paymongo // get latest status on this checkout from paymongo
$checkout = $paymongo->getCheckout($gt->getExtTransactionId()); $checkout = $paymongo->getCheckout($gt->getExtTransactionId());
@ -506,6 +516,17 @@ class VehicleController extends ApiController
$row['latest_insurance'] = $insurance; $row['latest_insurance'] = $insurance;
} }
// get active subscription row
if ($include_active_sub) {
$active_sub = null;
$sobj = $cv->getLatestSubscription();
if (!empty($sobj) && $sobj->getStatus() == SubscriptionStatus::ACTIVE) {
$active_sub = $sobj;
}
$row['active_subscription'] = $active_sub;
}
return $row; return $row;
} }

View file

@ -754,8 +754,6 @@ class JobOrderController extends Controller
$promo_id = $req->request->get('promo'); $promo_id = $req->request->get('promo');
$cvid = $req->request->get('cvid'); $cvid = $req->request->get('cvid');
$service_charges = $req->request->get('service_charges', []); $service_charges = $req->request->get('service_charges', []);
$flag_coolant = $req->request->get('flag_coolant', false);
$flag_sealant = $req->request->get('flag_sealant', false);
// coordinates // coordinates
// need to check if lng and lat are set // need to check if lng and lat are set
@ -786,9 +784,7 @@ class JobOrderController extends Controller
->setCustomerVehicle($cv) ->setCustomerVehicle($cv)
->setIsTaxable() ->setIsTaxable()
->setSource(TransactionOrigin::CALL) ->setSource(TransactionOrigin::CALL)
->setPriceTier($price_tier) ->setPriceTier($price_tier);
->setHasCoolant($flag_coolant)
->setHasSealant($flag_sealant);
/* /*
// if it's a jumpstart or troubleshoot only, we know what to charge already // if it's a jumpstart or troubleshoot only, we know what to charge already

View file

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

View file

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

View file

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

View file

@ -441,12 +441,6 @@ class JobOrder
*/ */
protected $flag_cust_new; protected $flag_cust_new;
// only for tire service, if it requires sealant or not
/**
* @ORM\Column(type="boolean")
*/
protected $flag_sealant;
public function __construct() public function __construct()
{ {
$this->date_create = new DateTime(); $this->date_create = new DateTime();
@ -464,7 +458,6 @@ class JobOrder
$this->trade_in_type = null; $this->trade_in_type = null;
$this->flag_rider_rating = false; $this->flag_rider_rating = false;
$this->flag_coolant = false; $this->flag_coolant = false;
$this->flag_sealant = false;
$this->priority = 0; $this->priority = 0;
$this->meta = []; $this->meta = [];
@ -1263,15 +1256,4 @@ class JobOrder
return $this->flag_cust_new; return $this->flag_cust_new;
} }
public function setHasSealant($flag = true)
{
$this->flag_sealant = $flag;
return $this;
}
public function hasSealant()
{
return $this->flag_sealant;
}
} }

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

@ -0,0 +1,214 @@
<?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;
// 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->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 getGatewayTransactions()
{
// TODO: get gateway transactions here via type and metadata
}
}

View file

@ -31,7 +31,6 @@ class TireRepair implements InvoiceRuleInterface
public function compute($criteria, &$total) public function compute($criteria, &$total)
{ {
$stype = $criteria->getServiceType(); $stype = $criteria->getServiceType();
$has_sealant = $criteria->hasSealant();
$pt_id = $criteria->getPriceTier(); $pt_id = $criteria->getPriceTier();
$items = []; $items = [];
@ -57,28 +56,8 @@ class TireRepair implements InvoiceRuleInterface
'price' => $price, 'price' => $price,
]; ];
$qty_fee = bcmul($qty, $price, 2); $qty_price = bcmul($price, $qty, 2);
$total_price = $qty_fee; $total['total_price'] = bcadd($total['total_price'], $qty_price, 2);
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);
} }
return $items; return $items;
@ -152,28 +131,4 @@ class TireRepair implements InvoiceRuleInterface
return $title; 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;
}
} }

View file

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

View file

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

View file

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

View file

@ -69,14 +69,6 @@ class InvoiceManager implements InvoiceGeneratorInterface
->setCustomerVehicle($jo->getCustomerVehicle()) ->setCustomerVehicle($jo->getCustomerVehicle())
->setPriceTier($price_tier); ->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 // set if taxable
// NOTE: ideally, this should be a parameter when calling generateInvoiceCriteria. But that // 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 // would mean adding it as a parameter to the call, impacting all calls

View file

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

View file

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

View file

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

View file

@ -1207,8 +1207,6 @@
<script> <script>
var invoiceItems = []; var invoiceItems = [];
var hasCoolant = 0;
var hasSealant = 0;
// location search autocomplete // location search autocomplete
var input = document.getElementById('m_gmap_address'); var input = document.getElementById('m_gmap_address');
@ -1272,19 +1270,6 @@ $(function() {
{% endif %} {% endif %}
{% endif %} {% endif %}
{% endfor %} {% 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 %} {% endif %}
} }
@ -1845,8 +1830,6 @@ $(function() {
'cvid': cvid, 'cvid': cvid,
'coord_lng': lng, 'coord_lng': lng,
'coord_lat': lat, 'coord_lat': lat,
'flag_coolant': hasCoolant,
'flag_sealant': hasSealant,
} }
}).done(function(response) { }).done(function(response) {
// mark as invoice changed // mark as invoice changed

View file

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

View file

@ -9,6 +9,7 @@ battery_size_tradein_brand: Trade-in Motolite
battery_size_tradein_premium: Trade-in Premium battery_size_tradein_premium: Trade-in Premium
battery_size_tradein_other: Trade-in Other battery_size_tradein_other: Trade-in Other
battery_size_tradein_lazada: Trade-in Lazada battery_size_tradein_lazada: Trade-in Lazada
battery_Size_sub_recurring_fee: Subscription Recurring Fee
add_cust_vehicle_battery_info: This vehicle is using a Motolite battery add_cust_vehicle_battery_info: This vehicle is using a Motolite battery
jo_title_pdf: Motolite Res-Q Job Order jo_title_pdf: Motolite Res-Q Job Order
country_code_prefix: '+63' country_code_prefix: '+63'

View file

@ -1 +0,0 @@
INSERT INTO service_offering (name, code, fee) VALUES ('Tire Sealant Fee', 'tire_sealant_fee', '200.00');