YeboSafe Escrow Lifecycle
Complete documentation of the escrow state machine.
State Machine
┌─────────────────────────────────────┐
│ PENDING │
│ (Initial state after creation) │
└─────────────────┬───────────────────┘
│
┌──────────────────────────┼──────────────────────────┐
│ │ │
▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ ACCEPTED │ │ REFUSED │ │ CANCELLED │
│ (Committed) │ │ (Declined) │ │ (Aborted) │
└───────┬──────┘ └──────────────┘ └──────┬───────┘
│ │
│ ▼
│ ┌──────────────┐
│ │ REFUNDED │
│ │ (Funds back) │
│ └──────────────┘
│
┌───────┴───────────────────┐
│ │
▼ ▼
┌──────────────┐ ┌──────────────┐
│ COMPLETED │ │ DISPUTED │
│ (Code used) │ │ (Conflict) │
└──────────────┘ └──────────────┘State Transitions
PENDING → ACCEPTED
Trigger: Merchant calls POST /escrows/:id/accept
Actions:
- Validate status is
PENDING - Update status to
ACCEPTED - Create audit log entry
- Fire
escrow.acceptedwebhook
Meaning: Merchant commits to deliver the goods/service.
PENDING → REFUSED
Trigger: Merchant calls POST /escrows/:id/refuse
Body:
{ "reason": "Out of stock" }Actions:
- Validate status is
PENDING - Update status to
REFUSED - Create audit log with reason
- Fire
escrow.refusedwebhook
Meaning: Merchant cannot or will not fulfill the order.
PENDING → CANCELLED
Trigger: Merchant calls POST /escrows/:id/cancel
Body:
{ "reason": "Customer requested cancellation" }Actions:
- Validate status allows cancellation
- Update status to
CANCELLED - Set
cancelledAtandcancelReason - Create audit log
- Fire
escrow.cancelledwebhook
Meaning: Transaction cancelled before fulfillment.
ACCEPTED → COMPLETED
Trigger: Merchant calls POST /escrows/:id/complete with completion code
Body:
{ "completionCode": "123456" }Actions:
- Validate code matches escrow
- Validate status is
ACCEPTED - Credit merchant wallet with amount
- Update status to
COMPLETED - Set
completedAt - Create wallet entry
- Create audit log
- Fire
escrow.completedwebhook
Meaning: Delivery confirmed, funds released to merchant.
ACCEPTED → DISPUTED
Trigger: Either party calls POST /escrows/:id/dispute
Body:
{ "reason": "Item not as described" }Actions:
- Validate status is not final
- Update status to
DISPUTED - Set
disputedAtanddisputeReason - Create audit log
- Fire
escrow.disputedwebhook
Meaning: Conflict requires resolution.
CANCELLED → REFUNDED
Trigger: Admin action (not implemented in MVP)
Actions:
- Process refund to original payment method
- Update status to
REFUNDED - Set
refundedAt - Create audit log
Meaning: Funds returned to payer.
Completion Code
A 6-digit numeric code generated at escrow creation.
Generation
function generateCompletionCode(): string {
return Math.floor(100000 + Math.random() * 900000).toString();
}Uniqueness
Ensured at creation:
let completionCode = generateCompletionCode();
let isUnique = false;
while (!isUnique) {
const existing = await prisma.escrowTransaction.findFirst({
where: { completionCode }
});
if (!existing) {
isUnique = true;
} else {
completionCode = generateCompletionCode();
}
}Usage Flow
- Merchant creates escrow, receives
completionCode - Merchant provides code to payer (via receipt, email, etc.)
- Upon successful delivery, payer gives code to merchant
- Merchant submits code to complete escrow
- Funds release to merchant wallet
Audit Trail
Every state change is logged:
await prisma.escrowLog.create({
data: {
escrowId: id,
action: 'accepted', // 'created', 'completed', etc.
actorId: merchantId, // Who performed action
metadata: { ... } // Additional context
}
});Log Actions:
created- Escrow createdaccepted- Merchant acceptedrefused- Merchant refused (with reason)completed- Code used, funds releaseddisputed- Dispute opened (with reason)cancelled- Cancelled (with reason)refunded- Funds returned
Validation Rules
| From | To | Allowed | Error Code |
|---|---|---|---|
| PENDING | ACCEPTED | ✓ | - |
| PENDING | REFUSED | ✓ | - |
| PENDING | CANCELLED | ✓ | - |
| ACCEPTED | COMPLETED | ✓ | - |
| ACCEPTED | DISPUTED | ✓ | - |
| ACCEPTED | CANCELLED | ✓ | - |
| COMPLETED | * | ✗ | ALREADY_COMPLETED |
| DISPUTED | * | ✗ | ALREADY_DISPUTED |
| CANCELLED | REFUNDED | ✓ (admin) | - |
| * | PENDING | ✗ | - |