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 Ecommerce Payment Laravel Package

zarulizham/laravel-ecommerce-payment

A Laravel package for adding ecommerce payment support to your application, helping you integrate payment flows and streamline checkout handling. Suitable for projects that need a simple, framework-native starting point for accepting payments online.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and publish.

composer require zarulizham/laravel-ecommerce-payment
php artisan vendor:publish --provider="Zarulizham\EcommercePayment\EcommercePaymentServiceProvider"

Publish tags may be split — try --tag=config and --tag=migrations separately if the combined publish dumps more than you want. Then run php artisan migrate for the transaction table.

Where to look first. Do not start with the README — start with config/ecommerce-payment.php after publishing it. That file is the contract: it tells you which gateways the abstraction actually implements (despite the package name, the description claims a multi-gateway abstraction while the title says AmBank eCommerce), what the credential keys are, and which URL/endpoint knobs exist for sandbox vs. production. Read it before writing a single line of application code, because the shape of that config determines whether you're integrating a generic gateway bus or a single hard-coded AmBank client.

Second stop: vendor/zarulizham/laravel-ecommerce-payment/src/. This is a small package (one star, one maintainer) with empty release notes on 2.0.7. Budget 30–60 minutes to read the source directly. You specifically want to confirm:

  • The facade class name and its bound contract in the service provider
  • The signature of the "initiate payment" method (argument order, whether it takes an amount in minor units)
  • How the callback/webhook payload is parsed and whether it is signature-verified
  • Whether transaction status is read from your DB or re-queried from the gateway

First use case: a single hosted-redirect payment. The lowest-risk vertical slice is:

  1. Customer submits an order → your controller creates a local Order and calls the package to initiate a payment.
  2. Package returns a redirect URL / payload → you redirect the customer to AmBank's hosted page.
  3. AmBank redirects back to your return_url and calls your callback_url server-to-server.
  4. You verify the transaction through the package, mark the order paid, and redirect the customer to a receipt.
use Zarulizham\EcommercePayment\Facades\EcommercePayment;

$payment = EcommercePayment::gateway('ambank')->create([
    'order_id'     => $order->reference,      // your idempotency anchor
    'amount'       => (int) round($order->total * 100), // verify minor-units in source
    'currency'     => 'MYR',
    'return_url'   => route('checkout.return'),
    'callback_url' => route('webhooks.ambank'),
    'description'  => "Order {$order->reference}",
]);

return redirect($payment->redirectUrl());

Treat every key name above as to be confirmed against the source — for a package with no changelog and a single adopter, the README and the code will drift, and the code wins.

Implementation Patterns

1. Put the package behind your own adapter from day one. This is the single most important pattern for a 1-star dependency on a payment path. Define one interface and one implementation; nothing else in your app ever references the vendor namespace.

interface PaymentGateway
{
    public function start(Order $order): PaymentRedirect;
    public function verify(string $gatewayRef): PaymentResult;
    public function handleCallback(Request $request): PaymentResult;
}

Benefits that pay for themselves quickly: you can swap to a direct AmBank integration or a different provider by writing one class; you can fake the whole thing in tests without HTTP mocking; and when 2.0.8 ships with no notes, the blast radius of the upgrade is the adapter file only.

2. Split the return URL and the callback URL into two different code paths. They are not the same event and conflating them is the classic payment bug.

  • return_url — the browser coming back. This is untrusted user input. It can be replayed, spoofed, or never arrive at all (customer closes the tab).
  • callback_url — the server-to-server webhook. This is the source of truth.

Make the return handler read-only: look up the order, show a "pending / paid / failed" page based on local state, and never mark anything paid from the redirect alone. The callback handler does the state transition, and it must call verify() against the gateway rather than trusting the posted fields.

public function callback(Request $request)
{
    $result = $this->gateway->handleCallback($request); // hits gateway or verifies signature

    DB::transaction(function () use ($result) {
        $order = Order::where('reference', $result->orderRef)->lockForUpdate()->firstOrFail();
        if ($order->status === 'paid') {
            return; // idempotent: duplicate callback, do nothing
        }
        $order->update(['status' => 'paid', 'paid_at' => now(), 'gateway_ref' => $result->ref]);
        PaidOrder::dispatch($order); // fulfilment, email, stock decrement
    });

    return response('OK', 200);
}

3. Assume duplicate, out-of-order, and delayed callbacks. Gateways retry. Return 200 fast and do the real work in a queued job if fulfilment is slow (email, inventory, third-party provisioning). Guard with lockForUpdate() plus a status check as above, or a gateway_events table with a unique index on (gateway, gateway_ref, event_type).

4. Model status as an explicit local state machine, not a boolean. pending → paid → refunded with failed / expired as terminal-ish branches. Store the raw gateway payload in a json column alongside it. When a customer disputes a charge, that payload is the only evidence you'll have.

5. Reconcile on a schedule. Callbacks get lost — firewalls, deploy windows, misconfigured URLs. A nightly command that finds orders stuck in pending past N minutes and calls verify() closes the gap and is worth writing before go-live, not after the first incident.

Order::where('status', 'pending')
    ->where('created_at', '<', now()->subMinutes(30))
    ->chunkById(100, fn ($orders) => $orders->each(
        fn ($o) => VerifyPaymentJob::dispatch($o)
    ));

6. Environment switching. Keep sandbox credentials in .env and confirm in the config file whether the gateway base URL is selected by an env/sandbox key or by swapping the whole credential block. Never let production and sandbox credentials coexist in a way the code can silently pick the wrong one — assert the environment at boot in a test.

7. Testing. Bind a fake PaymentGateway implementation in the container for feature tests, and keep a small number of HTTP-level tests that replay captured sandbox callback payloads through handleCallback(). That's where signature verification bugs hide.

Gotchas and Tips

The empty 2.0.7 release notes are the dominant risk. A patch bump on a payment package with no described changes means you cannot answer "what did we just take on?" Before merging any upgrade, git diff the previous tag against the new one in the repository. Do not interpret the absence of notes as the absence of changes. Pin the exact version in composer.json ("2.0.7", not "^2.0") and let a human review each bump.

Package name vs. description mismatch. The title says AmBank eCommerce payment gateway; the description advertises a generic gateway abstraction with configurable providers. Those are different products. Read the source and confirm how many gateways actually exist before you architect around a single shared abstraction for multiple providers. If only AmBank is implemented, don't build a multi-gateway UI in anticipation.

Signature verification is the thing to check hardest. If handleCallback() does not verify a signing key/HMAC, you must add verification in your own route before doing anything with the payload — otherwise anyone who guesses your callback URL can mark orders paid. Check whether the package exposes a verification method; if it doesn't, put the callback route behind a shared secret path plus IP allowlist and verify the transaction server-side with verify() before any state change.

Never log card data. The package handles redirect-style payments, so full PANs shouldn't transit your app — but the raw request/response payloads may still contain sensitive fields. When you persist the gateway payload for audit, run it through a redaction list first. Check whether Laravel's TrimStrings/ConvertEmptyStringsToNull middleware is corrupting the callback payload: exclude your webhook route from VerifyCsrfToken and consider excluding it from TrimStrings, since a trailing/leading space in a signed field will break HMAC checks in confusing ways.

Amount representation and rounding. Verify in source whether amounts are integers in minor units (sen) or floats. Floats on money cause off-by-one failures that only appear at certain price points. Convert once, at the boundary, with round() and an explicit cast.

php artisan config:cache traps. Any config read at runtime with env() inside the package will return null once config is cached — a classic third-party package bug that only shows up in production deploy. Grep the source for env( outside of config/. If you find any, move your own values into config/ and pass them through.

Publishing migrations you may not want. If the package owns a payments table, decide early whether you'll use it or keep your own. Using both creates two sources of truth that drift. Prefer your own table and let the package's migration be a no-op, or commit to the package's models fully — don't mix.

Callback URL must be publicly reachable and stable. Not behind auth middleware, not behind a staging basic-auth prompt, not on a preview domain that rotates. Put it on a route that never changes across deploys. Local development needs a tunnel (ngrok/expose) — test the callback path early, because it's the part that's hardest to simulate by hand.

Timeouts and error handling for create(). If the gateway call times out, you don't know whether the payment was created. Don't retry blindly — retry with the same order_id and rely on the gateway's idempotency, or re-query by reference before retrying. Blind retries create double charges.

No published security policy and one star of adoption means you are the QA department. Log the package in your dependency register with version, owner, and review date. Set a re-evaluation trigger: the next minor (2.1), 90 days with no improvement in release notes, or any security advisory in a related package — whichever comes first. If the maintainer goes quiet and you're on a regulated path, the adapter pattern above is what makes exiting a one-file change instead of a refactor.

Extension points worth identifying. Before you write workarounds, check the service provider for bind/singleton calls — most small packages bind an interface you can rebind to your own implementation, letting you override behavior (logging, HTTP client, HTTP timeout) without forking. If no such seam exists, that's a strong argument for keeping your adapter thin and treating the package as a black box you could replace in an afternoon.

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.
steadrun/laravel-monitor
labyrinthe/connect-laravel
elmogy/layan
drewdan/sentinel-client
dskripchenko/laravel-llm
fatryst/nuonuo
stevenay/myanfont
gmostafa/php-graphql-client
xeviant/paystack
snowsoft/faker
meditationsteps/blogetc
animalyoung/laravel-influxdb
onix-solutions/sms-telera
lonely-lights/prosetta
oxygen/data
oxygen/core
jengo/base
xiloryx/log-collector
a21ns1g4ts/filament-stripe
la-php/laravel-admin