asciisd/cashier-core
Core billing toolkit for Laravel: subscription management, invoicing, and payment gateway abstractions. It packages reusable Cashier foundations so you can add recurring payments to any Laravel app with minimal boilerplate and consistent APIs.
Install the package and publish its config:
composer require asciisd/cashier-core
php artisan vendor:publish --tag=cashier-core-config
Add a xoala connection to config/cashier-core.php (or your config/cashier.php connections array) using the shape from the release notes — driver, base_url, member_id, secure_key, and the mandatory totype. Keep every value in .env; the sandbox base URL is https://secure-checkout-sandbox.xoala.com.
Where to look first, in order:
xoala block in config/cashier-core.php — it documents the optional keys the release notes only allude to..claude/skills/xoala/ — the vendored gateway contract. This is your only reliable reference; the vendor's public doc site renders samples from JavaScript and blanks the brand name, so bookmarking the URL is not a substitute.cashier-core-views publish tag, if you need to brand the interstitial.The canonical first use case is a deposit: call charge() on the connection, get back a signed, expiring link, and redirect the customer to it. The package serves a page that renders and auto-submits Xoala's Standard Checkout form, so the customer lands on the hosted payment page without you ever touching the form markup.
Resolve the connection, don't hardcode the driver. Go through the Cashier connection resolver rather than instantiating the driver, so multi-gateway apps can swap xoala for anything else at config level. Guard any capability-specific code with supports() — for Xoala that means charge and webhook are true, and refund, capture, authorize, void will throw. Branch on capabilities, not on driver name.
Treat charge() as local, not remote. This is the key mental shift with Xoala: charge() performs no HTTP. It mints a merchantTransactionId, persists the transaction, computes the checksum, and hands you the signed link. That means charge() cannot fail from a network timeout — failures surface later, at the hosted page. Your error handling belongs around the redirect and the callback, not around the charge call.
Callback handling is webhook-only for state. Since there is no return-URL round trip you control, drive order status from the Xoala callback and treat any customer-facing "thank you" page as cosmetic. Verify against your own transaction row (look it up by merchantTransactionId) rather than trusting callback payload fields wholesale.
Brand the interstitial via the publish tag. Publish cashier-core-views and override the Blade view if the default page looks out of place. This is a real, supported extension point — the release explicitly added the tag for it.
Disable routes outside web contexts. The checkout page lives under the same cashier-core.routes.enabled flag and Cashier::ignoreRoutes() guard as the webhook routes. In queue workers, console commands, or a service that only originates payments, turn routes off and let the originating app handle rendering.
Pin the branch alias deliberately. dev-2.x now resolves to 2.5.x-dev. If any environment requires dev-2.x, run composer update and confirm you actually land on 2.5.x — the alias had been stale at 2.3.x since 2.4.0, so previously "working" installs were silently a minor behind.
totype is the trap. It is account-specific and participates in the request checksum, and the provider refuses to resolve the connection without it. A wrong totype does not fail at config validation — it fails opaquely at the hosted page, which is the worst place to debug. Verify it against Xoala's onboarding paperwork before your first sandbox run and assert it in a config test.
The checkout page has no web middleware on purpose. No session, no CSRF token, no auth. Do not add @csrf, do not expect auth()->user(), and do not assume flash messages survive. If you need context on that page, it has to come from the signed URL or the transaction row. If you publish and customize the view, resist the urge to "fix" the middleware group.
The signed middleware means the link expires and is tamper-evident. Do not cache the link, email it, log it in full, or store it as a durable "pay now" URL. Mint a fresh one per redirect. Links that leak into logs are also links that 403 confusingly when someone tries them later.
Rate limiting is 30/min/IP. Fine for humans, fatal for load tests and for anything behind an unconfigured proxy — if your load balancer isn't in TrustProxies, every request looks like one IP and you will throttle your whole customer base. Set trusted proxies before going live.
The checksum is recomputed at render time from the transaction row and never persisted. Two consequences: the transaction row must exist and be intact when the customer hits the link (don't prune pending transactions aggressively), and if you mutate relevant columns between charge() and render, the checksum will no longer match what the gateway expects. Treat the transaction row as append-only for the fields the driver reads.
Refunds, captures, voids throw. If your domain model assumes symmetric payment operations, Xoala will break it. Plan for manual reconciliation or an admin tool that talks to the Xoala dashboard, and encode that constraint in your supports() checks so the failure is a clean guard rather than a runtime exception deep in a job.
The vendor docs are not a debugging aid. The doc site is JS-rendered and blanks the brand name throughout, so copy-pasted samples lose meaning. When something fails at the hosted page, the vendored contract under .claude/skills/xoala/ is the only authoritative source — keep it in the repo and review it when you upgrade.
Upgrade path is clean. No breaking changes in 2.5.0; existing connections and webhook routes are untouched and the new route group only registers when routes are enabled. The only action item is the branch-alias fix, which affects dev-2.x consumers only.
How can I help you explore Laravel packages today?