Gateway integration¶
Supported gateways¶
| Provider constant | Value | Class |
|---|---|---|
GatewayProvider::MOBILEPAY | mobilepay | MobilePayGateway |
GatewayProvider::FRISBII | frisbii | FrisbiiGateway |
Frisbii surfaces multiple WooCommerce payment methods: Credit Card, Apple Pay, Google Pay, MobilePay Online, iDEAL, and PayPal.
Using gateways¶
Never call gateway clients directly from outside the Core layer. Always go through the container-provided services:
// Get agreement service from container
$agreementService = $container->get( AgreementService::class );
// Create an agreement (mandate) for a subscription
$response = $agreementService->createAgreement( $createAgreementRequest );
// Get payment service
$paymentService = $container->get( PaymentService::class );
// Create a recurring payment against an existing agreement
$response = $paymentService->createRecurringPayment( $createRecurringPaymentRequest );
Request factory pattern¶
Build request objects via the provided factories rather than instantiating them directly:
$factory = $container->get( PaymentRequestFactory::class );
$request = $factory->createCaptureRequest( $payment );
Gateway interface hierarchy¶
BasePaymentGatewayInterface
├── PaymentGatewayInterface # create, capture, refund, cancel
├── RecurringPaymentGatewayInterface # createRecurring, captureRecurring, …
├── AgreementSessionGatewayInterface # createAgreementSession
└── PaymentSessionGatewayInterface # createPaymentSession
Both MobilePayGateway and FrisbiiGateway implement the relevant interfaces for their supported operations.
Customer provisioning¶
Some providers require the customer to exist on their side before an agreement can be created. Agreement adapters for those implement CustomerProvisioningAdapterInterface, which exposes the provider customer handle and the payload used to create the customer.
FrisbiiGateway::ensureCustomerExists() resolves a handle Frisbii will accept — creating the customer when it is missing — and records it on the request, so the payload is built from that same handle. It runs on every path that names the customer: both agreement session types, the standalone charge session, and the payment method import.
Recording the handle needs ResolvedCustomerHandleAwareInterface, which is deliberately separate from CustomerAwareRequestInterface: knowing which customer a request is for is universal, whereas carrying a resolved provider handle is a capability only some providers need. It is checked with instanceof, like CustomerProvisioningAdapterInterface. A request that does not implement it still gets its customer provisioned — it just gets the natural handle, with no escalation, because there would be nowhere to record the result.
Guest orders¶
A guest has no WooCommerce account, so there is no customer handle to derive and nothing to provision. The customer is named after the order instead:
Two properties matter. It says plainly that this is not a shop customer — a real one is <prefix><customer id>, so the two can never be confused — and it is the same handle every time for the same order. That stability is deliberate: PaymentService::createPayment() creates a fresh session for an order whose amount has changed, and a new handle per attempt would leave a trail of customers per order and risk error 99, since the existing invoice already belongs to the first one.
We never call POST /v1/customer for a guest — the session's inline customer object is what creates it — and we never refer to that customer again.
Frisbii's own generate_handle is deliberately not used: it names the customer cust-<sequence>, which sits in Frisbii's namespace, reads like a reference to a customer of ours, and discloses volume.
What the code used to do was worse than either: the handle came from prefixHandle( null ), the bare prefix, so with PAYMENTHUB_HANDLE_PREFIX set every guest order in the shop shared one customer.
Only the handle is sent, so a provider customer record looks the same no matter which flow created it. Customer details are deliberately not synced.
The handle is a candidate, not a fact¶
The natural handle is the WooCommerce customer ID prefixed with PAYMENTHUB_HANDLE_PREFIX — see Configuration. It is only where the search starts.
Frisbii soft-deletes customers, and its documentation for DELETE /v1/customer/{handle} states that the customer handle is not changed so it cannot be re-used. There is no endpoint to restore one. A deleted customer therefore keeps its handle forever while being unusable, and GET /v1/customer/{handle} answers 200 for it — so the status code alone proves nothing. A customer counts as usable only when the handle it returns is the one that was asked for and it carries no deleted timestamp.
When the natural handle is unusable, the next candidate is tried:
Nothing is stored. Because the order is fixed, the same handle is found again on every later call, in every flow — which is what keeps a customer reachable after their Frisbii record has been deleted. The search is capped at ten candidates: a silent endless loop would be worse than a loud failure.
The prefix must stay stable per environment; changing it makes every existing provider customer unreachable.
Reading a provider handle back¶
Handles are built by the adapter, so reading them belongs there too: ProviderHandleAdapterInterface exposes customerHandleBelongsTo() and orderIdFromHandle(), and FrisbiiPaymentAdapter implements them.
Without an inverse, callers improvise, and every improvisation breaks the moment PAYMENTHUB_HANDLE_PREFIX is set — a prefixed handle is neither an integer nor an order id:
Both were live in FrisbiiWebhookHandler and made every webhook-created payment fail on an environment with a prefix configured.
The same applies to finding a payment again: order_id is an integer column, so a handle has to be read back before it can be looked up. ext_payment_id stores the handle as it came and remains the fallback.
Importing an existing payment method¶
Gateways that can adopt a card which already exists at the provider — rather than creating one through a checkout session — implement AgreementImportGatewayInterface. FrisbiiGateway does, by exchanging a one-time token for a permanent payment method reference through POST /v1/payment_method.
The import goes through the same customer provisioning as a normal agreement, so a card can be attached to a customer who has never been through a Frisbii checkout — or whose Frisbii record has been deleted. It is used to migrate cards from a previous provider — see Migrations.