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 || [];
}