diff --git a/config/routes/apiv2.yaml b/config/routes/apiv2.yaml index af316cb6..1aa86e16 100644 --- a/config/routes/apiv2.yaml +++ b/config/routes/apiv2.yaml @@ -317,4 +317,41 @@ apiv2_insurance_body_types: apiv2_loyalty_register: path: /apiv2/loyalty/register controller: App\Controller\CustomerAppAPI\LoyaltyController::register - methods: [POST] \ No newline at end of file + 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] diff --git a/config/services.yaml b/config/services.yaml index d87c16bb..268c0bf4 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -17,6 +17,13 @@ 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)%" services: # default configuration for services in *this* file @@ -114,7 +121,6 @@ services: arguments: $em: "@doctrine.orm.entity_manager" $paymongo: "@App\\Service\\PayMongoConnector" - $webhook_id: "%env(PAYMONGO_WEBHOOK_ID)%" # rider tracker service App\Service\RiderTracker: @@ -238,8 +244,6 @@ 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: diff --git a/src/Command/ProcessLatePaymongoTransactionsCommand.php b/src/Command/ProcessLatePaymongoTransactionsCommand.php index 8bdc66ca..50e3dc58 100644 --- a/src/Command/ProcessLatePaymongoTransactionsCommand.php +++ b/src/Command/ProcessLatePaymongoTransactionsCommand.php @@ -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(); } diff --git a/src/Controller/BatterySizeController.php b/src/Controller/BatterySizeController.php index 177780e8..0ffb3444 100644 --- a/src/Controller/BatterySizeController.php +++ b/src/Controller/BatterySizeController.php @@ -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!' diff --git a/src/Controller/CustomerAppAPI/ApiController.php b/src/Controller/CustomerAppAPI/ApiController.php index fdc0ec59..577a05a6 100644 --- a/src/Controller/CustomerAppAPI/ApiController.php +++ b/src/Controller/CustomerAppAPI/ApiController.php @@ -13,6 +13,7 @@ use App\Ramcar\JOStatus; use App\Entity\Warranty; use App\Entity\JobOrder; use App\Entity\CustomerSession; +use App\Service\PayMongoConnector; 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!'; } + + protected function initializeSubscriptionPayMongoConnector(PayMongoConnector &$pm) + { + $pm->initialize( + $this->getParameter('subscription_paymongo_public_key'), + $this->getParameter('subscription_paymongo_secret_key'), + ); + } } diff --git a/src/Controller/CustomerAppAPI/CustomerController.php b/src/Controller/CustomerAppAPI/CustomerController.php index 2abfe2e5..c8cc9e53 100644 --- a/src/Controller/CustomerAppAPI/CustomerController.php +++ b/src/Controller/CustomerAppAPI/CustomerController.php @@ -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 { @@ -43,13 +44,10 @@ class CustomerController extends ApiController ]); } - 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']); @@ -67,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(); } @@ -129,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) { diff --git a/src/Controller/CustomerAppAPI/InsuranceController.php b/src/Controller/CustomerAppAPI/InsuranceController.php index 271ebdb8..0916d8fa 100644 --- a/src/Controller/CustomerAppAPI/InsuranceController.php +++ b/src/Controller/CustomerAppAPI/InsuranceController.php @@ -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, diff --git a/src/Controller/CustomerAppAPI/StaticContentController.php b/src/Controller/CustomerAppAPI/StaticContentController.php new file mode 100644 index 00000000..0dec39cb --- /dev/null +++ b/src/Controller/CustomerAppAPI/StaticContentController.php @@ -0,0 +1,34 @@ +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(), + ]); + } +} diff --git a/src/Controller/CustomerAppAPI/SubscriptionController.php b/src/Controller/CustomerAppAPI/SubscriptionController.php new file mode 100644 index 00000000..2a22961d --- /dev/null +++ b/src/Controller/CustomerAppAPI/SubscriptionController.php @@ -0,0 +1,315 @@ +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, + ]); + } + */ +} diff --git a/src/Controller/CustomerAppAPI/VehicleController.php b/src/Controller/CustomerAppAPI/VehicleController.php index c8fd15e9..3c62ebec 100644 --- a/src/Controller/CustomerAppAPI/VehicleController.php +++ b/src/Controller/CustomerAppAPI/VehicleController.php @@ -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($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 $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($cv, true, true, $paymongo, $params); } // response @@ -393,7 +395,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 +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; if ($cv->getCurrentBattery() != null) @@ -437,6 +439,8 @@ class VehicleController extends ApiController 'cv_id' => $cv->getID(), 'mfg_id' => $cv->getVehicle()->getManufacturer()->getID(), 'make_id' => $cv->getVehicle()->getID(), + 'mfg_name' => $cv->getVehicle()->getManufacturer()->getName(), + 'make_name' => $cv->getVehicle()->getMake(), 'name' => $cv_name, 'plate_num' => $cv->getPlateNumber(), '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 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()); @@ -506,6 +516,17 @@ class VehicleController extends ApiController $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; } diff --git a/src/Controller/StaticContentController.php b/src/Controller/StaticContentController.php index 44c77595..95b9c8bd 100644 --- a/src/Controller/StaticContentController.php +++ b/src/Controller/StaticContentController.php @@ -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(); diff --git a/src/Entity/BatterySize.php b/src/Entity/BatterySize.php index 8b47d6af..3d7112fc 100644 --- a/src/Entity/BatterySize.php +++ b/src/Entity/BatterySize.php @@ -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; + } } diff --git a/src/Entity/CustomerVehicle.php b/src/Entity/CustomerVehicle.php index a569d4b2..c39e46c0 100644 --- a/src/Entity/CustomerVehicle.php +++ b/src/Entity/CustomerVehicle.php @@ -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; @@ -122,6 +123,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 +313,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; + } } diff --git a/src/Entity/Subscription.php b/src/Entity/Subscription.php new file mode 100644 index 00000000..1906bbf9 --- /dev/null +++ b/src/Entity/Subscription.php @@ -0,0 +1,214 @@ +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 + } +} diff --git a/src/Ramcar/SubscriptionStatus.php b/src/Ramcar/SubscriptionStatus.php new file mode 100644 index 00000000..9c957497 --- /dev/null +++ b/src/Ramcar/SubscriptionStatus.php @@ -0,0 +1,20 @@ + 'Pending', + 'active' => 'Active', + 'ended' => 'Ended', + 'cancelled' => 'Cancelled', + 'repossessed' => 'Reposessed', + ]; +} diff --git a/src/Service/CustomerHandler/ResqCustomerHandler.php b/src/Service/CustomerHandler/ResqCustomerHandler.php index f8685801..2f947cee 100644 --- a/src/Service/CustomerHandler/ResqCustomerHandler.php +++ b/src/Service/CustomerHandler/ResqCustomerHandler.php @@ -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(), ]; diff --git a/src/Service/PayMongoConnector.php b/src/Service/PayMongoConnector.php index 3abafc22..90748ac8 100644 --- a/src/Service/PayMongoConnector.php +++ b/src/Service/PayMongoConnector.php @@ -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,251 @@ 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 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, + ], + ], + ]; + + 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) { $error = ['message' => $e->getMessage()]; diff --git a/templates/battery-size/form.html.twig b/templates/battery-size/form.html.twig index 06d3fadd..2138c5ab 100644 --- a/templates/battery-size/form.html.twig +++ b/templates/battery-size/form.html.twig @@ -71,15 +71,24 @@
-
- -
- - -
-
+
+ +
+ + +
+
+
+ +
+ + +
+
diff --git a/templates/static-content/form.html.twig b/templates/static-content/form.html.twig index dece1005..0a1f4f0d 100644 --- a/templates/static-content/form.html.twig +++ b/templates/static-content/form.html.twig @@ -39,7 +39,7 @@ - +
@@ -48,7 +48,7 @@ - +
@@ -70,8 +70,19 @@ {% endblock %} +{% block stylesheets %} + +{% endblock %} + {% block scripts %} +