Appearance
Incomplete Order Cancellation
Flow ID: SY-03 | Module(s): job, eshop, checkout | Complexity: High Last Updated: 2026-09-29 — 4.124.0 resync: citation drift corrections (
Adv_settings.php,Adv_order_model.php,PaymentConfirmationService.php) and the retired REST webhook controller (Advisable-com/ecommercen#721) replaced by the actualcancelPayment()callers; earlier (2026-09-14) citation refresh: thegetXPaySettings()citation moved again, toregistry_helper.php:585-595, aftergetVivaWalletExternalSettings()(Advisable-com/ecommercen#729) was inserted betweengetVivaWalletMergedSettings()andgetJccSettings(), pushing every later helper further down the file; thepay_by_bank_expirationadmin-field citation was corrected fromAdv_settings.php:1363(which is unrelated PayPal settings code) toAdv_settings.php:1564(the actual->set_rules('pay_by_bank_expiration', '', 'trim')call); Advisable-com/ecommercen#688:set_status()now also restores gift stock onCANCELEDcancellations (Adv_order_model.php:1830-1832,restoreOrderGifts()), gated by the newshop_order.gifts_appliedcolumn, with a matchingRestoreGiftStockOnCanceledListeneron the modern REST cancellation path -- see Stock Restoration, Code Flow step 3, and Data Model below; Advisable-com/ecommercen#706: thegetXPaySettings()citation moved toregistry_helper.php:534-542aftergetVivaWalletMergedSettings()was added above it in the same helper file; the helper itself is unchanged; Advisable-com/ecommercen#531: an unparseableentry_datetime(NULL or a legacy zero-date row) no longer fatals the whole cron run -- the new sharedorderEntryDate()helper skips just that order (logged, left PENDING) and the batch continues;PAY_BY_BANK/EXPIRATIONis now validated the same wayXPAY/EXPIRATIONalready was, falling back to 86400s (24h) when unset/non-numeric, and the gift-card sibling job'sgiftCardDateTimeIntervalToDropconfig item gets an analogousPT180Mfallback; Advisable-com/ecommercen#500:getCardPayWays()now coversxpay/klarna_payments/ethniki_nbgpay(17 payways total, closing the drift that stranded their PENDING orders), XPay gained a registry-configurable grace period (XPAY/EXPIRATION) before probing Nexi, and the deprecatedCronjob::order_debris()route now delegates to the job instead of duplicating it; Advisable-com/ecommercen#530:tests/Unit/Helpers/PayWayDebrisCoverageTest.phpnow also guards a second, independent payway-list invariant (isOrderPaidAtDeliveryByPayWay(), see AD-34) -- thegetCardPayWays()behavior documented below is unchanged
Business Overview
When a customer initiates checkout with an online payment method but never completes payment, the order remains in PENDING status indefinitely. Two independent paths handle cancellation:
- REST-driven cancellation (
PaymentConfirmationService::cancelPayment()): invoked immediately by the REST endpointPOST /rest/checkout/cancel-payment/{orderId}, byRest\Order\Controllers\Order, and by thePollPayByBankStatusjob (the REST webhook family was retired in Advisable-com/ecommercen#721). This is the primary path for REST-placed orders. - Scheduled cleanup (
AdvCancelIncompleteOrderscron): runs every 5 minutes as a safety net for stuck PENDING orders that the REST path missed (e.g. abandonment without a gateway callback). It cancels stale PENDING orders, restores stock, returns loyalty points, releases used coupons, and fires ERP/cancel hooks for downstream integrations. An order whoseentry_datetimecannot be trusted (NULL, or a legacy'0000-00-00 00:00:00'zero-date row) is skipped, not cancelled -- logged at error level and left PENDING for a human to fix, rather than aborting the whole run or being silently auto-cancelled (Advisable-com/ecommercen#531).
The cron job has special handling for four payment gateways that require external API calls before cancellation:
- PayByBank -- cancel the pending bank transfer via the PayByBank API before cancelling the order.
- Iris -- check whether the customer actually paid via the Iris API; if paid, accept the order instead of cancelling.
- PayPal Advanced -- verify capture status via PayPal REST API; cancel only if not completed.
- NexiXPay -- after a configurable grace period elapses (registry
XPAY/EXPIRATION, default 180 minutes), query the Nexi XPay API foroperationResult; if PAID, accept the order, otherwise cancel.
Architecture
AdvCancelIncompleteOrders
|
+--> order_model::getDebrisOrders() query PENDING card-payment orders
|
+--> per-gateway handler:
| +--> orderEntryDate($order) SHARED entry_datetime guard (default cards, PayByBank, XPay)
| | null (unparseable) --> return; skip this order, batch continues (#531)
| |
| +--> cancelOrder($order) SHARED cancel side-effects (every path)
| | +--> set_status('CANCELED', '+') cancel + restore product stock + restore gift stock (#688)
| | +--> returnPointsToCustomers() loyalty point rollback
| | +--> cancelCoupon() release the applied coupon
| | +--> internalApiOrderCancelHook() ERP cancel webhook
| |
| +--> cancelPendingDefaultCards() timeout-based cancel (most gateways) --> cancelOrder()
| +--> cancelPendingPayByBank() SDK cancelOrder() + insertPBBLog() --> cancelOrder()
| +--> handlePendingIrisOrders() [CANCELED] --> cancelOrder() | [PAID] --> update_order(PAID) + ErpHook
| +--> handlePendingPaypalAdvancedOrders() [not COMPLETED] --> cancelOrder()
| +--> handlePendingXpayOrders() [not PAID] --> cancelOrder() | [PAID] --> update_order(PAID) + ErpHookNote: every cancellation branch routes through the shared cancelOrder() helper, so all five gateways (default cards, PayByBank, Iris, PayPal Advanced, XPay) release the applied coupon via cancelCoupon() on cancel (Advisable-com/ecommercen#290).
Key Files
| File | Role |
|---|---|
ecommercen/job/libraries/AdvCancelIncompleteOrders.php | Job implementation |
application/modules/job/libraries/CancelIncompleteOrders.php | Client-overridable subclass |
application/controllers/Cronjob.php | Deprecated manual-trigger route (/cronjob/order_debris); order_debris() delegates to (new CancelIncompleteOrders())->executeCommand([]) instead of duplicating the switch (Advisable-com/ecommercen#500) |
ecommercen/eshop/models/Adv_order_model.php | getDebrisOrders(), set_status() |
ecommercen/libraries/internal/OrderCancelHookFireTrait.php | ERP cancel hook trait |
ecommercen/libraries/internal/OrderForErpHookFireTrait.php | ERP acceptance hook trait |
src/PaymentGateways/Iris/Iris.php | Iris payment gateway client |
src/PaymentGateways/PayPal/PayPalRestApi.php | PayPal Advanced REST client |
src/PaymentGateways/NexiXPay/XPay.php | Nexi XPay payment gateway client (getOrderStatus, mapOperationResultToStatus) |
ecommercen/gift_cards/jobs/AdvCancelPendingGiftCards.php | Pending gift-card reconciliation (mirrors this flow for gift cards) |
ecommercen/eshop/models/Adv_viva_logging_model.php | Payment logging (modern port: src/Domains/Checkout/VivaLogging/ — #148; legacy model still active) |
ecommercen/coupons/models/Adv_coupons_model.php | Coupon release |
Code Flow
1. Order Selection
order_model->getDebrisOrders() queries the shop_order table:
sql
SELECT id, order_serial, payway, entry_datetime, coupon_id, tran_ticket
FROM shop_order
WHERE status = 'PENDING'
AND payway IN ('proxypay','paypal','alpha','ethniki','ethniki_nbgpay','ethniki_ee',
'eurobank','paybybank','piraeus','apcopay','vivawallet','jcc','iris',
'paypaladvanced','stripe','klarna_payments','xpay')
ORDER BY id ASCThe getCardPayWays() helper (ecommercen/helpers/eshop_helper.php) defines the list of eligible payment methods -- 17 entries as of Advisable-com/ecommercen#500, which added ethniki_nbgpay, klarna_payments, and xpay after finding they parked orders at PENDING but were missing from this list (see Known Issues). Cash-on-delivery and bank-transfer orders are excluded because they do not require immediate online payment -- along with delivery, bank_transfer, and paid_at_store, whose Adv_checkout handlers write PENDING_ACCEPTED rather than PENDING, so getDebrisOrders()'s status = 'PENDING' filter never selects them regardless of this list. getCardPayWays() is despite its name not "card payways" but "payways that park an order at PENDING" -- it legitimately includes non-cards (paybybank, iris, klarna_payments). A drift guard, tests/Unit/Helpers/PayWayDebrisCoverageTest.php, pins every allPayWays() entry other than those three PENDING_ACCEPTED payways as required here. That same test class also pins a second, independent invariant added by Advisable-com/ecommercen#530 -- the sibling helper isOrderPaidAtDeliveryByPayWay()'s landing-status partition of allPayWays(), which governs voucher (shipping-label) eligibility rather than debris-cron coverage (see AD-34 Voucher Generation) -- so the file is not debris-cron-only despite its name; the getCardPayWays() behavior described above is unaffected by that second guard.
2. Per-Gateway Handling
The job routes each order to the appropriate handler via a switch on payway:
Default Card Gateways (proxypay, paypal, alpha, ethniki, ethniki_nbgpay, ethniki_ee, eurobank, piraeus, apcopay, stripe, klarna_payments)
Timeout-based cancellation with gateway-specific grace periods. klarna_payments is routed here on purpose: the modern REST checkout's KlarnaAdapter writes PENDING by design and its fraud_status webhook is the single confirmation point, so a lost webhook is swept by this same 3-hour default rather than a dedicated API probe.
entry_datetime is parsed via the shared orderEntryDate($order): ?DateTime helper before any timeout math runs. A NULL or legacy zero-date ('0000-00-00 00:00:00') value returns null from the helper, and the caller returns immediately -- the order is skipped (left PENDING, logged at error level) rather than raising a fatal Error from calling ->add() on a parse failure or, worse, being silently auto-cancelled by a zero-date value that a naive false-only check would treat as valid (Advisable-com/ecommercen#531).
| Gateway | Timeout | Rationale |
|---|---|---|
vivawallet | 2 days (P2D) | Viva Wallet redirects can take longer |
jcc | 20 minutes (PT20M) | Fast redirect flow |
| All others | 3 hours (PT180M) | Standard timeout |
If now > entry_datetime + timeout, delegates to the shared cancelOrder($order) helper, which runs:
set_status(order_serial, 'CANCELED', false, '+', 'order_debris')-- cancels and restores stock (+mode)returnPointsToCustomers()-- rolls back loyalty points ifPOINT_SYSTEM.IS_ENABLEDcancelCoupon()-- marks the applied coupon unused viacoupons_model->markCouponUnused()internalApiOrderCancelHook()-- fires theInternalApiOrderCancelHookfor ERP sync
All five cancellation paths (default cards, PayByBank, Iris, PayPal Advanced, XPay) route their cancel branch through cancelOrder(), so every gateway releases the applied coupon (Advisable-com/ecommercen#290 — previously only this default-cards path did).
PayByBank
Uses the configurable expiration from registry PAY_BY_BANK.EXPIRATION (in seconds), validated by payByBankExpirationSeconds():
entry_datetimeis parsed via the same sharedorderEntryDate()guard described above; anullresult skips the order instead of throwing.- Wait until
entry_datetime + EXPIRATIONhas passed.payByBankExpirationSeconds()(ecommercen/job/libraries/AdvCancelIncompleteOrders.php) validates the registry value to a positive int before it reaches theDateIntervalspec string, falling back toPAY_BY_BANK_DEFAULT_EXPIRATION_SECONDS = 86400(24 hours) for any unset, empty, or non-numeric value -- mirroringxPayExpirationSeconds()(Advisable-com/ecommercen#500, #531). The admin field behind this registry key (ecommercen/settings/controllers/Adv_settings.php:1564) validates withtrimalone (norequired, nonumeric), and no shop ships a default row for it, so this fallback is the only thing standing between an empty/blank admin field and an invalidDateIntervalspec ('PTS') that would abort the whole cron run. - Call
Factories::payByBank()->cancelOrder(order_serial)to cancel the pending bank transfer. - Log the result to the PayByBank log table (
insertPBBLog()). - Cancel via the shared
cancelOrder()helper -- set_status + restore stock, return loyalty points, release the applied coupon, fire the ERP cancel hook (even if the API call fails, the log captures the error).
Iris
Checks actual payment status before deciding:
- Load Iris order record via
order_model->getIrisRecordsByOrderId(). - Call
Iris->getOrderStatus(['orderId' => irisOrderId])to query Iris. - Interpret response via
IrisHelper::interpretIrisResponse():- PENDING or no response: skip (try again on next run).
- CANCELED: cancel via
cancelOrder()-- cancel, return points, release the applied coupon, fire cancel hook. - PAID: accept the order -- update status to
PAID, setmeta_datatojob:CancelIncompleteOrders:handlePendingIrisOrders, and fire the ERP acceptance hook (internalApiOrderForErpHook()).
PayPal Advanced
Verifies capture status via PayPal REST API:
- If
tran_ticketis present, callPayPalRestApi->getOrderDetails(tran_ticket). - Check
purchase_units[0].payments.captures[0].status. - If
tran_ticketis empty or status is notCOMPLETED: cancel viacancelOrder()-- cancel, return points, release the applied coupon, fire cancel hook. - If
COMPLETED: the order is left as-is (no action taken in current code).
NexiXPay
Verifies payment status with Nexi XPay before deciding, but only after a grace period elapses -- added by Advisable-com/ecommercen#500, since making the previously-unreachable xpay branch reachable would otherwise have started cancelling orders mid-redirect:
- Entry-datetime guard:
entry_datetimeis parsed via the sharedorderEntryDate()guard described under Default Card Gateways above; anullresult skips the order before the grace-period math below runs. - Grace-period guard (
xPayExpirationSeconds(),ecommercen/job/libraries/AdvCancelIncompleteOrders.php): ifnow <= entry_datetime + XPAY.EXPIRATION seconds, return immediately and let the next 5-minute run retry. The window defaults to10800seconds (180 minutes) and is read from registryXPAY/EXPIRATION; any unset, empty, non-numeric, or non-positive registry value falls back to the default rather than reaching theDateIntervalspec string (which would throw and abort the whole cron run). There is no admin settings field for this key -- it is registry-only.PAY_BY_BANK/EXPIRATIONis validated the same way bypayByBankExpirationSeconds()(Advisable-com/ecommercen#531) -- see the PayByBank section above. - Call
XPay->getOrderStatus(order_serial)to fetch the Nexi XPay order record - Map
operationResultthroughXPay->mapOperationResultToStatus()to a normalized status - If response is empty or status is not
PAID: cancel via the sharedcancelOrder()helper -- set_status + return points + release coupon + ERP cancel hook - If status is
PAID(payment succeeded but customer never returned):update_order(status=PAID)+internalApiOrderForErpHook()
3. REST-Driven Cancellation
PaymentConfirmationService::cancelPayment() (src/Domains/Checkout/PaymentConfirmationService.php:139-190) is the primary cancellation path for REST-placed orders. The REST webhook controller (src/Rest/Webhooks) was retired in Advisable-com/ecommercen#721 and no longer exists. Current callers:
- REST endpoint:
POST /rest/checkout/cancel-payment/{orderId}—Checkout::cancelPayment()(src/Rest/Checkout/Controllers/Checkout.php:1682). - Order REST controller:
Rest\Order\Controllers\Order(src/Rest/Order/Controllers/Order.php:290). - PayByBank polling job:
PollPayByBankStatus(src/Domains/Checkout/Jobs/PollPayByBankStatus.php:107for orders the gateway no longer knows,:128for cancelled gateway statuses).
The method:
- Idempotency guard (
:148): iforder.status === 'CANCELED', returnstrueimmediately (idempotent success). Returnsfalseif the order is not found (:143-145). - Stock restoration (
:175): calls$this->stockService->restoreStockForOrder($orderId)— equivalent to the cron'sset_status('CANCELED', '+')stock-restore path. - Event dispatch (
:181): dispatchesOrderCanceledevent, which triggers registered listeners for loyalty-point restore, coupon-usage decrement, and gift-stock restore (RestoreGiftStockOnCanceledListener,src/Domains/Order/Event/Listeners/RestoreGiftStockOnCanceledListener.php-- Advisable-com/ecommercen#688). - Returns
trueon success.
The AdvCancelIncompleteOrders cron acts as the safety net for orders where no cancel/confirm signal was received (no REST cancel-payment call, no gateway callback: customer abandoned the gateway page, network timeout, gateway outage). It selects only status = 'PENDING' orders, so any order already cancelled by cancelPayment() is skipped automatically.
4. Stock Restoration
The set_status() method's stockMode = '+' parameter triggers returnOrderStock(), which iterates the order basket and adds quantities back to product_codes.stock.
Since Advisable-com/ecommercen#688, set_status() also restores gift stock under a narrower gate -- stockMode === '+' and status === 'CANCELED' (ecommercen/eshop/models/Adv_order_model.php:1830-1832): it calls restoreOrderGifts($orderSerial) (:1031-1073), which claims the once-only shop_order.gifts_applied marker via claimGiftsApplied() (:1125-1137) and, on a successful claim, restores each gift's counter via gifts_model->restoreCounter(). Because every legacy cancellation path documented in this flow converges on the shared cancelOrder() helper (ecommercen/job/libraries/AdvCancelIncompleteOrders.php:194), which calls set_status($order->order_serial, 'CANCELED', false, '+', 'order_debris'), gift stock is restored for every gateway (default cards, PayByBank, Iris, PayPal Advanced, XPay) on cancellation.
5. Loyalty Point Rollback
When POINT_SYSTEM.IS_ENABLED is true in registry, the job loads the loyalty library and calls returnPointsToCustomers() to credit back any points the customer spent on the order.
Data Model
Primary Table: shop_order
| Column | Type | Role |
|---|---|---|
id | int | PK |
order_serial | varchar | Human-readable order number |
status | varchar | Current status (filtered for PENDING) |
payway | varchar | Payment gateway identifier |
entry_datetime | datetime | Order creation timestamp (used for timeout) |
coupon_id | int/null | FK to applied coupon |
tran_ticket | varchar/null | Payment transaction reference (PayPal) |
canceled_date | datetime | Set on cancellation |
meta_data | text | Audit trail for status changes |
gifts_applied | tinyint(1) | 1 while gift stock is currently consumed for this order; cleared by claimGiftsApplied() on restore (database/migrations/20260831120000_add_gifts_applied_to_shop_order.php:13, Advisable-com/ecommercen#688) |
Related Tables
| Table | Role |
|---|---|
shop_order_basket | Order line items (stock restoration source) |
product_codes | Product SKU stock levels |
iris_orders | Iris payment gateway order mapping |
pay_by_bank_log | PayByBank API call history |
coupons | Coupon usage tracking |
customer_points | Loyalty point ledger |
Configuration
Job Scheduling (application/config/jobs.php)
php
['command' => 'CancelIncompleteOrders', 'schedule' => '*/5 * * * *', 'graceTime' => 300, 'retryTimes' => 3]Runs every 5 minutes in the core queue. Enabled by default.
Registry Settings
| Group | Key | Purpose |
|---|---|---|
PAY_BY_BANK | EXPIRATION | Timeout in seconds before PayByBank orders are cancelled (default 86400 = 24 hours if unset, empty, or non-numeric -- validated by payByBankExpirationSeconds(), Advisable-com/ecommercen#531; has an admin field, ecommercen/settings/controllers/Adv_settings.php:1564, but it is validated with trim alone and no shop ships a default row) |
XPAY | EXPIRATION | Grace period in seconds before a PENDING xpay order is probed against the Nexi API (default 10800 = 180 minutes if unset, empty, non-numeric, or non-positive). No admin UI field -- registry-only (Advisable-com/ecommercen#500). |
POINT_SYSTEM | IS_ENABLED | Whether to rollback loyalty points on cancellation |
Environment / Gateway Configuration
- Iris: configured via
getIrisSettings()helper - PayPal Advanced: configured via
getPaypalAdvancedSettings()helper - PayByBank: instantiated via
Factories::payByBank()factory - NexiXPay: configured via
getXPaySettings()inecommercen/helpers/registry_helper.php:585-595;XPayclient lazy-instantiated viaxPay()getter; grace period read from registryXPAY/EXPIRATIONviaxPayExpirationSeconds()
Client Extension Points
Override the job class: Extend
AdvCancelIncompleteOrdersinapplication/modules/job/libraries/CancelIncompleteOrders.phpto:- Add custom payment gateway handlers (new
casein the switch) - Change timeout logic for specific gateways
- Add custom notification on cancellation
- Add custom payment gateway handlers (new
Override
getCardPayWays(): Redefine inapplication/helpers/(the base isfunction_exists()-guarded) to include or exclude payment methods from debris cleanup. Caution: omitting a payway whose checkout handler writes PENDING silently strands its orders forever -- no stock restore, no loyalty-points return, no coupon release, no ERP cancel hook, and no error raised anywhere. This is exactly howxpay,klarna_payments, andethniki_nbgpaydrifted out of coverage upstream (Advisable-com/ecommercen#500); a client override does not automatically inherit that fix and must independently include any PENDING-writing payway it adds.Custom ERP hooks: The
InternalApiOrderCancelHookandInternalApiOrderForErpHookfire webhooks that ERP integrations can subscribe to for order sync.Adjust schedule: Modify the cron expression and grace time in
jobs.php.
Business Rules
| Rule | Description |
|---|---|
| Card payments only | Only orders with online payment methods (from getCardPayWays()) are considered |
| Gateway-specific timeouts | Viva Wallet: 2 days, JCC: 20 min, XPay: registry XPAY.EXPIRATION (default 3 hours), all others: 3 hours |
| PayByBank respects registry | Timeout driven by PAY_BY_BANK.EXPIRATION registry value (seconds); defaults to 86400 (24 hours) for any unset/empty/non-numeric value (Advisable-com/ecommercen#531) |
| XPay respects registry | Grace period driven by XPAY.EXPIRATION registry value (seconds); no admin UI field, defaults to 180 minutes for any unset/invalid value |
Untrustworthy entry_datetime = skip | An order whose entry_datetime fails to parse cleanly (NULL or a legacy zero-date row) is left PENDING and logged, never auto-cancelled and never allowed to abort the run (Advisable-com/ecommercen#531) |
| Iris may accept | If Iris reports PAID, the order is accepted rather than cancelled |
| Iris PENDING = skip | PENDING Iris orders are retried on the next job run |
| XPay may accept | If Nexi XPay reports PAID, the order is accepted; otherwise cancelled -- but only once the XPAY.EXPIRATION grace period has elapsed, so an order whose shopper is still mid-redirect on the Nexi hosted page is left alone for the next run. |
| Stock always restored | set_status() with stockMode='+' restores product stock on cancellation; when status === 'CANCELED' it also restores gift stock via restoreOrderGifts() (Advisable-com/ecommercen#688) |
| Coupon released | Applied coupons are marked unused so the customer can reuse them |
| Points returned | Loyalty points are credited back when POINT_SYSTEM.IS_ENABLED |
| ERP notification | Cancel and accept hooks fire for all cancellations/acceptances |
| Idempotent | Only PENDING orders are selected; once cancelled, they will not be reprocessed |
Gift Card Reconciliation
A parallel job, AdvCancelPendingGiftCards (ecommercen/gift_cards/jobs/AdvCancelPendingGiftCards.php), reconciles PENDING gift-card orders on the same cadence. It differs from the main flow in three ways:
- Blanket cancel for most payways: gift-card orders older than
giftCardDateTimeIntervalToDropare mass-cancelled viagift_card_orders_model->cancelPending(), except those paid withiris,paypaladvanced, orxpay(:43). - API-verified payways route to dedicated handlers:
cancelPendingIrisOrders()-- usesIris->getOrderStatus()andIrisHelper::interpretIrisResponse()cancelPendingPaypalAdvancedOrders()-- usesPayPalRestApi->getOrderDetails()(guarded by!empty($order->tran_ticket); a ticket-less order falls through tocancelGiftCard()instead of throwing, mirroring the main flow's "empty ticket => cancel" rule -- Advisable-com/ecommercen#416)cancelPendingXpayOrders()-- usesXPay->getOrderStatus()andXPay->mapOperationResultToStatus()(:146-169)
- Accept/cancel uses gift-card model methods:
gift_card_orders_model->acceptGiftCard($id)andcancelGiftCard($id)instead ofset_status(). giftCardDateTimeIntervalToDropis now validated the same way the main flow validatesXPAY/PAY_BY_BANKEXPIRATION: the newdateTimeIntervalToDrop()helper guards a missing/empty/non-string config value and a malformedDateIntervalspec, falling back toDEFAULT_DATE_TIME_INTERVAL_TO_DROP = 'PT180M'(3 hours -- identical to the value this repo'sapplication/config/app.php:537ships) and logging at error level (Advisable-com/ecommercen#531). The live vector is a client fork whoseapplication/config/app.phppredates thegiftCardDateTimeIntervalToDropkey: previously,config->item()returningnullfed straight intonew \DateInterval(null), which throws before the job's first query runs, aborting the entire gift-card reconciliation run.
Known Issues & Security Gaps
- No grace period for Iris (open): unlike
vivawallet(2 days) and nowxpay(registryXPAY/EXPIRATION, default 180 minutes -- closed for XPay by Advisable-com/ecommercen#500), a PENDINGirisorder hits the Iris API on every cron run from creation, so if the customer is still completing the Iris flow the order could in principle be flagged CANCELED before they finish. Not yet fixed; this issue previously also covered XPay, which is now resolved. xpay/klarna_payments/ethniki_nbgpaypayway-coverage gap -- resolved (Advisable-com/ecommercen#500): these three branches were previously unreachable dead code --getCardPayWays()(ecommercen/helpers/eshop_helper.php) omitted all three, so the sole order sourcegetDebrisOrders()could never select their PENDING orders even though the job's switch already handled them (the gift-card job'sAdvCancelPendingGiftCardsspecial-casedxpaythe whole time, underscoring that it was a registered, live payway). Fixed by adding all three togetCardPayWays(), bringing it to 17 entries; a drift guard (tests/Unit/Helpers/PayWayDebrisCoverageTest.php) now pins the invariant. That same test class was later extended (Advisable-com/ecommercen#530, see item 4 below) with a second, independent invariant over the sibling voucher-eligibility helper -- the class name predates that second guard and is kept as-is since released changelogs already cite it by name.- Duplicate XPay handler in deprecated
Cronjob::order_debris()-- resolved (Advisable-com/ecommercen#500): the manually-triggerable route used to carry a full duplicate of the switch and per-gateway handlers -- including the same then-deadxpaybranch -- at permanent risk of drifting from the job (the #290 coupon-release fix had already landed only in the job, not this duplicate).order_debris()now delegates to(new CancelIncompleteOrders())->executeCommand([]), so the route and the scheduled job are guaranteed to behave identically going forward. - Observation:
getCardPayWays()andisOrderPaidAtDeliveryByPayWay()'s exclusion list are now set-identical -- not a cue to unify them. After Advisable-com/ecommercen#530, both lists hold the same 17 payways, because both independently partitionallPayWays()along the same PENDING-vs-PENDING_ACCEPTEDlanding-status boundary thatAdv_checkout's handlers actually produce (see AD-34 Voucher Generation). This is a deliberate observation, not a design invariant to enforce, and unifying the two lists is explicitly out of scope here: a client fork that overrides only one of the two helpers would silently diverge from the other, a client-override migration hazard. If unification is ever pursued, it needs its own scoped issue with an explicit override-migration plan. - One bad
entry_datetimeaborting the whole cron run -- resolved (Advisable-com/ecommercen#531):cancelPendingDefaultCards(),cancelPendingPayByBank(), andhandlePendingXpayOrders()each fedentry_datetimeintoDateTime::createFromFormat()and called->add()on the result one line later.entry_datetimeisdatetime DEFAULT NULL, soNULL(or an empty/garbage value) is structurally possible; for those,createFromFormat()returnsfalse, andfalse->add()is a fatalErrorin PHP 8 that killed the wholeexecuteCommand()foreach -- stranding every remaining PENDING order in that run (stock reserved, coupons held, points withheld) and re-fataling every 5 minutes until the row was hand-fixed. A legacy zero-date row ('0000-00-00 00:00:00') is a second, distinct bad-data shape:createFromFormat()does not returnfalsefor it -- it returns a validDateTimerolled back to-0001-11-30with a "parsed date was invalid" warning fromDateTime::getLastErrors(). Afalse-only guard would have let that object through as valid and silently auto-cancelled the order, since a year-0001timestamp trivially clears every grace window. A related hazard in the same file:cancelPendingPayByBank()interpolated the rawPAY_BY_BANK/EXPIRATIONregistry value into aDateIntervalspec with no validation, so an empty/non-numeric value (reachable via the unvalidatedpay_by_bank_expirationadmin field) built an invalid spec and threw, the same batch-abort. Fixed by the sharedorderEntryDate()guard (which inspectsDateTime::getLastErrors()in addition to the return value, so both bad-data shapes are treated as unparseable and the order is skipped rather than cancelled or auto-cancelled) and bypayByBankExpirationSeconds(), mirroringxPayExpirationSeconds()(#500). The gift-card sibling jobAdvCancelPendingGiftCardshad the analogous hazard aroundgiftCardDateTimeIntervalToDrop, fixed the same way.
Related Flows
- SY-01 Cron Job Framework -- job scheduling and execution
- AD-03 Order Management -- PENDING to CANCELED transition
- CF-08 Payment Processing -- payment gateway integration
- SY-16 PayByBank Polling -- complementary PayByBank status checking