From db75e7becd3f0a6070b584bc179ed03980205fee Mon Sep 17 00:00:00 2001 From: Ramon Gutierrez Date: Wed, 6 Sep 2023 04:05:15 +0800 Subject: [PATCH 01/22] Add insurance endpoints, insurance and paymongo connectors #761 --- config/routes/apiv2.yaml | 31 ++ config/routes/insurance.yaml | 12 +- config/routes/paymongo.yaml | 6 + config/services.yaml | 7 + .../CustomerAppAPI/InsuranceController.php | 291 ++++++++++++++++++ src/Controller/PayMongoController.php | 23 ++ src/Entity/CustomerVehicle.php | 18 ++ src/Entity/InsuranceApplication.php | 238 ++++++++++++++ src/Ramcar/InsuranceApplicationStatus.php | 16 + src/Ramcar/InsuranceClientType.php | 14 + src/Ramcar/InsuranceMVType.php | 32 ++ src/Ramcar/InsuranceVehicleLine.php | 18 ++ src/Service/InsuranceConnector.php | 123 +++++--- src/Service/PayMongoConnector.php | 122 ++++++++ 14 files changed, 904 insertions(+), 47 deletions(-) create mode 100644 config/routes/paymongo.yaml create mode 100644 src/Controller/CustomerAppAPI/InsuranceController.php create mode 100644 src/Controller/PayMongoController.php create mode 100644 src/Entity/InsuranceApplication.php create mode 100644 src/Ramcar/InsuranceApplicationStatus.php create mode 100644 src/Ramcar/InsuranceClientType.php create mode 100644 src/Ramcar/InsuranceMVType.php create mode 100644 src/Ramcar/InsuranceVehicleLine.php create mode 100644 src/Service/PayMongoConnector.php diff --git a/config/routes/apiv2.yaml b/config/routes/apiv2.yaml index af7d0084..8f781ca7 100644 --- a/config/routes/apiv2.yaml +++ b/config/routes/apiv2.yaml @@ -260,4 +260,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..cf8b2974 100644 --- a/config/routes/insurance.yaml +++ b/config/routes/insurance.yaml @@ -1,6 +1,16 @@ # insurance insurance_listener: - path: /api/insurance/listen + 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 new file mode 100644 index 00000000..3cccfa97 --- /dev/null +++ b/config/routes/paymongo.yaml @@ -0,0 +1,6 @@ +# paymongo + +paymongo_listener: + path: /paymongo/listen + controller: App\Controller\PayMongoController::listen + methods: [POST] diff --git a/config/services.yaml b/config/services.yaml index b59e8008..a9e5b16e 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -216,6 +216,13 @@ services: $username: "%env(INSURANCE_USERNAME)%" $password: "%env(INSURANCE_PASSWORD)%" + # 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..a32e67c0 --- /dev/null +++ b/src/Controller/CustomerAppAPI/InsuranceController.php @@ -0,0 +1,291 @@ +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', // accepted values are: 'commercial', 'private' + ]); + + if (!$validity['is_valid']) { + return new ApiResponse(false, $validity['error']); + } + + // 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']); + } + + // build checkout item and metadata + $items = [ + [ + 'name' => "Insurance Premium", + 'description' => "Premium fee for vehicle insurance", + 'quantity' => 1, + 'amount' => (int)bcmul($result['response']['premium'], 100), + 'currency' => 'PHP', + ], + ]; + + $metadata = [ + 'customer_id' => $cust->getID(), + 'customer_vehicle_id' => $cv->getID(), + ]; + + // create paymongo checkout resource + $checkout = $paymongo->createCheckout( + $cust, + $items, + implode("-", [$cust->getID(), $result['response']['id']]), + "Motolite RES-Q Vehicle Insurance", + $router->generate('insurance_payment_success', [], UrlGeneratorInterface::ABSOLUTE_URL), + $router->generate('insurance_payment_cancel', [], UrlGeneratorInterface::ABSOLUTE_URL), + $metadata, + ); + if (!$checkout['success']) { + return new APIResponse(false, $checkout['error']['message']); + } + + $checkout_url = $checkout['response']['data']['attributes']['checkout_url']; + + // store application in db + $app = new InsuranceApplication(); + $app->setDateSubmitted(new DateTime()); + $app->setCustomer($cust); + $app->setCustomerVehicle($cv); + $app->setTransactionID($result['response']['id']); + $app->setPremiumAmount($result['response']['premium']); + $app->setStatus(InsuranceApplicationStatus::CREATED); + $app->setCheckoutURL($checkout_url); + $app->setCheckoutID($checkout['response']['data']['id']); + $app->setMetadata($input); + $this->em->persist($app); + $this->em->flush(); + + // return + return new ApiResponse(true, '', [ + 'app_id' => $app->getID(), + 'checkout_url' => $checkout_url, + ]); + } + + 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/PayMongoController.php b/src/Controller/PayMongoController.php new file mode 100644 index 00000000..052cc521 --- /dev/null +++ b/src/Controller/PayMongoController.php @@ -0,0 +1,23 @@ +request->all(); + error_log(print_r($payload, true)); + + return $this->json([ + 'success' => true, + 'payload' => $payload, + ]); + } +} diff --git a/src/Entity/CustomerVehicle.php b/src/Entity/CustomerVehicle.php index a5525a38..281d14db 100644 --- a/src/Entity/CustomerVehicle.php +++ b/src/Entity/CustomerVehicle.php @@ -114,6 +114,13 @@ class CustomerVehicle */ protected $flag_active; + // link to insurance + /** + * @ORM\OneToOne(targetEntity="InsuranceApplication", inversedBy="customer_vehicle") + * @ORM\JoinColumn(name="insurance_application_id", referencedColumnName="id", nullable=true) + */ + protected $insurance_application; + public function __construct() { $this->flag_active = true; @@ -282,4 +289,15 @@ class CustomerVehicle { return $this->flag_active; } + + public function setInsuranceApplication(InsuranceApplication $application) + { + $this->insurance_application = $application; + return $this; + } + + public function getInsuranceApplication() + { + return $this->insurance_application; + } } diff --git a/src/Entity/InsuranceApplication.php b/src/Entity/InsuranceApplication.php new file mode 100644 index 00000000..d08b5317 --- /dev/null +++ b/src/Entity/InsuranceApplication.php @@ -0,0 +1,238 @@ +date_submitted = new DateTime(); + $this->date_paid = null; + $this->date_completed = 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 setDateSubmitted(DateTime $date) + { + $this->date_submitted = $date; + return $this; + } + + public function getDateSubmitted() + { + return $this->date_submitted; + } + + public function setTransactionID($id) + { + return $this->transaction_id = $id; + } + + public function getTransactionID() + { + return $this->transaction_id; + } + + public function setPremiumAmount($amount) + { + return $this->premium_amount = $amount; + } + + public function getPremiumAmount() + { + return $this->premium_amount; + } + + 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 setDatePaid(DateTime $date) + { + $this->date_paid = $date; + return $this; + } + + public function getDatePaid() + { + return $this->date_paid; + } + + public function setDateCompleted(DateTime $date) + { + $this->date_completed = $date; + return $this; + } + + public function getDateCompleted() + { + return $this->date_completed; + } + + public function setMetadata($metadata) + { + return $this->metadata = $metadata; + } + + public function getMetadata() + { + 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/Ramcar/InsuranceApplicationStatus.php b/src/Ramcar/InsuranceApplicationStatus.php new file mode 100644 index 00000000..821615eb --- /dev/null +++ b/src/Ramcar/InsuranceApplicationStatus.php @@ -0,0 +1,16 @@ + 'Created', + 'paid' => 'Paid', + 'completed' => 'Completed', + ]; +} 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/Service/InsuranceConnector.php b/src/Service/InsuranceConnector.php index e10066a2..adfcfaef 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,41 @@ 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())); + + 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..d26ef7fd --- /dev/null +++ b/src/Service/PayMongoConnector.php @@ -0,0 +1,122 @@ +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' => "0" . $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' => $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, + ], + ], + ]; + + 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) + ]; + } +} -- 2.43.5 From 95f771c2523c3f786c38555da2f181a9c8ca117f Mon Sep 17 00:00:00 2001 From: Ramon Gutierrez Date: Thu, 7 Sep 2023 06:54:30 +0800 Subject: [PATCH 02/22] Update field requirements for is_public and vehicle_use_type #761 --- .../CustomerAppAPI/InsuranceController.php | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/Controller/CustomerAppAPI/InsuranceController.php b/src/Controller/CustomerAppAPI/InsuranceController.php index a32e67c0..168f572a 100644 --- a/src/Controller/CustomerAppAPI/InsuranceController.php +++ b/src/Controller/CustomerAppAPI/InsuranceController.php @@ -72,13 +72,28 @@ class InsuranceController extends ApiController //'orcr_file', // this is a file // mv_type_id specific fields - 'vehicle_use_type', // accepted values are: 'commercial', 'private' + //'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'); -- 2.43.5 From c5a8bda95ae0c63187509726052dc6ba89b70859 Mon Sep 17 00:00:00 2001 From: Ramon Gutierrez Date: Sat, 9 Sep 2023 00:42:42 +0800 Subject: [PATCH 03/22] Return premium amount on insurance application endpoint #761 --- src/Controller/CustomerAppAPI/InsuranceController.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Controller/CustomerAppAPI/InsuranceController.php b/src/Controller/CustomerAppAPI/InsuranceController.php index 168f572a..5832d336 100644 --- a/src/Controller/CustomerAppAPI/InsuranceController.php +++ b/src/Controller/CustomerAppAPI/InsuranceController.php @@ -184,6 +184,7 @@ class InsuranceController extends ApiController return new ApiResponse(true, '', [ 'app_id' => $app->getID(), 'checkout_url' => $checkout_url, + 'premium_amount' => (string)$result['response']['premium'], ]); } -- 2.43.5 From ee021da453fe64977a2ff2d94f5cd7121a72c563 Mon Sep 17 00:00:00 2001 From: Ramon Gutierrez Date: Wed, 20 Sep 2023 06:08:00 +0800 Subject: [PATCH 04/22] Handle paymongo webhooks #761 --- config/packages/security.yaml | 4 + config/routes/insurance.yaml | 10 - config/routes/paymongo.yaml | 10 + config/services.yaml | 10 + .../CustomerAppAPI/InsuranceController.php | 55 +++-- src/Controller/PayMongoController.php | 85 +++++++- src/Entity/GatewayTransaction.php | 204 ++++++++++++++++++ src/Entity/InsuranceApplication.php | 102 +++------ .../GatewayTransactionListener.php | 75 +++++++ src/Ramcar/TransactionStatus.php | 18 ++ src/Service/InsuranceConnector.php | 3 +- src/Service/PayMongoConnector.php | 7 +- templates/paymongo/cancelled.html.twig | 22 ++ templates/paymongo/success.html.twig | 22 ++ 14 files changed, 515 insertions(+), 112 deletions(-) create mode 100644 src/Entity/GatewayTransaction.php create mode 100644 src/EntityListener/GatewayTransactionListener.php create mode 100644 src/Ramcar/TransactionStatus.php create mode 100644 templates/paymongo/cancelled.html.twig create mode 100644 templates/paymongo/success.html.twig 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 -- 2.43.5 From a002da6aad28cb580c922dd5c8c7172455cef353 Mon Sep 17 00:00:00 2001 From: Ramon Gutierrez Date: Wed, 20 Sep 2023 06:14:22 +0800 Subject: [PATCH 05/22] Add entity null check for paymongo webhooks #761 --- src/Controller/PayMongoController.php | 8 +++++--- src/EntityListener/GatewayTransactionListener.php | 4 ++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/Controller/PayMongoController.php b/src/Controller/PayMongoController.php index 167752e4..245cedfa 100644 --- a/src/Controller/PayMongoController.php +++ b/src/Controller/PayMongoController.php @@ -63,9 +63,11 @@ class PayMongoController extends Controller $metadata = $event['attributes']['metadata']; $obj = $this->getTransaction($metadata['transaction_id']); - // mark as paid - $obj->setStatus(TransactionStatus::PAID); - $this->em->flush(); + if (!empty($obj)) { + // mark as paid + $obj->setStatus(TransactionStatus::PAID); + $this->em->flush(); + } return $this->json([ 'success' => true, diff --git a/src/EntityListener/GatewayTransactionListener.php b/src/EntityListener/GatewayTransactionListener.php index 1181a12b..745fc5f9 100644 --- a/src/EntityListener/GatewayTransactionListener.php +++ b/src/EntityListener/GatewayTransactionListener.php @@ -58,7 +58,7 @@ class GatewayTransactionListener 'gateway_transaction' => $gt_obj, ]); - if ($obj) { + if (!empty($obj)) { // mark as paid $obj->setDatePay(new DateTime()); $obj->setStatus(InsuranceApplicationStatus::PAID); @@ -67,7 +67,7 @@ class GatewayTransactionListener // flag on api as paid $result = $this->ic->tagApplicationPaid($obj->getID()); - if (!$result['success']) { + if (!$result['success'] || $result['response']['transaction_code'] !== 'GR004') { error_log("INSURANCE MARK AS PAID FAILED FOR " . $obj->getID() . ": " . $result['error']['message']); } } -- 2.43.5 From 38023bdb00791fa0cdb2fbe426b62131af6ebbf2 Mon Sep 17 00:00:00 2001 From: Ramon Gutierrez Date: Wed, 20 Sep 2023 20:40:00 +0800 Subject: [PATCH 06/22] Add insurance information to customer vehicle endpoint #761 --- .../CustomerAppAPI/VehicleController.php | 17 ++++++++++++++ src/Entity/CustomerVehicle.php | 22 +++++++++++-------- src/Entity/GatewayTransaction.php | 11 ---------- src/Entity/InsuranceApplication.php | 1 + src/Ramcar/InsuranceApplicationStatus.php | 4 ++++ 5 files changed, 35 insertions(+), 20 deletions(-) diff --git a/src/Controller/CustomerAppAPI/VehicleController.php b/src/Controller/CustomerAppAPI/VehicleController.php index 51a8ce80..d56a86ee 100644 --- a/src/Controller/CustomerAppAPI/VehicleController.php +++ b/src/Controller/CustomerAppAPI/VehicleController.php @@ -176,6 +176,22 @@ class VehicleController extends ApiController if ($cv->getName() != null) $cv_name = $cv->getName(); + // get latest insurance row + $insurance = null; + $iobj = $cv->getLatestInsuranceApplication(); + if (!empty($iobj)) { + $gt = $iobj->getGatewayTransaction(); + + $insurance = [ + 'id' => $iobj->getID(), + 'status' => $iobj->getStatus(), + '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 + ]; + } + $cv_list[] = [ 'cv_id' => $cv->getID(), 'mfg_id' => $cv->getVehicle()->getManufacturer()->getID(), @@ -192,6 +208,7 @@ class VehicleController extends ApiController 'is_motolite' => $cv->hasMotoliteBattery() ? 1 : 0, 'is_active' => $cv->isActive() ? 1 : 0, 'warranty' => $warranty, + 'latest_insurance' => $insurance, ]; } diff --git a/src/Entity/CustomerVehicle.php b/src/Entity/CustomerVehicle.php index 281d14db..a120c71e 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 @@ -116,15 +118,13 @@ class CustomerVehicle // link to insurance /** - * @ORM\OneToOne(targetEntity="InsuranceApplication", inversedBy="customer_vehicle") - * @ORM\JoinColumn(name="insurance_application_id", referencedColumnName="id", nullable=true) + * @ORM\OneToMany(targetEntity="InsuranceApplication", mappedBy="customer_vehicle") */ - protected $insurance_application; + protected $insurance_applications; public function __construct() { $this->flag_active = true; - $this->job_orders = new ArrayCollection(); } @@ -290,14 +290,18 @@ class CustomerVehicle return $this->flag_active; } - public function setInsuranceApplication(InsuranceApplication $application) + public function getInsuranceApplications() { - $this->insurance_application = $application; - return $this; + return $this->insurance_applications; } - public function getInsuranceApplication() + public function getLatestInsuranceApplication() { - return $this->insurance_application; + $criteria = Criteria::create() + ->where(Criteria::expr()->notIn('status', [InsuranceApplicationStatus::EXPIRED, InsuranceApplicationStatus::CANCELLED])) + ->orderBy(['date_submit' => Criteria::DESC]) + ->setMaxResults(1); + + return $this->insurance_applications->matching($criteria)[0]; } } diff --git a/src/Entity/GatewayTransaction.php b/src/Entity/GatewayTransaction.php index 2bd6ecb8..e53a7f4b 100644 --- a/src/Entity/GatewayTransaction.php +++ b/src/Entity/GatewayTransaction.php @@ -180,17 +180,6 @@ class GatewayTransaction 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; diff --git a/src/Entity/InsuranceApplication.php b/src/Entity/InsuranceApplication.php index 7cda138c..d582feff 100644 --- a/src/Entity/InsuranceApplication.php +++ b/src/Entity/InsuranceApplication.php @@ -33,6 +33,7 @@ class InsuranceApplication /** * @ORM\ManyToOne(targetEntity="CustomerVehicle", inversedBy="insurance") * @ORM\JoinColumn(name="customer_vehicle_id", referencedColumnName="id") + * @Assert\NotBlank() */ protected $customer_vehicle; diff --git a/src/Ramcar/InsuranceApplicationStatus.php b/src/Ramcar/InsuranceApplicationStatus.php index 821615eb..8908a3a8 100644 --- a/src/Ramcar/InsuranceApplicationStatus.php +++ b/src/Ramcar/InsuranceApplicationStatus.php @@ -7,10 +7,14 @@ class InsuranceApplicationStatus extends NameValue const CREATED = 'created'; const PAID = 'paid'; const COMPLETED = 'completed'; + const CANCELLED = 'cancelled'; + const EXPIRED = 'expired'; const COLLECTION = [ 'created' => 'Created', 'paid' => 'Paid', 'completed' => 'Completed', + 'cancelled' => 'Cancelled', + 'expired' => 'Expired', ]; } -- 2.43.5 From dd9c9dd0ecfd80891f3a452cfef5ac3cfee39c10 Mon Sep 17 00:00:00 2001 From: Ramon Gutierrez Date: Tue, 26 Sep 2023 04:53:13 +0800 Subject: [PATCH 07/22] Add endpoint for getting individual vehicle info #761 --- config/routes/apiv2.yaml | 5 + .../CustomerAppAPI/VehicleController.php | 134 +++++++++++------- 2 files changed, 91 insertions(+), 48 deletions(-) diff --git a/config/routes/apiv2.yaml b/config/routes/apiv2.yaml index 8f781ca7..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 diff --git a/src/Controller/CustomerAppAPI/VehicleController.php b/src/Controller/CustomerAppAPI/VehicleController.php index d56a86ee..9835208a 100644 --- a/src/Controller/CustomerAppAPI/VehicleController.php +++ b/src/Controller/CustomerAppAPI/VehicleController.php @@ -107,6 +107,34 @@ class VehicleController extends ApiController } + public function getVehicle(Request $req, $id) + { + // 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), + ]); + } + public function updateVehicle(Request $req, $id) { // check requirements @@ -162,54 +190,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(); - - // get latest insurance row - $insurance = null; - $iobj = $cv->getLatestInsuranceApplication(); - if (!empty($iobj)) { - $gt = $iobj->getGatewayTransaction(); - - $insurance = [ - 'id' => $iobj->getID(), - 'status' => $iobj->getStatus(), - '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 - ]; - } - - $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, - 'latest_insurance' => $insurance, - ]; + $cv_list[] = $this->generateVehicleInfo($cv); } // response @@ -302,6 +283,63 @@ class VehicleController extends ApiController return new ApiResponse(); } + protected function generateVehicleInfo(CustomerVehicle $cv, $include_insurance = false) + { + $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(); + + $insurance = [ + 'id' => $iobj->getID(), + 'status' => $iobj->getStatus(), + '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 + ]; + } + + $row['latest_insurance'] = $insurance; + } + + return $row; + } + protected function checkVehicleRequirements(Request $req) { // validate params -- 2.43.5 From 9b616cf398c66ff66d7875a6df188219a6152dce Mon Sep 17 00:00:00 2001 From: Ramon Gutierrez Date: Mon, 9 Oct 2023 01:15:12 +0800 Subject: [PATCH 08/22] Handle insurance callbacks, add support for non-JO related FCM notifications #761 --- .../CustomerAppAPI/InsuranceController.php | 6 +- src/Controller/InsuranceController.php | 96 ++++++++++++++++++- src/Entity/InsuranceApplication.php | 17 ++++ src/Service/FCMSender.php | 51 +++++++--- translations/messages.en.yaml | 26 +++-- 5 files changed, 168 insertions(+), 28 deletions(-) diff --git a/src/Controller/CustomerAppAPI/InsuranceController.php b/src/Controller/CustomerAppAPI/InsuranceController.php index eca85b69..a3c5624a 100644 --- a/src/Controller/CustomerAppAPI/InsuranceController.php +++ b/src/Controller/CustomerAppAPI/InsuranceController.php @@ -158,7 +158,6 @@ class InsuranceController extends ApiController $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(); @@ -179,9 +178,9 @@ class InsuranceController extends ApiController $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, - 'checkout_id' => $checkout['response']['data']['id'], ]); // store application in db @@ -191,8 +190,11 @@ class InsuranceController extends ApiController $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 diff --git a/src/Controller/InsuranceController.php b/src/Controller/InsuranceController.php index b720253e..f13c83d1 100644 --- a/src/Controller/InsuranceController.php +++ b/src/Controller/InsuranceController.php @@ -2,22 +2,116 @@ namespace App\Controller; +use App\Ramcar\InsuranceApplicationStatus; +use App\Service\FCMSender; 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 + + // 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']); + + if (!empty($obj)) { + // mark as completed + $obj->setStatus(InsuranceApplicationStatus::COMPLETED); + $obj->setCOC($payload['coc_url']); + $this->em->flush(); + + // send notification + $this->fcmclient->sendEvent($obj->getCustomer(), "insurance_fcm_title_completed", "insurance_fcm_body_completed"); + } + + 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"); + } + + 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/Entity/InsuranceApplication.php b/src/Entity/InsuranceApplication.php index d582feff..c9c19bef 100644 --- a/src/Entity/InsuranceApplication.php +++ b/src/Entity/InsuranceApplication.php @@ -74,6 +74,12 @@ class InsuranceApplication */ protected $date_complete; + // external transaction id + /** + * @ORM\Column(type="string", length=255, nullable=true) + */ + protected $ext_transaction_id; + // form data when submitting the application /** * @ORM\Column(type="json") @@ -179,6 +185,17 @@ class InsuranceApplication return $this->date_complete; } + 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; 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/translations/messages.en.yaml b/translations/messages.en.yaml index 78080119..79c348c8 100644 --- a/translations/messages.en.yaml +++ b/translations/messages.en.yaml @@ -160,13 +160,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!' -- 2.43.5 From e3a79361bd12d33a149ffa4c5fdea82ebf074735 Mon Sep 17 00:00:00 2001 From: Ramon Gutierrez Date: Mon, 9 Oct 2023 14:05:25 +0800 Subject: [PATCH 09/22] Remove auth from insurance listener. Fail silently if invalid callback given #761 --- config/packages/security.yaml | 4 ++++ src/Controller/InsuranceController.php | 7 +++++++ src/Controller/PayMongoController.php | 7 +++++++ 3 files changed, 18 insertions(+) diff --git a/config/packages/security.yaml b/config/packages/security.yaml index b745b4e3..7e9140dc 100644 --- a/config/packages/security.yaml +++ b/config/packages/security.yaml @@ -53,6 +53,10 @@ security: pattern: ^\/test_capi\/ security: false + insurance: + pattern: ^\/insurance\/ + security: false + paymongo: pattern: ^\/paymongo\/ security: false diff --git a/src/Controller/InsuranceController.php b/src/Controller/InsuranceController.php index f13c83d1..a60efe90 100644 --- a/src/Controller/InsuranceController.php +++ b/src/Controller/InsuranceController.php @@ -39,6 +39,13 @@ class InsuranceController extends Controller // END DEBUG + // if no transaction code given, silently fail + if (empty($attr['transaction_code'])) { + return $this->json([ + 'success' => true, + ]); + } + // get event type and process accordingly $event_name = $payload['transaction_code']; diff --git a/src/Controller/PayMongoController.php b/src/Controller/PayMongoController.php index 245cedfa..82625c36 100644 --- a/src/Controller/PayMongoController.php +++ b/src/Controller/PayMongoController.php @@ -34,6 +34,13 @@ class PayMongoController extends Controller // END DEBUG + // if no event type given, silently fail + if (empty($attr['type'])) { + return $this->json([ + 'success' => true, + ]); + } + // get event type and process accordingly $attr = $payload['data']['attributes']; $event = $attr['data']; -- 2.43.5 From cbe84d1cc88538219291329bbc14dba99fceab6c Mon Sep 17 00:00:00 2001 From: Ramon Gutierrez Date: Mon, 9 Oct 2023 15:48:05 +0800 Subject: [PATCH 10/22] Set insurance callback handler to process raw json body #761 --- src/Controller/InsuranceController.php | 4 +++- src/Controller/PayMongoController.php | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Controller/InsuranceController.php b/src/Controller/InsuranceController.php index a60efe90..6c72c17f 100644 --- a/src/Controller/InsuranceController.php +++ b/src/Controller/InsuranceController.php @@ -25,7 +25,7 @@ class InsuranceController extends Controller public function listen(Request $req) { - $payload = $req->request->all(); + $payload = json_decode($req->getContent(), true); // DEBUG @file_put_contents(__DIR__ . '/../../var/log/insurance.log', print_r($payload, true) . "\r\n----------------------------------------\r\n\r\n", FILE_APPEND); @@ -41,6 +41,8 @@ class InsuranceController extends Controller // if no transaction code given, silently fail if (empty($attr['transaction_code'])) { + error_log("Invalid insurance callback received: " . print_r($payload, true)); + return $this->json([ 'success' => true, ]); diff --git a/src/Controller/PayMongoController.php b/src/Controller/PayMongoController.php index 82625c36..fe79464b 100644 --- a/src/Controller/PayMongoController.php +++ b/src/Controller/PayMongoController.php @@ -36,6 +36,7 @@ class PayMongoController extends Controller // if no event type given, silently fail if (empty($attr['type'])) { + error_log("Invalid paymongo callback received: " . print_r($payload, true)); return $this->json([ 'success' => true, ]); -- 2.43.5 From c0c6fe26f73abbc884ea71f9ac9d101eca602605 Mon Sep 17 00:00:00 2001 From: Ramon Gutierrez Date: Mon, 9 Oct 2023 15:50:28 +0800 Subject: [PATCH 11/22] Fix wrong parameter being checked for invalid insurance callback #761 --- src/Controller/InsuranceController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Controller/InsuranceController.php b/src/Controller/InsuranceController.php index 6c72c17f..b76874fe 100644 --- a/src/Controller/InsuranceController.php +++ b/src/Controller/InsuranceController.php @@ -40,7 +40,7 @@ class InsuranceController extends Controller // END DEBUG // if no transaction code given, silently fail - if (empty($attr['transaction_code'])) { + if (empty($payload['transaction_code'])) { error_log("Invalid insurance callback received: " . print_r($payload, true)); return $this->json([ -- 2.43.5 From 516fbcfcbad6ca220d88e60a8586e10dfee9bc17 Mon Sep 17 00:00:00 2001 From: Ramon Gutierrez Date: Mon, 9 Oct 2023 15:51:33 +0800 Subject: [PATCH 12/22] Add missing dependency to insurance callback handler #761 --- src/Controller/InsuranceController.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Controller/InsuranceController.php b/src/Controller/InsuranceController.php index b76874fe..329cc59b 100644 --- a/src/Controller/InsuranceController.php +++ b/src/Controller/InsuranceController.php @@ -4,6 +4,7 @@ namespace App\Controller; use App\Ramcar\InsuranceApplicationStatus; use App\Service\FCMSender; +use App\Entity\InsuranceApplication; use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\HttpFoundation\Request; -- 2.43.5 From c332757a533534a3c07898a03b0e26fed1d321f3 Mon Sep 17 00:00:00 2001 From: Ramon Gutierrez Date: Mon, 9 Oct 2023 16:35:06 +0800 Subject: [PATCH 13/22] Fix paymongo handler not checking for the correct payload parameter on validation #761 --- src/Controller/PayMongoController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Controller/PayMongoController.php b/src/Controller/PayMongoController.php index fe79464b..02f75928 100644 --- a/src/Controller/PayMongoController.php +++ b/src/Controller/PayMongoController.php @@ -35,7 +35,7 @@ class PayMongoController extends Controller // END DEBUG // if no event type given, silently fail - if (empty($attr['type'])) { + if (empty($payload['data'])) { error_log("Invalid paymongo callback received: " . print_r($payload, true)); return $this->json([ 'success' => true, -- 2.43.5 From d3054760d3201cfcab464f479de3dc77592d2703 Mon Sep 17 00:00:00 2001 From: Ramon Gutierrez Date: Mon, 9 Oct 2023 16:56:41 +0800 Subject: [PATCH 14/22] Add external transaction id to vehicle info endpoint #761 --- src/Controller/CustomerAppAPI/VehicleController.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Controller/CustomerAppAPI/VehicleController.php b/src/Controller/CustomerAppAPI/VehicleController.php index 9835208a..a4be2ca5 100644 --- a/src/Controller/CustomerAppAPI/VehicleController.php +++ b/src/Controller/CustomerAppAPI/VehicleController.php @@ -326,6 +326,7 @@ class VehicleController extends ApiController $insurance = [ 'id' => $iobj->getID(), + 'ext_transaction_id' => $iobj->getExtTransactionId(), 'status' => $iobj->getStatus(), 'coc_url' => $iobj->getCOC(), 'checkout_url' => $gt->getMetadata()['checkout_url'], -- 2.43.5 From da87b386a324a5883df5a9375c4533bda970cd98 Mon Sep 17 00:00:00 2001 From: Ramon Gutierrez Date: Mon, 9 Oct 2023 17:02:23 +0800 Subject: [PATCH 15/22] Add changelog to insurance info on vehicle endpoint #761 --- src/Controller/CustomerAppAPI/VehicleController.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Controller/CustomerAppAPI/VehicleController.php b/src/Controller/CustomerAppAPI/VehicleController.php index a4be2ca5..446f43b3 100644 --- a/src/Controller/CustomerAppAPI/VehicleController.php +++ b/src/Controller/CustomerAppAPI/VehicleController.php @@ -333,6 +333,9 @@ class VehicleController extends ApiController 'transaction_status' => $gt->getStatus(), 'premium_amount' => (string)bcdiv($gt->getAmount(), 100), // NOTE: hard expressing as string so it's consistent ]; + + // get information changelog + $insurance['changelog'] = $iobj->getMetadata()['changes'] ?? []; } $row['latest_insurance'] = $insurance; -- 2.43.5 From 502e9a11cdac3bd0f2a4327bb1919b72a2962166 Mon Sep 17 00:00:00 2001 From: Ramon Gutierrez Date: Wed, 11 Oct 2023 15:39:07 +0800 Subject: [PATCH 16/22] Add customer vehicle id to insurance FCM notifications #761 --- src/Controller/InsuranceController.php | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Controller/InsuranceController.php b/src/Controller/InsuranceController.php index 329cc59b..91b1b855 100644 --- a/src/Controller/InsuranceController.php +++ b/src/Controller/InsuranceController.php @@ -80,7 +80,9 @@ class InsuranceController extends Controller $this->em->flush(); // send notification - $this->fcmclient->sendEvent($obj->getCustomer(), "insurance_fcm_title_completed", "insurance_fcm_body_completed"); + $this->fcmclient->sendEvent($obj->getCustomer(), "insurance_fcm_title_completed", "insurance_fcm_body_completed", [ + 'cv_id' => $obj->getCustomerVehicle()->getID(), + ]); } return $this->json([ @@ -108,7 +110,9 @@ class InsuranceController extends Controller $this->em->flush(); // send notification - $this->fcmclient->sendEvent($obj->getCustomer(), "insurance_fcm_title_updated", "insurance_fcm_body_updated"); + $this->fcmclient->sendEvent($obj->getCustomer(), "insurance_fcm_title_updated", "insurance_fcm_body_updated", [ + 'cv_id' => $obj->getCustomerVehicle()->getID(), + ]); } return $this->json([ -- 2.43.5 From 38262151e8d564ca0780932ad76117003fe86ffe Mon Sep 17 00:00:00 2001 From: Ramon Gutierrez Date: Wed, 11 Oct 2023 17:06:37 +0800 Subject: [PATCH 17/22] Add dates to vehicle info insurance info #761 --- .../CustomerAppAPI/VehicleController.php | 5 +++++ src/Controller/InsuranceController.php | 7 +++++++ src/Entity/InsuranceApplication.php | 18 ++++++++++++++++++ 3 files changed, 30 insertions(+) diff --git a/src/Controller/CustomerAppAPI/VehicleController.php b/src/Controller/CustomerAppAPI/VehicleController.php index 446f43b3..24d28a4f 100644 --- a/src/Controller/CustomerAppAPI/VehicleController.php +++ b/src/Controller/CustomerAppAPI/VehicleController.php @@ -323,6 +323,8 @@ class VehicleController extends ApiController $iobj = $cv->getLatestInsuranceApplication(); if (!empty($iobj)) { $gt = $iobj->getGatewayTransaction(); + $date_complete = $iobj->getDateComplete(); + $date_expire = $iobj->getDateExpire(); $insurance = [ 'id' => $iobj->getID(), @@ -332,6 +334,9 @@ class VehicleController extends ApiController '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 diff --git a/src/Controller/InsuranceController.php b/src/Controller/InsuranceController.php index 91b1b855..c4fd4800 100644 --- a/src/Controller/InsuranceController.php +++ b/src/Controller/InsuranceController.php @@ -73,9 +73,16 @@ class InsuranceController extends Controller { $obj = $this->getApplication($payload['id']); + $now = new DateTime; + + // TODO: replacing this with actual callback response once provided + $expiry = $now->modify('+1 year'); + if (!empty($obj)) { // mark as completed $obj->setStatus(InsuranceApplicationStatus::COMPLETED); + $obj->setDateComplete($now); + $obj->setDateExpire($expiry); $obj->setCOC($payload['coc_url']); $this->em->flush(); diff --git a/src/Entity/InsuranceApplication.php b/src/Entity/InsuranceApplication.php index c9c19bef..3887e259 100644 --- a/src/Entity/InsuranceApplication.php +++ b/src/Entity/InsuranceApplication.php @@ -74,6 +74,12 @@ class InsuranceApplication */ protected $date_complete; + // date the application is set to expire + /** + * @ORM\Column(type="datetime", nullable=true) + */ + protected $date_expire; + // external transaction id /** * @ORM\Column(type="string", length=255, nullable=true) @@ -91,6 +97,7 @@ class InsuranceApplication $this->date_submit = new DateTime(); $this->date_pay = null; $this->date_complete = null; + $this->date_expire = null; $this->metadata = []; } @@ -185,6 +192,17 @@ class InsuranceApplication 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; -- 2.43.5 From 708e9a67ccb528ac7ae3601594978e7f081aee26 Mon Sep 17 00:00:00 2001 From: Ramon Gutierrez Date: Fri, 13 Oct 2023 14:13:57 +0800 Subject: [PATCH 18/22] Update insurance callback handler to expect formdata #761 --- src/Controller/InsuranceController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Controller/InsuranceController.php b/src/Controller/InsuranceController.php index c4fd4800..63dc8649 100644 --- a/src/Controller/InsuranceController.php +++ b/src/Controller/InsuranceController.php @@ -26,7 +26,7 @@ class InsuranceController extends Controller public function listen(Request $req) { - $payload = json_decode($req->getContent(), true); + $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); -- 2.43.5 From ac2cf864f20a952751bc22f7d5253eab60bd5fcc Mon Sep 17 00:00:00 2001 From: Ramon Gutierrez Date: Fri, 13 Oct 2023 17:38:03 +0800 Subject: [PATCH 19/22] Handle expiry date from insurance completed callback #761 --- src/Controller/InsuranceController.php | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/Controller/InsuranceController.php b/src/Controller/InsuranceController.php index 63dc8649..97744d27 100644 --- a/src/Controller/InsuranceController.php +++ b/src/Controller/InsuranceController.php @@ -72,11 +72,8 @@ class InsuranceController extends Controller protected function handleAuthenticated($payload) { $obj = $this->getApplication($payload['id']); - - $now = new DateTime; - - // TODO: replacing this with actual callback response once provided - $expiry = $now->modify('+1 year'); + $now = new DateTime(); + $expiry = DateTime::createFromFormat("Y-m-d", $payload['expiry_date']); if (!empty($obj)) { // mark as completed -- 2.43.5 From b1ff62d9ec6bc4a00c9b6c27dc6dfdfd2178fe62 Mon Sep 17 00:00:00 2001 From: Ramon Gutierrez Date: Tue, 17 Oct 2023 14:43:06 +0800 Subject: [PATCH 20/22] Add back insurance status to vehicle list so latest status can be displayed #761 --- src/Controller/CustomerAppAPI/VehicleController.php | 2 +- src/Entity/CustomerVehicle.php | 6 ++++-- src/Ramcar/InsuranceApplicationStatus.php | 2 -- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Controller/CustomerAppAPI/VehicleController.php b/src/Controller/CustomerAppAPI/VehicleController.php index 24d28a4f..99bee31f 100644 --- a/src/Controller/CustomerAppAPI/VehicleController.php +++ b/src/Controller/CustomerAppAPI/VehicleController.php @@ -190,7 +190,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); + $cv_list[] = $this->generateVehicleInfo($cv, true); } // response diff --git a/src/Entity/CustomerVehicle.php b/src/Entity/CustomerVehicle.php index a120c71e..a569d4b2 100644 --- a/src/Entity/CustomerVehicle.php +++ b/src/Entity/CustomerVehicle.php @@ -298,10 +298,12 @@ class CustomerVehicle public function getLatestInsuranceApplication() { $criteria = Criteria::create() - ->where(Criteria::expr()->notIn('status', [InsuranceApplicationStatus::EXPIRED, InsuranceApplicationStatus::CANCELLED])) + ->where(Criteria::expr()->notIn('status', [InsuranceApplicationStatus::CANCELLED])) ->orderBy(['date_submit' => Criteria::DESC]) ->setMaxResults(1); - return $this->insurance_applications->matching($criteria)[0]; + $result = $this->insurance_applications->matching($criteria); + + return !empty($result) ? $result[0] : null; } } diff --git a/src/Ramcar/InsuranceApplicationStatus.php b/src/Ramcar/InsuranceApplicationStatus.php index 8908a3a8..8480239c 100644 --- a/src/Ramcar/InsuranceApplicationStatus.php +++ b/src/Ramcar/InsuranceApplicationStatus.php @@ -8,13 +8,11 @@ class InsuranceApplicationStatus extends NameValue const PAID = 'paid'; const COMPLETED = 'completed'; const CANCELLED = 'cancelled'; - const EXPIRED = 'expired'; const COLLECTION = [ 'created' => 'Created', 'paid' => 'Paid', 'completed' => 'Completed', 'cancelled' => 'Cancelled', - 'expired' => 'Expired', ]; } -- 2.43.5 From 2432a08c4d085f7f62ed6cb761d7a8ec99c6b3f5 Mon Sep 17 00:00:00 2001 From: Ramon Gutierrez Date: Mon, 23 Oct 2023 01:17:05 +0800 Subject: [PATCH 21/22] Add support for transient status between making a payment and receiving the webhook #761 --- .../CustomerAppAPI/VehicleController.php | 41 +++++++++++++++---- 1 file changed, 34 insertions(+), 7 deletions(-) diff --git a/src/Controller/CustomerAppAPI/VehicleController.php b/src/Controller/CustomerAppAPI/VehicleController.php index 99bee31f..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,7 +108,7 @@ class VehicleController extends ApiController } - public function getVehicle(Request $req, $id) + public function getVehicle(Request $req, $id, PayMongoConnector $paymongo) { // check requirements $validity = $this->validateRequest($req); @@ -131,7 +132,7 @@ class VehicleController extends ApiController // response return new ApiResponse(true, '', [ - 'vehicle' => $this->generateVehicleInfo($cv, true), + 'vehicle' => $this->generateVehicleInfo($cv, true, $paymongo), ]); } @@ -169,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); @@ -190,7 +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) { - $cv_list[] = $this->generateVehicleInfo($cv, true); + $cv_list[] = $this->generateVehicleInfo($cv, true, $paymongo); } // response @@ -283,7 +284,7 @@ class VehicleController extends ApiController return new ApiResponse(); } - protected function generateVehicleInfo(CustomerVehicle $cv, $include_insurance = false) + protected function generateVehicleInfo(CustomerVehicle $cv, $include_insurance = false, PayMongoConnector $paymongo) { $battery_id = null; if ($cv->getCurrentBattery() != null) @@ -325,11 +326,37 @@ class VehicleController extends ApiController $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' => $iobj->getStatus(), + 'status' => $status, 'coc_url' => $iobj->getCOC(), 'checkout_url' => $gt->getMetadata()['checkout_url'], 'transaction_status' => $gt->getStatus(), -- 2.43.5 From ff753d5452f90f7273822633f7b94d59fe973e2e Mon Sep 17 00:00:00 2001 From: Ramon Gutierrez Date: Mon, 23 Oct 2023 12:42:09 +0800 Subject: [PATCH 22/22] Remove initial 0 from mobile number passed to paymongo checkout api #761 --- src/Service/PayMongoConnector.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Service/PayMongoConnector.php b/src/Service/PayMongoConnector.php index 34ec1f6c..ad2750a2 100644 --- a/src/Service/PayMongoConnector.php +++ b/src/Service/PayMongoConnector.php @@ -28,7 +28,7 @@ class PayMongoConnector // build billing info $billing = [ 'name' => implode(" ", [$cust->getFirstName(), $cust->getLastName()]), - 'phone' => "0" . $cust->getPhoneMobile(), + 'phone' => $cust->getPhoneMobile(), ]; if ($cust->getEmail()) { -- 2.43.5