Overview
Every mechanic has a wallet backed by the double-entry ledger. Earnings flow through three balance stages before reaching the mechanic’s bank account:
Wallet Balances
Each mechanic wallet tracks three balances:
| Balance | Meaning | Can withdraw? |
|---|
| Pending | Earned but inside dispute window (default 72h) | ❌ |
| Available | Past dispute window, ready for payout | ✅ |
| Locked | Frozen by an active dispute | ❌ |
All balances are cached in the wallets table but derived from wallet_transactions, which are themselves linked to ledger_entries. If drift is detected, WalletService.recalculateWalletBalance() recomputes from the transaction log.
Earnings Lifecycle
Wallet API (Mechanic)
Get Wallet Summary
GET /api/v1/wallet/summary
Authorization: Bearer MECHANIC_TOKEN
{
"data": {
"pending": 36000,
"available": 150000,
"locked": 0,
"total": 186000,
"currency": "INR"
}
}
Get Transaction History
GET /api/v1/wallet/transactions?limit=20&offset=0
Authorization: Bearer MECHANIC_TOKEN
{
"data": [
{
"id": "uuid",
"transaction_type": "earning",
"amount": 36000,
"balance_type": "pending",
"description": "Earnings pending (dispute window): job abc-123",
"created_at": "2026-07-07T10:30:00Z"
}
]
}
Get Payout History
GET /api/v1/wallet/payouts?limit=20&offset=0
Authorization: Bearer MECHANIC_TOKEN
UPI Account Management
Mechanics must add and verify a UPI account before receiving payouts. Verification uses RazorpayX’s VPA validation pipeline.
Verification Flow
Add UPI Account
POST /api/v1/wallet/upi
Authorization: Bearer MECHANIC_TOKEN
Content-Type: application/json
{
"vpa": "mechanic@upi",
"beneficiary_name": "Ravi Kumar"
}
Response (201):
{
"data": {
"upiAccountId": "uuid",
"message": "Verification initiated"
}
}
Verification happens asynchronously. The mechanic receives a push notification when verified or if manual review is needed. The first UPI account added is automatically set as primary.
List UPI Accounts
GET /api/v1/wallet/upi
Authorization: Bearer MECHANIC_TOKEN
Admin: Manually Approve UPI
POST /api/v1/wallet/upi/{upiAccountId}/approve
Authorization: Bearer ADMIN_TOKEN
Disputes
Either party can raise a dispute on a completed or paid job. Disputes freeze the mechanic’s pending earnings until resolved.
Dispute Lifecycle
Raise a Dispute
POST /api/v1/wallet/disputes
Authorization: Bearer CUSTOMER_OR_MECHANIC_TOKEN
Content-Type: application/json
{
"job_id": "uuid-of-job",
"reason": "Mechanic charged for parts that were not used in the repair"
}
What happens:
- Dispute row created (status:
open)
- Mechanic’s pending earnings for this job are frozen in the ledger
- Both parties receive push notifications
Get Disputes for a Job
GET /api/v1/wallet/disputes/job/{jobId}
Authorization: Bearer TOKEN
Admin: Resolve a Dispute
PATCH /api/v1/wallet/disputes/{disputeId}/resolve
Authorization: Bearer ADMIN_TOKEN
Content-Type: application/json
{
"resolution": "resolved_customer",
"resolution_notes": "Parts were indeed not used per photo evidence"
}
| Resolution | Effect |
|---|
resolved_customer | Mechanic loses frozen earnings; customer gets refund |
resolved_mechanic | Frozen earnings released to mechanic’s available balance |
rejected | Same as resolved_mechanic (dispute was invalid) |
Refunds
Refunds are admin-initiated only. The flow:
Initiate Refund
POST /api/v1/wallet/refunds
Authorization: Bearer ADMIN_TOKEN
Content-Type: application/json
{
"payment_id": "uuid-of-payment",
"amount": 25000,
"reason": "Partial refund — unused parts"
}
- Omit
amount for a full refund
- Over-refunding is rejected (400)
- Deduction is from mechanic’s available balance first, then pending
List Refunds for a Payment
GET /api/v1/wallet/refunds/{paymentId}
Authorization: Bearer TOKEN
Settlement Cycle
Settlements run automatically via BullMQ cron:
| Schedule | Job | What it does |
|---|
| Every hour | release-earnings | Moves pending → available for jobs past the dispute window |
| Daily 02:00 UTC | run-settlement-batch | Pays out all mechanics with available balance ≥ ₹100 |
Settlement Batch Flow
Eligibility Criteria
A mechanic is included in a settlement batch when:
- Their available wallet balance ≥
MIN_PAYOUT_AMOUNT_PAISA (default ₹100)
- They have a verified UPI account set as primary
- The UPI account has a valid
razorpay_fund_account_id
Ledger Architecture
Every financial operation creates immutable ledger_entries using double-entry accounting:
System Accounts
| Account | Purpose |
|---|
PLATFORM_COLLECTIONS | Holds collected payments before payout |
PLATFORM_COMMISSION | Accumulates platform commission revenue |
PLATFORM_COD_RECEIVABLE | Tracks COD amounts owed by mechanics |
PLATFORM_CANCELLATION_REVENUE | Cancellation fee revenue |
Immutability Guarantee
Ledger entries are append-only. Database triggers prevent UPDATE and DELETE on ledger_entries. Corrections are made by inserting reversal entries with reversal_of_id pointing to the original.
Configuration
Financial parameters are dynamic via the platform_config table:
| Key | Default | Description |
|---|
COMMISSION_RATE_BPS | 2000 | Platform commission (20%) |
MIN_PAYOUT_AMOUNT_PAISA | 10000 | ₹100 minimum withdrawal |
DISPUTE_WINDOW_HOURS | 72 | Hours before pending → available |
CUSTOMER_CANCELLATION_FEE_PAISA | 5000 | ₹50 late cancellation fee |
MECHANIC_CANCEL_PENALTY_THRESHOLD | 3 | Consecutive cancels before penalty |
MECHANIC_CANCEL_PENALTY_PAISA | 10000 | ₹100 penalty amount |
These can be changed at runtime without redeployment. Environment variables serve as fallback defaults.