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.