vanilo/payment
Vanilo Payment brings payment processing to Vanilo e-commerce apps: manage payment methods, integrate gateways, record transactions, and link payments to orders via Laravel-friendly, extensible APIs. It offers a consistent abstraction for multiple providers and custom drivers.
vanilo/payment is the payment abstraction layer of the Vanilo e-commerce suite, not a standalone payment integration. It gives you a Payment model tied to any "payable" (usually an Order), a PaymentMethod model, a gateway registry, a return/cancel callback controller, and a set of events. The actual PSP integrations live in sibling packages (vanilo/omnipay, PayPal/Stripe addons, or your own gateway class).
Minimal install:
composer require vanilo/payment
In practice you'll almost never install this alone — pulling vanilo/framework (or the full vanilo/vanilo metapackage) brings it in, along with vanilo/contracts and vanilo/concord (model swappability) which this module depends on.
First steps after install:
payment_methods and payments. They are auto-loaded by the service provider; just php artisan migrate. If you need to inspect/override them, publish with php artisan vendor:publish --tag=vanilo-payment (tag names vary by version — check vendor/vanilo/payment/src/Providers/ for the actual publish groups).config/payment.php. This is where gateway classes and model overrides are declared.PaymentMethod whose gateway string isn't in the registry will blow up when you try to pay.use Vanilo\Payment\Models\PaymentMethod;
PaymentMethod::create([
'name' => 'Pay on Delivery',
'gateway' => 'cash', // must match a registered gateway key
'enabled' => true,
]);
First real use case: a checkout that shows enabled payment methods, creates a Payment against the order, hands off to the gateway, and lets the gateway's redirect/return callback finalise the order.
Creating a payment against a payable. The payments table is polymorphic (payable_type/payable_id), so anything implementing the Payable contract works — normally Vanilo\Order\Models\Order:
use Vanilo\Payment\Models\Payment;
$payment = Payment::create([
'payment_method_id' => $method->id,
'amount' => $order->total(),
'currency' => $order->currency(),
'payable_type' => $order->getMorphClass(),
'payable_id' => $order->id,
]);
Using getMorphClass() rather than get_class() matters if you have a morph map configured.
The gateway handshake. All gateways implement Vanilo\Payment\Contracts\PaymentGateway. The typical three-step flow is: build a PaymentRequest, ask the gateway to create it, then branch on the PaymentResponse:
use Vanilo\Payment\Payment\Requests\PaymentRequest;
$request = new PaymentRequest($payment);
$request->setReturnUrl(route('vanilo.payment.return'));
$request->setCancelUrl(route('vanilo.payment.cancel'));
$response = $payment->getGateway()->createPaymentRequest($request);
if ($response->hasRedirect()) {
return redirect($response->getRedirectUrl());
}
if ($response->getStatus()->isPaid()) {
// synchronous approval, e.g. cash / manual
}
Response objects expose a PaymentStatus (getStatus()), getTransactionId(), getAmountPaid(), getMessage(), and the redirect helpers. Treat getStatus() as the source of truth, not the presence of a transaction id.
Handling the callback. The module ships Vanilo\Payment\Http\Controllers\PaymentController with return and cancel actions behind the vanilo.payment.return / vanilo.payment.cancel named routes. The controller resolves the payment from the request, asks the gateway to verify status (getPaymentStatus(PaymentRequest)), persists the result, and fires the domain events. Override the routes in your app if your checkout needs a custom post-payment landing page — just point setReturnUrl() at the route you actually register.
Reacting to state changes. Wire your order lifecycle to payment events rather than polling the gateway:
use Vanilo\Payment\Events\PaymentPaid;
use Vanilo\Payment\Events\PaymentFailed;
Event::listen(PaymentPaid::class, fn ($e) => $e->payment->payable->markAsPaid());
Event::listen(PaymentFailed::class, fn ($e) => $e->payment->payable->markAsFailed($e->payment));
Because the events carry the Payment, you can always walk back to the order via $payment->getPayable().
Custom gateway. Implement PaymentGateway (and typically extend the abstract gateway base class if one is provided in your version) with at minimum createPaymentRequest() and getPaymentStatus(), optionally createRefundRequest(). Register the class in config/payment.php under the gateways array, or programmatically through the registry service the module exposes. Keep the class thin: translate Vanilo's PaymentRequest into the PSP SDK call, and map the PSP's answer back to a PaymentResponse with a PaymentStatus.
Model customisation via Concord. Every model in the module (Payment, PaymentMethod, PaymentTransaction if present) is swappable. Add your subclass to the models section of config/concord.php, extend the base class, and the module's internals will resolve your class everywhere. This is the sanctioned extension point for adding columns or business logic — do not patch the vendor package.
Testing pattern. Create an in-memory gateway stub that always returns a fixed PaymentResponse, register it in the test's config, and assert on the events and the resulting Payment::status. This keeps your checkout tests free of network calls.
gateway column on PaymentMethod is a plain string. A typo won't be caught until $payment->getGateway() resolves, then it throws. Validate the gateway key at PaymentMethod creation time in your admin form (an in: rule against the registry keys).currency explicitly on the Payment. Some PSPs reject mismatched or unsupported currencies, and the failure surfaces deep inside the gateway call rather than in validation.getPaymentStatus() before marking anything paid. Anyone can hit the return URL. Gateways with webhooks typically need a separate, idempotent endpoint — the shipped return route is a redirect handler, not a webhook receiver, so don't conflate them.Payment::status already being final before dispatching your own side effects (emails, stock decrement).php artisan config:cache bakes in the gateway map. If you register gateways dynamically in a service provider that reads from the DB, that will not survive a config cache — prefer the config-file registration for anything static.payments has a foreign key to payment_methods. If your app has an existing payments table from a previous version, publish the migrations and diff them before running — Vanilo has changed column names (amount_paid, transaction_id, remote_id) across major versions.PaymentPaid/PaymentFailed/PaymentCancelled listeners in your code. If the order never flips to paid, the first place to look is whether the event listener is registered and whether the controller actually reached the "paid" branch.Payment via Concord affects every package in the suite that references it. Subclass carefully: keep the base class's table name, casts, and event behaviour, and don't change $fillable in ways that break sibling packages.PaymentStatus has more states than "paid/failed" — PENDING, AUTHORIZED, TIMEOUT, DECLINED, REFUND, PARTIALLY_REFUNDED. Gateways that do pre-authorisation return AUTHORIZED first; your order should not be marked paid until PAID. Check isPaid() / isPending() helpers rather than comparing raw integers.PaymentGateway implementation → event listeners → route overwrite. Avoid editing vendor files; the package is small and a fork will diverge quickly.vanilo/framework and the Vanilo app skeleton, not in direct composer requires. Judge it by the quality of the Vanilo docs and the sibling gateway packages, not by its own star count.How can I help you explore Laravel packages today?