Skip to content

Payment Processing

Flow ID: CF-08 Module(s): checkout, Checkout domain Complexity: Very High Last Updated: 2026-05-27

Business Overview

Payment processing handles 19 payment method registrations (18 distinct adapter classes) through a central dispatcher. After checkout confirmation (CF-07), the Adv_checkout::run() method routes to the appropriate gateway on the legacy path; the REST path goes through PaymentInitializerFactory. Each follows: create order → redirect/render form → handle callback → update status.

All 18 adapter classes now have modern REST adapters registered via PaymentInitializerFactory. The legacy Adv_checkout::run() switch statement remains the path for storefront (non-REST) checkout; both paths share the same underlying gateway libraries.


API Reference

Modern REST Endpoints

MethodPathAuthDescription
POST/rest/checkout/place-orderGuest/CustomerCreate order and initialize payment in one call; returns {orderId, orderSerial, status, redirectUrl}
GET/rest/checkout/payment-methodsGuestDiscover available payment methods for the current config; returns [{key, label, requiresRedirect}]
GET/rest/checkout/payment-status/{orderId}CustomerCheck payment result after redirect

GET /rest/checkout/payment-methods — Route rest_routes.php:1709 (+ locale-prefixed :1717); added in commit 633ae4bd7 (#228). Controller src/Rest/Checkout/Controllers/Checkout.php:464-479 calls PaymentInitializerFactory::create() then PaymentInitializer::getRegisteredPaymentMethods() (:65-77). Only returns adapters whose gate conditions are met (credentials configured). The PAYMENT_METHOD_LABELS const (Checkout.php:50-60) covers 9 keys; all others return the raw key as label.

Browse endpoints interactively in the API Reference.

Legacy

TypePathPurpose
Internalcheckout->run($checkoutData)Payment dispatch
Callback/checkout/get_response/{gateway}Gateway returns
Webhook/checkout/handle/{gateway}/{action}Async notifications

Payment Gateways

All 19 registrations now go through PaymentInitializerFactory. Registration order: PaymentInitializerFactory.php:69-89. Any missing settings helper or empty required credential causes the adapter to be silently skipped — that payway will not appear in GET /rest/checkout/payment-methods or be accepted at POST /rest/checkout/place-order.

AdapterKeyRedirectGate
DeliveryAdapterdeliveryNoAlways
BankTransferAdapterbank_transferNoAlways
PaidAtStoreAdapterpaid_at_storeNoAlways
StripeAdapterstripeYesSTRIPE.SECRET_KEY
VivaWalletAdaptervivawalletYesgetVivaWalletSettings() creds
PayPalAdapterpaypaladvancedYesgetPaypalAdvancedSettings() client_id + secret
PayPalExpressAdapterpaypalYesgetPaypalSettings() client_id + secret
JccAdapterjccYesgetJccSettings() username + password
IrisAdapteririsYesgetIrisSettings() username, password, customerCode, checkDigit
KlarnaAdapterklarna_paymentsYesgetKlarnaSettings() username + password
EthnikiAdapterethnikiYes (iframe)getEthnikiBankSettings() publicKey + privateKey
EthnikiNbgPayAdapterethniki_nbgpayYesgetEthnikiNBGPaySettings() appID + appKey
EthnikiEEAdapterethniki_eeYes (iframe)getEthnikiEEBankSettings() merchantId, directApiKey, host, apiVersion
XPayAdapterxpayYesgetXPaySettings() API_KEY
PiraeusAdapterpiraeusYesgetPiraeusBankSettings()PiraeusConfig::isConfigured()
CardLinkAdapter (Alpha)alphaYesgetAlphaBankSettings() ID, SSK, Submit
CardLinkAdapter (Eurobank)eurobankYesgetEurobankSettings() merchantId, secret, url
ApcoPayAdapterapcopayYesgetApcoPaySettings() profileId, secret, merchantId, merchantPassword
PayByBankAdapterpaybybankNogetPayByBankSettings() payByBankApiKey + payByBankApiUrl

19 registrations, 18 distinct classes. Note: KlarnaAdapter key is klarna_payments (not klarna); PayPalExpressAdapter key is paypal (distinct from paypaladvanced); PayByBankAdapter has supportsRedirect() === false — it returns a bank payment code, not a redirect URL.


Per-Gateway initializePayment Notes

  • CardLink (Alpha/Eurobank): POST redirect; CardLink v2 SHA-256 digest over concatenated fields + secret; CardLinkAdapter.php:52-123
  • JCC: calls Jcc::registerOrder() (amount in integer cents, ISO 4217 numeric currency); returns GET redirect to formUrl; JccAdapter.php:49-75
  • IRIS: calls Iris::registerOrder(); GET redirect to bankSelectionToolUrl; IrisAdapter.php:45-72
  • Klarna: requires klarna_authorization_token in paymentData; Klarna::createOrder() converts token; KlarnaAdapter.php:61-142
  • Ethniki iBank: GET redirect to Simplify JS SDK URL with data-* params; EthnikiAdapter.php:58-82
  • NBGPay: NBGPayHelper::createHPPLink(); phone split via PhoneHelper; locale injected from config_item('language_abbr'); EthnikiNbgPayAdapter.php:51-86
  • Ethniki EE: NBGEEHelper::beginNewSession(); session-based validation; Greek fields uppercased+transliterated; EthnikiEEAdapter.php:80-151
  • XPay: XPay::createHostedPaymentOrder(); notification URL = legacy checkout/xPayHook; XPayAdapter.php:52-78
  • Piraeus: SOAP Piraeus::issueTicket(); POST redirect with form params; confirmation via REST webhook; PiraeusAdapter.php:37-74
  • ApcoPay: ApcoPay::getPaymentUrl(); language elgr mapped; statusUrl = legacy checkout/apcopay_status; ApcoPayAdapter.php:71-116
  • PayByBank: PayByBank::createOrder(); NO redirect — returns bank payment code; confirmation via REST webhook; PayByBankAdapter.php:45-70
  • PayPal Express: PayPal Orders v2 createOrder with user_action: CONTINUE; GET redirect to approve/payer-action link; PayPalExpressAdapter.php:41-98

Universal Success Path

1. order_model->set_status(orderSerial, 'PAID', ignoreStock=true)
2. order_model->set_is_paid(orderSerial)
3. afterOrderSuccessHooks()         → ERP sync
4. adv_mailer->order_complete()     → confirmation email
5. sendSmsSuccess()                 → SMS notification
6. informLowStock()                 → admin alert
7. if (guest): customer_exit()
8. cart->destroy()                  → clear session cart

Universal Failure Path

1. set_status(orderSerial, 'CANCELED', stockMode='+')  → return stock
2. cancelCoupon()                   → markCouponUnused()
3. returnPointsToCustomers()        → loyalty rollback
4. afterOrderCancelHooks()          → ERP sync (cancel)

REST Webhook Coverage

PaywayREST webhookValidation
stripePOST /rest/webhooks/stripeHMAC-SHA256 via STRIPE.WEBHOOK_SECRET
vivawalletPOST /rest/webhooks/vivawalletempty body → verification key exchange
paypaladvanced/paypalPOST /rest/webhooks/paypalserver-side re-fetch via PayPal Orders v2 GET (commit 4d15de008)
piraeusPOST /rest/webhooks/piraeusHMAC validateNotificationResponse()
paybybankPOST /rest/webhooks/paybybanknone (mirrors legacy unsigned pattern)

jcc, iris, klarna, ethniki×3, xpay, alpha, eurobank, and apcopay still confirm via legacy CI3 callback routes (e.g. checkout/xPayHook, checkout/apcopay_status), resolving REST-placed orders by shared order_serial. A REST-only deployment still requires legacy checkout routes to be mounted for these gateways.


Business Rules

RuleDescription
Order must be PENDINGAll success handlers check status before updating
Stock not adjusted on successignoreStock=true — stock deducted at creation
Stock returned on cancelreturnOrderStock() adds qty back
Coupon rolled back on cancelUsage counter decremented
Points rolled back on cancelBoth spent and earned reversed
Guest logged out on successcustomer_exit() clears session
Installments validatedminOrderAmount threshold + allowed values per gateway

Client Extension Points

TypeDetails
Legacy hooksafterOrderSuccessHooks(), afterOrderCancelHooks(), successExtras()
Modern adaptersImplement PaymentAdapterInterface for custom gateways
DI overrideReplace PaymentInitializerFactory for custom adapter registration
Gateway credentialsAll configured via Registry (per-gateway group)

Klarna Legacy AJAX API

These endpoints are served by AdvApiKlarna (ecommercen/api/controllers/AdvApiKlarna.php) and manage the Klarna Payments lifecycle from the admin panel and storefront. All endpoints read POST data and return JSON via sendOutput(). Bot requests are rejected.

Base path: /api/klarna/{method} (routed via api/api_klarna)

MethodPathParametersAuthResponseDescription
POST/api/klarna/openPaymentSessionintent, purchase_country, purchase_currency, locale, order_amount, transportationCost, couponDiscount, giftPackagingCost, pointsDiscountSession{session_id, client_token, ...}Create a Klarna payment session with computed order lines from the current cart; stores session_id in shop_order_klarna_payment
POST/api/klarna/updatePaymentSessionsession_id, order_amount, transportationCost, couponDiscount, giftPackagingCost, pointsDiscountSession{success: bool}Update an existing Klarna session (e.g., after cart or address changes) with recalculated order lines
POST/api/klarna/cancelOrderklarnaPaymentsOrderId, orderIdSession{} on success; 403 on failureCancel a Klarna order; updates payment status to canceled and sets shop order status to CANCELED
POST/api/klarna/captureOrderklarnaPaymentsOrderId, orderIdSession{} on success; 403 on failureCapture (charge) a previously authorized Klarna order for the full total_vat amount; updates payment status to captured
POST/api/klarna/refundOrderklarnaPaymentsOrderId, orderIdSession{} on success; 403 on failureRefund a captured Klarna order for the full total_vat amount; updates payment status to refunded and sets shop order status to CANCELED

Order lines: Generated by KlarnaHelper::generateOrderLines() from current cart contents, live pricing data, transportation cost, coupon discount, gift packaging cost, and points discount.


Data Model

Key tables involved in payment processing

TablePurpose
shop_orderOrder record — status transitions (PENDING -> PAID or CANCELED), payway identifies gateway
shop_order_basketOrder line items — stock adjusted on creation, returned on cancellation
shop_order_klarna_paymentKlarna-specific session data (session_id, order_id, status: authorized/captured/refunded/canceled)
shop_order_klarna_paymentsExtended Klarna Payments tracking (see schema below)
couponsCoupon usage counter — incremented on order creation, decremented on cancellation
shop_customerCustomer record — loyalty points balance updated (deducted pre-payment, returned on cancellation)
shop_order_transactionsPayment transaction log (gateway reference, amount, status, timestamp)
pbb_loggingPayByBank transaction log (~14.5K rows in production); tracks PBB payment lifecycle events
xpay_loggingXPay transaction audit trail — request/response payloads (TEXT JSON), flow discriminator (order vs gift_card) for cross-flow token isolation; the securityToken is embedded in response_data JSON and read back by getStoredSecurityToken() (:308-335) for webhook verification. No dedicated token column.

For the full shop_order column schema, see AD-03 Order Management.

shop_order_klarna_payments — Klarna Payments extended tracking

ColumnTypeDescription
idINT AUTO_INCREMENTPrimary key
klarna_payments_order_idVARCHARKlarna-assigned order identifier
fraud_statusVARCHARKlarna fraud assessment result
authorized_payment_methodVARCHARPayment method authorized by the customer (e.g., pay_later, slice_it)
session_idVARCHARKlarna session identifier (links to shop_order_klarna_payment.session_id)
authorization_tokenVARCHARToken received after customer authorization, used to place the Klarna order
shop_order_idINTFK to shop_order.id
statusVARCHARPayment status (authorized, captured, refunded, canceled)
auto_captureTINYINTWhether the order was auto-captured on authorization (1 = yes)

NBG EE (Ethniki Enterprise Edition) — Undocumented Gateway

NBG EE is completely undocumented externally. It uses a session-based checkout flow with version-specific endpoints:

  • V62: CREATE_CHECKOUT_SESSION — initiates a checkout session with the bank
  • V63: INITIATE_CHECKOUT — starts the checkout process within an established session

The flow relies on server-side session state rather than token-based authorization. Validation is session-based (no digest or HMAC). This gateway has no public API documentation from NBG — all integration knowledge is derived from implementation code and internal communications.

XPay (Nexi) — securityToken echo, no HMAC

XPay Greece does not use HMAC for webhook authenticity. Instead, the gateway returns a securityToken in the HPP-create response (src/PaymentGateways/NexiXPay/XPay.php:145), persists it in xpay_logging keyed by (order_serial, flow), and echoes it back in every server-to-server xPayHook notification. The controller compares via hash_equals() (:297) and fails closed if either side is missing.

The flow column ('order' vs 'gift_card') ensures a gift-card webhook cannot accept a regular order's stored token even when GIFT_CARDS.ORDER_PREFIX is empty and serials happen to collide.


Headless Checkout Endpoints (REST, v4.99.3+)

The modern headless commerce layer provides stateless checkout via REST:

MethodPathAuthResponseNotes
POST/rest/checkout/shippingAnyAvailable transportersRequires countryAlpha2 in body
POST/rest/checkout/coupon/validateAny{valid: true/false}Requires couponCode in body
POST/rest/checkout/totalsAnyCart totalsReturns 404 "No cart found" without active cart
GET/rest/checkout/payment-status/{orderId}CustomerPayment statusPost-payment polling

These endpoints use the PlaceOrderService orchestrator with ShippingCalculator, CouponValidator, and all 19 registered payment adapters.


Known Issues & Security Gaps

  1. XPay EUR hardcode is legacy-path only: ecommercen/checkout/controllers/Adv_checkout.php:3474 passes 'EUR' as the currency regardless of the active storefront currency on the legacy storefront path only. The modern XPayAdapter correctly passes $context->currencyCode (XPayAdapter.php:62) and is not affected. Gift card payments on the legacy path correctly use $this->currentCurrency->code. See Advisable-com/ecommercen#249 for follow-up scope.

  2. Stale OpenAPI description in paymentMethods() controller (src/Rest/Checkout/Controllers/Checkout.php:452-458): the OpenAPI description still claims Eurobank, Ethniki×3, Piraeus, JCC, Iris, Klarna, PayByBank, and PayPal Express are "intentionally NOT surfaced" and "will appear once ported" — but all of these ARE ported and ARE registered in PaymentInitializerFactory. The generated OpenAPI spec is misleading for API consumers.

  3. payment-methods returns raw keys as labels for 10 of 19 payways: PAYMENT_METHOD_LABELS (Checkout.php:50-60) covers only 9 keys. eurobank, jcc, iris, klarna_payments, ethniki, ethniki_nbgpay, ethniki_ee, piraeus, paybybank, and paypal all return their raw key as the display label. Minor gap for headless clients that rely on the label field.

  4. No dedicated REST webhook for 8 gateways: jcc, iris, klarna, ethniki×3, xpay, alpha, eurobank, and apcopay confirm payment via legacy CI3 callback routes (e.g. checkout/xPayHook, checkout/apcopay_status), resolving REST-placed orders by shared order_serial. A REST-only deployment still requires legacy checkout routes to be mounted for these gateways.

For additional XPay webhook and security gaps, see CF-09 Payment Webhooks and IN-23 Nexi XPay Greece.


Wiki Guides: Stripe integration setup and keys — see Stripe Guide. Gateway calls are protected by circuit breakers — see Circuit Breaker Guide. Post-payment hooks run as deferred tasks — see Deferred Task Guide.

Shared Patterns