diff --git a/config/packages/security.yaml b/config/packages/security.yaml index acb25b0a..b745b4e3 100644 --- a/config/packages/security.yaml +++ b/config/packages/security.yaml @@ -53,6 +53,10 @@ security: pattern: ^\/test_capi\/ security: false + paymongo: + pattern: ^\/paymongo\/ + security: false + cust_api_v2: pattern: ^\/apiv2\/(?!register|register\/|number_confirm|number_confirm\/|code_validate|code_validate\/|resend_code|resend_code\/|version_check|version_check\/|account|account\/|account_code_validate|account_code_validate\/|account_resend_code|account_resend_code\/) provider: api_v2_provider diff --git a/config/routes/insurance.yaml b/config/routes/insurance.yaml index cf8b2974..27a40144 100644 --- a/config/routes/insurance.yaml +++ b/config/routes/insurance.yaml @@ -4,13 +4,3 @@ insurance_listener: path: /insurance/listen controller: App\Controller\InsuranceController::listen methods: [POST] - -insurance_payment_success: - path: /insurance/payment/success - controller: App\Controller\InsuranceController::paymentSuccess - methods: [GET] - -insurance_payment_cancel: - path: /insurance/payment/cancel - controller: App\Controller\InsuranceController::paymentCancel - methods: [GET] \ No newline at end of file diff --git a/config/routes/paymongo.yaml b/config/routes/paymongo.yaml index 3cccfa97..df0ced68 100644 --- a/config/routes/paymongo.yaml +++ b/config/routes/paymongo.yaml @@ -4,3 +4,13 @@ paymongo_listener: path: /paymongo/listen controller: App\Controller\PayMongoController::listen methods: [POST] + +paymongo_payment_success: + path: /paymongo/success + controller: App\Controller\PayMongoController::paymentSuccess + methods: [GET] + +paymongo_payment_cancelled: + path: /paymongo/cancelled + controller: App\Controller\PayMongoController::paymentCancelled + methods: [GET] \ No newline at end of file diff --git a/config/services.yaml b/config/services.yaml index a9e5b16e..b19ecabc 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -216,6 +216,16 @@ services: $username: "%env(INSURANCE_USERNAME)%" $password: "%env(INSURANCE_PASSWORD)%" + # entity listener for gateway transactions + App\EntityListener\GatewayTransactionListener: + arguments: + $em: "@doctrine.orm.entity_manager" + $ic: "@App\\Service\\InsuranceConnector" + tags: + - name: doctrine.orm.entity_listener + event: 'postUpdate' + entity: 'App\Entity\GatewayTransaction' + # paymongo connector App\Service\PayMongoConnector: arguments: diff --git a/src/Controller/CustomerAppAPI/InsuranceController.php b/src/Controller/CustomerAppAPI/InsuranceController.php index 5832d336..eca85b69 100644 --- a/src/Controller/CustomerAppAPI/InsuranceController.php +++ b/src/Controller/CustomerAppAPI/InsuranceController.php @@ -11,12 +11,13 @@ use App\Service\InsuranceConnector; use App\Service\PayMongoConnector; use App\Entity\InsuranceApplication; +use App\Entity\GatewayTransaction; use App\Entity\CustomerVehicle; use App\Ramcar\InsuranceApplicationStatus; use App\Ramcar\InsuranceMVType; use App\Ramcar\InsuranceClientType; - +use App\Ramcar\TransactionStatus; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; use DateTime; @@ -131,51 +132,65 @@ class InsuranceController extends ApiController $req->files->get('orcr_file') ); if (!$result['success']) { - return new APIResponse(false, $result['error']['message']); + return new ApiResponse(false, $result['error']['message']); } + $premium_amount_int = (int)bcmul($result['response']['premium'], 100); + // build checkout item and metadata $items = [ [ 'name' => "Insurance Premium", 'description' => "Premium fee for vehicle insurance", 'quantity' => 1, - 'amount' => (int)bcmul($result['response']['premium'], 100), + 'amount' => $premium_amount_int, 'currency' => 'PHP', ], ]; - $metadata = [ - 'customer_id' => $cust->getID(), - 'customer_vehicle_id' => $cv->getID(), - ]; + $now = new DateTime(); + + // create gateway transaction + $gt = new GatewayTransaction(); + $gt->setCustomer($cust); + $gt->setDateCreate($now); + $gt->setAmount($premium_amount_int); + $gt->setStatus(TransactionStatus::PENDING); + $gt->setGateway('paymongo'); // TODO: define values elsewhere + $gt->setType('insurance_premium'); // TODO: define values elsewhere + $gt->setExtTransactionId($result['response']['id']); + $this->em->persist($gt); + $this->em->flush(); // create paymongo checkout resource $checkout = $paymongo->createCheckout( $cust, $items, - implode("-", [$cust->getID(), $result['response']['id']]), + $gt->getID(), "Motolite RES-Q Vehicle Insurance", - $router->generate('insurance_payment_success', [], UrlGeneratorInterface::ABSOLUTE_URL), - $router->generate('insurance_payment_cancel', [], UrlGeneratorInterface::ABSOLUTE_URL), - $metadata, + $router->generate('paymongo_payment_success', [], UrlGeneratorInterface::ABSOLUTE_URL), + $router->generate('paymongo_payment_cancelled', [], UrlGeneratorInterface::ABSOLUTE_URL), + ['transaction_id' => $gt->getID()], // NOTE: passing this here too for payment resource metadata ); if (!$checkout['success']) { - return new APIResponse(false, $checkout['error']['message']); + return new ApiResponse(false, $checkout['error']['message']); } $checkout_url = $checkout['response']['data']['attributes']['checkout_url']; + // add checkout url and id to transaction metadata + $gt->setMetadata([ + 'checkout_url' => $checkout_url, + 'checkout_id' => $checkout['response']['data']['id'], + ]); + // store application in db $app = new InsuranceApplication(); - $app->setDateSubmitted(new DateTime()); + $app->setDateSubmit($now); $app->setCustomer($cust); $app->setCustomerVehicle($cv); - $app->setTransactionID($result['response']['id']); - $app->setPremiumAmount($result['response']['premium']); + $app->setGatewayTransaction($gt); $app->setStatus(InsuranceApplicationStatus::CREATED); - $app->setCheckoutURL($checkout_url); - $app->setCheckoutID($checkout['response']['data']['id']); $app->setMetadata($input); $this->em->persist($app); $this->em->flush(); @@ -200,7 +215,7 @@ class InsuranceController extends ApiController // get maker list $result = $this->client->getVehicleMakers(); if (!$result['success']) { - return new APIResponse(false, $result['error']['message']); + return new ApiResponse(false, $result['error']['message']); } return new ApiResponse(true, '', [ @@ -220,7 +235,7 @@ class InsuranceController extends ApiController // get maker list $result = $this->client->getVehicleModels($maker_id); if (!$result['success']) { - return new APIResponse(false, $result['error']['message']); + return new ApiResponse(false, $result['error']['message']); } return new ApiResponse(true, '', [ @@ -240,7 +255,7 @@ class InsuranceController extends ApiController // get maker list $result = $this->client->getVehicleTrims($model_id); if (!$result['success']) { - return new APIResponse(false, $result['error']['message']); + return new ApiResponse(false, $result['error']['message']); } return new ApiResponse(true, '', [ diff --git a/src/Controller/PayMongoController.php b/src/Controller/PayMongoController.php index 052cc521..167752e4 100644 --- a/src/Controller/PayMongoController.php +++ b/src/Controller/PayMongoController.php @@ -2,6 +2,8 @@ namespace App\Controller; +use App\Entity\GatewayTransaction; +use App\Ramcar\TransactionStatus; use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\HttpFoundation\Request; @@ -10,14 +12,89 @@ use Symfony\Bundle\FrameworkBundle\Controller\Controller; class PayMongoController extends Controller { - public function listen(Request $req, EntityManagerInterface $em) + protected $em; + + public function __construct(EntityManagerInterface $em) { - $payload = $req->request->all(); - error_log(print_r($payload, true)); + $this->em = $em; + } + + public function listen(Request $req) + { + $payload = json_decode($req->getContent(), true); + + // DEBUG + @file_put_contents(__DIR__ . '/../../var/log/paymongo.log', print_r($payload, true) . "\r\n----------------------------------------\r\n\r\n", FILE_APPEND); + + /* + return $this->json([ + 'success' => true, + ]); + */ + + // END DEBUG + + // get event type and process accordingly + $attr = $payload['data']['attributes']; + $event = $attr['data']; + $event_name = $attr['type']; + + switch ($event_name) { + case "payment.paid": + return $this->handlePaymentPaid($event); + break; + case "payment.failed": + return $this->handlePaymentPaid($event); + break; + case "payment.refunded": // TODO: handle refunds + case "payment.refund.updated": + case "checkout_session.payment.paid": + default: + break; + } return $this->json([ 'success' => true, - 'payload' => $payload, ]); } + + protected function handlePaymentPaid($event) + { + $metadata = $event['attributes']['metadata']; + $obj = $this->getTransaction($metadata['transaction_id']); + + // mark as paid + $obj->setStatus(TransactionStatus::PAID); + $this->em->flush(); + + return $this->json([ + 'success' => true, + ]); + } + + protected function handlePaymentFailed(Request $req) + { + // TODO: do something about failed payments? + return $this->json([ + 'success' => true, + ]); + } + + protected function getTransaction($id) + { + //$class_name = 'App\\Entity\\' . $type; + //$instance = new $class_name; + + return $this->em->getRepository(GatewayTransaction::class)->find($id); + } + + public function paymentSuccess(Request $req) + { + return $this->render('paymongo/success.html.twig'); + } + + public function paymentCancelled(Request $req) + { + return $this->render('paymongo/cancelled.html.twig'); + } } diff --git a/src/Entity/GatewayTransaction.php b/src/Entity/GatewayTransaction.php new file mode 100644 index 00000000..2bd6ecb8 --- /dev/null +++ b/src/Entity/GatewayTransaction.php @@ -0,0 +1,204 @@ +date_create = new DateTime(); + $this->status = TransactionStatus::PENDING; + $this->metadata = []; + } + + public function getID() + { + return $this->id; + } + + public function setCustomer(Customer $customer) + { + $this->customer = $customer; + return $this; + } + + public function getCustomer() + { + return $this->customer; + } + + public function setDateCreate(DateTime $date) + { + $this->date_create = $date; + return $this; + } + + public function getDateCreate() + { + return $this->date_create; + } + + public function setDatePay(DateTime $date) + { + $this->date_pay = $date; + return $this; + } + + public function getDatePay() + { + return $this->date_pay; + } + + public function setAmount($amount) + { + $this->amount = $amount; + return $this; + } + + public function getAmount() + { + return $this->amount; + } + + public function setStatus($status) + { + $this->status = $status; + return $this; + } + + public function getStatus() + { + return $this->status; + } + + public function setType($type) + { + $this->type = $type; + return $this; + } + + public function getType() + { + return $this->type; + } + + public function setGateway($gateway) + { + $this->gateway = $gateway; + return $this; + } + + public function getGateway() + { + return $this->gateway; + } + + public function setExtTransactionId($transaction_id) + { + $this->ext_transaction_id = $transaction_id; + return $this; + } + + public function getExtTransactionId() + { + return $this->ext_transaction_id; + } + + public function setCallbackClass($callback_class) + { + $this->callback_class = $callback_class; + return $this; + } + + public function getCallbackClass() + { + return $this->callback_class; + } + + public function setMetadata($metadata) + { + $this->metadata = $metadata; + return $this; + } + + public function getMetadata() + { + return $this->metadata; + } +} diff --git a/src/Entity/InsuranceApplication.php b/src/Entity/InsuranceApplication.php index d08b5317..7cda138c 100644 --- a/src/Entity/InsuranceApplication.php +++ b/src/Entity/InsuranceApplication.php @@ -36,19 +36,12 @@ class InsuranceApplication */ protected $customer_vehicle; - // paramount transaction id + // gateway transaction /** - * @ORM\Column(type="string", length=32) - * @Assert\NotBlank() + * @ORM\OneToOne(targetEntity="GatewayTransaction") + * @ORM\JoinColumn(name="gateway_transaction_id", referencedColumnName="id") */ - protected $transaction_id; - - // premium amount - /** - * @ORM\Column(type="decimal", precision=7, scale=2) - * @Assert\NotBlank() - */ - protected $premium_amount; + protected $gateway_transaction; // status /** @@ -66,19 +59,19 @@ class InsuranceApplication /** * @ORM\Column(type="datetime") */ - protected $date_submitted; + protected $date_submit; // date the application was paid /** * @ORM\Column(type="datetime", nullable=true) */ - protected $date_paid; + protected $date_pay; // date the application was marked as completed by the insurance api /** * @ORM\Column(type="datetime", nullable=true) */ - protected $date_completed; + protected $date_complete; // form data when submitting the application /** @@ -86,23 +79,11 @@ class InsuranceApplication */ protected $metadata; - // paymongo checkout url - /** - * @ORM\Column(type="string", length=255, nullable=true) - */ - protected $checkout_url; - - // paymongo checkout id - /** - * @ORM\Column(type="string", length=32, nullable=true) - */ - protected $checkout_id; - public function __construct() { - $this->date_submitted = new DateTime(); - $this->date_paid = null; - $this->date_completed = null; + $this->date_submit = new DateTime(); + $this->date_pay = null; + $this->date_complete = null; $this->metadata = []; } @@ -133,35 +114,26 @@ class InsuranceApplication return $this->customer_vehicle; } - public function setDateSubmitted(DateTime $date) + public function setDateSubmit(DateTime $date) { - $this->date_submitted = $date; + $this->date_submit = $date; return $this; } - public function getDateSubmitted() + public function getDateSubmit() { - return $this->date_submitted; + return $this->date_submit; } - public function setTransactionID($id) + public function setGatewayTransaction(GatewayTransaction $transaction) { - return $this->transaction_id = $id; + $this->gateway_transaction = $transaction; + return $this; } - public function getTransactionID() + public function getGatewayTransaction() { - return $this->transaction_id; - } - - public function setPremiumAmount($amount) - { - return $this->premium_amount = $amount; - } - - public function getPremiumAmount() - { - return $this->premium_amount; + return $this->gateway_transaction; } public function setStatus($status) @@ -184,26 +156,26 @@ class InsuranceApplication return $this->coc_url; } - public function setDatePaid(DateTime $date) + public function setDatePay(DateTime $date) { - $this->date_paid = $date; + $this->date_pay = $date; return $this; } - public function getDatePaid() + public function getDatePay() { - return $this->date_paid; + return $this->date_pay; } - public function setDateCompleted(DateTime $date) + public function setDateComplete(DateTime $date) { - $this->date_completed = $date; + $this->date_complete = $date; return $this; } - public function getDateCompleted() + public function getDateComplete() { - return $this->date_completed; + return $this->date_complete; } public function setMetadata($metadata) @@ -215,24 +187,4 @@ class InsuranceApplication { return $this->metadata; } - - public function setCheckoutURL($url) - { - return $this->checkout_url = $url; - } - - public function getCheckoutURL() - { - return $this->checkout_url; - } - - public function setCheckoutID($id) - { - return $this->checkout_id = $id; - } - - public function getCheckoutID() - { - return $this->checkout_id; - } } diff --git a/src/EntityListener/GatewayTransactionListener.php b/src/EntityListener/GatewayTransactionListener.php new file mode 100644 index 00000000..1181a12b --- /dev/null +++ b/src/EntityListener/GatewayTransactionListener.php @@ -0,0 +1,75 @@ +em = $em; + $this->ic = $ic; + } + + public function postUpdate(GatewayTransaction $gt_obj, LifecycleEventArgs $args) + { + // get transaction changes + $em = $args->getEntityManager(); + $uow = $em->getUnitOfWork(); + $changeset = $uow->getEntityChangeSet($gt_obj); + + if (array_key_exists('status', $changeset)) { + $field_changes = $changeset['status']; + + $prev_value = $field_changes[0] ?? null; + $new_value = $field_changes[1] ?? null; + + // only do something if the status has changed to paid + if ($prev_value !== $new_value && $new_value === TransactionStatus::PAID) { + // handle based on type + // TODO: add types here as we go. there's probably a better way to do this. + switch ($gt_obj->getType()) { + case 'insurance_premium': + return $this->handleInsurancePremium($gt_obj); + break; + default: + break; + } + } + } + } + + protected function handleInsurancePremium($gt_obj) + { + // get insurance application object + $obj = $this->em->getRepository(InsuranceApplication::class)->findOneBy([ + 'gateway_transaction' => $gt_obj, + ]); + + if ($obj) { + // mark as paid + $obj->setDatePay(new DateTime()); + $obj->setStatus(InsuranceApplicationStatus::PAID); + $this->em->flush(); + } + + // flag on api as paid + $result = $this->ic->tagApplicationPaid($obj->getID()); + if (!$result['success']) { + error_log("INSURANCE MARK AS PAID FAILED FOR " . $obj->getID() . ": " . $result['error']['message']); + } + } +} + diff --git a/src/Ramcar/TransactionStatus.php b/src/Ramcar/TransactionStatus.php new file mode 100644 index 00000000..37c55061 --- /dev/null +++ b/src/Ramcar/TransactionStatus.php @@ -0,0 +1,18 @@ + 'Pending', + 'paid' => 'Paid', + 'cancelled' => 'Cancelled', + 'refunded' => 'Refunded', + ]; +} diff --git a/src/Service/InsuranceConnector.php b/src/Service/InsuranceConnector.php index adfcfaef..b56415c0 100644 --- a/src/Service/InsuranceConnector.php +++ b/src/Service/InsuranceConnector.php @@ -110,6 +110,7 @@ class InsuranceConnector error_log("Insurance API Error: " . $error['message']); error_log(Psr7\Message::toString($e->getRequest())); + error_log($e->getResponse()->getBody()->getContents()); if ($e->hasResponse()) { $error['response'] = Psr7\Message::toString($e->getResponse()); @@ -121,7 +122,7 @@ class InsuranceConnector ]; } - //error_log(print_r(json_decode($response->getBody(), true), true)); + error_log(print_r(json_decode($response->getBody(), true), true)); return [ 'success' => true, diff --git a/src/Service/PayMongoConnector.php b/src/Service/PayMongoConnector.php index d26ef7fd..34ec1f6c 100644 --- a/src/Service/PayMongoConnector.php +++ b/src/Service/PayMongoConnector.php @@ -51,18 +51,21 @@ class PayMongoConnector * ['name', 'description', 'quantity', 'amount', 'currency'] */ 'line_items' => $items, - 'reference_number' => $ref_no, + 'reference_number' => (string)$ref_no, 'cancel_url' => $cancel_url, 'success_url' => $success_url, 'statement_descriptor' => $description, 'send_email_receipt' => true, 'show_description' => true, 'show_line_items' => false, - 'metadata' => $metadata, ], ], ]; + if (!empty($metadata)) { + $body['data']['attributes']['metadata'] = $metadata; + } + return $this->doRequest('/v1/checkout_sessions', 'POST', $body); } diff --git a/templates/paymongo/cancelled.html.twig b/templates/paymongo/cancelled.html.twig new file mode 100644 index 00000000..b8a64d68 --- /dev/null +++ b/templates/paymongo/cancelled.html.twig @@ -0,0 +1,22 @@ + + + + + + Payment Cancelled + + + + + + \ No newline at end of file diff --git a/templates/paymongo/success.html.twig b/templates/paymongo/success.html.twig new file mode 100644 index 00000000..e481152a --- /dev/null +++ b/templates/paymongo/success.html.twig @@ -0,0 +1,22 @@ + + + + + + Payment Successful + + + + + + \ No newline at end of file