Skip to content

YeboSafe Services Deep Dive

EscrowService

Location: yebosafe-api/src/services/escrow.service.ts

Core escrow management service.

createEscrow

Creates a new escrow transaction.

typescript
interface CreateEscrowRequest {
  merchantId: string;
  payerName?: string;
  payerEmail?: string;
  payerPhone?: string;
  amount: number;           // Must be > 0
  currency?: string;        // Default: 'USD'
  description?: string;
  metadata?: Record<string, unknown>;
  webhookUrl?: string;      // Override merchant default
}

static async createEscrow(data: CreateEscrowRequest) {
  // 1. Validate amount
  if (data.amount <= 0) throw new Error('INVALID_AMOUNT');
  
  // 2. Generate unique completion code (6 digits)
  let completionCode = generateCompletionCode();
  while (await codeExists(completionCode)) {
    completionCode = generateCompletionCode();
  }
  
  // 3. Create escrow record
  const escrow = await prisma.escrowTransaction.create({
    data: {
      merchantId: data.merchantId,
      payerName: data.payerName,
      payerEmail: data.payerEmail,
      payerPhone: data.payerPhone,
      amount: new Prisma.Decimal(data.amount),
      currency: data.currency || 'USD',
      description: data.description,
      metadata: data.metadata,
      status: 'PENDING',
      completionCode,
      webhookUrl: data.webhookUrl,
    },
  });
  
  // 4. Log action
  await createLog(escrow.id, 'created', data.merchantId);
  
  // 5. Fire webhook
  await WebhookService.fire(merchantId, 'escrow.created', escrow);
  
  return escrow;
}

listEscrows

Paginated list for a merchant.

typescript
interface EscrowFilters {
  status?: EscrowStatus;
  page?: number;      // Default: 1
  limit?: number;     // Default: 20
}

static async listEscrows(merchantId: string, filters: EscrowFilters) {
  const { status, page = 1, limit = 20 } = filters;
  const offset = (page - 1) * limit;

  const [escrows, total] = await Promise.all([
    prisma.escrowTransaction.findMany({
      where: { merchantId, ...(status && { status }) },
      orderBy: { createdAt: 'desc' },
      take: limit,
      skip: offset,
      include: { logs: { orderBy: { createdAt: 'asc' } } },
    }),
    prisma.escrowTransaction.count({ where: { merchantId, ...(status && { status }) } }),
  ]);

  return {
    escrows,
    total,
    page,
    limit,
    pages: Math.ceil(total / limit)
  };
}

acceptEscrow

Merchant accepts and commits to deliver.

typescript
static async acceptEscrow(id: string, merchantId: string) {
  const escrow = await findEscrow(id, merchantId);
  
  if (escrow.status !== 'PENDING') {
    throw new Error('ESCROW_NOT_PENDING');
  }
  
  const updated = await prisma.escrowTransaction.update({
    where: { id },
    data: { status: 'ACCEPTED' },
  });
  
  await createLog(id, 'accepted', merchantId);
  await WebhookService.fire(merchantId, 'escrow.accepted', updated);
  
  return updated;
}

refuseEscrow

Merchant declines the escrow.

typescript
static async refuseEscrow(id: string, merchantId: string, reason: string) {
  const escrow = await findEscrow(id, merchantId);
  
  if (escrow.status !== 'PENDING') {
    throw new Error('ESCROW_NOT_PENDING');
  }
  
  const updated = await prisma.escrowTransaction.update({
    where: { id },
    data: { status: 'REFUSED' },
  });
  
  await createLog(id, 'refused', merchantId, { reason });
  await WebhookService.fire(merchantId, 'escrow.refused', { ...updated, refuseReason: reason });
  
  return updated;
}

completeEscrow

Complete with completion code and release funds.

typescript
static async completeEscrow(completionCode: string, merchantId: string) {
  const escrow = await prisma.escrowTransaction.findFirst({
    where: { completionCode, merchantId },
  });
  
  if (!escrow) throw new Error('INVALID_CODE');
  if (escrow.status === 'COMPLETED') throw new Error('ALREADY_COMPLETED');
  if (escrow.status === 'DISPUTED') throw new Error('ESCROW_DISPUTED');
  if (escrow.status === 'CANCELLED') throw new Error('ESCROW_CANCELLED');
  if (escrow.status !== 'ACCEPTED') throw new Error('ESCROW_NOT_ACCEPTED');
  
  // Credit merchant wallet
  await WalletService.creditWallet(
    merchantId,
    Number(escrow.amount),
    escrow.currency,
    `Escrow completed: ${escrow.id}`,
    escrow.id
  );
  
  const updated = await prisma.escrowTransaction.update({
    where: { id: escrow.id },
    data: { status: 'COMPLETED', completedAt: new Date() },
  });
  
  await createLog(escrow.id, 'completed', merchantId, { amount: Number(escrow.amount) });
  await WebhookService.fire(merchantId, 'escrow.completed', updated);
  
  return updated;
}

disputeEscrow

Open a dispute.

typescript
static async disputeEscrow(id: string, merchantId: string, reason: string) {
  const escrow = await findEscrow(id, merchantId);
  
  if (escrow.status === 'COMPLETED') throw new Error('ALREADY_COMPLETED');
  if (escrow.status === 'DISPUTED') throw new Error('ALREADY_DISPUTED');
  if (['CANCELLED', 'REFUSED'].includes(escrow.status)) {
    throw new Error('ESCROW_ALREADY_CLOSED');
  }
  
  const updated = await prisma.escrowTransaction.update({
    where: { id },
    data: {
      status: 'DISPUTED',
      disputedAt: new Date(),
      disputeReason: reason
    },
  });
  
  await createLog(id, 'disputed', merchantId, { reason });
  await WebhookService.fire(merchantId, 'escrow.disputed', updated);
  
  return updated;
}

cancelEscrow

Cancel before acceptance.

typescript
static async cancelEscrow(id: string, merchantId: string, reason: string) {
  const escrow = await findEscrow(id, merchantId);
  
  if (['COMPLETED', 'DISPUTED', 'CANCELLED'].includes(escrow.status)) {
    throw new Error(`ALREADY_${escrow.status}`);
  }
  
  const updated = await prisma.escrowTransaction.update({
    where: { id },
    data: {
      status: 'CANCELLED',
      cancelledAt: new Date(),
      cancelReason: reason
    },
  });
  
  await createLog(id, 'cancelled', merchantId, { reason });
  await WebhookService.fire(merchantId, 'escrow.cancelled', updated);
  
  return updated;
}

lookupByCode

Public lookup (limited info).

typescript
static async lookupByCode(code: string) {
  const escrow = await prisma.escrowTransaction.findFirst({
    where: { completionCode: code },
    select: {
      id: true,
      reference: true,
      amount: true,
      currency: true,
      description: true,
      status: true,
      createdAt: true,
      merchant: { select: { name: true } },
    },
  });
  
  if (!escrow) return null;
  
  return {
    id: escrow.id,
    reference: escrow.reference,
    amount: Number(escrow.amount),
    currency: escrow.currency,
    description: escrow.description,
    status: escrow.status,
    createdAt: escrow.createdAt,
    merchantName: escrow.merchant.name,
  };
}

adminStats

Platform-wide statistics.

typescript
static async adminStats() {
  const [totalEscrows, completedEscrows, merchantCount, activeEscrows] = await Promise.all([
    prisma.escrowTransaction.count(),
    prisma.escrowTransaction.count({ where: { status: 'COMPLETED' } }),
    prisma.merchant.count({ where: { isActive: true } }),
    prisma.escrowTransaction.count({
      where: { status: { in: ['PENDING', 'ACCEPTED'] } },
    }),
  ]);

  const volumeResult = await prisma.escrowTransaction.aggregate({
    _sum: { amount: true },
    where: { status: 'COMPLETED' },
  });

  return {
    totalEscrows,
    completedEscrows,
    activeEscrows,
    merchantCount,
    totalVolume: Number(volumeResult._sum.amount || 0),
    completionRate: totalEscrows > 0
      ? Math.round((completedEscrows / totalEscrows) * 100)
      : 0,
  };
}

WalletService

Location: yebosafe-api/src/services/wallet.service.ts

Merchant wallet management.

creditWallet

Add funds to merchant wallet.

typescript
static async creditWallet(
  merchantId: string,
  amount: number,
  currency: string,
  description: string,
  reference?: string
) {
  // Get or create wallet
  let wallet = await prisma.merchantWallet.findUnique({
    where: { merchantId },
  });
  
  if (!wallet) {
    wallet = await prisma.merchantWallet.create({
      data: { merchantId, balance: 0, currency },
    });
  }
  
  // Update balance and log transaction
  await prisma.$transaction([
    prisma.merchantWallet.update({
      where: { id: wallet.id },
      data: { balance: { increment: amount } },
    }),
    prisma.walletEntry.create({
      data: {
        walletId: wallet.id,
        type: 'CREDIT',
        amount,
        description,
        reference,
      },
    }),
  ]);
}

getBalance

typescript
static async getBalance(merchantId: string) {
  const wallet = await prisma.merchantWallet.findUnique({
    where: { merchantId },
  });
  
  return {
    balance: Number(wallet?.balance || 0),
    currency: wallet?.currency || 'USD',
  };
}

getTransactions

typescript
static async getTransactions(merchantId: string, limit = 50) {
  const wallet = await prisma.merchantWallet.findUnique({
    where: { merchantId },
    include: {
      transactions: {
        orderBy: { createdAt: 'desc' },
        take: limit,
      },
    },
  });
  
  return wallet?.transactions || [];
}

One chat. Everything done.