Skip to content

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:

  1. Validate status is PENDING
  2. Update status to ACCEPTED
  3. Create audit log entry
  4. Fire escrow.accepted webhook

Meaning: Merchant commits to deliver the goods/service.

PENDING → REFUSED ​

Trigger: Merchant calls POST /escrows/:id/refuse

Body:

json
{ "reason": "Out of stock" }

Actions:

  1. Validate status is PENDING
  2. Update status to REFUSED
  3. Create audit log with reason
  4. Fire escrow.refused webhook

Meaning: Merchant cannot or will not fulfill the order.

PENDING → CANCELLED ​

Trigger: Merchant calls POST /escrows/:id/cancel

Body:

json
{ "reason": "Customer requested cancellation" }

Actions:

  1. Validate status allows cancellation
  2. Update status to CANCELLED
  3. Set cancelledAt and cancelReason
  4. Create audit log
  5. Fire escrow.cancelled webhook

Meaning: Transaction cancelled before fulfillment.

ACCEPTED → COMPLETED ​

Trigger: Merchant calls POST /escrows/:id/complete with completion code

Body:

json
{ "completionCode": "123456" }

Actions:

  1. Validate code matches escrow
  2. Validate status is ACCEPTED
  3. Credit merchant wallet with amount
  4. Update status to COMPLETED
  5. Set completedAt
  6. Create wallet entry
  7. Create audit log
  8. Fire escrow.completed webhook

Meaning: Delivery confirmed, funds released to merchant.

ACCEPTED → DISPUTED ​

Trigger: Either party calls POST /escrows/:id/dispute

Body:

json
{ "reason": "Item not as described" }

Actions:

  1. Validate status is not final
  2. Update status to DISPUTED
  3. Set disputedAt and disputeReason
  4. Create audit log
  5. Fire escrow.disputed webhook

Meaning: Conflict requires resolution.

CANCELLED → REFUNDED ​

Trigger: Admin action (not implemented in MVP)

Actions:

  1. Process refund to original payment method
  2. Update status to REFUNDED
  3. Set refundedAt
  4. Create audit log

Meaning: Funds returned to payer.

Completion Code ​

A 6-digit numeric code generated at escrow creation.

Generation ​

typescript
function generateCompletionCode(): string {
  return Math.floor(100000 + Math.random() * 900000).toString();
}

Uniqueness ​

Ensured at creation:

typescript
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 ​

  1. Merchant creates escrow, receives completionCode
  2. Merchant provides code to payer (via receipt, email, etc.)
  3. Upon successful delivery, payer gives code to merchant
  4. Merchant submits code to complete escrow
  5. Funds release to merchant wallet

Audit Trail ​

Every state change is logged:

typescript
await prisma.escrowLog.create({
  data: {
    escrowId: id,
    action: 'accepted',       // 'created', 'completed', etc.
    actorId: merchantId,      // Who performed action
    metadata: { ... }         // Additional context
  }
});

Log Actions:

  • created - Escrow created
  • accepted - Merchant accepted
  • refused - Merchant refused (with reason)
  • completed - Code used, funds released
  • disputed - Dispute opened (with reason)
  • cancelled - Cancelled (with reason)
  • refunded - Funds returned

Validation Rules ​

FromToAllowedError Code
PENDINGACCEPTED✓-
PENDINGREFUSED✓-
PENDINGCANCELLED✓-
ACCEPTEDCOMPLETED✓-
ACCEPTEDDISPUTED✓-
ACCEPTEDCANCELLED✓-
COMPLETED*✗ALREADY_COMPLETED
DISPUTED*✗ALREADY_DISPUTED
CANCELLEDREFUNDED✓ (admin)-
*PENDING✗-

One chat. Everything done.