Payment drivers
Needs review
Needs review: Adding a driver with PaymentGateway::extend() is derived from Illuminate\Support\Manager and has no test or example in the repository. Check it end to end before relying on it.
The payments package (Ulams\Payments\) turns a payable, such as a cart order, into a
Payment and runs it through a gateway driver. The cart package is its main user. For the
admin side see Sales.
Drivers
Section titled “Drivers”All drivers are in payments/src/Gateway/Drivers/, extend AbstractDriver and implement
GatewayDriverContract:
| Driver | Name | What it does | requiredParameters() |
|---|---|---|---|
FreeDriver |
free |
settles zero-amount payments, refuses anything else | none |
StripeDriver |
stripe |
PaymentIntents; webhook verified with the Stripe-Signature HMAC |
return_url, payment_method |
Przelewy24Driver |
przelewy24 |
redirect flow; callback signature checked against the CRC, then verified with P24 | return_url, email |
RevenueCatDriver |
revenuecat |
in-app purchases checked by a ReceiptVerifier class from config |
none |
A payment with amount 0 always uses free.
The driver contract
Section titled “The driver contract”interface GatewayDriverContract{ public function purchase(Payment $payment, array $parameters = []): ResponseInterface; public function callback(Request $request, array $parameters = []): CallbackResponse; public function callbackRefund(Request $request, array $parameters = []): CallbackRefundResponse; public function refund(Request $request, Payment $payment, array $parameters = []): ResponseInterface; public static function requiredParameters(): array; public function throwExceptionForResponse(ResponseInterface $response): void; public function throwExceptionIfMissingParameters(array $parameters): void; public function ableToRenew(): bool;}ResponseInterfaceis Omnipay’sOmnipay\Common\Message\ResponseInterface. The package hasNoneGatewayResponseandFailedGatewayResponsefor drivers without an Omnipay gateway.callback()returnsCallbackResponse::success(),::failed()or::rejected(). Verify the provider’s signature here: a callback is an unauthenticated request.AbstractDrivertakesPaymentsConfigin its constructor and implements the parameter checks.ableToRenew()returnsfalsethere;Przelewy24Driveroverrides it for recurring products.
Resolution and configuration
Section titled “Resolution and configuration”Ulams\Payments\Gateway\GatewayManager extends Illuminate\Support\Manager. It is bound as
payment-gateway (facade PaymentGateway), and its default driver is
ulams_payments.default_gateway. Each built-in driver has a create<Name>Driver() factory
that throws GatewayConfigException when the gateway is disabled or not configured. The
Payments facade (PaymentsService) is the entry point for the rest of the app:
Payments::processPayable($order)->purchase($parameters) and
Payments::processPayment($payment)->callback($request).
Config (payments/src/config.php, key ulams_payments) and the settings an admin can change
per tenant (Providers/SettingsServiceProvider.php):
| Key | Admin setting | Public |
|---|---|---|
default_gateway |
yes, in:Free,Stripe,Przelewy24 |
yes |
default_currency |
yes | yes |
drivers.stripe.enabled, publishable_key |
yes | yes |
drivers.stripe.secret_key, webhook_secret |
yes | no |
drivers.przelewy24.enabled |
yes | yes |
drivers.przelewy24.live, merchant_id, pos_id, api_key, crc |
yes | no |
drivers.revenuecat.enabled |
yes | yes |
drivers.revenuecat.receipt_verifier |
no (config or env only) | no |
Webhooks and callbacks
Section titled “Webhooks and callbacks”Routes in payments/src/routes.php, under api/, without authentication:
| Method | Path | Handler |
|---|---|---|
GET |
/api/payments-gateways |
enabled gateways and their required parameters |
| any | /api/payments-gateways/callback/{payment} |
Payments::processPayment($payment)->callback($request); 400 when rejected |
POST |
/api/payments-gateways/webhook/stripe |
StripeDriver::verifiedEvent(), then the payment from the PaymentIntent metadata |
| any | /api/payments-gateways/callback/refund/{payment} |
refund callback |
PaymentProcessor::callback() runs in a transaction with lockForUpdate, so a callback that
arrives twice does not settle a payment twice. The resulting events are in
payments/src/Events/: PaymentRegistered, PaymentSuccess, PaymentCancelled and
PaymentFailed. Each carries the user and the payment. The cart marks an order paid in
PaymentSuccessListener, and templates can email on PaymentSuccess.
Adding a provider
Section titled “Adding a provider”There is no plugin API for gateways. Two paths:
Inside the payments package (supported)
Section titled “Inside the payments package (supported)”- Write
Gateway/Drivers/<Name>Driver.phpextendingAbstractDriver. Verify every callback’s signature, usehash_equalsfor comparisons, and makecallback()safe to call twice. - Add a
create<Name>Driver()factory toGatewayManagerthat checks the gateway is enabled and configured, plus getters onEntities/PaymentsConfig. - Add the config keys to
config.phpand register them inProviders/SettingsServiceProvider.php: secrets with$public = false, and the name in thedefault_gatewayin:rule. - Add the driver to
PaymentsService::listEnabledGateways()andlistGatewaysWithRequiredParameters(), soisDriverEnabled()accepts it andGET /api/payments-gatewayslists it. - Test the purchase, a valid and a forged callback, and a duplicate callback.
From your own package (partial)
Section titled “From your own package (partial)”GatewayManager inherits extend() from Laravel’s Manager, so another package can add a
driver without editing payments:
use Ulams\Payments\Facades\PaymentGateway;
PaymentGateway::extend('paypal', fn ($app) => new PayPalDriver($this->getPaymentsConfig()));The driver then resolves by name: PaymentGateway::driver('paypal'), and the generic callback
route works for payments created with it. But PaymentsService::listEnabledGateways() is
hard-coded to stripe, przelewy24 and revenuecat. A request’s gateway parameter naming
your driver is therefore ignored, GET /api/payments-gateways does not list it, and the
admin setting rejects it as the default gateway. Custom names are also case-sensitive. Making
custom drivers first-class needs a change in PaymentsService.