Skip to main content

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:
BalanceMeaningCan withdraw?
PendingEarned but inside dispute window (default 72h)
AvailablePast dispute window, ready for payout
LockedFrozen 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:
  1. Dispute row created (status: open)
  2. Mechanic’s pending earnings for this job are frozen in the ledger
  3. 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"
}
ResolutionEffect
resolved_customerMechanic loses frozen earnings; customer gets refund
resolved_mechanicFrozen earnings released to mechanic’s available balance
rejectedSame 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:
ScheduleJobWhat it does
Every hourrelease-earningsMoves pending → available for jobs past the dispute window
Daily 02:00 UTCrun-settlement-batchPays out all mechanics with available balance ≥ ₹100

Settlement Batch Flow

Eligibility Criteria

A mechanic is included in a settlement batch when:
  1. Their available wallet balance ≥ MIN_PAYOUT_AMOUNT_PAISA (default ₹100)
  2. They have a verified UPI account set as primary
  3. 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

AccountPurpose
PLATFORM_COLLECTIONSHolds collected payments before payout
PLATFORM_COMMISSIONAccumulates platform commission revenue
PLATFORM_COD_RECEIVABLETracks COD amounts owed by mechanics
PLATFORM_CANCELLATION_REVENUECancellation 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:
KeyDefaultDescription
COMMISSION_RATE_BPS2000Platform commission (20%)
MIN_PAYOUT_AMOUNT_PAISA10000₹100 minimum withdrawal
DISPUTE_WINDOW_HOURS72Hours before pending → available
CUSTOMER_CANCELLATION_FEE_PAISA5000₹50 late cancellation fee
MECHANIC_CANCEL_PENALTY_THRESHOLD3Consecutive cancels before penalty
MECHANIC_CANCEL_PENALTY_PAISA10000₹100 penalty amount
These can be changed at runtime without redeployment. Environment variables serve as fallback defaults.