diff --git a/CHANGELOG.md b/CHANGELOG.md index 172e3a3..a2fe256 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,14 @@ # CHANGELOG -## 2020-12-18 +## 1.3.0 (2021-01-19) +- Error message standardization + +## 1.2.2 (2020-12-18) - Do not show API key on admin page -## 2020-12-10 +## 1.2.1 (2020-12-10) - Improve callback endpoint security to check order number from source of truth -## 2020-07-02 +## 1.2.0 (2020-07-02) - Refactor xendit_order table for all versions - Ensure all Xendit orders are recorded in DB \ No newline at end of file diff --git a/opencart1.5.x/upload/catalog/controller/payment/xendit.php b/opencart1.5.x/upload/catalog/controller/payment/xendit.php index 7d4b3c7..ae808c6 100644 --- a/opencart1.5.x/upload/catalog/controller/payment/xendit.php +++ b/opencart1.5.x/upload/catalog/controller/payment/xendit.php @@ -4,6 +4,7 @@ class ControllerPaymentXendit extends Controller { const EXT_ID_PREFIX = 'opencart-xendit-'; + const MINIMUM_AMOUNT = 10000; public function index() { $this->load->language('payment/xendit'); @@ -32,9 +33,18 @@ public function process_payment() { Xendit::set_public_key($api_key['public_key']); $store_name = $this->config->get('config_name'); + $amount = (int)$order['total']; + + if ($amount < self::MINIMUM_AMOUNT) { + $json['error'] = 'The minimum amount for using this payment is IDR ' . self::MINIMUM_AMOUNT . '. Please put more item(s) to reach the minimum amount. Code: 100001'; + + $this->response->addHeader('Content-Type: application/json'); + return $this->response->setOutput(json_encode($json)); + } + $request_payload = array( 'external_id' => self::EXT_ID_PREFIX . $order_id, - 'amount' => (int)$order['total'], + 'amount' => $amount, 'payer_email' => $order['email'], 'description' => 'Payment for order #' . $order_id . ' at ' . $store_name, 'client_type' => 'INTEGRATION', @@ -49,9 +59,14 @@ public function process_payment() { try { $response = Xendit::request($request_url, Xendit::METHOD_POST, $request_payload, $request_options); - + if (isset($response['error_code'])) { - $json['error'] = $response['message']; + $message = $response['message']; + + if (isset($response['code'])) { + $message .= " Code: " . $response['code']; + } + $json['error'] = $message; } else { $this->model_payment_xendit->addOrder($order, $response, $this->config->get('payment_xendit_environment'), 'invoice'); diff --git a/opencart1.5.x/upload/catalog/controller/payment/xenditcc.php b/opencart1.5.x/upload/catalog/controller/payment/xenditcc.php index 90f77b6..890ff30 100644 --- a/opencart1.5.x/upload/catalog/controller/payment/xenditcc.php +++ b/opencart1.5.x/upload/catalog/controller/payment/xenditcc.php @@ -4,6 +4,7 @@ class ControllerPaymentXenditCC extends Controller { const EXT_ID_PREFIX = 'opencart-xendit-'; + const MINIMUM_AMOUNT = 5000; public function index() { $this->load->language('payment/xenditcc'); @@ -32,10 +33,19 @@ public function process_payment() { ); $store_name = $this->config->get('config_name'); + $amount = (int)$order['total']; + + if ($amount < self::MINIMUM_AMOUNT) { + $json['error'] = 'The minimum amount for using this payment is IDR ' . self::MINIMUM_AMOUNT . '. Please put more item(s) to reach the minimum amount. Code: 100001'; + + $this->response->addHeader('Content-Type: application/json'); + return $this->response->setOutput(json_encode($json)); + } + $request_payload = array( 'external_id' => self::EXT_ID_PREFIX . $order_id, 'token_id' => $this->request->post['token_id'], - 'amount' => (int)$order['total'], + 'amount' => $amount, 'return_url' => $this->url->link('payment/xenditcc/process_3ds') ); $request_url = '/payment/xendit/credit-card/hosted-3ds'; @@ -52,7 +62,12 @@ public function process_payment() { $response = Xendit::request($request_url, Xendit::METHOD_POST, $request_payload, $request_options); if (isset($response['error_code'])) { - $json['error'] = $response['message']; + $message = $response['message']; + + if (isset($response['code'])) { + $message .= " Code: " . $response['code']; + } + $json['error'] = $message; } else { $response['external_id'] = $request_payload['external_id']; //original response doesn't return external_id @@ -90,8 +105,12 @@ public function process_3ds() { Xendit::set_public_key($api_key['public_key']); if (!isset($this->request->get['hosted_3ds_id'])) { - $message = 'Empty authentication. Cancelling order.'; + $message = $this->map_failure_reason('AUTHENTICATION_FAILED'); $this->cancel_order($order_id, $message); + + $redir_url = $this->url->link('extension/payment/xenditcc/failure', 'message=' . urlencode($message), 'SSL'); + $this->response->redirect($redir_url); + return; } $hosted_3ds_id = $this->request->get['hosted_3ds_id']; @@ -105,16 +124,23 @@ public function process_3ds() { 'should_use_public_key' => true ) ); - + if (isset($hosted_3ds['error_code'])) { - $redir_url = $this->url->link('payment/xenditcc/failure'); + $message = $this->map_failure_reason('AUTHENTICATION_FAILED'); + $this->cancel_order($order_id, $message); + + $redir_url = $this->url->link('extension/payment/xenditcc/failure', 'message=' . urlencode($message), 'SSL'); $this->response->redirect($redir_url); return; } - + if ('VERIFIED' !== $hosted_3ds['status']) { - $message = 'Authentication failed. Cancelling order.'; + $message = $this->map_failure_reason('AUTHENTICATION_FAILED'); $this->cancel_order($order_id, $message); + + $redir_url = $this->url->link('extension/payment/xenditcc/failure', 'message=' . urlencode($message), 'SSL'); + $this->response->redirect($redir_url); + return; } $token_id = $hosted_3ds['token_id']; @@ -138,6 +164,19 @@ public function process_3ds() { ) ); + if (isset($charge['error_code'])) { + $message = $charge['message']; + + if (isset($charge['code'])) { + $message .= " Code: " . $charge['code']; + } + $this->cancel_order($order_id, $message); + + $redir_url = $this->url->link('extension/payment/xenditcc/failure', 'message=' . urlencode($message), 'SSL'); + $this->response->redirect($redir_url); + return; + } + $this->process_order($charge, $order_id, $charge_data); } catch (Exception $e) { $redir_url = $this->url->link('payment/xenditcc/failure'); @@ -152,6 +191,7 @@ public function failure() { $this->document->setTitle($this->language->get('heading_title')); $this->data['heading_title'] = $this->language->get('heading_title'); $this->data['text_failure'] = $this->language->get('text_failure'); + $this->data['message'] = isset($this->request->get['message']) ? $this->request->get['message'] : 'We encountered an issue while processing the checkout. Please contact us. Code: 100007'; $this->data['column_left'] = $this->getChild('common/column_left'); $this->data['column_right'] = $this->getChild('common/column_right'); @@ -167,10 +207,10 @@ public function failure() { private function process_order($charge, $order_id, $charge_data) { if ($charge['status'] !== 'CAPTURED') { - $message = 'Charge failed. Cancelling order. Charge ID: ' . $charge['id']; + $message = $this->map_failure_reason($charge['failure_reason']); $this->cancel_order($order_id, $message); - $redir_url = $this->url->link('payment/xenditcc/failure'); + $redir_url = $this->url->link('extension/payment/xenditcc/failure', 'message=' . urlencode($message), 'SSL'); $this->response->redirect($redir_url); return; } @@ -218,4 +258,29 @@ private function get_api_key() { ); } } + + private function map_failure_reason($failure_reason) { + $card_declined_reason = 'Card declined by the issuer bank. Please try with another card or contact the bank directly.'; + switch ($failure_reason) { + case 'CARD_DECLINED': + return $card_declined_reason . ' Code: 200011'; + case 'STOLEN_CARD': + return $card_declined_reason . ' Code: 200013'; + case 'INSUFFICIENT_BALANCE': + return 'Card declined due to insufficient balance. Ensure sufficient balance is available, or try another card. Code: 200012'; + case 'INVALID_CVN': + return 'Card declined due to incorrect card details. Please try again. Code: 200015'; + case 'INACTIVE_CARD': + return $card_declined_reason . ' Code: 200014'; + case 'EXPIRED_CARD': + return 'Card declined due to expiration. Please try again with another card. Code: 200010'; + case 'PROCESSOR_ERROR': + return 'We encountered an issue while processing the checkout. Please try again. Code: 200009'; + case 'AUTHENTICATION_FAILED': + return 'The authentication process failed. Please try again. Code: 200001'; + case 'UNEXPECTED_PLUGIN_ISSUE': + return 'We encountered an issue processing your checkout, please contact us. Code: 100007'; + default: return $failure_reason; + } + } } \ No newline at end of file diff --git a/opencart1.5.x/upload/catalog/view/theme/default/template/payment/xenditcc.tpl b/opencart1.5.x/upload/catalog/view/theme/default/template/payment/xenditcc.tpl index 757694e..a0a2c77 100644 --- a/opencart1.5.x/upload/catalog/view/theme/default/template/payment/xenditcc.tpl +++ b/opencart1.5.x/upload/catalog/view/theme/default/template/payment/xenditcc.tpl @@ -87,17 +87,31 @@ is_multiple_use: true }; - // Validation - if (data.card_number == '') { - alert('Please fill in Credit Card Number.'); + if (!data.card_number || !data.card_cvn || !data.card_exp_month || !data.card_exp_year) { + buttonConfirm.attr('disabled', false); + + alert('Card information is incomplete. Please complete it and try again. Code: 200034'); + return; + } + + if (!Xendit.card.validateCardNumber(data.card_number)) { + buttonConfirm.attr('disabled', false); + + alert('Invalid Card Number. Please make sure the card is Visa / Mastercard / JCB. Code: 200030'); return; } - if (expMonth == '' || expYear == '') { - alert('Please fill in Card Expiry Month & Year.'); + + if (!Xendit.card.validateCvnForCardType(data.card_cvn, data.card_number)) { + buttonConfirm.attr('disabled', false); + + alert('The CVC/CVN that you entered is less than 3 digits. Please enter the correct value and try again. Code: 200032'); return; } - if (data.card_cvn == '') { - alert('Please fill in CVN.'); + + if (!Xendit.card.validateExpiry(data.card_exp_month, data.card_exp_year)) { + buttonConfirm.attr('disabled', false); + + alert('The card expiry that you entered does not meet the expected format. Please try again by entering the 2 digits of the month (MM) and the last 2 digits of the year (YY). Code: 200031'); return; } @@ -107,7 +121,7 @@ if (err) { buttonConfirm.attr('disabled', false); - alert('Tokenization error. Error code:' + err.error_code); + alert('We encountered an issue while processing the checkout. Please contact us. Code: 200035'); return; } diff --git a/opencart2.0.x-2.2.x/upload/catalog/controller/payment/xendit.php b/opencart2.0.x-2.2.x/upload/catalog/controller/payment/xendit.php index 7d2fcef..21ff01a 100644 --- a/opencart2.0.x-2.2.x/upload/catalog/controller/payment/xendit.php +++ b/opencart2.0.x-2.2.x/upload/catalog/controller/payment/xendit.php @@ -5,6 +5,7 @@ class Controllerpaymentxendit extends Controller { const EXT_ID_PREFIX = 'opencart-xendit-'; + const MINIMUM_AMOUNT = 10000; public function index() { @@ -41,9 +42,18 @@ public function process_payment() Xendit::set_public_key($api_key['public_key']); $store_name = $this->config->get('config_name'); + $amount = (int)$order['total']; + + if ($amount < self::MINIMUM_AMOUNT) { + $json['error'] = 'The minimum amount for using this payment is IDR ' . self::MINIMUM_AMOUNT . '. Please put more item(s) to reach the minimum amount. Code: 100001'; + + $this->response->addHeader('Content-Type: application/json'); + return $this->response->setOutput(json_encode($json)); + } + $request_payload = array( 'external_id' => self::EXT_ID_PREFIX . $order_id, - 'amount' => (int) $order['total'], + 'amount' => $amount, 'payer_email' => $order['email'], 'description' => 'Payment for order #' . $order_id . ' at ' . $store_name, 'client_type' => 'INTEGRATION', @@ -60,8 +70,13 @@ public function process_payment() $response = Xendit::request($request_url, Xendit::METHOD_POST, $request_payload, $request_options); if (isset($response['error_code'])) { - $json['error'] = $response['message']; - } + $message = $response['message']; + + if (isset($response['code'])) { + $message .= " Code: " . $response['code']; + } + $json['error'] = $message; + } else { $this->model_payment_xendit->addOrder($order, $response, $this->config->get('xendit_environment'), 'invoice'); $message = 'Invoice ID: ' . $response['id'] . '. Redirecting..'; diff --git a/opencart2.0.x-2.2.x/upload/catalog/controller/payment/xenditcc.php b/opencart2.0.x-2.2.x/upload/catalog/controller/payment/xenditcc.php index 5894243..1d932ac 100644 --- a/opencart2.0.x-2.2.x/upload/catalog/controller/payment/xenditcc.php +++ b/opencart2.0.x-2.2.x/upload/catalog/controller/payment/xenditcc.php @@ -5,6 +5,7 @@ class Controllerpaymentxenditcc extends Controller { const EXT_ID_PREFIX = 'opencart-xendit-'; + const MINIMUM_AMOUNT = 5000; public function index() { $this->load->language('payment/xenditcc'); @@ -37,10 +38,19 @@ public function process_payment() { ); $store_name = $this->config->get('config_name'); + $amount = (int)$order['total']; + + if ($amount < self::MINIMUM_AMOUNT) { + $json['error'] = 'The minimum amount for using this payment is IDR ' . self::MINIMUM_AMOUNT . '. Please put more item(s) to reach the minimum amount. Code: 100001'; + + $this->response->addHeader('Content-Type: application/json'); + return $this->response->setOutput(json_encode($json)); + } + $request_payload = array( 'external_id' => self::EXT_ID_PREFIX . $order_id, 'token_id' => $this->request->post['token_id'], - 'amount' => (int)$order['total'], + 'amount' => $amount, 'return_url' => $this->url->link('payment/xenditcc/process_3ds') ); $request_url = '/payment/xendit/credit-card/hosted-3ds'; @@ -57,7 +67,12 @@ public function process_payment() { $response = Xendit::request($request_url, Xendit::METHOD_POST, $request_payload, $request_options); if (isset($response['error_code'])) { - $json['error'] = 'Failed to authenticate, please try again.'; + $message = $response['message']; + + if (isset($response['code'])) { + $message .= " Code: " . $response['code']; + } + $json['error'] = $message; } else { $response['external_id'] = $request_payload['external_id']; @@ -96,10 +111,10 @@ public function process_3ds() { Xendit::set_public_key($api_key['public_key']); if (!isset($this->request->get['hosted_3ds_id'])) { - $message = 'Empty authentication. Cancelling order.'; + $message = $this->map_failure_reason('AUTHENTICATION_FAILED'); $this->cancel_order($order_id, $message); - $redir_url = $this->url->link('payment/xenditcc/failure'); + $redir_url = $this->url->link('extension/payment/xenditcc/failure', 'message=' . urlencode($message), 'SSL'); $this->response->redirect($redir_url); return; } @@ -117,16 +132,19 @@ public function process_3ds() { ); if (isset($hosted_3ds['error_code'])) { - $redir_url = $this->url->link('payment/xenditcc/failure'); + $message = $this->map_failure_reason('AUTHENTICATION_FAILED'); + $this->cancel_order($order_id, $message); + + $redir_url = $this->url->link('extension/payment/xenditcc/failure', 'message=' . urlencode($message), 'SSL'); $this->response->redirect($redir_url); return; } if ('VERIFIED' !== $hosted_3ds['status']) { - $message = 'Authentication failed. Cancelling order.'; + $message = $this->map_failure_reason('AUTHENTICATION_FAILED'); $this->cancel_order($order_id, $message); - $redir_url = $this->url->link('payment/xenditcc/failure'); + $redir_url = $this->url->link('extension/payment/xenditcc/failure', 'message=' . urlencode($message), 'SSL'); $this->response->redirect($redir_url); return; } @@ -152,6 +170,19 @@ public function process_3ds() { ) ); + if (isset($charge['error_code'])) { + $message = $charge['message']; + + if (isset($charge['code'])) { + $message .= " Code: " . $charge['code']; + } + $this->cancel_order($order_id, $message); + + $redir_url = $this->url->link('extension/payment/xenditcc/failure', 'message=' . urlencode($message), 'SSL'); + $this->response->redirect($redir_url); + return; + } + $this->process_order($charge, $order_id); } catch (Exception $e) { $redir_url = $this->url->link('payment/xenditcc/failure'); @@ -166,6 +197,7 @@ public function failure() { $this->document->setTitle($this->language->get('heading_title')); $data['heading_title'] = $this->language->get('heading_title'); $data['text_failure'] = $this->language->get('text_failure'); + $data['message'] = isset($this->request->get['message']) ? $this->request->get['message'] : 'We encountered an issue while processing the checkout. Please contact us. Code: 100007'; $data['column_left'] = $this->load->controller('common/column_left'); $data['column_right'] = $this->load->controller('common/column_right'); @@ -184,9 +216,10 @@ public function failure() { private function process_order($charge, $order_id) { if ($charge['status'] !== 'CAPTURED') { - $message = 'Charge failed. Cancelling order. Charge id: ' . $charge['id']; + $message = $this->map_failure_reason($charge['failure_reason']); $this->cancel_order($order_id, $message); - $redir_url = $this->url->link('payment/xenditcc/failure'); + + $redir_url = $this->url->link('extension/payment/xenditcc/failure', 'message=' . urlencode($message), 'SSL'); $this->response->redirect($redir_url); return; } @@ -233,4 +266,29 @@ private function get_api_key() { ); } } + + private function map_failure_reason($failure_reason) { + $card_declined_reason = 'Card declined by the issuer bank. Please try with another card or contact the bank directly.'; + switch ($failure_reason) { + case 'CARD_DECLINED': + return $card_declined_reason . ' Code: 200011'; + case 'STOLEN_CARD': + return $card_declined_reason . ' Code: 200013'; + case 'INSUFFICIENT_BALANCE': + return 'Card declined due to insufficient balance. Ensure sufficient balance is available, or try another card. Code: 200012'; + case 'INVALID_CVN': + return 'Card declined due to incorrect card details. Please try again. Code: 200015'; + case 'INACTIVE_CARD': + return $card_declined_reason . ' Code: 200014'; + case 'EXPIRED_CARD': + return 'Card declined due to expiration. Please try again with another card. Code: 200010'; + case 'PROCESSOR_ERROR': + return 'We encountered an issue while processing the checkout. Please try again. Code: 200009'; + case 'AUTHENTICATION_FAILED': + return 'The authentication process failed. Please try again. Code: 200001'; + case 'UNEXPECTED_PLUGIN_ISSUE': + return 'We encountered an issue processing your checkout, please contact us. Code: 100007'; + default: return $failure_reason; + } + } } \ No newline at end of file diff --git a/opencart2.0.x-2.2.x/upload/catalog/view/theme/default/template/payment/xendit_failed.tpl b/opencart2.0.x-2.2.x/upload/catalog/view/theme/default/template/payment/xendit_failed.tpl index ea14541..6cb7ead 100644 --- a/opencart2.0.x-2.2.x/upload/catalog/view/theme/default/template/payment/xendit_failed.tpl +++ b/opencart2.0.x-2.2.x/upload/catalog/view/theme/default/template/payment/xendit_failed.tpl @@ -2,6 +2,7 @@