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 Myanmar Payments Laravel Package

laranex/laravel-myanmar-payments

Laravel Myanmar Payments helps you add Myanmar payment gateways and wallets to Laravel apps with a clean, configurable API for payment requests, callbacks, and transaction handling—reducing boilerplate so you can ship local checkout flows faster.

View on GitHub
Deep Wiki
Context7

Getting Started

Install, pinned. Do not use a floating constraint on a payments dependency — this package is on 4.0.0-alpha.17 and the API is not stable between alphas.

composer require laranex/laravel-myanmar-payments:4.0.0-alpha.17 --with-all-dependencies

Your composer.json needs "minimum-stability": "dev" with "prefer-stable": true for the alpha to resolve, and the lockfile must be committed. Upgrades are explicit, reviewed events — never composer update on a Friday.

Publish the config. It is the real API surface of the package: driver keys, base URLs per environment, credential env-var names, callback path defaults.

php artisan vendor:publish --provider="Laranex\\LaravelMyanmarPayments\\LaravelMyanmarPaymentsServiceProvider"
# or --tag=myanmar-payments-config if the provider publishes multiple tags

Read in this order — the release notes contain nothing but a compare link, so the source is the documentation:

  1. README.md — install and the one happy-path example.
  2. config/myanmar-payments.php (name may differ) — which drivers are registered, and whether each is sandbox/production switchable.
  3. src/Drivers/ — the definitive list of what actually works. Compare against the marketing list (KBZ Pay, Wave Money, CB Pay, …); anything absent or throwing NotImplemented is a stub.
  4. src/ callback/verification handler — confirm signatures are genuinely verified, not just parsed.
  5. tests/ — captured gateway payloads are the best spec you will get for field names and casing.

Populate credentials, one block per gateway, and keep sandbox creds in .env.testing so a test run can never touch production:

MYANMAR_PAYMENTS_DEFAULT=kbz_pay
KBZ_PAY_MERCHANT_ID=
KBZ_PAY_APP_ID=
KBZ_PAY_SECRET_KEY=
KBZ_PAY_SANDBOX=true

First use case — one gateway, end to end, in sandbox. Resist multi-gateway until this works: initiate a payment from a controller, redirect the user to the returned payment URL, receive the callback on your route, verify the signature, transition the order to paid, and confirm the ledger matches the sandbox dashboard. Method names differ per alpha; treat this as illustrative and confirm against the driver class:

$payment = MyanmarPayments::driver('kbz_pay')->createPayment([
    'order_id'     => $order->reference,      // your idempotency anchor
    'amount'       => $order->total,          // integer, smallest unit the gateway expects
    'currency'     => 'MMK',
    'callback_url' => route('payments.callback', $order),
]);

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

Do the spike on a throwaway branch or in artisan tinker before any UI exists.

Implementation Patterns

Wrap it in your own adapter. The single highest-value pattern. One interface, one implementation per PSP, the package as the engine inside:

interface PaymentGateway
{
    public function initiate(Order $order): PaymentIntent;   // redirect URL + our reference
    public function verifyCallback(Request $request): CallbackResult; // normalized, validated
    public function refund(Order $order, int $amount, string $reason): RefundResult;
    public function status(string $gatewayRef): PaymentStatus;
}

This gives you an escape hatch when an alpha bump renames something, one place to normalize result objects into your domain events, and the ability to run a second gateway on a direct integration while you evaluate.

Inject the manager, don't call the facade statically. Resolve the manager (or your adapter) from the container in the constructor. Static facade calls in controllers are untestable and hide the dependency.

Model payment state explicitly. A payment_attempts table with (order_id, gateway, idempotency_key) uniquely indexed, a gateway_transaction_id column, and a status enum (pending → paid → refunded, plus failed and expired). Every row is a replayable record of what you sent and what came back. Never derive payment state from the order's paid_at alone.

Send your own reference as the gateway's order id. Gateway-side ids change; yours don't. Combined with the unique index, this is your idempotency primitive — the package does not supply one.

Webhook handling, in this exact order:

public function callback(Request $request, Order $order)
{
    $raw = $request->getContent();                 // before anything decodes/re-encodes

    $result = $this->gateway->verifyCallback($request); // throws on bad signature
    abort_unless($result->valid, 403);

    ProcessGatewayCallback::dispatch($order, $result->payload)->onQueue('payments');
    return response('OK', 200);                    // fast, before any work
}

Return 200 quickly and do the domain work on a queue. Gateways retry on non-2xx, and a slow handler means duplicate deliveries.

Guard against amount tampering. Compare the callback's amount and currency against the stored attempt before marking paid. Never trust the payload's amount.

Queue outbound initiation too. PSP calls are I/O with per-gateway latency; a hung socket inside the checkout request adds seconds to the user's experience. Set explicit connect and request timeouts on the underlying HTTP client.

Add a daily reconciliation command. Call status() / pull the settlement report for everything stuck in pending past a threshold and anything the gateway settled that your ledger doesn't show. This is the only defense against silently lost callbacks, and it is your job — the package won't do it.

Re-emit your own events. Listen to the package's events if convenient, but dispatch PaymentSucceeded / PaymentFailed from your adapter so the rest of the app never depends on the package's namespace.

Routing across gateways: choose a driver from currency, user preference, or a fallback chain (kbz_pay → wave_money) with the fallback recorded on the attempt row.

Testing: if the package builds on Laravel's HTTP client, Http::fake() gives you fast, deterministic tests. If it ships its own Guzzle client, Http::fake() silently won't intercept — check before you build a suite around it. Capture real sandbox payloads into fixtures and feature-test the callback route with them, including the bad-signature case.

Gotchas and Tips

Pin exactly, and read the diff. Alpha bumps have no usable release notes; git diff v4.0.0-alpha.16...v4.0.0-alpha.17 -- src/ config/ is your changelog. Treat every bump as potentially breaking, run sandbox regression, and stage before production.

Diff the config file after every upgrade. vendor:publish --force quietly overwrites; renamed config keys are the classic alpha-upgrade failure and they fail silently — a driver falls back to defaults and points at the wrong base URL.

Verify that signature verification exists per driver. The README advertises it; confirm each driver implements it, and confirm it doesn't just parse. If a driver is missing it, reject its callbacks in your own middleware before they reach the package handler. Also check for timing-safe comparison rather than === on the HMAC.

Raw body, not $request->all(). Re-encoding JSON before hashing breaks the HMAC. Related: global middleware that trims strings or converts empty strings to null will mutate the payload — scope those out of the callback route.

CSRF. Exclude the callback path (Laravel 11: $middleware->validateCsrfTokens(except: ['payments/*/callback']); older: VerifyCsrfToken::$except). Also restrict to POST, and throttle — an unsigned-callback probe wave shows up as a 403/429 storm worth alerting on.

Config cache. php artisan config:cache on deploy, config:clear in the rollback hook. Credential rotation without a rebuild leaves the old secret live — the classic "we rotated the key and callbacks kept failing" incident.

Octane / long-lived workers. Never use config()->set() for per-tenant credentials; the mutated config leaks into the next request in the same worker. Use a scoped container binding or construct a fresh driver with explicit config. The same applies if the manager caches driver instances — a cached KBZ driver will keep the first tenant's credentials forever.

Money units. Confirm whether each gateway wants integer kyat or minor units (cents). Mix by one decimal place and you'll be off by 100×, which usually surfaces as a gateway-side validation error — but not always. Never use floats; store integers and format at the edge.

Timezones and expiry. Gateways operate on Asia/Yangon; store UTC and convert at the boundary. Payment links often expire in 5–15 minutes — don't mint them at order creation and redirect the user an hour later.

Duplicate and out-of-order callbacks. Gateways retry and can deliver a failed after a paid. Handle transition legality (a terminal state only moves forward) and key the handler on gateway_transaction_id.

Exception surface is uneven. Catch both the package's own exceptions and leaked Illuminate\Http\Client\RequestException at the adapter boundary, normalize, and log the gateway response body with secrets and signatures redacted. Don't log full callback payloads at info — they carry customer PII.

Sandbox leakage. Add an explicit test asserting that APP_ENV=testing resolves to the sandbox base URL. One wrong config key and your test suite is creating real payment requests.

Extension points. For an unsupported PSP, look for a manager extend('name', fn () => new YourDriver(...)) hook and implement the driver interface yourself rather than forking. Listen to events rather than editing vendor code. If you do need patches while upstream is quiet, keep a fork behind a repositories: [{type: vcs}] entry — don't hand-edit vendor/.

Failure detection beats retries. The signals worth instrumenting: unmatched settlements, webhook 4xx rate (a broken signature check reads as a 401 wall), pending-age threshold per attempt, and PSP timeout rate on initiation. Each maps to a distinct incident, and retrying won't fix any of them.

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.
adachsoft/changelog-linter
adachsoft/collection
payerurl/telegrambot-crypto-payment-instant-payout
dskripchenko/laravel-hybrid-search
riorizkyrainey/lara-ced
mehedimi/static-vel
zharp/zharp
nonsapiens/bigquery-model-sync
lgu-bayambang-ict-office/lgu-laravel-development-standards
andydefer/laravel-support
se7enxweb/flysystem
lampwick/laravel
jeslage/statamic-emojis
micahdshackelford/rmap-laravel
draftsmaninc/draftsman
square-bit/laravel-volition
himito/laravel-mailman
htinlynn/genius-helper
maco-studios/hephaestus
elmogy/larafast