resq/src/Controller/CustomerAppAPI/SubscriptionController.php

388 lines
14 KiB
PHP

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