Appearance
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
| Method | Path | Auth | Description |
|---|---|---|---|
| POST | /rest/checkout/place-order | Guest/Customer | Create order and initialize payment in one call; returns {orderId, orderSerial, status, redirectUrl} |
| GET | /rest/checkout/payment-methods | Guest | Discover available payment methods for the current config; returns [{key, label, requiresRedirect}] |
| GET | /rest/checkout/payment-status/{orderId} | Customer | Check 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
| Type | Path | Purpose |
|---|---|---|
| Internal | checkout->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.
| Adapter | Key | Redirect | Gate |
|---|---|---|---|
| DeliveryAdapter | delivery | No | Always |
| BankTransferAdapter | bank_transfer | No | Always |
| PaidAtStoreAdapter | paid_at_store | No | Always |
| StripeAdapter | stripe | Yes | STRIPE.SECRET_KEY |
| VivaWalletAdapter | vivawallet | Yes | getVivaWalletSettings() creds |
| PayPalAdapter | paypaladvanced | Yes | getPaypalAdvancedSettings() client_id + secret |
| PayPalExpressAdapter | paypal | Yes | getPaypalSettings() client_id + secret |
| JccAdapter | jcc | Yes | getJccSettings() username + password |
| IrisAdapter | iris | Yes | getIrisSettings() username, password, customerCode, checkDigit |
| KlarnaAdapter | klarna_payments | Yes | getKlarnaSettings() username + password |
| EthnikiAdapter | ethniki | Yes (iframe) | getEthnikiBankSettings() publicKey + privateKey |
| EthnikiNbgPayAdapter | ethniki_nbgpay | Yes | getEthnikiNBGPaySettings() appID + appKey |
| EthnikiEEAdapter | ethniki_ee | Yes (iframe) | getEthnikiEEBankSettings() merchantId, directApiKey, host, apiVersion |
| XPayAdapter | xpay | Yes | getXPaySettings() API_KEY |
| PiraeusAdapter | piraeus | Yes | getPiraeusBankSettings() → PiraeusConfig::isConfigured() |
| CardLinkAdapter (Alpha) | alpha | Yes | getAlphaBankSettings() ID, SSK, Submit |
| CardLinkAdapter (Eurobank) | eurobank | Yes | getEurobankSettings() merchantId, secret, url |
| ApcoPayAdapter | apcopay | Yes | getApcoPaySettings() profileId, secret, merchantId, merchantPassword |
| PayByBankAdapter | paybybank | No | getPayByBankSettings() 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 toformUrl;JccAdapter.php:49-75 - IRIS: calls
Iris::registerOrder(); GET redirect tobankSelectionToolUrl;IrisAdapter.php:45-72 - Klarna: requires
klarna_authorization_tokeninpaymentData;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 viaPhoneHelper; locale injected fromconfig_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 = legacycheckout/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(); languageel→grmapped; statusUrl = legacycheckout/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
createOrderwithuser_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 cartUniversal 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
| Payway | REST webhook | Validation |
|---|---|---|
| stripe | POST /rest/webhooks/stripe | HMAC-SHA256 via STRIPE.WEBHOOK_SECRET |
| vivawallet | POST /rest/webhooks/vivawallet | empty body → verification key exchange |
| paypaladvanced/paypal | POST /rest/webhooks/paypal | server-side re-fetch via PayPal Orders v2 GET (commit 4d15de008) |
| piraeus | POST /rest/webhooks/piraeus | HMAC validateNotificationResponse() |
| paybybank | POST /rest/webhooks/paybybank | none (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
| Rule | Description |
|---|---|
| Order must be PENDING | All success handlers check status before updating |
| Stock not adjusted on success | ignoreStock=true — stock deducted at creation |
| Stock returned on cancel | returnOrderStock() adds qty back |
| Coupon rolled back on cancel | Usage counter decremented |
| Points rolled back on cancel | Both spent and earned reversed |
| Guest logged out on success | customer_exit() clears session |
| Installments validated | minOrderAmount threshold + allowed values per gateway |
Client Extension Points
| Type | Details |
|---|---|
| Legacy hooks | afterOrderSuccessHooks(), afterOrderCancelHooks(), successExtras() |
| Modern adapters | Implement PaymentAdapterInterface for custom gateways |
| DI override | Replace PaymentInitializerFactory for custom adapter registration |
| Gateway credentials | All 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)
| Method | Path | Parameters | Auth | Response | Description |
|---|---|---|---|---|---|
| POST | /api/klarna/openPaymentSession | intent, purchase_country, purchase_currency, locale, order_amount, transportationCost, couponDiscount, giftPackagingCost, pointsDiscount | Session | {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/updatePaymentSession | session_id, order_amount, transportationCost, couponDiscount, giftPackagingCost, pointsDiscount | Session | {success: bool} | Update an existing Klarna session (e.g., after cart or address changes) with recalculated order lines |
| POST | /api/klarna/cancelOrder | klarnaPaymentsOrderId, orderId | Session | {} on success; 403 on failure | Cancel a Klarna order; updates payment status to canceled and sets shop order status to CANCELED |
| POST | /api/klarna/captureOrder | klarnaPaymentsOrderId, orderId | Session | {} on success; 403 on failure | Capture (charge) a previously authorized Klarna order for the full total_vat amount; updates payment status to captured |
| POST | /api/klarna/refundOrder | klarnaPaymentsOrderId, orderId | Session | {} on success; 403 on failure | Refund 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
| Table | Purpose |
|---|---|
shop_order | Order record — status transitions (PENDING -> PAID or CANCELED), payway identifies gateway |
shop_order_basket | Order line items — stock adjusted on creation, returned on cancellation |
shop_order_klarna_payment | Klarna-specific session data (session_id, order_id, status: authorized/captured/refunded/canceled) |
shop_order_klarna_payments | Extended Klarna Payments tracking (see schema below) |
coupons | Coupon usage counter — incremented on order creation, decremented on cancellation |
shop_customer | Customer record — loyalty points balance updated (deducted pre-payment, returned on cancellation) |
shop_order_transactions | Payment transaction log (gateway reference, amount, status, timestamp) |
pbb_logging | PayByBank transaction log (~14.5K rows in production); tracks PBB payment lifecycle events |
xpay_logging | XPay 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_ordercolumn schema, see AD-03 Order Management.
shop_order_klarna_payments — Klarna Payments extended tracking
| Column | Type | Description |
|---|---|---|
id | INT AUTO_INCREMENT | Primary key |
klarna_payments_order_id | VARCHAR | Klarna-assigned order identifier |
fraud_status | VARCHAR | Klarna fraud assessment result |
authorized_payment_method | VARCHAR | Payment method authorized by the customer (e.g., pay_later, slice_it) |
session_id | VARCHAR | Klarna session identifier (links to shop_order_klarna_payment.session_id) |
authorization_token | VARCHAR | Token received after customer authorization, used to place the Klarna order |
shop_order_id | INT | FK to shop_order.id |
status | VARCHAR | Payment status (authorized, captured, refunded, canceled) |
auto_capture | TINYINT | Whether 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:
| Method | Path | Auth | Response | Notes |
|---|---|---|---|---|
| POST | /rest/checkout/shipping | Any | Available transporters | Requires countryAlpha2 in body |
| POST | /rest/checkout/coupon/validate | Any | {valid: true/false} | Requires couponCode in body |
| POST | /rest/checkout/totals | Any | Cart totals | Returns 404 "No cart found" without active cart |
| GET | /rest/checkout/payment-status/{orderId} | Customer | Payment status | Post-payment polling |
These endpoints use the PlaceOrderService orchestrator with ShippingCalculator, CouponValidator, and all 19 registered payment adapters.
Known Issues & Security Gaps
XPay EUR hardcode is legacy-path only:
ecommercen/checkout/controllers/Adv_checkout.php:3474passes'EUR'as the currency regardless of the active storefront currency on the legacy storefront path only. The modernXPayAdaptercorrectly 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.Stale OpenAPI description in
paymentMethods()controller (src/Rest/Checkout/Controllers/Checkout.php:452-458): the OpenAPIdescriptionstill 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 inPaymentInitializerFactory. The generated OpenAPI spec is misleading for API consumers.payment-methodsreturns 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.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 sharedorder_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.
Related Flows
- CF-05 Cart Management — cart cleared on success
- CF-06 Order Preview — generates checkout data
- CF-07 Order Confirmation — order creation before payment
- CF-09 Payment Webhooks — async callbacks
- CF-13 Coupons — coupon rolled back on failure
- CF-23 Gift Cards — gift card payment via Viva Wallet
- CF-32 Loyalty Points — points rolled back on failure
- SY-02 Order Status Emails — confirmation email on success
- AD-03 Order Management — admin order status
- IN-08 ERP Integrations — ERP sync after payment success
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
- SY-24 Email Dispatch — order confirmation and cancellation emails dispatched after payment resolution
- SY-26 Circuit Breaker — protects external payment gateway API calls from cascading failures
- SY-27 Deferred Tasks — ERP sync (
afterOrderSuccessHooks) and SMS notifications run as deferred tasks