YeboShops Payment System
Deep dive into the escrow-based Secure Payment system and wallet management.
System Overview
YeboShops uses an internal wallet system with escrow-based secure payments for P2P transactions. This eliminates the need for external payment gateways while providing buyer protection.
┌─────────────────────────────────────────────────────────────────┐
│ Payment Flow │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────┐ ┌──────────┐ │
│ │ Buyer │ │ Seller │ │
│ │ Wallet │ │ Wallet │ │
│ └────┬─────┘ └─────┬────┘ │
│ │ │ │
│ │ 1. CREATE PAYMENT │ │
│ │ (Debit buyer) │ │
│ ▼ │ │
│ ┌──────────────┐ │ │
│ │ ESCROW │ 2. ACCEPT │ │
│ │ (PENDING) │───────────────────────────────►│ │
│ └──────────────┘ (Add to unconfirmed) │ │
│ │ │ │
│ │ │ │
│ │ 3. COMPLETE (with code) │ │
│ └─────────────────────────────────────────►│ │
│ (Move to confirmed) │ │
│ │ │
└─────────────────────────────────────────────────────────────────┘Wallet System
Balance Types
| Type | Description | Withdrawable |
|---|---|---|
balance | Confirmed, cleared funds | ✅ Yes |
unconfirmedBalance | Pending in escrow | ❌ No |
Wallet Model
typescript
interface Wallet {
id: string;
userId: string;
balance: number; // Confirmed balance
unconfirmedBalance: number; // Escrow balance
currency: string; // Inherited from user's country
isActive: boolean;
lastTransactionAt: Date;
}Wallet Service Operations
typescript
// Create wallet from user's country profile
const wallet = await WalletService.createWalletFromUserProfile(userId);
// Ensure user has wallet (create if needed)
const wallet = await WalletService.ensureUserHasWallet(userId);
// Update balance (with transaction logging)
await WalletService.updateBalance(
walletId,
100.00, // amount
'debit', // type: 'credit' | 'debit'
'Purchase payment',
paymentId, // reference
'SECURE_PAYMENT' // referenceType
);
// Update unconfirmed balance (escrow)
await WalletService.updateUnconfirmedBalance(
walletId,
100.00,
'credit',
'Payment accepted'
);
// Confirm balance (move from unconfirmed to confirmed)
await WalletService.confirmBalance(
walletId,
100.00,
'Payment completed'
);Secure Payment Flow
1. Payment Creation
When buyer initiates a payment:
typescript
const payment = await SecurePaymentService.createSecurePayment({
buyerId: 'user_123',
shopId: 'shop_456',
amount: 500.00,
description: 'iPhone 12 Pro',
productId: 'prod_789',
chatId: 'chat_abc'
});
// Result:
{
paymentId: 'SP1703184000001234', // Unique ID
completionCode: '482916', // 6-digit code (given to buyer)
status: 'PENDING',
amount: 500.00
}What happens:
- Validate buyer exists
- Validate shop exists and is active
- Check buyer has sufficient balance
- Debit buyer's wallet (balance → escrow)
- Generate unique completion code
- Create SecurePayment record
2. Seller Accepts Payment
Seller reviews and accepts the payment:
typescript
await SecurePaymentService.acceptSecurePayment({
paymentId: 'SP1703184000001234',
userId: 'seller_id' // Must be shop owner
});
// Status: PENDING → ACCEPTEDWhat happens:
- Verify payment is PENDING
- Verify user is shop owner
- Add amount to seller's
unconfirmedBalance - Update status to ACCEPTED
3. Payment Completion
After buyer receives goods, they provide the completion code to seller:
typescript
await SecurePaymentService.completeSecurePayment({
completionCode: '482916',
userId: 'seller_id' // Seller enters the code
});
// Status: ACCEPTED → COMPLETEDWhat happens:
- Find payment by completion code
- Verify payment is ACCEPTED
- Verify user is shop owner
- Move amount from seller's
unconfirmedBalancetobalance - Update status to COMPLETED
Alternative Flows
Seller Refuses Payment
If seller can't fulfill the order:
typescript
await SecurePaymentService.refuseSecurePayment({
paymentId: 'SP1703184000001234',
reason: 'Item out of stock',
userId: 'seller_id'
});
// Status: PENDING → REFUSED
// Buyer wallet: RefundedBuyer Disputes Payment
If there's an issue with the transaction:
typescript
await SecurePaymentService.disputeSecurePayment({
paymentId: 'SP1703184000001234',
reason: 'Item not as described',
userId: 'buyer_id' // or seller_id
});
// Status: ACCEPTED → DISPUTEDDispute rules:
- Buyer can dispute any PENDING or ACCEPTED payment
- Seller can only dispute ACCEPTED payments (e.g., buyer won't provide code)
- Admin must resolve disputes manually
Seller Cancels Payment
typescript
await SecurePaymentService.cancelSecurePayment({
paymentId: 'SP1703184000001234',
reason: 'Cannot complete transaction',
userId: 'seller_id'
});
// Status: PENDING/ACCEPTED → CANCELLED
// Buyer wallet: RefundedPayment Status Lifecycle
┌───────────┐
│ PENDING │
└─────┬─────┘
│
┌───────────────┼───────────────┐
│ │ │
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ ACCEPTED │ │ REFUSED │ │CANCELLED │
└────┬─────┘ └──────────┘ └──────────┘
│
┌────┴────┐
│ │
▼ ▼
┌──────────┐ ┌──────────┐
│COMPLETED │ │ DISPUTED │
└──────────┘ └────┬─────┘
│
▼
┌──────────┐
│CANCELLED │ (after resolution)
└──────────┘Transaction Logging
Every wallet operation creates a transaction record:
typescript
interface WalletTransaction {
id: string;
walletId: string;
userId: string;
type: 'CREDIT' | 'DEBIT';
amount: number;
balanceBefore: number;
balanceAfter: number;
description: string;
reference: string; // SecurePayment ID
referenceType: WalletRefType;
metadata: object;
}
enum WalletRefType {
SECURE_PAYMENT,
DEPOSIT,
WITHDRAWAL,
TRANSFER,
REFUND
}Example Transaction History
| Type | Amount | Description | Reference |
|---|---|---|---|
| DEBIT | -500 | Secure payment to TechShop | SP170318... |
| CREDIT | +500 | Secure payment refund | SP170318... |
| CREDIT | +100 | Deposit | DEP170318... |
Payment Audit Trail
Every payment action is logged:
typescript
interface SecurePaymentLog {
id: string;
paymentId: string;
action: 'created' | 'accepted' | 'refused' | 'completed' | 'disputed' | 'cancelled';
userId: string;
ipAddress: string;
userAgent: string;
metadata: object;
createdAt: Date;
}API Endpoints
Create Payment
http
POST /secure-payments
Authorization: Bearer <token>
{
"shopId": "shop_456",
"amount": 500.00,
"description": "iPhone 12 Pro",
"productId": "prod_789",
"chatId": "chat_abc"
}Accept Payment (Seller)
http
POST /secure-payments/:paymentId/accept
Authorization: Bearer <token>Complete Payment (Seller enters code)
http
POST /secure-payments/complete
Authorization: Bearer <token>
{
"completionCode": "482916"
}Get Payment Details
http
GET /secure-payments/:paymentId
Authorization: Bearer <token>Get User's Payments
http
GET /secure-payments?status=PENDING&limit=20
Authorization: Bearer <token>Security Considerations
Completion Code
- 6-digit numeric code (100000-999999)
- Generated server-side with crypto randomness
- Unique across all active payments
- Only buyer knows the code
- Only seller can use it to complete
Authorization
- Only buyer can create payments
- Only shop owner can accept/refuse/cancel
- Both parties can dispute
- Admin can resolve disputes
Transaction Atomicity
All financial operations use Prisma transactions:
typescript
await prisma.$transaction(async (tx) => {
// Debit buyer
await WalletService.updateBalance(buyerWalletId, amount, 'debit', ..., tx);
// Create payment
const payment = await tx.securePayment.create({ ... });
// Log action
await tx.securePaymentLog.create({ ... });
return payment;
});Wallet Balance API
http
GET /wallet/balance
Authorization: Bearer <token>
Response:
{
"balance": 1000.00,
"unconfirmedBalance": 500.00,
"totalBalance": 1500.00,
"currency": "SZL"
}http
GET /wallet/transactions?limit=20&type=credit
Authorization: Bearer <token>
Response:
{
"transactions": [...],
"total": 45
}