134 lines
7.9 KiB
Markdown
134 lines
7.9 KiB
Markdown
---
|
|
title: Payout Flow
|
|
tags: [flow, payment, payout, shkeeper, seller]
|
|
related_models: ["[[Payment]]"]
|
|
related_apis: ["POST /api/payment/shkeeper/payout", "GET /api/payment/shkeeper/payout/:taskId"]
|
|
---
|
|
|
|
# Payout Flow
|
|
|
|
How the **seller receives the escrowed crypto** once the order is complete. Two variants are implemented:
|
|
|
|
1. **SHKeeper Payouts API** (`shkeeperPayoutService.ts`) — the gateway signs and broadcasts on behalf of the platform.
|
|
2. **Manual admin wallet payout** (`admin-wallet-payout.tsx`) — an admin connects their own wallet and signs the transfer; the tx hash is reported back to the backend.
|
|
|
|
Both result in `Payment.escrowState = 'released'` and an outgoing `Payment` record with `direction: 'out'`.
|
|
|
|
## Actors
|
|
|
|
- **Admin** (or scheduled system trigger) — initiates the payout.
|
|
- **Seller** — recipient, has saved their wallet address under `User.profile.walletAddress`.
|
|
- **Backend** — `shkeeperPayoutService.createPayoutTask` and the manual confirmation routes.
|
|
- **SHKeeper Payouts API** — `POST https://pay.amn.gg/api/v1/payout` (per SHKeeper docs).
|
|
- **Blockchain (BSC)** — final on-chain settlement.
|
|
- **MongoDB** — separate `Payment` document with `direction: 'out'`.
|
|
|
|
## Preconditions
|
|
|
|
- The original pay-in `Payment` has `escrowState = 'funded'` (or `releasable`).
|
|
- The seller has set `profile.walletAddress` (validated `^0x...` format).
|
|
- The corresponding `PurchaseRequest` is in a status that allows payout (`delivered`, `confirming`, `seller_paid`, or `completed`).
|
|
|
|
## Step-by-step narrative
|
|
|
|
### SHKeeper-mediated payout
|
|
|
|
1. Admin (or the auto-release scheduler — not yet implemented) hits `POST /api/payment/shkeeper/payout` with `{ purchaseRequestId, sellerOfferId, buyerId, sellerId, amount, recipientAddress, token?, network? }`.
|
|
2. Backend `shkeeperPayoutService.createPayoutTask` (`shkeeperPayoutService.ts:40-150`):
|
|
- Validates ObjectIds and the `recipientAddress` (`startsWith('0x')`).
|
|
- **Idempotency**: `Payment.findOne({ purchaseRequestId, sellerOfferId, sellerId, provider:'shkeeper', direction:'out', status: { $in:['pending','processing','completed'] } })` — if found, reuses it.
|
|
- Creates a new `Payment` document with `direction: 'out'`, `escrowState: 'releasing'`, `blockchain.receiver = recipientAddress`.
|
|
- Calls SHKeeper Payouts API (`POST /api/v1/payout`) with the body documented at <https://shkeeper.io/api/#tag/Payouts>. SHKeeper returns a `task_id`.
|
|
- Stores `Payment.providerPaymentId = task_id`, `metadata.shkeeperTaskId = task_id`, `metadata.payoutType = 'seller-payment'`.
|
|
3. Polling or webhook: when SHKeeper completes the payout, it pushes a webhook (or the backend polls `GET /api/v1/payout/{task_id}`) and the system flips `Payment.status = 'completed'`, `escrowState = 'released'`, populates `blockchain.transactionHash`.
|
|
4. The original pay-in `Payment` is updated in tandem: `escrowState = 'released'`, `PurchaseRequest.status = 'seller_paid'` → `completed`.
|
|
5. Notifications: `notifyPayoutSent` to the seller, internal admin log.
|
|
|
|
### Manual admin payout
|
|
|
|
1. Admin opens the request detail in the admin view; the admin-step component `admin-wallet-payout.tsx` shows the recipient and amount.
|
|
2. Admin connects their wallet (`useWeb3` / `web3Service.connect()`).
|
|
3. Admin clicks "Send payout"; wagmi triggers `transfer(recipient, amount)` on the USDT contract.
|
|
4. After confirmation, the admin clicks "Confirm in system", which POSTs `POST /api/payment/admin/confirm-tx/:paymentId` with `{ txHash, kind: 'release' }`.
|
|
5. Backend `confirmAdminTx(paymentId, txHash, 'release')` (`shkeeperService.ts:628-647`) sets `status: 'completed'`, `escrowState: 'released'`, `blockchain.transactionHash = txHash`.
|
|
|
|
### Sequence diagram (SHKeeper payout)
|
|
|
|
```mermaid
|
|
sequenceDiagram
|
|
autonumber
|
|
actor A as Admin/System
|
|
participant BE as Backend
|
|
participant DB as MongoDB
|
|
participant SK as SHKeeper Payout API
|
|
participant BC as BSC
|
|
actor S as Seller
|
|
|
|
A->>BE: POST /api/payment/shkeeper/payout
|
|
BE->>DB: Payment.create({direction:"out", escrowState:"releasing"})
|
|
BE->>SK: POST /api/v1/payout {to, amount, crypto}
|
|
SK-->>BE: { task_id, status:"pending" }
|
|
BE->>DB: Payment.providerPaymentId=task_id
|
|
SK->>BC: signed payout tx (managed wallet)
|
|
BC-->>SK: confirmed
|
|
SK->>BE: webhook payout-completed (or BE polls)
|
|
BE->>DB: Payment.status="completed"\nescrowState="released"\ntxHash
|
|
BE->>DB: pay-in Payment.escrowState="released"\nPurchaseRequest.status="seller_paid"
|
|
BE->>S: notifyPayoutSent
|
|
```
|
|
|
|
## API calls
|
|
|
|
| Method | Endpoint | Source |
|
|
|---|---|---|
|
|
| `POST` | `/api/payment/shkeeper/payout` | `shkeeperPayoutRoutes.ts` → `createPayoutTask` |
|
|
| `GET` | `/api/payment/shkeeper/payout/:taskId` | Polls SHKeeper task status |
|
|
| `POST` | `/api/payment/admin/confirm-tx/:paymentId` | Manual admin confirmation |
|
|
| `GET` | `/api/payment/admin/payouts` | List payouts (admin dashboard) |
|
|
|
|
## Database writes
|
|
|
|
- **`payments`** — new outgoing document; updates to `status`, `escrowState`, `blockchain.transactionHash` as the task progresses.
|
|
- **`payments`** (pay-in counterpart) — `escrowState = 'released'`.
|
|
- **`purchaserequests`** — `status` advances to `seller_paid` → `completed`.
|
|
- **`notifications`** — seller payout receipt.
|
|
|
|
## Socket events emitted
|
|
|
|
- **`payment-status`** (admin) on each transition.
|
|
- **`purchase-request-update`** `status-changed`.
|
|
|
|
## Side effects
|
|
|
|
- **`fix-transaction-hashes.js`** at repo root (`backend/fix-transaction-hashes.js`) — script used to backfill missing `blockchain.transactionHash` on payouts where the SHKeeper webhook arrived without the txid (e.g. signature length mismatch in dev). Run locally with the same Mongo URI to repair stale documents. Use it as the reference for the data-fix pattern — pull recent payouts, query SHKeeper for invoice/task details, write back the hash.
|
|
- **Hash repair** — periodic reconciliation against SHKeeper invoice GET endpoints ensures bookkeeping accuracy.
|
|
|
|
## Error / edge cases
|
|
|
|
- **Invalid recipient address** → throws synchronously, no DB record created.
|
|
- **SHKeeper insufficient hot-wallet balance** → SHKeeper returns an error; payout task stays `pending`, backend logs.
|
|
- **Duplicate payout request** → idempotency: existing payment returned with no extra SHKeeper call.
|
|
- **Payout reverted on chain** → SHKeeper marks the task `failed`; backend sets `Payment.status = 'failed'`, `escrowState = 'failed'`. Admin retries.
|
|
- **Missing `transactionHash` after success** → use `fix-transaction-hashes.js` to backfill.
|
|
- **Manual payout signed but never confirmed in system** → on-chain transfer happened, but `Payment.escrowState` stays `releasing`. Admin can run a reconciliation script that scans the escrow wallet's outgoing txs and matches by amount/timestamp.
|
|
- **Seller changes wallet address mid-flight** → the saved `recipientAddress` is the snapshot taken at payout creation; subsequent profile changes do not affect in-flight payouts.
|
|
|
|
> [!warning] Auto-release is not yet implemented
|
|
> Today, payouts are admin-initiated. The flow is ready for an automatic trigger when [[Delivery Confirmation Flow]] completes — implement a cron job or queue worker that scans for `PurchaseRequest.status='delivered'` and auto-creates payouts after a configurable grace period.
|
|
|
|
## Linked flows
|
|
|
|
- [[Escrow Flow]] — sets up the conditions under which payout is allowed.
|
|
- [[Delivery Confirmation Flow]] — green-lights the payout.
|
|
- [[Dispute Flow]] — can divert funds to a refund instead.
|
|
- [[Notification Flow]] — payout receipt to seller.
|
|
|
|
## Source files
|
|
|
|
- Backend: `backend/src/services/payment/shkeeper/shkeeperPayoutService.ts`
|
|
- Backend: `backend/src/services/payment/shkeeper/shkeeperPayoutRoutes.ts`
|
|
- Backend: `backend/src/services/payment/shkeeper/shkeeperService.ts:614-647` (build & confirm admin tx payload)
|
|
- Backend: `backend/fix-transaction-hashes.js` (reconciliation script)
|
|
- Frontend: `frontend/src/sections/request/components/admin-steps/admin-wallet-payout.tsx`
|
|
- Frontend: `frontend/src/web3/web3Service.ts`
|