AbanteCart version: 1.4.4
Extension: Paypal Commerce
Payment action: Capture (Sale)
File affected: extensions/paypal_commerce/storefront/controller/responses/extension/paypal_commerce.php
Background
After updating to 1.4.4, PayPal payments were being taken but orders were not appearing in the admin, and customers were being left on the checkout confirmation page with their cart still full. I want to stress that this may be specific to my own setup or update path. I'm not certain these are universal bugs, but I couldn't find any existing posts about it and wanted to share what I found in case it helps anyone else in the same situation.
Symptoms
Customer pays via PayPal successfully & money is taken
Order does not appear in AbanteCart admin (or appears under "Incomplete" status, which is filtered out of the default admin order view)
After payment, customer is left on the checkout confirmation page with their cart still full, no redirect to the order confirmation page
PayPal error log shows: Paypal webhook PAYMENT.CAPTURE.COMPLETED: order ID XXXXX / Paypal related OrderId: XXXXXXXXXXXXXXXXX but not found in the database
What I think was happening
After digging through the code I found three separate issues that combined to break the checkout flow. These may have been introduced during my update to 1.4.4 or may be specific to my configuration, I can't say for certain. I'm sharing them here in case anyone else hits the same problems.
Issue 1: array_merge null crash in captureOrder()
$this->session->data['fc'] appeared to be null in my checkout path, causing a fatal error that crashed the entire captureOrder() method:
array_merge(): Argument #2 must be of type array, null given
Issue 2: Possible race condition: orders stuck as "incomplete"
Even without Issue 1, captureOrder() doesn't confirm the AbanteCart order itself, it relies on the browser making a subsequent call to send() → processGenericOrder() to do that. In my case, PayPal's PAYMENT.CAPTURE.COMPLETED webhook was arriving before the browser's send() call ran. The webhook handler calls update() on the order, which appears to do nothing on an unconfirmed/incomplete order. The result was orders remaining in "Incomplete" status, hidden from the default admin filter. This timing may vary between setups.
Issue 3: Duplicate INSERT crashing processGenericOrder(), cart never clearing
Once I fixed Issue 2 by saving the PayPal order record in captureOrder(), the subsequent send() call reached processGenericOrder(), which unconditionally calls savePaypalOrder() again for the same order. Since savePaypalOrder() uses a plain INSERT with no duplicate handling, this threw a database error that silently killed the method, so the checkout/finalize redirect URL was never returned to the browser and the cart was never cleared.
What I did to fix it
Fix 1: array_merge null crash
In captureOrder(), find:
Replace with:
Fix 2: Confirm order and save PayPal record immediately in captureOrder()
In captureOrder(), find the closing of the if ($orderInfo) block followed by the catch:
Replace with:
Fix 3: Guard duplicate savePaypalOrder in processGenericOrder()
In processGenericOrder(), find:
Replace with:
Fix 4: Improve webhook fallback in processWebHook()
In processWebHook(), find:
Bonus: Order numbers missing from PayPal transaction exports
I also noticed that the "Custom Number" column in PayPal's transaction export spreadsheet was blank for all orders placed after updating to 1.4.4 (or maybe earlier). In older versions this column showed the AbanteCart order number, which is useful for reconciliation. The custom_id field appears to have been removed from the PayPal purchase unit payload in 1.4.4, possibly intentionally since the webhook lookup mechanism changed to use reference_id instead. Adding it back as a plain order number restores the column in exports without affecting anything else.
In prepareOrderData(), find:
Note for existing incomplete orders
If you have orders already stuck in "Incomplete" status where payment was successfully taken, go to Sales → Orders, filter by "Incomplete" status, open each affected order, change the status to Processing (or your configured success status), and tick "Notify Customer" to send the confirmation email. I think this works, not fully tested yet.
I'm not a core developer so there may be good reasons some of this works differently by design, happy to be corrected. (Please don't flame me if I've gone overboard)
Posting in case it's useful to anyone else who updated to 1.4.4 and is seeing the same symptoms.
Extension: Paypal Commerce
Payment action: Capture (Sale)
File affected: extensions/paypal_commerce/storefront/controller/responses/extension/paypal_commerce.php
Background
After updating to 1.4.4, PayPal payments were being taken but orders were not appearing in the admin, and customers were being left on the checkout confirmation page with their cart still full. I want to stress that this may be specific to my own setup or update path. I'm not certain these are universal bugs, but I couldn't find any existing posts about it and wanted to share what I found in case it helps anyone else in the same situation.
Symptoms
Customer pays via PayPal successfully & money is taken
Order does not appear in AbanteCart admin (or appears under "Incomplete" status, which is filtered out of the default admin order view)
After payment, customer is left on the checkout confirmation page with their cart still full, no redirect to the order confirmation page
PayPal error log shows: Paypal webhook PAYMENT.CAPTURE.COMPLETED: order ID XXXXX / Paypal related OrderId: XXXXXXXXXXXXXXXXX but not found in the database
What I think was happening
After digging through the code I found three separate issues that combined to break the checkout flow. These may have been introduced during my update to 1.4.4 or may be specific to my configuration, I can't say for certain. I'm sharing them here in case anyone else hits the same problems.
Issue 1: array_merge null crash in captureOrder()
$this->session->data['fc'] appeared to be null in my checkout path, causing a fatal error that crashed the entire captureOrder() method:
array_merge(): Argument #2 must be of type array, null given
Issue 2: Possible race condition: orders stuck as "incomplete"
Even without Issue 1, captureOrder() doesn't confirm the AbanteCart order itself, it relies on the browser making a subsequent call to send() → processGenericOrder() to do that. In my case, PayPal's PAYMENT.CAPTURE.COMPLETED webhook was arriving before the browser's send() call ran. The webhook handler calls update() on the order, which appears to do nothing on an unconfirmed/incomplete order. The result was orders remaining in "Incomplete" status, hidden from the default admin filter. This timing may vary between setups.
Issue 3: Duplicate INSERT crashing processGenericOrder(), cart never clearing
Once I fixed Issue 2 by saving the PayPal order record in captureOrder(), the subsequent send() call reached processGenericOrder(), which unconditionally calls savePaypalOrder() again for the same order. Since savePaypalOrder() uses a plain INSERT with no duplicate handling, this threw a database error that silently killed the method, so the checkout/finalize redirect URL was never returned to the browser and the cart was never cleared.
What I did to fix it
Fix 1: array_merge null crash
In captureOrder(), find:
Code Select
$this->session->data['fc'] = array_merge($order->data, $this->session->data['fc']);Replace with:
Code Select
$this->session->data['fc'] = array_merge((array)$order->data, (array)$this->session->data['fc']);Fix 2: Confirm order and save PayPal record immediately in captureOrder()
In captureOrder(), find the closing of the if ($orderInfo) block followed by the catch:
Code Select
$order->buildOrderData($this->session->data['fc']);
$order->saveOrder();
}
} catch (Exception|Error $e) {Replace with:
Code Select
$order->buildOrderData($this->session->data['fc']);
$order->saveOrder();
}
$confirmOrderId = (int)($orderId ?: $this->session->data['order_id']);
if ($confirmOrderId && $result->getId()) {
/** @var ModelCheckoutOrder $oMdl */
$oMdl = $this->loadModel('checkout/order');
$confirmedOrderInfo = $oMdl->getOrder($confirmOrderId);
$incompleteStatusId = (int)$this->order_status->getStatusByTextId('incomplete');
$currentStatusId = (int)($confirmedOrderInfo['order_status_id'] ?? 0);
if ($confirmedOrderInfo && (!$currentStatusId || $currentStatusId == $incompleteStatusId)) {
$settledStatusId = $this->config->get('paypal_commerce_transaction_type') == 'capture'
? $this->config->get('paypal_commerce_status_success_settled')
: $this->config->get('paypal_commerce_status_success_unsettled');
$oMdl->confirm(
$confirmOrderId,
$settledStatusId ?: $this->order_status->getStatusByTextId('pending')
);
}
if (!$mdl->getPaypalOrder($confirmOrderId)) {
$mdl->savePaypalOrder($confirmOrderId, [
'id' => $ppOrderId,
'transaction_id' => $result->getId(),
]);
}
}
} catch (Exception|Error $e) {Fix 3: Guard duplicate savePaypalOrder in processGenericOrder()
In processGenericOrder(), find:
Code Select
$mdl->savePaypalCustomer($this->customer->getId(), $transactionDetails['payer']['payer_id']);
$mdl->savePaypalOrder(
$orderId,
[
'id' => $transactionDetails['id'],
'transaction_id' => $transactionDetails['id'],
]
);Replace with:
Code Select
$mdl->savePaypalCustomer($this->customer->getId(), $transactionDetails['payer']['payer_id']);
if (!$mdl->getPaypalOrder($orderId)) {
$mdl->savePaypalOrder(
$orderId,
[
'id' => $transactionDetails['id'],
'transaction_id' => $transactionDetails['id'],
]
);
}Fix 4: Improve webhook fallback in processWebHook()
In processWebHook(), find:
Code Select
/** @var ModelCheckoutOrder $oMdl */
$oMdl = $this->loadModel('checkout/order');
$oMdl->update(
$orderId,
$this->data['order_status_id'],
'Order updated by Paypal webhook request.'
);Replace with:Code Select
/** @var ModelCheckoutOrder $oMdl */
$oMdl = $this->loadModel('checkout/order');
/** @var ModelExtensionPaypalCommerce $mdl */
$mdl = $this->loadModel('extension/paypal_commerce');
$webhookOrderInfo = $oMdl->getOrder($orderId);
$incompleteStatusId = (int)$this->order_status->getStatusByTextId('incomplete');
$currentStatusId = (int)($webhookOrderInfo['order_status_id'] ?? 0);
if ($webhookOrderInfo && (!$currentStatusId || $currentStatusId == $incompleteStatusId)) {
$oMdl->confirm($orderId, $this->data['order_status_id']);
if (!$mdl->getPaypalOrder($orderId)) {
$ppOrderId = $inData['parsed']['resource']['supplementary_data']['related_ids']['order_id'] ?? '';
$mdl->savePaypalOrder($orderId, [
'id' => $ppOrderId,
'transaction_id' => $inData['parsed']['resource']['id'] ?? '',
]);
}
} else {
$oMdl->update(
$orderId,
$this->data['order_status_id'],
'Order updated by Paypal webhook request.'
);
}Bonus: Order numbers missing from PayPal transaction exports
I also noticed that the "Custom Number" column in PayPal's transaction export spreadsheet was blank for all orders placed after updating to 1.4.4 (or maybe earlier). In older versions this column showed the AbanteCart order number, which is useful for reconciliation. The custom_id field appears to have been removed from the PayPal purchase unit payload in 1.4.4, possibly intentionally since the webhook lookup mechanism changed to use reference_id instead. Adding it back as a plain order number restores the column in exports without affecting anything else.
In prepareOrderData(), find:
Code Select
$this->data['pp']['purchase_units'][0] = [
'reference_id' => $ppOrderData['data']['reference_id'] ? : $this->session->data['reference_id'],
'amount' => [Replace with:Code Select
$this->data['pp']['purchase_units'][0] = [
'reference_id' => $ppOrderData['data']['reference_id'] ? : $this->session->data['reference_id'],
'custom_id' => (string) $orderId,
'amount' => [Note for existing incomplete orders
If you have orders already stuck in "Incomplete" status where payment was successfully taken, go to Sales → Orders, filter by "Incomplete" status, open each affected order, change the status to Processing (or your configured success status), and tick "Notify Customer" to send the confirmation email. I think this works, not fully tested yet.
I'm not a core developer so there may be good reasons some of this works differently by design, happy to be corrected. (Please don't flame me if I've gone overboard)
Posting in case it's useful to anyone else who updated to 1.4.4 and is seeing the same symptoms.
