Skip to main content
A custom payment driver can be built to support any payment provider, by implementing the Lunar\Core\Contracts\PaymentType contract and registering it with the Payments manager.

Overview

Payments in Lunar are driver-based. Each driver is responsible for authorizing a payment, capturing an intent, and issuing refunds, then recording the result as transactions against the order. Lunar ships an OfflinePayment driver for cash-in-hand and manual scenarios; anything else, including card payments, is added as a driver. lunarphp/stripe is Lunar’s first-party reference implementation and is the best place to see a complete driver in production use. See Stripe for the add-on itself, and Payments for day-to-day configuration and usage.

The PaymentType contract

A driver implements Lunar\Core\Contracts\PaymentType:
Rather than implementing the interface directly, a driver typically extends Lunar\Core\PaymentTypes\AbstractPayment, which implements cart(), order(), withData(), setConfig(), and allowPartialPayment(), leaving only authorize(), refund(), and capture() to be written. AbstractPayment also defines getPaymentChecks(Transaction $transaction): Lunar\Core\DataObjects\PaymentChecks. This method is not part of the PaymentType contract itself, but every transaction resolves its driver and calls it through Transaction::paymentChecks(), so a driver that performs verification checks (AVS, postal code, CVC, and similar) should override it. The default implementation returns an empty PaymentChecks collection.

Registering a driver

A driver is registered with the Payments manager, usually from a service provider’s boot() method:
The driver is then made available by mapping a payment type to it in config/lunar/payments.php:
Whatever is set under a type’s driver key is passed to setConfig() automatically when the driver is resolved, so any additional configuration a driver needs can be added alongside it.

Verifying the amount and currency

Before an order is placed, a driver must confirm that the amount and currency it received from the provider match the cart or order total it is authorizing. Skipping this check allows a stale or tampered payment intent to place an order at the wrong price.
authorize() should call this check as soon as the provider’s response is available, and return the failure immediately if it does not pass.

Scaling amounts

Lunar stores prices as integers scaled by Currency::decimal_places, which a merchant can set independently of what a payment provider expects. Never assume two decimal places: convert through decimal_places explicitly when talking to a provider, and convert back the same way when reading an amount from it.
lunarphp/stripe’s Lunar\Stripe\Managers\StripeManager::toStripeAmount() and fromStripeAmount() are a complete worked example of this conversion, including handling zero-decimal and three-decimal provider currencies.

Recording transactions

Every authorization, capture, and refund a driver performs should be recorded as a Lunar\Core\Models\Transaction against the order.

Authorizing

If a payment is not captured immediately, its transaction should use the type intent. When it is later captured, create a second transaction related to the intent through parent_transaction_id:
If the payment is captured straight away, record it directly with type capture instead.

Capturing

If the provider already charged the card at authorization time, capturing can be skipped entirely, as with OfflinePayment.
Do not capture an amount greater than the original intent amount. Capturing less than the intent amount is treated by most providers as a partial refund, after which no further capture can take place against that intent.

Refunding

Only a capture transaction can be refunded. To refund a payment that has not yet been captured, capture a smaller amount instead.
Transaction exposes refund(int $amount, $notes = null) and capture(int $amount = 0) convenience methods that delegate to the transaction’s own driver, resolved through Payments::driver($transaction->driver).