laravel-maib-ecommerce maintained by fynduck
Laravel maib e-Commerce
A Laravel package for the maib (Moldova Agroindbank) e-Commerce payment gateway. It implements the maibmerchants e-Commerce API natively in Laravel — HTTP client, container bindings, a facade, config, events, signature verification and an optional payments table.
Requirements
- PHP 8.2+
- Laravel 10, 11, 12 or 13
Installation
composer require fynduck/laravel-maib-ecommerce
Publish the config:
php artisan vendor:publish --tag=maib-config
Add your project credentials (issued in the maibmerchants.md dashboard after activation) to .env:
MAIB_PROJECT_ID=your-project-id
MAIB_PROJECT_SECRET=your-project-secret
MAIB_SIGNATURE_KEY=your-signature-key
# Optional
MAIB_BASE_URL=https://api.maibmerchants.md/v1/
MAIB_CALLBACK_ROUTE=/maib/callback
MAIB_STORE_PAYMENTS=false
The access token is generated, cached and refreshed automatically — you never handle tokens.
Usage
All methods are available on the Maib facade and return a PaymentResult object.
Direct payment
Only amount, currency and clientIp are required; everything else is optional. You can
pass the Currency and Language enums instead of raw strings to avoid mistyping — the
client converts them automatically:
use Fynduck\MaibEcommerce\Facades\Maib;
use Fynduck\MaibEcommerce\Enums\Currency;
use Fynduck\MaibEcommerce\Enums\Language;
$payment = Maib::pay([
// required
'amount' => 10.25,
'currency' => Currency::MDL, // or 'MDL'
'clientIp' => $request->ip(),
'language' => Language::EN, // or 'en' (ro | ru | en)
// optional
'description' => 'Order #123', // max 124 chars
'clientName' => 'John Doe', // max 128 chars
'email' => 'john@example.com',
'phone' => '069123456', // max 40 chars
'orderId' => '123', // max 36 chars
'delivery' => 1.25, // delivery price
'items' => [
['id' => 'SKU-1', 'name' => 'Product 1', 'price' => 2.50, 'quantity' => 2],
['id' => 'SKU-2', 'name' => 'Product 2', 'price' => 4.00, 'quantity' => 1],
],
'callbackUrl' => route('maib.callback'),
'okUrl' => route('checkout.success'),
'failUrl' => route('checkout.failed'),
]);
// Persist $payment->payId, then redirect the customer to the maib checkout page:
return redirect()->away($payment->payUrl);
The same optional keys (description, clientName, email, phone, orderId,
delivery, items, okUrl, failUrl, callbackUrl) apply to hold(), saveRecurring()
and saveOneclick(). For the authoritative, always-current list of every parameter and its
constraints, see the maib e-Commerce API docs.
Two-step payment (hold + complete)
$hold = Maib::hold(['amount' => 10.25, 'currency' => 'MDL', 'clientIp' => $request->ip()]);
// ...later, capture all or part of the held amount:
$captured = Maib::complete(['payId' => $hold->payId, 'confirmAmount' => 10.25]);
Refund
$refund = Maib::refund(['payId' => $payId, 'refundAmount' => 10.25]);
Payment info
$info = Maib::payInfo($payId); // 36-char payId
$info->statusEnum(); // PaymentStatus enum
Recurring & one-click
// Register a card (redirects to checkout); maib returns a billerId on the callback.
$save = Maib::saveRecurring(['billerExpiry' => '1230', 'currency' => 'MDL', 'clientIp' => $ip]);
return redirect()->away($save->payUrl);
// Later, charge the stored card without customer interaction:
$charge = Maib::executeRecurring(['billerId' => $billerId, 'amount' => 6.25, 'currency' => 'MDL']);
// One-click works the same way:
$save = Maib::saveOneclick(['billerExpiry' => '1230', 'currency' => 'MDL', 'clientIp' => $ip]);
$charge = Maib::executeOneclick(['billerId' => $billerId, 'amount' => 6.25, 'currency' => 'MDL', 'clientIp' => $ip]);
// Remove a stored card:
Maib::deleteCard($billerId);
Callbacks (webhooks)
maib sends a signed notification to your callbackUrl after each payment. The package
registers a route (default POST /maib/callback, named maib.callback) guarded by a
signature-verification middleware. Requests with an invalid signature are rejected with a
403 before reaching your application.
On a valid callback the package dispatches:
Fynduck\MaibEcommerce\Events\PaymentCallbackReceived— every callbackFynduck\MaibEcommerce\Events\PaymentSucceeded— when status isOKFynduck\MaibEcommerce\Events\PaymentFailed— when status isFAILED
Each event carries a CallbackResult ($event->callback) with a typed PaymentResult
($event->callback->payment) and the raw verified result array.
Registering listeners
Map a listener to the event in your App\Providers\EventServiceProvider:
namespace App\Providers;
use App\Listeners\MarkOrderAsPaid;
use Fynduck\MaibEcommerce\Events\PaymentFailed;
use Fynduck\MaibEcommerce\Events\PaymentSucceeded;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
class EventServiceProvider extends ServiceProvider
{
protected $listen = [
PaymentSucceeded::class => [
MarkOrderAsPaid::class,
],
PaymentFailed::class => [
// App\Listeners\NotifyCustomerOfFailure::class,
],
];
}
On Laravel 11+ the
EventServiceProvideris no longer in the default skeleton. You can still create it (and register it inbootstrap/providers.php), or just bind the listener inAppServiceProvider::boot():Event::listen(PaymentSucceeded::class, MarkOrderAsPaid::class);
Run listeners on the queue (recommended)
The callback request comes from maib and is waiting on your HTTP response — and maib
may retry the notification if you are slow or error out. Do not do slow work (DB writes,
sending mail, calling other APIs) inline in the listener: that blocks the response and risks
duplicate callbacks. Instead make the listener queued by implementing ShouldQueue, so the
controller returns 200 immediately and the work runs on a worker:
namespace App\Listeners;
use Fynduck\MaibEcommerce\Events\PaymentSucceeded;
use Illuminate\Contracts\Queue\ShouldQueue;
class MarkOrderAsPaid implements ShouldQueue
{
public string $queue = 'payments';
public function handle(PaymentSucceeded $event): void
{
$payId = $event->callback->payId();
// mark your order as paid, notify the customer, etc.
}
}
Make sure a queue worker is running (php artisan queue:work). Because callbacks can be
re-delivered, keep listeners idempotent — key off payId so processing the same
notification twice is harmless.
Security: the signature is validated using your
MAIB_SIGNATURE_KEYwith the maib algorithm (recursive key sort → append key →:-join →base64(sha256())) and a timing-safe comparison. Never expose the signature key.
Optional payment persistence
To record every callback in a maib_payments table:
php artisan vendor:publish --tag=maib-migrations
php artisan migrate
Then set MAIB_STORE_PAYMENTS=true. Incoming callbacks are upserted into the
Fynduck\MaibEcommerce\Models\MaibPayment model (keyed by pay_id). The core client works
without this table — persistence is entirely opt-in.
Testing
composer test # Pest
composer lint # Pint (dry run)
composer analyse # PHPStan / Larastan
License
MIT