diff --git a/config/packages/security.yaml b/config/packages/security.yaml index acb25b0a..7e9140dc 100644 --- a/config/packages/security.yaml +++ b/config/packages/security.yaml @@ -53,6 +53,14 @@ security: pattern: ^\/test_capi\/ security: false + insurance: + pattern: ^\/insurance\/ + 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/apiv2.yaml b/config/routes/apiv2.yaml index af7d0084..ec999b90 100644 --- a/config/routes/apiv2.yaml +++ b/config/routes/apiv2.yaml @@ -45,6 +45,11 @@ apiv2_cust_vehicle_add: controller: App\Controller\CustomerAppAPI\VehicleController::addVehicle methods: [POST] +apiv2_cust_vehicle_info: + path: /apiv2/vehicles/{id} + controller: App\Controller\CustomerAppAPI\VehicleController::getVehicle + methods: [GET] + apiv2_cust_vehicle_update: path: /apiv2/vehicles/{id} controller: App\Controller\CustomerAppAPI\VehicleController::updateVehicle @@ -260,4 +265,35 @@ apiv2_account_delete_resend_code: apiv2_account_delete_code_validate: path: /apiv2/account_delete_code_validate controller: App\Controller\CustomerAppAPI\AccountController::validateDeleteCode + methods: [POST] + +# insurance +apiv2_insurance_vehicle_maker_list: + path: /apiv2/insurance/vehicles/makers + controller: App\Controller\CustomerAppAPI\InsuranceController::getVehicleMakers + methods: [GET] + +apiv2_insurance_vehicle_model_list: + path: /apiv2/insurance/vehicles/models/{maker_id} + controller: App\Controller\CustomerAppAPI\InsuranceController::getVehicleModels + methods: [GET] + +apiv2_insurance_vehicle_trim_list: + path: /apiv2/insurance/vehicles/trims/{model_id} + controller: App\Controller\CustomerAppAPI\InsuranceController::getVehicleTrims + methods: [GET] + +apiv2_insurance_vehicle_mv_type_list: + path: /apiv2/insurance/mvtypes + controller: App\Controller\CustomerAppAPI\InsuranceController::getMVTypes + methods: [GET] + +apiv2_insurance_vehicle_client_type_list: + path: /apiv2/insurance/clienttypes + controller: App\Controller\CustomerAppAPI\InsuranceController::getClientTypes + methods: [GET] + +apiv2_insurance_application_create: + path: /apiv2/insurance/application + controller: App\Controller\CustomerAppAPI\InsuranceController::createApplication methods: [POST] \ No newline at end of file diff --git a/config/routes/insurance.yaml b/config/routes/insurance.yaml index bf3e3814..27a40144 100644 --- a/config/routes/insurance.yaml +++ b/config/routes/insurance.yaml @@ -1,6 +1,6 @@ # insurance insurance_listener: - path: /api/insurance/listen + path: /insurance/listen controller: App\Controller\InsuranceController::listen methods: [POST] diff --git a/config/routes/paymongo.yaml b/config/routes/paymongo.yaml new file mode 100644 index 00000000..df0ced68 --- /dev/null +++ b/config/routes/paymongo.yaml @@ -0,0 +1,16 @@ +# paymongo + +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 b59e8008..b19ecabc 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -216,6 +216,23 @@ 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: + $base_url: "%env(PAYMONGO_BASE_URL)%" + $public_key: "%env(PAYMONGO_PUBLIC_KEY)%" + $secret_key: "%env(PAYMONGO_SECRET_KEY)%" + # entity listener for customer vehicle warranty code history App\EntityListener\CustomerVehicleSerialListener: arguments: diff --git a/src/Controller/CustomerAppAPI/InsuranceController.php b/src/Controller/CustomerAppAPI/InsuranceController.php new file mode 100644 index 00000000..a3c5624a --- /dev/null +++ b/src/Controller/CustomerAppAPI/InsuranceController.php @@ -0,0 +1,324 @@ +client = $client; + } + + public function createApplication(Request $req, PayMongoConnector $paymongo, UrlGeneratorInterface $router) + { + // validate params + $validity = $this->validateRequest($req, [ + // internal + 'customer_vehicle_id', + + // client info + 'client_type', + 'first_name', + //'middle_name', // not required + 'surname', + 'corporate_name', + + // client contact info + 'address_number', + //'address_street', // not required + //'address_building', // not required + 'address_barangay', + 'address_city', + 'address_province', + 'zipcode', + 'mobile_number', + 'email_address', + + // car info + 'make', + 'model', + 'series', + 'color', + //'plate_number', // NOTE: we get this from the internal cv record instead + 'mv_file_number', + 'motor_number', + 'serial_chasis', // NOTE: this is how it's spelled on their API + 'year_model', + 'mv_type_id', + 'body_type', + //'is_public', // not required, boolean, only show field if mv_type_id in [4, 13] + //'orcr_file', // this is a file + + // mv_type_id specific fields + //'vehicle_use_type', // not required, only show field if mv_type_id is not in [4, 13]. accepted values are: 'commercial', 'private' + ]); + + if (!$validity['is_valid']) { + return new ApiResponse(false, $validity['error']); + } + + // conditionally require is_public or vehicle_use_type + switch ($req->request->get('mv_type_id')) { + case 4: + case 13: + if (empty($req->request->get('is_public'))) { + return new ApiResponse(false, 'Missing required parameter(s): is_public is required when mv_type_id is in [4, 13]'); + } + break; + default: + if (empty($req->request->get('vehicle_use_type'))) { + return new ApiResponse(false, 'Missing required parameter(s): vehicle_use_type is required when mv_type_id is not in [4, 13]'); + } + break; + } + + // require the orcr file + if ($req->files->get('orcr_file') === null) { + return new ApiResponse(false, 'Missing required file: orcr_file'); + } + + // get our listener url + $notif_url = $router->generate('insurance_listener', [], UrlGeneratorInterface::ABSOLUTE_URL); + + // get customer and cv info + $cust = $this->session->getCustomer(); + $cv = $this->em->getRepository(CustomerVehicle::class)->find($req->request->get('customer_vehicle_id')); + if ($cv == null) { + return new ApiResponse(false, 'Invalid customer vehicle id.'); + } + + // confirm that customer vehicle belongs to customer + if ($cv->getCustomer()->getID() != $cust->getID()) { + return new ApiResponse(false, 'Vehicle does not belong to customer.'); + } + + // process all our inputs first + $input = $req->request->all(); + + if (!isset($input['is_public'])) { + $input['is_public'] = false; + } + + $input['line'] = $this->getLineType($input['mv_type_id'], $input['vehicle_use_type'], $input['is_public']); + + // submit insurance application + $result = $this->client->createApplication( + $cv, + $notif_url, + $input, + $req->files->get('orcr_file') + ); + if (!$result['success']) { + 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' => $premium_amount_int, + 'currency' => 'PHP', + ], + ]; + + $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 + $this->em->persist($gt); + $this->em->flush(); + + // create paymongo checkout resource + $checkout = $paymongo->createCheckout( + $cust, + $items, + $gt->getID(), + "Motolite RES-Q Vehicle Insurance", + $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']); + } + + $checkout_url = $checkout['response']['data']['attributes']['checkout_url']; + + // add checkout url and id to transaction metadata + $gt->setExtTransactionId($checkout['response']['data']['id']); + $gt->setMetadata([ + 'checkout_url' => $checkout_url, + ]); + + // store application in db + $app = new InsuranceApplication(); + $app->setDateSubmit($now); + $app->setCustomer($cust); + $app->setCustomerVehicle($cv); + $app->setGatewayTransaction($gt); + $app->setStatus(InsuranceApplicationStatus::CREATED); + $app->setExtTransactionId($result['response']['id']); + $app->setMetadata($input); + $this->em->persist($app); + + // save everything + $this->em->flush(); + + // return + return new ApiResponse(true, '', [ + 'app_id' => $app->getID(), + 'checkout_url' => $checkout_url, + 'premium_amount' => (string)$result['response']['premium'], + ]); + } + + public function getVehicleMakers(Request $req) + { + // validate params + $validity = $this->validateRequest($req); + + if (!$validity['is_valid']) { + return new ApiResponse(false, $validity['error']); + } + + // get maker list + $result = $this->client->getVehicleMakers(); + if (!$result['success']) { + return new ApiResponse(false, $result['error']['message']); + } + + return new ApiResponse(true, '', [ + 'makers' => $result['response']['data']['vehicleMakers'], + ]); + } + + public function getVehicleModels($maker_id, Request $req) + { + // validate params + $validity = $this->validateRequest($req); + + if (!$validity['is_valid']) { + return new ApiResponse(false, $validity['error']); + } + + // get maker list + $result = $this->client->getVehicleModels($maker_id); + if (!$result['success']) { + return new ApiResponse(false, $result['error']['message']); + } + + return new ApiResponse(true, '', [ + 'models' => $result['response']['data']['vehicleModels'], + ]); + } + + public function getVehicleTrims($model_id, Request $req) + { + // validate params + $validity = $this->validateRequest($req); + + if (!$validity['is_valid']) { + return new ApiResponse(false, $validity['error']); + } + + // get maker list + $result = $this->client->getVehicleTrims($model_id); + if (!$result['success']) { + return new ApiResponse(false, $result['error']['message']); + } + + return new ApiResponse(true, '', [ + 'trims' => $result['response']['data']['vehicleTrims'], + ]); + } + + public function getMVTypes(Request $req) + { + // validate params + $validity = $this->validateRequest($req); + + if (!$validity['is_valid']) { + return new ApiResponse(false, $validity['error']); + } + + return new ApiResponse(true, '', [ + 'mv_types' => InsuranceMVType::getCollection(), + ]); + } + + public function getClientTypes(Request $req) + { + // validate params + $validity = $this->validateRequest($req); + + if (!$validity['is_valid']) { + return new ApiResponse(false, $validity['error']); + } + + return new ApiResponse(true, '', [ + 'mv_types' => InsuranceClientType::getCollection(), + ]); + } + + protected function getLineType($mv_type_id, $vehicle_use_type, $is_public = false) + { + $line = ''; + + // NOTE: this is a bit of a hack since we're hardcoding values, but this is fine for now + switch ($mv_type_id) { + case '3': + $line = 'mcoc'; + break; + case '4': + case '13': + if ($is_public) { + $line = 'lcoc'; + } else { + $line = 'mcoc'; + } + break; + default: + if ($vehicle_use_type === 'commercial') { + $line = 'ccoc'; + } else { + $line = 'pcoc'; + } + break; + } + + return $line; + } +} diff --git a/src/Controller/CustomerAppAPI/VehicleController.php b/src/Controller/CustomerAppAPI/VehicleController.php index 51a8ce80..abe65df8 100644 --- a/src/Controller/CustomerAppAPI/VehicleController.php +++ b/src/Controller/CustomerAppAPI/VehicleController.php @@ -8,7 +8,8 @@ use Catalyst\ApiBundle\Component\Response as ApiResponse; use App\Entity\CustomerVehicle; use App\Entity\VehicleManufacturer; use App\Entity\Vehicle; - +use App\Ramcar\InsuranceApplicationStatus; +use App\Service\PayMongoConnector; use DateTime; class VehicleController extends ApiController @@ -107,6 +108,34 @@ class VehicleController extends ApiController } + public function getVehicle(Request $req, $id, PayMongoConnector $paymongo) + { + // check requirements + $validity = $this->validateRequest($req); + + if (!$validity['is_valid']) { + return new ApiResponse(false, $validity['error']); + } + + // get customer vehicle + $cv = $this->em->getRepository(CustomerVehicle::class)->find($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() != $this->session->getCustomer()->getID()) { + return new ApiResponse(false, 'Invalid vehicle.'); + } + + // response + return new ApiResponse(true, '', [ + 'vehicle' => $this->generateVehicleInfo($cv, true, $paymongo), + ]); + } + public function updateVehicle(Request $req, $id) { // check requirements @@ -141,7 +170,7 @@ class VehicleController extends ApiController ]); } - public function listVehicles(Request $req) + public function listVehicles(Request $req, PayMongoConnector $paymongo) { // validate params $validity = $this->validateRequest($req); @@ -162,37 +191,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) { - $battery_id = null; - if ($cv->getCurrentBattery() != null) - $battery_id = $cv->getCurrentBattery()->getID(); - - $wty_ex = null; - if ($cv->getWarrantyExpiration() != null) - $wty_ex = $cv->getWarrantyExpiration()->format('Y-m-d'); - - $warranty = $this->findWarranty($cv->getPlateNumber()); - - $cv_name = ''; - if ($cv->getName() != null) - $cv_name = $cv->getName(); - - $cv_list[] = [ - 'cv_id' => $cv->getID(), - 'mfg_id' => $cv->getVehicle()->getManufacturer()->getID(), - 'make_id' => $cv->getVehicle()->getID(), - 'name' => $cv_name, - 'plate_num' => $cv->getPlateNumber(), - 'model_year' => $cv->getModelYear(), - 'color' => $cv->getColor(), - 'condition' => $cv->getStatusCondition(), - 'fuel_type' => $cv->getFuelType(), - 'wty_code' => $cv->getWarrantyCode(), - 'wty_expire' => $wty_ex, - 'curr_batt_id' => $battery_id, - 'is_motolite' => $cv->hasMotoliteBattery() ? 1 : 0, - 'is_active' => $cv->isActive() ? 1 : 0, - 'warranty' => $warranty, - ]; + $cv_list[] = $this->generateVehicleInfo($cv, true, $paymongo); } // response @@ -285,6 +284,98 @@ class VehicleController extends ApiController return new ApiResponse(); } + protected function generateVehicleInfo(CustomerVehicle $cv, $include_insurance = false, PayMongoConnector $paymongo) + { + $battery_id = null; + if ($cv->getCurrentBattery() != null) + $battery_id = $cv->getCurrentBattery()->getID(); + + $wty_ex = null; + if ($cv->getWarrantyExpiration() != null) + $wty_ex = $cv->getWarrantyExpiration()->format('Y-m-d'); + + $warranty = $this->findWarranty($cv->getPlateNumber()); + + $cv_name = ''; + if ($cv->getName() != null) + $cv_name = $cv->getName(); + + $row = [ + 'cv_id' => $cv->getID(), + 'mfg_id' => $cv->getVehicle()->getManufacturer()->getID(), + 'make_id' => $cv->getVehicle()->getID(), + 'name' => $cv_name, + 'plate_num' => $cv->getPlateNumber(), + 'model_year' => $cv->getModelYear(), + 'color' => $cv->getColor(), + 'condition' => $cv->getStatusCondition(), + 'fuel_type' => $cv->getFuelType(), + 'wty_code' => $cv->getWarrantyCode(), + 'wty_expire' => $wty_ex, + 'curr_batt_id' => $battery_id, + 'is_motolite' => $cv->hasMotoliteBattery() ? 1 : 0, + 'is_active' => $cv->isActive() ? 1 : 0, + 'warranty' => $warranty, + ]; + + // get latest insurance row + if ($include_insurance) { + $insurance = null; + $iobj = $cv->getLatestInsuranceApplication(); + if (!empty($iobj)) { + $gt = $iobj->getGatewayTransaction(); + $date_complete = $iobj->getDateComplete(); + $date_expire = $iobj->getDateExpire(); + $status = $iobj->getStatus(); + + error_log("\r\nTHIS IS THE CURRENT STATUS: " . $status . "\r\n"); + + // handle the very transient state between a payment being made and receiving the paymongo webhook + // 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: + // get latest status on this checkout from paymongo + $checkout = $paymongo->getCheckout($gt->getExtTransactionId()); + + if ($checkout['success']) { + $payment_intent = $checkout['response']['data']['attributes']['payment_intent'] ?? null; + if (!empty($payment_intent)) { + $intent_status = $payment_intent['attributes']['status'] ?? null; + + // TODO: define these paymongo payment intent statuses elsewhere + if ($intent_status === 'processing' || $intent_status === 'succeeded') { + $status = InsuranceApplicationStatus::PAID; + } + } + } + break; + default: + break; + } + + $insurance = [ + 'id' => $iobj->getID(), + 'ext_transaction_id' => $iobj->getExtTransactionId(), + 'status' => $status, + 'coc_url' => $iobj->getCOC(), + 'checkout_url' => $gt->getMetadata()['checkout_url'], + 'transaction_status' => $gt->getStatus(), + 'premium_amount' => (string)bcdiv($gt->getAmount(), 100), // NOTE: hard expressing as string so it's consistent + 'date_submit' => $iobj->getDateSubmit()->format('Y-m-d H:i:s'), + 'date_complete' => $date_complete ? $date_complete->format('Y-m-d H:i:s') : null, + 'date_expire' => $date_expire ? $date_expire->format('Y-m-d H:i:s') : null, + ]; + + // get information changelog + $insurance['changelog'] = $iobj->getMetadata()['changes'] ?? []; + } + + $row['latest_insurance'] = $insurance; + } + + return $row; + } + protected function checkVehicleRequirements(Request $req) { // validate params diff --git a/src/Controller/InsuranceController.php b/src/Controller/InsuranceController.php index b720253e..97744d27 100644 --- a/src/Controller/InsuranceController.php +++ b/src/Controller/InsuranceController.php @@ -2,22 +2,134 @@ namespace App\Controller; +use App\Ramcar\InsuranceApplicationStatus; +use App\Service\FCMSender; +use App\Entity\InsuranceApplication; use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Bundle\FrameworkBundle\Controller\Controller; +use DateTime; + class InsuranceController extends Controller { - public function listen(Request $req, EntityManagerInterface $em) + protected $em; + protected $fcmclient; + + public function __construct(EntityManagerInterface $em, FCMSender $fcmclient) + { + $this->em = $em; + $this->fcmclient = $fcmclient; + } + + public function listen(Request $req) { $payload = $req->request->all(); + + // DEBUG + @file_put_contents(__DIR__ . '/../../var/log/insurance.log', print_r($payload, true) . "\r\n----------------------------------------\r\n\r\n", FILE_APPEND); error_log(print_r($payload, true)); + /* + return $this->json([ + 'success' => true, + ]); + */ + + // END DEBUG + + // if no transaction code given, silently fail + if (empty($payload['transaction_code'])) { + error_log("Invalid insurance callback received: " . print_r($payload, true)); + + return $this->json([ + 'success' => true, + ]); + } + + // get event type and process accordingly + $event_name = $payload['transaction_code']; + + switch ($event_name) { + case 'GR002': + return $this->handleAuthenticated($payload); + break; + case 'GR003': + return $this->handleUpdateMade($payload); + break; + default: + break; + } + return $this->json([ 'success' => true, 'payload' => $payload, ]); } + + protected function handleAuthenticated($payload) + { + $obj = $this->getApplication($payload['id']); + $now = new DateTime(); + $expiry = DateTime::createFromFormat("Y-m-d", $payload['expiry_date']); + + if (!empty($obj)) { + // mark as completed + $obj->setStatus(InsuranceApplicationStatus::COMPLETED); + $obj->setDateComplete($now); + $obj->setDateExpire($expiry); + $obj->setCOC($payload['coc_url']); + $this->em->flush(); + + // send notification + $this->fcmclient->sendEvent($obj->getCustomer(), "insurance_fcm_title_completed", "insurance_fcm_body_completed", [ + 'cv_id' => $obj->getCustomerVehicle()->getID(), + ]); + } + + return $this->json([ + 'success' => true, + ]); + } + + protected function handleUpdateMade($payload) + { + $obj = $this->getApplication($payload['id']); + + if (!empty($obj)) { + $metadata = $obj->getMetadata(); + + // initialize change list if not present + if (empty($metadata['changes'])) { + $metadata['changes'] = []; + } + + $now = new DateTime; + $metadata['changes'][$now->format('Y-m-d H:i:s')] = $payload['data']; + + // update metadata to record change + $obj->setMetadata($metadata); + $this->em->flush(); + + // send notification + $this->fcmclient->sendEvent($obj->getCustomer(), "insurance_fcm_title_updated", "insurance_fcm_body_updated", [ + 'cv_id' => $obj->getCustomerVehicle()->getID(), + ]); + } + + return $this->json([ + 'success' => true, + ]); + } + + protected function getApplication($transaction_id) + { + $result = $this->em->getRepository(InsuranceApplication::class)->findBy([ + 'ext_transaction_id' => $transaction_id, + ], [], 1); + + return !empty($result) ? $result[0] : false; + } } diff --git a/src/Controller/PayMongoController.php b/src/Controller/PayMongoController.php new file mode 100644 index 00000000..02f75928 --- /dev/null +++ b/src/Controller/PayMongoController.php @@ -0,0 +1,110 @@ +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 + + // if no event type given, silently fail + if (empty($payload['data'])) { + error_log("Invalid paymongo callback received: " . print_r($payload, true)); + return $this->json([ + 'success' => true, + ]); + } + + // 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, + ]); + } + + protected function handlePaymentPaid($event) + { + $metadata = $event['attributes']['metadata']; + $obj = $this->getTransaction($metadata['transaction_id']); + + if (!empty($obj)) { + // 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/CustomerVehicle.php b/src/Entity/CustomerVehicle.php index a5525a38..a569d4b2 100644 --- a/src/Entity/CustomerVehicle.php +++ b/src/Entity/CustomerVehicle.php @@ -2,11 +2,13 @@ namespace App\Entity; +use App\Ramcar\InsuranceApplicationStatus; use Doctrine\ORM\Mapping as ORM; use Doctrine\Common\Collections\ArrayCollection; use Symfony\Component\Validator\Constraints as Assert; use DateTime; +use Doctrine\Common\Collections\Criteria; /** * @ORM\Entity @@ -114,10 +116,15 @@ class CustomerVehicle */ protected $flag_active; + // link to insurance + /** + * @ORM\OneToMany(targetEntity="InsuranceApplication", mappedBy="customer_vehicle") + */ + protected $insurance_applications; + public function __construct() { $this->flag_active = true; - $this->job_orders = new ArrayCollection(); } @@ -282,4 +289,21 @@ class CustomerVehicle { return $this->flag_active; } + + public function getInsuranceApplications() + { + return $this->insurance_applications; + } + + public function getLatestInsuranceApplication() + { + $criteria = Criteria::create() + ->where(Criteria::expr()->notIn('status', [InsuranceApplicationStatus::CANCELLED])) + ->orderBy(['date_submit' => Criteria::DESC]) + ->setMaxResults(1); + + $result = $this->insurance_applications->matching($criteria); + + return !empty($result) ? $result[0] : null; + } } diff --git a/src/Entity/GatewayTransaction.php b/src/Entity/GatewayTransaction.php new file mode 100644 index 00000000..e53a7f4b --- /dev/null +++ b/src/Entity/GatewayTransaction.php @@ -0,0 +1,193 @@ +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 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 new file mode 100644 index 00000000..3887e259 --- /dev/null +++ b/src/Entity/InsuranceApplication.php @@ -0,0 +1,226 @@ +date_submit = new DateTime(); + $this->date_pay = null; + $this->date_complete = null; + $this->date_expire = 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 $cv = null) + { + $this->customer_vehicle = $cv; + return $this; + } + + public function getCustomerVehicle() + { + return $this->customer_vehicle; + } + + public function setDateSubmit(DateTime $date) + { + $this->date_submit = $date; + return $this; + } + + public function getDateSubmit() + { + return $this->date_submit; + } + + public function setGatewayTransaction(GatewayTransaction $transaction) + { + $this->gateway_transaction = $transaction; + return $this; + } + + public function getGatewayTransaction() + { + return $this->gateway_transaction; + } + + public function setStatus($status) + { + return $this->status = $status; + } + + public function getStatus() + { + return $this->status; + } + + public function setCOC($url) + { + return $this->coc_url = $url; + } + + public function getCOC() + { + return $this->coc_url; + } + + public function setDatePay(DateTime $date) + { + $this->date_pay = $date; + return $this; + } + + public function getDatePay() + { + return $this->date_pay; + } + + public function setDateComplete(DateTime $date) + { + $this->date_complete = $date; + return $this; + } + + public function getDateComplete() + { + return $this->date_complete; + } + + public function setDateExpire(DateTime $date) + { + $this->date_expire = $date; + return $this; + } + + public function getDateExpire() + { + return $this->date_expire; + } + + public function setExtTransactionId($transaction_id) + { + $this->ext_transaction_id = $transaction_id; + return $this; + } + + public function getExtTransactionId() + { + return $this->ext_transaction_id; + } + + public function setMetadata($metadata) + { + return $this->metadata = $metadata; + } + + public function getMetadata() + { + return $this->metadata; + } +} diff --git a/src/EntityListener/GatewayTransactionListener.php b/src/EntityListener/GatewayTransactionListener.php new file mode 100644 index 00000000..745fc5f9 --- /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 (!empty($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'] || $result['response']['transaction_code'] !== 'GR004') { + error_log("INSURANCE MARK AS PAID FAILED FOR " . $obj->getID() . ": " . $result['error']['message']); + } + } +} + diff --git a/src/Ramcar/InsuranceApplicationStatus.php b/src/Ramcar/InsuranceApplicationStatus.php new file mode 100644 index 00000000..8480239c --- /dev/null +++ b/src/Ramcar/InsuranceApplicationStatus.php @@ -0,0 +1,18 @@ + 'Created', + 'paid' => 'Paid', + 'completed' => 'Completed', + 'cancelled' => 'Cancelled', + ]; +} diff --git a/src/Ramcar/InsuranceClientType.php b/src/Ramcar/InsuranceClientType.php new file mode 100644 index 00000000..1532ef7b --- /dev/null +++ b/src/Ramcar/InsuranceClientType.php @@ -0,0 +1,14 @@ + 'Individual', + 'c' => 'Corporate', + ]; +} diff --git a/src/Ramcar/InsuranceMVType.php b/src/Ramcar/InsuranceMVType.php new file mode 100644 index 00000000..73755821 --- /dev/null +++ b/src/Ramcar/InsuranceMVType.php @@ -0,0 +1,32 @@ + "Car", + '2' => "Shuttle Bus", + '3' => "Motorcycle", + '4' => "Motorcycle with Sidecar", + '5' => "Non-Conventional MV", + '8' => "Sports Utility Vehicle", + '9' => "Truck", + '10' => "Trailer", + '11' => "UV Private", + '12' => "UV Commercial", + '13' => "Tricycle", + ]; +} diff --git a/src/Ramcar/InsuranceVehicleLine.php b/src/Ramcar/InsuranceVehicleLine.php new file mode 100644 index 00000000..7e66426d --- /dev/null +++ b/src/Ramcar/InsuranceVehicleLine.php @@ -0,0 +1,18 @@ + 'Private Car', + 'mcoc' => 'Motorcycle / Motorcycle with Sidecar / Tricycle (Private)', + 'ccoc' => 'Commercial Vehicle', + 'lcoc' => 'Motorcycle / Motorcycle with Sidecar / Tricycle (Public)', + ]; +} 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/FCMSender.php b/src/Service/FCMSender.php index 412ba6af..4b2c646d 100644 --- a/src/Service/FCMSender.php +++ b/src/Service/FCMSender.php @@ -2,6 +2,7 @@ namespace App\Service; +use App\Entity\Customer; use Symfony\Contracts\Translation\TranslatorInterface; use Fcm\FcmClient; use Fcm\Push\Notification; @@ -53,42 +54,62 @@ class FCMSender public function sendJoEvent(JobOrder $job_order, $title, $body, $data = []) { - // get all v2 sessions + // get customer object + $cust = $job_order->getCustomer(); + + // attach jo info + $data['jo_id'] = $job_order->getID(); + $data['jo_status'] = $job_order->getStatus(); + + // send the event + return $this->sendEvent($cust, $title, $body, $data); + } + + public function sendEvent(Customer $cust, $title, $body, $data = []) + { + // get all v2 devices + $devices = $this->getDevices($cust); + + if (empty($devices)) { + return false; + } + + // send fcm notification + $result = $this->send(array_keys($devices), $this->translator->trans($title), $this->translator->trans($body), $data); + + return $result; + } + + protected function getDevices(Customer $cust) + { $sessions = []; - $cust_user = $job_order->getCustomer()->getCustomerUser(); + $device_ids = []; + + $cust_user = $cust->getCustomerUser(); if (!empty($cust_user)) { $sessions = $cust_user->getMobileSessions(); } if (empty($sessions)) { error_log("no sessions to send fcm notification to"); - return; + return false; } - $device_ids = []; - // send to every customer session foreach ($sessions as $sess) { $device_id = $sess->getDevicePushID(); if (!empty($device_id) && !isset($device_ids[$device_id])) { - // send fcm notification + // send to this device $device_ids[$device_id] = true; } } if (empty($device_ids)) { error_log("no devices to send fcm notification to"); - return; + return false; } - // attach jo info - $data['jo_id'] = $job_order->getID(); - $data['jo_status'] = $job_order->getStatus(); - - // send fcm notification - $result = $this->send(array_keys($device_ids), $this->translator->trans($title), $this->translator->trans($body), $data); - - return $result; + return $device_ids; } } diff --git a/src/Service/InsuranceConnector.php b/src/Service/InsuranceConnector.php index e10066a2..b56415c0 100644 --- a/src/Service/InsuranceConnector.php +++ b/src/Service/InsuranceConnector.php @@ -2,6 +2,11 @@ namespace App\Service; +use App\Entity\CustomerVehicle; +use GuzzleHttp\Client; +use GuzzleHttp\Psr7; +use GuzzleHttp\Exception\RequestException; + class InsuranceConnector { protected $base_url; @@ -17,37 +22,68 @@ class InsuranceConnector $this->hash = $this->generateHash(); } - public function createApplication($notif_url, $client_info, $client_contact_info, $car_info) + public function createApplication(CustomerVehicle $cv, $notif_url, $data, $orcr_file) { $body = [ 'notif_url' => $notif_url, - 'client_info' => $client_info, - 'client_contact_info' => $client_contact_info, - 'car_info' => $car_info, + 'client_info' => [ + 'client_type' => $data['client_type'], + 'first_name' => $data['first_name'], + 'middle_name' => $data['middle_name'] ?? null, + 'surname' => $data['surname'], + 'corporate_name' => $data['corporate_name'], + ], + 'client_contact_info' => [ + 'address_number' => $data['address_number'], + 'address_street' => $data['address_street'] ?? null, + 'address_building' => $data['address_building'] ?? null, + 'address_barangay' => $data['address_barangay'], + 'address_city' => $data['address_city'], + 'address_province' => $data['address_province'], + 'zipcode' => (int)$data['zipcode'], + 'mobile_number' => $data['mobile_number'], + 'email_address' => $data['email_address'], + ], + 'car_info' => [ + 'make' => $data['make'], + 'model' => $data['model'], + 'series' => $data['series'], + 'color' => $data['color'], + 'plate_number' => $cv->getPlateNumber(), + 'mv_file_number' => $data['mv_file_number'], + 'motor_number' => $data['motor_number'], + 'serial_chasis' => $data['serial_chasis'], + 'year_model' => (int)$data['year_model'], + 'mv_type_id' => (int)$data['mv_type_id'], + 'body_type' => $data['body_type'], + 'is_public' => (bool)$data['is_public'], + 'line' => $data['line'], + 'orcr_file' => base64_encode(file_get_contents($orcr_file->getPathname())), + ], ]; - return $this->doRequest('/api/v1/ctpl/applications', true, $body); + return $this->doRequest('/api/v1/ctpl/applications', 'POST', $body); } public function tagApplicationPaid($application_id) { $url = '/api/v1/ctpl/application/' . $application_id . '/paid'; - return $this->doRequest($url, true); + return $this->doRequest($url, 'POST'); } public function getVehicleMakers() { - return $this->doRequest('/api/v1/ctpl/vehicle-makers'); + return $this->doRequest('/api/v1/ctpl/vehicle-makers', 'GET'); } - public function getVehicleModels() + public function getVehicleModels($maker_id) { - return $this->doRequest('/api/v1/ctpl/vehicle-models'); + return $this->doRequest('/api/v1/ctpl/vehicle-models?maker_id='. $maker_id, 'GET'); } - public function getVehicleTrims() + public function getVehicleTrims($model_id) { - return $this->doRequest('/api/v1/ctpl/vehicle-trims'); + return $this->doRequest('/api/v1/ctpl/vehicle-trims?model_id='. $model_id, 'GET'); } protected function generateHash() @@ -55,46 +91,42 @@ class InsuranceConnector return base64_encode($this->username . ":" . $this->password); } - protected function doRequest($url, $is_post = false, $body = []) + protected function doRequest($url, $method, $body = []) { - $curl = curl_init(); - - $options = [ - CURLOPT_URL => $this->base_url . '/' . $url, - CURLOPT_POST => $is_post, - CURLOPT_RETURNTRANSFER => true, - CURLOPT_HTTPHEADER => [ - 'Content-Type: application/json', - 'Authorization: Basic ' . $this->hash, - ], + $client = new Client(); + $headers = [ + 'Content-Type' => 'application/json', + 'accept' => 'application/json', + 'authorization' => 'Basic '. $this->hash, ]; - // add post body if present - if (!empty($body)) { - $options[CURLOPT_POSTFIELDS] = json_encode($body); + try { + $response = $client->request($method, $this->base_url . '/' . $url, [ + 'json' => $body, + 'headers' => $headers, + ]); + } catch (RequestException $e) { + $error = ['message' => $e->getMessage()]; + + 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()); + } + + return [ + 'success' => false, + 'error' => $error, + ]; } - curl_setopt_array($curl, $options); - $res = curl_exec($curl); + error_log(print_r(json_decode($response->getBody(), true), true)); - curl_close($curl); - - error_log('Insurance API connector'); - error_log(print_r($options, true)); - error_log($res); - - // response - return $this->handleResponse($res); - } - - protected function handleResponse($res) - { - $inv_res = json_decode($res, true); - - // make sure result is always an array - if ($inv_res == null) - return []; - - return $inv_res; + return [ + 'success' => true, + 'response' => json_decode($response->getBody(), true) + ]; } } diff --git a/src/Service/PayMongoConnector.php b/src/Service/PayMongoConnector.php new file mode 100644 index 00000000..ad2750a2 --- /dev/null +++ b/src/Service/PayMongoConnector.php @@ -0,0 +1,125 @@ +base_url = $base_url; + $this->public_key = $public_key; + $this->secret_key = $secret_key; + $this->hash = $this->generateHash(); + } + + public function createCheckout(Customer $cust, $items, $ref_no = null, $description = null, $success_url = null, $cancel_url = null, $metadata = []) + { + // build billing info + $billing = [ + 'name' => implode(" ", [$cust->getFirstName(), $cust->getLastName()]), + 'phone' => $cust->getPhoneMobile(), + ]; + + if ($cust->getEmail()) { + $billing['email'] = $cust->getEmail(); + } + + // build the request body + $body = [ + 'data' => [ + 'attributes' => [ + 'description' => $description, + 'billing' => $billing, + // NOTE: this may be variable later, hardcoding for now + 'payment_method_types' => [ + 'card', + 'paymaya', + 'gcash', + ], + /* NOTE: format for line items: + * ['name', 'description', 'quantity', 'amount', 'currency'] + */ + 'line_items' => $items, + '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, + ], + ], + ]; + + if (!empty($metadata)) { + $body['data']['attributes']['metadata'] = $metadata; + } + + return $this->doRequest('/v1/checkout_sessions', 'POST', $body); + } + + public function getCheckout($checkout_id) + { + return $this->doRequest('/v1/checkout_sessions/' . $checkout_id, 'GET'); + } + + protected function generateHash() + { + return base64_encode($this->secret_key); + } + + protected function doRequest($url, $method, $body = []) + { + $client = new Client(); + $headers = [ + 'Content-Type' => 'application/json', + 'accept' => 'application/json', + 'authorization' => 'Basic '. $this->hash, + ]; + + try { + $response = $client->request($method, $this->base_url . '/' . $url, [ + 'json' => $body, + 'headers' => $headers, + ]); + } catch (RequestException $e) { + $error = ['message' => $e->getMessage()]; + + ob_start(); + var_dump($body); + $varres = ob_get_clean(); + error_log($varres); + + error_log("--------------------------------------"); + error_log($e->getResponse()->getBody()->getContents()); + + error_log("PayMongo API Error: " . $error['message']); + error_log(Psr7\Message::toString($e->getRequest())); + + if ($e->hasResponse()) { + $error['response'] = Psr7\Message::toString($e->getResponse()); + } + + return [ + 'success' => false, + 'error' => $error, + ]; + } + + return [ + 'success' => true, + 'response' => json_decode($response->getBody(), true) + ]; + } +} 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 diff --git a/translations/messages.en.yaml b/translations/messages.en.yaml index b0ed4bba..7278212d 100644 --- a/translations/messages.en.yaml +++ b/translations/messages.en.yaml @@ -161,13 +161,19 @@ menu.database.ownershiptypes: 'Ownership Types' menu.database.serviceofferings: 'Service Offerings' # fcm jo status updates -jo_fcm_title_outlet_assign: Looking for riders -jo_fcm_title_driver_assigned: Rider found -jo_fcm_title_driver_arrived: Rider nearby -jo_fcm_title_cancelled: Order cancelled -jo_fcm_title_fulfilled: Thank you! -jo_fcm_body_outlet_assign: We're assigning a rider for your order, please wait. -jo_fcm_body_driver_assigned: A rider is on their way. -jo_fcm_body_driver_arrived: Your order is almost there! -jo_fcm_body_cancelled: Your order has been cancelled. -jo_fcm_body_fulfilled: Order complete! Your receipt is ready. +jo_fcm_title_outlet_assign: 'Looking for riders' +jo_fcm_title_driver_assigned: 'Rider found' +jo_fcm_title_driver_arrived: 'Rider nearby' +jo_fcm_title_cancelled: 'Order cancelled' +jo_fcm_title_fulfilled: 'Thank you!' +jo_fcm_body_outlet_assign: 'We`re assigning a rider for your order, please wait.' +jo_fcm_body_driver_assigned: 'A rider is on their way.' +jo_fcm_body_driver_arrived: 'Your order is almost there!' +jo_fcm_body_cancelled: 'Your order has been cancelled.' +jo_fcm_body_fulfilled: 'Order complete! Your receipt is ready.' + +# fcm insurance +insurance_fcm_title_updated: 'Application updated' +insurance_fcm_title_completed: 'Application completed' +insurance_fcm_body_updated: 'Some details on your insurance application have been updated.' +insurance_fcm_body_completed: 'Your insurance application has been processed!'