Skip to content

Commit 70be897

Browse files
authored
Merge pull request #67 from xendit/TPI-2484/fix-callback
improve callback to check order number from source of truth
2 parents c93ff45 + 2197a39 commit 70be897

10 files changed

+186
-90
lines changed

CHANGELOG.md

+6
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# CHANGELOG
22

3+
## 2.4.3 (2020-12-10)
4+
5+
Improvements:
6+
7+
- Improve callback endpoint security to check order number from source of truth
8+
39
## 2.4.2 (2020-11-23)
410

511
Improvements:

Xendit/M2Invoice/Controller/Checkout/CCCallback.m22.php

+64-32
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,35 @@
66
use Magento\Sales\Model\Order;
77
use Xendit\M2Invoice\Enum\LogDNALevel;
88

9+
/**
10+
* This callback is only for order in multishipping flow. For order
11+
* created in onepage checkout is handled in ProcessHosted.php
12+
*/
913
class CCCallback extends ProcessHosted
1014
{
1115
public function execute()
1216
{
1317
try {
14-
$orderIds = explode('-', $this->getRequest()->getParam('order_ids'));
18+
$post = $this->getRequest()->getContent();
19+
$callbackPayload = json_decode($post, true);
20+
21+
if (
22+
!isset($callbackPayload['id']) ||
23+
!isset($callbackPayload['hp_token']) ||
24+
!isset($callbackPayload['order_number'])
25+
) {
26+
$result = $this->getJsonResultFactory()->create();
27+
$result->setHttpResponseCode(\Magento\Framework\Webapi\Exception::HTTP_BAD_REQUEST);
28+
$result->setData([
29+
'status' => __('ERROR'),
30+
'message' => 'Callback body is invalid'
31+
]);
32+
33+
return $result;
34+
}
35+
$orderIds = explode('-', $callbackPayload['order_number']);
36+
$hostedPaymentId = $callbackPayload['id'];
37+
$hostedPaymentToken = $callbackPayload['hp_token'];
1538

1639
$shouldRedirect = false;
1740
$isError = false;
@@ -23,40 +46,49 @@ public function execute()
2346

2447
$payment = $order->getPayment();
2548

26-
if ($payment->getAdditionalInformation('xendit_hosted_payment_id') !== null) {
27-
$requestData = [
28-
'id' => $payment->getAdditionalInformation('xendit_hosted_payment_id'),
29-
'hp_token' => $payment->getAdditionalInformation('xendit_hosted_payment_token')
30-
];
31-
32-
if ($flag) { // complete hosted payment only once as status will be changed to USED
33-
$hostedPayment = $this->getCompletedHostedPayment($requestData);
34-
$flag = false;
35-
}
36-
37-
if (isset($hostedPayment['error_code'])) {
38-
$isError = true;
49+
$requestData = [
50+
'id' => $hostedPaymentId,
51+
'hp_token' => $hostedPaymentToken
52+
];
53+
54+
if ($flag) { // complete hosted payment only once as status will be changed to USED
55+
$hostedPayment = $this->getCompletedHostedPayment($requestData);
56+
$flag = false;
57+
}
58+
59+
if (isset($hostedPayment['error_code'])) {
60+
$isError = true;
61+
}
62+
else {
63+
if ($hostedPayment['order_number'] !== $callbackPayload['order_number']) {
64+
$result = $this->getJsonResultFactory()->create();
65+
$result->setHttpResponseCode(\Magento\Framework\Webapi\Exception::HTTP_BAD_REQUEST);
66+
$result->setData([
67+
'status' => __('ERROR'),
68+
'message' => 'Hosted payment is not for this order'
69+
]);
70+
71+
return $result;
3972
}
40-
else {
41-
if ($hostedPayment['paid_amount'] != $hostedPayment['amount']) {
42-
$order->setBaseDiscountAmount($hostedPayment['paid_amount'] - $hostedPayment['amount']);
43-
$order->setDiscountAmount($hostedPayment['paid_amount'] - $hostedPayment['amount']);
44-
$order->save();
45-
46-
$order->setBaseGrandTotal($order->getBaseGrandTotal() + $order->getBaseDiscountAmount());
47-
$order->setGrandTotal($order->getGrandTotal() + $order->getDiscountAmount());
48-
$order->save();
49-
}
50-
$payment->setAdditionalInformation('token_id', $hostedPayment['token_id']);
51-
$payment->setAdditionalInformation('xendit_installment', $hostedPayment['installment']);
73+
74+
if ($hostedPayment['paid_amount'] != $hostedPayment['amount']) {
75+
$order->setBaseDiscountAmount($hostedPayment['paid_amount'] - $hostedPayment['amount']);
76+
$order->setDiscountAmount($hostedPayment['paid_amount'] - $hostedPayment['amount']);
77+
$order->save();
5278

53-
$this->processSuccessfulTransaction(
54-
$order,
55-
$payment,
56-
'Xendit Credit Card payment completed. Transaction ID: ',
57-
$hostedPayment['charge_id']
58-
);
79+
$order->setBaseGrandTotal($order->getBaseGrandTotal() + $order->getBaseDiscountAmount());
80+
$order->setGrandTotal($order->getGrandTotal() + $order->getDiscountAmount());
81+
$order->save();
5982
}
83+
$payment->setAdditionalInformation('token_id', $hostedPayment['token_id']);
84+
$payment->setAdditionalInformation('xendit_installment', $hostedPayment['installment']);
85+
86+
$this->processSuccessfulTransaction(
87+
$order,
88+
$payment,
89+
'Xendit Credit Card payment completed. Transaction ID: ',
90+
$hostedPayment['charge_id']
91+
);
6092
}
6193
}
6294

Xendit/M2Invoice/Controller/Checkout/CCCallback.m23.php

+64-32
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,35 @@
1010
use Magento\Sales\Model\Order;
1111
use Xendit\M2Invoice\Enum\LogDNALevel;
1212

13+
/**
14+
* This callback is only for order in multishipping flow. For order
15+
* created in onepage checkout is handled in ProcessHosted.php
16+
*/
1317
class CCCallback extends ProcessHosted implements CsrfAwareActionInterface
1418
{
1519
public function execute()
1620
{
1721
try {
18-
$orderIds = explode('-', $this->getRequest()->getParam('order_ids'));
22+
$post = $this->getRequest()->getContent();
23+
$callbackPayload = json_decode($post, true);
24+
25+
if (
26+
!isset($callbackPayload['id']) ||
27+
!isset($callbackPayload['hp_token']) ||
28+
!isset($callbackPayload['order_number'])
29+
) {
30+
$result = $this->getJsonResultFactory()->create();
31+
$result->setHttpResponseCode(\Magento\Framework\Webapi\Exception::HTTP_BAD_REQUEST);
32+
$result->setData([
33+
'status' => __('ERROR'),
34+
'message' => 'Callback body is invalid'
35+
]);
36+
37+
return $result;
38+
}
39+
$orderIds = explode('-', $callbackPayload['order_number']);
40+
$hostedPaymentId = $callbackPayload['id'];
41+
$hostedPaymentToken = $callbackPayload['hp_token'];
1942

2043
$shouldRedirect = false;
2144
$isError = false;
@@ -27,40 +50,49 @@ public function execute()
2750

2851
$payment = $order->getPayment();
2952

30-
if ($payment->getAdditionalInformation('xendit_hosted_payment_id') !== null) {
31-
$requestData = [
32-
'id' => $payment->getAdditionalInformation('xendit_hosted_payment_id'),
33-
'hp_token' => $payment->getAdditionalInformation('xendit_hosted_payment_token')
34-
];
35-
36-
if ($flag) { // complete hosted payment only once as status will be changed to USED
37-
$hostedPayment = $this->getCompletedHostedPayment($requestData);
38-
$flag = false;
39-
}
40-
41-
if (isset($hostedPayment['error_code'])) {
42-
$isError = true;
53+
$requestData = [
54+
'id' => $hostedPaymentId,
55+
'hp_token' => $hostedPaymentToken
56+
];
57+
58+
if ($flag) { // complete hosted payment only once as status will be changed to USED
59+
$hostedPayment = $this->getCompletedHostedPayment($requestData);
60+
$flag = false;
61+
}
62+
63+
if (isset($hostedPayment['error_code'])) {
64+
$isError = true;
65+
}
66+
else {
67+
if ($hostedPayment['order_number'] !== $callbackPayload['order_number']) {
68+
$result = $this->getJsonResultFactory()->create();
69+
$result->setHttpResponseCode(\Magento\Framework\Webapi\Exception::HTTP_BAD_REQUEST);
70+
$result->setData([
71+
'status' => __('ERROR'),
72+
'message' => 'Hosted payment is not for this order'
73+
]);
74+
75+
return $result;
4376
}
44-
else {
45-
if ($hostedPayment['paid_amount'] != $hostedPayment['amount']) {
46-
$order->setBaseDiscountAmount($hostedPayment['paid_amount'] - $hostedPayment['amount']);
47-
$order->setDiscountAmount($hostedPayment['paid_amount'] - $hostedPayment['amount']);
48-
$order->save();
49-
50-
$order->setBaseGrandTotal($order->getBaseGrandTotal() + $order->getBaseDiscountAmount());
51-
$order->setGrandTotal($order->getGrandTotal() + $order->getDiscountAmount());
52-
$order->save();
53-
}
54-
$payment->setAdditionalInformation('token_id', $hostedPayment['token_id']);
55-
$payment->setAdditionalInformation('xendit_installment', $hostedPayment['installment']);
77+
78+
if ($hostedPayment['paid_amount'] != $hostedPayment['amount']) {
79+
$order->setBaseDiscountAmount($hostedPayment['paid_amount'] - $hostedPayment['amount']);
80+
$order->setDiscountAmount($hostedPayment['paid_amount'] - $hostedPayment['amount']);
81+
$order->save();
5682

57-
$this->processSuccessfulTransaction(
58-
$order,
59-
$payment,
60-
'Xendit Credit Card payment completed. Transaction ID: ',
61-
$hostedPayment['charge_id']
62-
);
83+
$order->setBaseGrandTotal($order->getBaseGrandTotal() + $order->getBaseDiscountAmount());
84+
$order->setGrandTotal($order->getGrandTotal() + $order->getDiscountAmount());
85+
$order->save();
6386
}
87+
$payment->setAdditionalInformation('token_id', $hostedPayment['token_id']);
88+
$payment->setAdditionalInformation('xendit_installment', $hostedPayment['installment']);
89+
90+
$this->processSuccessfulTransaction(
91+
$order,
92+
$payment,
93+
'Xendit Credit Card payment completed. Transaction ID: ',
94+
$hostedPayment['charge_id']
95+
);
6496
}
6597
}
6698

Xendit/M2Invoice/Controller/Checkout/Notification.m22.php

+24-11
Original file line numberDiff line numberDiff line change
@@ -150,12 +150,11 @@ public function handleEwalletCallback($callbackPayload) {
150150
if (isset($callbackPayload['failure_code'])) {
151151
$failureCode = $callbackPayload['failure_code'];
152152
}
153+
$prefix = $this->dataHelper->getExternalIdPrefix();
154+
$trimmedExternalId = str_replace($prefix . "-", "", $callbackPayload['external_id']);
155+
$order = $this->getOrderById($trimmedExternalId);
153156

154-
$temp = explode('-', $callbackPayload['external_id']);
155-
$orderId = end($temp);
156-
$order = $this->getOrderById($orderId);
157-
158-
return $this->checkOrder($order, true, $callbackPayload, null, $orderId);
157+
return $this->checkOrder($order, true, $callbackPayload, null, $trimmedExternalId);
159158
}
160159

161160
private function checkOrder($order, $isEwallet, $callbackPayload, $invoice, $callbackDescription) {
@@ -184,7 +183,20 @@ private function checkOrder($order, $isEwallet, $callbackPayload, $invoice, $cal
184183
}
185184

186185
if ($isEwallet) {
187-
$paymentStatus = $this->getEwalletStatus($callbackPayload['ewallet_type'], $callbackPayload['external_id']);
186+
$ewallet = $this->getEwallet($callbackPayload['ewallet_type'], $callbackPayload['external_id']);
187+
$paymentStatus = $ewallet['status'];
188+
189+
if ($ewallet['external_id'] !== $callbackPayload['external_id']) {
190+
$result = $this->jsonResultFactory->create();
191+
/** You may introduce your own constants for this custom REST API */
192+
$result->setHttpResponseCode(\Magento\Framework\Webapi\Exception::HTTP_BAD_REQUEST);
193+
$result->setData([
194+
'status' => __('ERROR'),
195+
'message' => 'Ewallet is not for this order'
196+
]);
197+
198+
return $result;
199+
}
188200
} else {
189201
$paymentStatus = $invoice['status'];
190202

@@ -272,25 +284,26 @@ private function getXenditInvoice($invoiceId)
272284
return $invoice;
273285
}
274286

275-
private function getEwalletStatus($ewalletType, $externalId)
287+
private function getEwallet($ewalletType, $externalId)
276288
{
277289
$ewalletUrl = $this->dataHelper->getCheckoutUrl() . "/payment/xendit/ewallets?ewallet_type=".$ewalletType."&external_id=".$externalId;
278290
$ewalletMethod = \Zend\Http\Request::METHOD_GET;
279291

280292
try {
281293
$response = $this->apiHelper->request($ewalletUrl, $ewalletMethod);
282294
} catch (\Magento\Framework\Exception\LocalizedException $e) {
283-
throw new \Magento\Framework\Exception\LocalizedException(
295+
throw new LocalizedException(
284296
new Phrase($e->getMessage())
285297
);
286298
}
287299

300+
$status = $response['status'];
288301
$statusList = array("COMPLETED", "PAID", "SUCCESS_COMPLETED"); //OVO, DANA, LINKAJA
289-
if (in_array($response['status'], $statusList)) {
290-
return "COMPLETED";
302+
if (in_array($status, $statusList)) {
303+
$response['status'] = "COMPLETED";
291304
}
292305

293-
return $response['status'];
306+
return $response;
294307
}
295308

296309
private function invoiceOrder($order, $transactionId)

Xendit/M2Invoice/Controller/Checkout/Notification.m23.php

+23-10
Original file line numberDiff line numberDiff line change
@@ -161,12 +161,11 @@ public function handleEwalletCallback($callbackPayload) {
161161
if (isset($callbackPayload['failure_code'])) {
162162
$failureCode = $callbackPayload['failure_code'];
163163
}
164+
$prefix = $this->dataHelper->getExternalIdPrefix();
165+
$trimmedExternalId = str_replace($prefix . "-", "", $callbackPayload['external_id']);
166+
$order = $this->getOrderById($trimmedExternalId);
164167

165-
$temp = explode('-', $callbackPayload['external_id']);
166-
$orderId = end($temp);
167-
$order = $this->getOrderById($orderId);
168-
169-
return $this->checkOrder($order, true, $callbackPayload, null, $orderId);
168+
return $this->checkOrder($order, true, $callbackPayload, null, $trimmedExternalId);
170169
}
171170

172171
private function checkOrder($order, $isEwallet, $callbackPayload, $invoice, $callbackDescription) {
@@ -195,7 +194,20 @@ private function checkOrder($order, $isEwallet, $callbackPayload, $invoice, $cal
195194
}
196195

197196
if ($isEwallet) {
198-
$paymentStatus = $this->getEwalletStatus($callbackPayload['ewallet_type'], $callbackPayload['external_id']);
197+
$ewallet = $this->getEwallet($callbackPayload['ewallet_type'], $callbackPayload['external_id']);
198+
$paymentStatus = $ewallet['status'];
199+
200+
if ($ewallet['external_id'] !== $callbackPayload['external_id']) {
201+
$result = $this->jsonResultFactory->create();
202+
/** You may introduce your own constants for this custom REST API */
203+
$result->setHttpResponseCode(\Magento\Framework\Webapi\Exception::HTTP_BAD_REQUEST);
204+
$result->setData([
205+
'status' => __('ERROR'),
206+
'message' => 'Ewallet is not for this order'
207+
]);
208+
209+
return $result;
210+
}
199211
} else {
200212
$paymentStatus = $invoice['status'];
201213

@@ -283,7 +295,7 @@ private function getXenditInvoice($invoiceId)
283295
return $invoice;
284296
}
285297

286-
private function getEwalletStatus($ewalletType, $externalId)
298+
private function getEwallet($ewalletType, $externalId)
287299
{
288300
$ewalletUrl = $this->dataHelper->getCheckoutUrl() . "/payment/xendit/ewallets?ewallet_type=".$ewalletType."&external_id=".$externalId;
289301
$ewalletMethod = \Zend\Http\Request::METHOD_GET;
@@ -296,12 +308,13 @@ private function getEwalletStatus($ewalletType, $externalId)
296308
);
297309
}
298310

311+
$status = $response['status'];
299312
$statusList = array("COMPLETED", "PAID", "SUCCESS_COMPLETED"); //OVO, DANA, LINKAJA
300-
if (in_array($response['status'], $statusList)) {
301-
return "COMPLETED";
313+
if (in_array($status, $statusList)) {
314+
$response['status'] = "COMPLETED";
302315
}
303316

304-
return $response['status'];
317+
return $response;
305318
}
306319

307320
private function invoiceOrder($order, $transactionId)

Xendit/M2Invoice/Controller/Checkout/SubscriptionCallback.m22.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ public function execute()
1515
$invoiceId = $payload['id'];
1616
$chargeId = $payload['credit_card_charge_id'];
1717

18-
//verify callback
18+
// verify callback to ensure payment exist in xendit side
1919
$callback = $this->getCallbackByInvoiceId($invoiceId);
2020
if (isset($callback['error_code']) || !isset($callback['status'])) {
2121
$result->setData([

0 commit comments

Comments
 (0)