hakhant / myanmar-payments
Laravel Myanmar payment gateway package for KBZPay, WaveMoney, 2C2P, AYA Pay, and MMQR.
Requires
- php: ^8.2
- firebase/php-jwt: ^7.0
- illuminate/contracts: ^10.0|^11.0|^12.0
- illuminate/http: ^10.0|^11.0|^12.0
- illuminate/support: ^10.0|^11.0|^12.0
- phpseclib/phpseclib: ^3.0
Requires (Dev)
- laravel/pint: ^1.17
- orchestra/testbench: ^8.0|^9.0|^10.0
- pestphp/pest: ^2.0|^3.0
- pestphp/pest-plugin-laravel: ^2.0|^3.0
- phpstan/phpstan: ^2.1
- rector/rector: ^2.4
README
Laravel Myanmar payment gateway package for KBZPay, WaveMoney (Wave Pay), 2C2P, and AYA Pay with MMQR support. It provides a single gateway-selection flow, typed request and response DTOs, refunds, callbacks, and Laravel container integration.
Supported Providers
Myanmar payment gateway support for Laravel:
- KBZPay
- WaveMoney (Wave Pay)
- 2C2P
- AYA Pay
- MMQR support for supported providers
Myanmar payments, Myanmar payment gateway, KBZPay Laravel, Wave Pay Laravel, WaveMoney payment gateway, 2C2P Laravel, MMQR Laravel.
Features
- Built for Myanmar payment integrations in Laravel apps
- Supports KBZPay, WaveMoney (Wave Pay), 2C2P, and AYA Pay
- Supports MMQR flows for supported providers
- Strict typing with
declare(strict_types=1); - PSR-4 autoloading
- Provider-driven architecture with
PaymentManager,GatewayContract, and provider-specific gateway adapters - Typed DTOs for payments, refunds, MMQR, and callbacks
- Provider capability contracts for refund, callback verification, and MMQR support
- Application use cases for payment creation, MMQR creation, refunds, status queries, and callback verification
- Provider adapter for 2C2P redirect checkout and refund maintenance
- Provider adapter for AYA Pay push payment, QR payment, status query, and refund
- Provider adapter for WaveMoney payment creation, MMQR creation, and callback verification
- Provider adapter for KBZPay payment, refund, callback verification, and MMQR
- Enum-based or string-based provider selection through
ProviderandPaymentManager::provider() - Laravel service provider and facade integration
- Tooling: Pint, Rector, PHPStan, Pest
Requirements
- PHP 8.2+
- Laravel 10/11/12
Installation
composer require hakhant/myanmar-payments
Publish Configuration
php artisan vendor:publish --tag=myanmar-payments-config
Environment
MM_PAYMENT_PROVIDER=kbzpay MM_PAYMENT_CALLBACK_TOLERANCE=300 TWOC2P_MERCHANT_ID= TWOC2P_SECRET_KEY= TWOC2P_MERCHANT_PRIVATE_KEY= TWOC2P_PUBLIC_KEY= TWOC2P_KEY_ID= TWOC2P_LOCALE=en TWOC2P_PAYMENT_DESCRIPTION=Payment TWOC2P_MAINTENANCE_VERSION=4.3 TWOC2P_REFUND_NOTIFY_URL= TWOC2P_REFUND_IDEMPOTENCY_ID= TWOC2P_PAYMENT_TOKEN_URL=https://sandbox-pgw.2c2p.com/payment/4.3/paymentToken TWOC2P_TRANSACTION_STATUS_URL=https://sandbox-pgw.2c2p.com/payment/4.3/transactionStatus TWOC2P_REFUND_URL=https://demo2.2c2p.com/2C2PFrontend/PaymentAction/2.0/action AYA_BASIC_TOKEN= AYA_PHONE= AYA_PASSWORD= AYA_SERVICE_CODE= AYA_TIME_LIMIT= AYA_LOGIN_URL=https://opensandbox.ayainnovation.com/merchant/1.0.0/thirdparty/merchant/login AYA_PUSH_PAYMENT_URL=https://opensandbox.ayainnovation.com/merchant/1.0.0/thirdparty/merchant/requestPushPayment AYA_PUSH_PAYMENT_V2_URL=https://opensandbox.ayainnovation.com/merchant/1.0.0/thirdparty/merchant/v2/requestPushPayment AYA_QUERY_PAYMENT_URL=https://opensandbox.ayainnovation.com/merchant/1.0.0/thirdparty/merchant/checkRequestPayment AYA_QR_PAYMENT_URL=https://opensandbox.ayainnovation.com/merchant/1.0.0/thirdparty/merchant/requestQRPayment AYA_REFUND_PAYMENT_URL=https://opensandbox.ayainnovation.com/merchant/1.0.0/thirdparty/merchant/refundPayment WAVEMONEY_MERCHANT_ID= WAVEMONEY_SECRET_KEY= WAVEMONEY_MERCHANT_NAME= WAVEMONEY_PAYMENT_DESCRIPTION=Payment WAVEMONEY_TTL_SECONDS=600 WAVEMONEY_PAYMENT_URL=https://testpayments.wavemoney.io:8107/payment WAVEMONEY_AUTHENTICATE_URL=https://testpayments.wavemoney.io/authenticate KBZPAY_MERCH_CODE= KBZPAY_MERCHANT_ID= KBZPAY_APP_ID= KBZPAY_SECRET= KBZPAY_PUBLIC_KEY= KBZPAY_CLIENT_CERTIFICATE_PATH= KBZPAY_CLIENT_CERTIFICATE_KEY_PATH= KBZPAY_CLIENT_CERTIFICATE_KEY_PASSPHRASE= KBZPAY_NOTIFY_URL=https://merchant.example.com/payments/kbzpay/callback KBZPAY_TRADE_TYPE=APP # KBZ endpoints (prod defaults) KBZPAY_PRECREATE_URL=https://api.kbzpay.com/payment/gateway/precreate KBZPAY_QUERYORDER_URL=https://api.kbzpay.com/payment/gateway/queryorder KBZPAY_REFUND_URL=https://api.kbzpay.com:8008/payment/gateway/refund KBZPAY_MMQR_URL=https://api.kbzpay.com/payment/gateway/precreate # UAT examples from KBZ docs: # KBZPAY_PRECREATE_URL=http://api-uat.kbzpay.com/payment/gateway/uat/precreate # KBZPAY_QUERYORDER_URL=http://api-uat.kbzpay.com/payment/gateway/uat/queryorder # KBZPAY_REFUND_URL=https://api-uat.kbzpay.com:18008/payment/gateway/uat/refund # KBZPAY_MMQR_URL=http://api-uat.kbzpay.com/payment/gateway/uat/precreate
Usage
Package Flow
- Configure provider credentials in
config/myanmar-payments.php - Resolve a gateway through
PaymentManageror theMyanmarPaymentsfacade, or use an application use case - Use the high-level manager/wrapper methods for most integrations:
createPayment(),queryStatus(),createMmqr(),refund(),verifyCallback() - Use capability helpers like
supportsMmqr()when your UI or flow depends on provider features - Drop down to
provider()only when you need direct access to a provider gateway - Handle typed DTO responses instead of raw provider payloads
Provider Capability Matrix
| Provider | Create Payment | Query Status | Refund | Verify Callback | MMQR |
|---|---|---|---|---|---|
| KBZPay | Yes | Yes | Yes | Yes | Yes |
| AYA Pay | Yes | Yes | Yes | No | Yes |
| WaveMoney | Yes | No | No | Yes | Yes |
| 2C2P | Yes | Yes | Yes | Yes | No |
Provider Selection
PaymentManager::provider() and MyanmarPayments::provider() accept either a provider string or the Provider enum.
use Hakhant\Payments\Application\PaymentManager; use Hakhant\Payments\Domain\Enums\Provider; public function checkout(PaymentManager $payments) { $gateway = $payments->provider(Provider::KBZPAY); // String values still work too: // $gateway = $payments->provider('kbzpay'); }
Recommended Integration Style
For most applications, prefer the higher-level PaymentManager methods instead of resolving a gateway manually.
use Hakhant\Payments\Application\PaymentManager; use Hakhant\Payments\Domain\DTO\PaymentRequest; use Hakhant\Payments\Domain\Enums\Provider; public function checkout(PaymentManager $payments) { return $payments->createPayment( new PaymentRequest( merchantReference: 'INV-1001', amount: 10000, currency: 'MMK', callbackUrl: 'https://example.com/payments/callback', redirectUrl: 'https://example.com/payments/return', ), Provider::KBZPAY, ); }
Use capability helpers when you need conditional behavior by provider:
if ($payments->supportsMmqr(Provider::AYA)) { // Show MMQR option in the UI }
use Hakhant\Payments\Application\PaymentManager; use Hakhant\Payments\Domain\DTO\PaymentRequest; use Hakhant\Payments\Domain\Enums\Provider; public function checkout(PaymentManager $payments) { $response = $payments->provider(Provider::TWOC2P)->createPayment( new PaymentRequest( merchantReference: 'INV-1001', amount: 10000, currency: 'MMK', callbackUrl: 'https://example.com/payments/callback', redirectUrl: 'https://example.com/payments/return', metadata: ['description' => 'Order INV-1001'] ) ); return redirect()->away((string) $response->paymentUrl); }
Query Payment Status
use Hakhant\Payments\Application\PaymentManager; use Hakhant\Payments\Domain\Enums\Provider; public function status(string $transactionId, PaymentManager $payments): array { $response = $payments->queryStatus($transactionId, Provider::TWOC2P); return [ 'transaction_id' => $response->transactionId, 'status' => $response->status->value, 'provider' => $response->provider, ]; }
For 2C2P, transactionId is the returned payment token because the transaction-status endpoint queries by payment token.
Refund
Provider-specific refund metadata:
AYA: passreference_numberKBZPay: optionally passrefund_request_noorrefundRequestNoto control the provider refund request identifier; otherwise the package uses<transactionId>-refund2C2P: refund-specific optional fields are configured through package config such asnotify_urlandidempotency_id
use Hakhant\Payments\Application\PaymentManager; use Hakhant\Payments\Domain\DTO\RefundRequest; use Hakhant\Payments\Domain\Enums\Provider; public function refund(string $transactionId, PaymentManager $payments): array { $response = $payments->refund(new RefundRequest( transactionId: $transactionId, amount: 10000, reason: 'Customer requested cancellation', metadata: [ 'refund_request_no' => 'REFUND-REQ-1001', ], ), Provider::KBZPAY); return [ 'refund_id' => $response->refundId, 'status' => $response->status->value, ]; }
Verify Callback Signature
Prefer verifying callbacks through PaymentManager, the facade, or the VerifyCallback use case instead of calling a gateway's verifyCallback() method directly. The shared entrypoints apply package-level callback protections before provider-specific signature verification:
- timestamp tolerance validation when
CallbackPayload::$timestampis provided - idempotency locking to reject duplicate callback deliveries within the configured tolerance window
Configure the tolerance with MM_PAYMENT_CALLBACK_TOLERANCE.
use Hakhant\Payments\Application\PaymentManager; use Hakhant\Payments\Domain\DTO\CallbackPayload; use Hakhant\Payments\Domain\Enums\Provider; use Illuminate\Http\Request; public function webhook(Request $request, PaymentManager $payments) { $payload = new CallbackPayload( payload: ['payload' => (string) $request->input('payload', '')], signature: '', timestamp: $request->integer('timestamp'), ); $valid = $payments->verifyCallback($payload, Provider::TWOC2P); abort_unless($valid, 401, 'Invalid signature'); return response()->json(['ok' => true]); }
2C2P Notes
- The implemented 2C2P provider supports hosted redirect checkout plus refund maintenance requests.
- Use a sufficiently long HS256 secret key.
firebase/php-jwtrejects short keys. createPayment()requests a payment token and returns the hostedwebPaymentUrl.queryStatus()uses the transaction-status endpoint and expects the payment token returned bycreatePayment().- Callback verification decodes and verifies the signed JWT payload returned by 2C2P.
refund()uses the payment-maintenance endpoint with XML wrapped in JWE/JWS using your merchant private key and the 2C2P public key.- Refund support requires PEM-formatted
TWOC2P_MERCHANT_PRIVATE_KEYandTWOC2P_PUBLIC_KEYvalues from the 2C2P key-exchange setup. TWOC2P_KEY_IDis optional and can be set when your 2C2P account expects akidheader in the signed JWS.- Published config now prefers snake_case refund keys such as
notify_urlandidempotency_id. - Legacy camelCase config keys such as
notifyURLandidempotencyIDare still accepted at runtime for backward compatibility. - Asynchronous refund completion callbacks and refund-status inquiry are not wrapped yet. The current package support covers refund initiation and mapping the immediate maintenance response.
WaveMoney Notes
createPayment()posts the documented form payload to WaveMoney/paymentand returns a redirectpaymentUrlusing/authenticate?transaction_id=....createMmqr()is supported through the same WaveMoney/paymentrequest flow and returnsMmqrResponse::qrCodeas the generated/authenticate?transaction_id=...URL.- Request hashing follows the WaveMoney formula:
time_to_live_in_seconds + merchant_id + order_id + amount + backend_result_url + merchant_reference_idusing HMAC SHA256. - Callback verification follows the WaveMoney callback formula and treats null values as the literal string
null, as required by docs. queryStatus()is intentionally unsupported for WaveMoney in this package because the provided docs define callback-driven status updates but no status inquiry endpoint.- Treat callback status
PAYMENT_CONFIRMEDas success and verify hash before updating payment state.
AYA Pay Notes
createPayment()uses AYA push-payment APIs and requiresmetadata['customer_phone'].- When
AYA_SERVICE_CODEormetadata['service_code']is set, the gateway uses AYA push payment v2; otherwise it uses the v1 push endpoint. queryStatus()calls AYAcheckRequestPaymentwithexternalTransactionId.createMmqr()calls AYArequestQRPaymentand mapsqrdatatoMmqrResponse::qrCode.refund()requiresRefundRequestmetadatareference_numberbecause AYA needs bothexternalTransactionIdandreferenceNumber.- AYA callback verification is not implemented yet because the provided swagger does not define a signed webhook/callback contract.
Webhook Security Notes
- Whitelist only the callback fields your provider signs. Avoid using full request payloads.
- Keep signature input format consistent with provider docs (raw body vs parsed fields).
- Reject callbacks with missing signature headers.
- Use
PaymentManager::verifyCallback(),MyanmarPayments::verifyCallback(), or theVerifyCallbackuse case so package-level timestamp and idempotency protections are applied consistently. - When the provider includes a callback timestamp, pass it into
CallbackPayload::$timestampso the package can reject stale callbacks usingMM_PAYMENT_CALLBACK_TOLERANCE. - The package rejects duplicate callback deliveries within the configured tolerance window before provider-specific verification runs.
- Return non-2xx for invalid signatures and do not mutate payment state.
- Log minimal callback metadata and redact sensitive values.
MMQR Usage
use Hakhant\Payments\Application\UseCases\CreateMmqr; use Hakhant\Payments\Domain\DTO\MmqrRequest; use Hakhant\Payments\Domain\Enums\Provider; public function createMmqr(CreateMmqr $createMmqr): array { $response = $createMmqr->handle(new MmqrRequest( merchantReference: 'MMQR-1001', amount: 10000, currency: 'MMK', notifyUrl: 'https://example.com/payments/mmqr/callback', metadata: ['invoice_no' => 'INV-1001'], ), Provider::AYA); return [ 'transaction_id' => $response->transactionId, 'status' => $response->status->value, 'qr_code' => $response->qrCode, 'qr_image' => $response->qrImage, ]; }
Supported MMQR providers in this package are KBZPay, AYA, and WaveMoney.
Notes by provider:
KBZPay: uses the standardkbz.payment.precreateendpoint for MMQR withtrade_type=PAY_BY_QRCODE; MMQRnotify_urlis sent as a top-level request field, and refunds can use client TLS certificates when KBZ requires them.AYA: uses the QR payment endpoint and maps returnedqrdataintoMmqrResponse::qrCode.WaveMoney: uses the same payment creation endpoint as normal checkout and returns the authenticate URL asqr_code.2C2P: MMQR is not implemented in this package because the current provider integration is focused on hosted checkout, status, refund maintenance, and callback verification.
For WaveMoney, qr_code is the Wave authenticate URL (.../authenticate?transaction_id=...) returned from payment initialization.
For AYA, qr_code is the returned qrdata string from requestQRPayment.
For KBZPay, qr_code is the raw EMVCo/MMQR payload returned by KBZ.
Facade Usage
use Hakhant\Payments\Domain\DTO\PaymentRequest; use Hakhant\Payments\Domain\Enums\Provider; use Hakhant\Payments\Facades\MyanmarPayments; $response = MyanmarPayments::createPayment( new PaymentRequest( merchantReference: 'INV-2001', amount: 25000, currency: 'MMK', callbackUrl: 'https://example.com/payments/callback', redirectUrl: 'https://example.com/payments/return' ), Provider::KBZPAY, );
Quality Commands
composer quality
composer format
composer analyse
composer test
composer refactor
Notes:
composer test,composer analyse, andcomposer refactorrun withXDEBUG_MODE=offby default for faster CLI runs and to avoid Herd/Xdebug restart noise.- Use
composer test:coveragewhen you want coverage output.
Documentation
For custom provider implementation details, see CONTRIBUTION.md.