Skip to content

Marketplace multi-vendeurs ​

Une plateforme où plusieurs vendeurs (par exemple des commerçants de Freetown, Bo et Kenema) encaissent via vous, avec une commission plateforme prélevée à chaque vente.

Architecture ​

[Client] paie ──> [Plateforme = vous] ──split──> [Vendeur A : 80 %]
                                              └─> [Vendeur B : 15 %]
                                              └─> [Plateforme : 5 % de commission]

Chaque vendeur est un marchand SOAMTAR à part entière, onboardé séparément (KYC propre). Vous, plateforme, êtes le marchand « facturant » : vous encaissez la transaction puis splittez vers les vendeurs.

Onboarding vendor ​

Quand un vendeur s'inscrit sur votre plateforme, vous devez :

  1. Créer son compte SOAMTAR (manuellement, ou plus tard via une API d'onboarding SOAMTAR, prévue).
  2. Récupérer son merchant_id.
  3. Le stocker associé à votre user vendor.
php
Schema::create('vendors', function (Blueprint $table) {
    $table->uuid('id')->primary();
    $table->string('name');
    $table->uuid('soamtar_merchant_id')->unique();
    $table->boolean('is_active')->default(true);
    $table->timestamps();
});

Paiement avec splits ​

php
class MarketplaceCheckoutController
{
    public function pay(CheckoutRequest $request, SoamtarClient $soamtar)
    {
        $cart = $request->validatedCart();   // ['vendor_id' => $items[]]
        $totalAmount = collect($cart)->sum(fn ($items) => collect($items)->sum('price'));

        // 1. Encaisser le total via la plateforme
        $order = Order::create([
            'reference' => 'MKT-'.strtoupper(Str::random(8)),
            'total' => $totalAmount,
            'status' => 'pending',
        ]);

        $payment = $soamtar->payments()->create([
            'merchant_ref' => $order->reference,
            'amount' => $totalAmount,
            'currency' => 'SLE',
            'provider' => 'orange_money_sl',
            'payer' => $request->user()->only(['phone', 'email', 'name']),
            'metadata' => ['order_id' => (string) $order->id],
            'return_url' => route('marketplace.success', $order),
            'cancel_url' => route('marketplace.cancel', $order),
        ], idempotencyKey: $order->id);

        // 2. Pré-créer les splits (avant même la confirmation paiement)
        //    SOAMTAR les distribuera au settlement dès que la transaction est success.
        $splits = collect($cart)->map(function ($items, $vendorId) use ($order) {
            $vendor = Vendor::findOrFail($vendorId);
            $vendorTotal = collect($items)->sum('price');
            $platformFeePct = 0.05;  // 5% commission

            return [
                'beneficiary_merchant_id' => $vendor->soamtar_merchant_id,
                'amount' => $vendorTotal,
                'fee_amount' => round($vendorTotal * $platformFeePct, 2),
                'description' => "Order {$order->reference} - Vendor {$vendor->name}",
            ];
        })->values()->all();

        $soamtar->payments()->createSplits(
            $payment['data']['id'],
            $splits,
            idempotencyKey: $order->id.':splits',
        );

        $order->update([
            'soamtar_id' => $payment['data']['id'],
            'soamtar_reference' => $payment['data']['reference'],
        ]);

        return redirect()->away($payment['data']['payment_url']);
    }
}

Concurrence sûre ​

POST/v1/payments/{id}/splits est sérialisé côté serveur : si deux requêtes essaient d'enregistrer des splits en parallèle sur la même transaction parent, la seconde voit les lignes de la première avant de valider sa propre somme. Aucun sur-split n'est possible.

Webhook : settle automatique au payment.success ​

À la réception du webhook payment.success, vous n'avez rien à faire pour distribuer aux vendors : SOAMTAR crédite automatiquement les balances des bénéficiaires au settlement.

Vous devez juste :

php
public function handle(Request $request, WebhookVerifier $verifier)
{
    $event = $verifier->verify($request);
    if ($event['event'] !== 'payment.success') return response('skip');

    $orderId = $event['data']['metadata']['order_id'] ?? null;
    $order = Order::find($orderId);
    if ($order && $order->status === 'pending') {
        $order->update(['status' => 'paid']);

        // Notifier chaque vendor de sa vente
        foreach ($order->cart as $vendorId => $items) {
            VendorNotifier::dispatch(Vendor::find($vendorId), $order, $items);
        }
    }

    return response('ok');
}

Remboursements dans une marketplace ​

Remboursez le client avec POST/v1/refunds sur le paiement d'origine. SOAMTAR recrédite le montant sur le portefeuille Orange Money du payeur et le débite de votre solde ; le résultat arrive par refund.success ou refund.failed. Voir la référence Remboursements.

Remboursement total ​

php
$soamtar->refunds()->create([
    'transaction_id' => $order->soamtar_id, // id SOAMTAR du paiement
    'reason' => 'customer_request',
    'description' => "Remboursement {$order->reference}",
], idempotencyKey: "refund:{$order->id}");   // sans amount = tout le montant restant

Remboursement d'un seul vendeur ​

Si un vendeur n'a pas livré et que vous ne remboursez que sa part, émettez un remboursement partiel de cette part et récupérez-la auprès du vendeur selon les conditions de votre plateforme :

php
$soamtar->refunds()->create([
    'transaction_id' => $order->soamtar_id,
    'amount' => '120.00',                     // la part du vendeur, en leones
    'reason' => 'vendor_not_delivered',
], idempotencyKey: "refund:{$order->id}:{$vendor->id}");

Pièges à éviter ​

PiègeSolution
Splitter un vendor non-actifVérifier Vendor::is_active avant le createSplits
Splitter avant confirmation paiementOK : les splits sont créés en is_settled = false et distribués au succès
Somme des splits ≠ totalPas grave si < total (la diff reste à la plateforme), refusé si > total
Vendor change de merchant_idEmpêcher (immutable une fois la première vente passée)
Commission variable selon vendorStocker commission_pct sur Vendor, lire à chaque split

Tests ​

php
it('creates splits for each vendor when paying a multi-vendor cart', function () {
    Http::fake([
        'app.so-amtar.com/v1/payments' => Http::response(['data' => ['id' => 'tx-1', 'reference' => 'TXN-X', 'payment_url' => 'https://...']], 201),
        'app.so-amtar.com/v1/payments/tx-1/splits' => Http::response(['data' => [/* 2 splits */]], 201),
    ]);

    $vendorA = Vendor::factory()->create(['soamtar_merchant_id' => 'vendor-a-uuid']);
    $vendorB = Vendor::factory()->create(['soamtar_merchant_id' => 'vendor-b-uuid']);

    $response = $this->actingAs(User::factory()->create())
        ->postJson('/marketplace/checkout', [
            'cart' => [
                $vendorA->id => [['price' => '400.00']],
                $vendorB->id => [['price' => '600.00']],
            ],
        ]);

    Http::assertSent(fn ($req) => str_ends_with($req->url(), '/splits') && count($req['splits']) === 2);
});

SOAMTAR Payment API Documentation.