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, ]); } */ }