Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Multi Payment Laravel Package

omalizadeh/laravel-multi-payment

Laravel Multi Payment offers a unified, driver-based API for integrating multiple payment gateways into Laravel applications. Configure providers, initiate payments, verify callbacks, and manage transactions through one consistent interface.

View on GitHub
Deep Wiki
Context7

Getting Started

Installation

composer require omalizadeh/laravel-multi-payment
php artisan vendor:publish --tag=payment-config

The published config/payment.php is the single source of truth: it defines the default gateway, per‑gateway credentials (merchant ID, terminal ID, API keys, sandbox flags), and the list of enabled drivers. Read this file first — it tells you exactly which gateways exist in the version you installed.

First look at the source tree

  • src/Payment/ — the orchestrator that holds amount, order ID, callback URL, gateway selection.
  • src/Gateways/ — one class per provider (Zarinpal, PayIR, IDPay, NextPay, Sadad, Saman, Parsian, Pasargad, Mellat, AsanPardakht, and now Paystar as of v3.1.0).
  • src/Contracts/ — the gateway contract; read this to understand what a driver must implement (purchase, verify, pay/redirect).
  • src/Exceptions/ — GatewayNotFoundException, PaymentFailedException, InvalidConfigurationException and friends. Catch these, not \Exception.

First use case — a single redirect checkout

use Omalizadeh\MultiPayment\Facades\Payment;

$payment = Payment::gateway('zarinpal')
    ->callbackUrl(route('payment.callback'))
    ->amount(50000)          // Rial, see Gotchas
    ->orderId($order->id)
    ->purchase();

return $payment->pay();       // returns a redirect View/Response to the gateway

Then a callback route:

Route::get('/payment/callback/{gateway}', function (string $gateway) {
    $transaction = Payment::gateway($gateway)->verify();

    if ($transaction->isSuccessful()) {
        // mark order paid, using $transaction->getReferenceId()
    }

    return redirect()->route('orders.show');
})->name('payment.callback');

Because the package is driver‑based, everything you learn on Zarinpal transfers verbatim to Paystar (or any other provider) — the only thing that changes is the string passed to gateway() and the credentials block in config.

Implementation Patterns

Config‑driven gateway selection (recommended default)

Never hardcode 'zarinpal' in controllers. Store the chosen gateway on the order or read it from config('payment.default'), then pass it through:

$gateway = $order->gateway ?? config('payment.default');
Payment::gateway($gateway)->callbackUrl(...)->purchase(...);

This makes the new Paystar driver a one‑line config change rather than a refactor.

One service class per concern, not per gateway

Wrap the package in a PaymentService that owns purchase + verify. Because the driver is resolved at runtime, you do not need a separate service per provider — resist the temptation to build ZarinpalService and PaystarService, that's the abstraction the package already gives you.

final class PaymentService
{
    public function start(Order $order): RedirectResponse
    {
        return Payment::gateway($order->gateway)
            ->callbackUrl(route('payment.callback', $order->gateway))
            ->amount($order->amount_rial)
            ->orderId((string) $order->id)
            ->purchase()
            ->pay();
    }

    public function finish(string $gateway): Transaction
    {
        return Payment::gateway($gateway)->verify();
    }
}

Persist the gateway on the record before redirecting

The callback URL is the only context the gateway gives you back. Encode the gateway in the callback route (/payment/callback/{gateway}) and store it on the order row. Belt and braces — if someone tampers with the segment you can reject it against the stored value.

Verification is the only source of truth

Never trust ?status=OK or the Authority/token query parameter alone. Only the result of verify() commits the order. Structure the callback as: verify → check isSuccessful() → idempotency guard (has this referenceId already been recorded?) → mark paid.

Multi‑gateway fallback / A‑B routing

For Iranian PSPs, downtime is real. A lightweight pattern:

foreach ([$primary, $fallback] as $gateway) {
    try {
        return $this->start(new PaymentContext($gateway));
    } catch (PaymentFailedException $e) {
        report($e);
    }
}

Keep fallback logic in your app layer, not in config — the package doesn't (and shouldn't) guess your business tolerance for switching PSPs mid‑order.

Adding a custom gateway

The contract‑based design is the extension point. Extend the abstract gateway, implement the required methods, register the driver key in config/payment.php, and it behaves identically to the bundled ones. This is also how you'd vendor‑fork Paystar if you need a parameter the driver doesn't expose yet.

Testing

Don't hit sandbox endpoints in a test suite. Either (a) enable each gateway's sandbox flag in config/payment.php under phpunit.xml env overrides, or (b) mock the orchestrator: bind a fake into the container and assert on the fluent calls (amount, orderId, callbackUrl). Assert your domain outcomes — order marked paid, idempotency respected — rather than the package's internals.

Gotchas and Tips

Amounts are in Rial, not Toman. This is the single most common production bug with every Iranian gateway package. If your UI shows Toman, multiply by 10 once, at the boundary, and name the column amount_rial so nobody re‑converts. Some drivers convert internally and some don't — check the driver before assuming.

Config caching will bite you. php artisan config:cache in deploy is expected, but changing credentials in .env afterwards does nothing until you recache. If a gateway suddenly "rejects the merchant", check the cached config first.

verify() is not idempotent by default. Gateways will re‑deliver callbacks (user hits refresh, browser retries). Wrap verification in a DB transaction with a unique index on the reference/transaction ID, or you'll double‑fulfil orders. This is app responsibility, not package responsibility.

Callback URL must exactly match the registered one. Trailing slashes, http vs https, and www vs apex can all cause a rejection at the gateway before your code ever runs. Generate it with route() and pin APP_URL in production.

Order ID types. Some drivers cast order IDs to strings and some gateways reject non‑numeric or over‑long values (Mellat in particular has tight limits). Pass (string) $order->id and keep IDs short/numeric.

Exceptions are thrown, not returned. A failed verify() throws PaymentFailedException; a mistyped gateway key throws GatewayNotFoundException (which is a config error, not a user error — log it at error, return 500). Return a friendly "payment failed" page only for the former.

Sandbox flags live in three places. Per‑gateway config, sometimes an env var, and sometimes an argument on purchase(). Grep the specific driver for sandbox before debugging — the source is authoritative, the README usually lags.

The Paystar addition (v3.1.0) is purely additive. No signature changes, no config keys removed, no deprecations — upgrading from v3.0.0 is a composer update with no migration. But note that any config file you've published previously won't contain the new paystar block; re‑publish the config (vendor:publish --tag=payment-config --force) or hand‑add the stanza, otherwise Payment::gateway('paystar') will fail on missing credentials.

Driver parity is not guaranteed. Each gateway has its own quirks (reference ID field names, supported currencies, refund support). The uniform interface is a lowest common denominator: getReferenceId() may be null on some drivers, refunds aren't universal, and error messages are whatever the PSP returned in Persian. Normalise them in your service layer if you surface them to users.

Version 3 is Laravel 9+ / PHP 8.1+. If you're on an older codebase, v3 won't install — and the v2→v3 jump did include breaking API changes, so pinning is wise if you maintain multiple apps on different Laravel majors.

Logging. Set a dedicated channel for payment traffic; the package itself logs sparingly. Log the request payload (minus credentials), the raw callback query, and the verify response — PSP disputes are unwinnable without the raw strings.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode
jjsquady/round-robin