> ## Documentation Index
> Fetch the complete documentation index at: https://internal.mechzie.in/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Wallet & Settlements

> Mechanic wallet, UPI verification, disputes, refunds, payouts, and settlement cycles

## 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:

```mermaid theme={null}
stateDiagram-v2
    [*] --> Pending: Payment captured
    Pending --> Available: Dispute window expires (72h)
    Pending --> Frozen: Dispute raised
    Frozen --> Available: Resolved in mechanic's favour
    Frozen --> Forfeited: Resolved in customer's favour
    Available --> PaidOut: Settlement batch runs
    PaidOut --> [*]
```

## 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

```mermaid theme={null}
sequenceDiagram
    participant Customer
    participant API
    participant Ledger
    participant Wallet
    participant Cron
    participant RazorpayX

    Customer->>API: Pays ₹450 for service
    API->>Ledger: 3-leg entry (received / earnings / commission)
    API->>Wallet: +₹360 pending balance

    Note over Cron: 72h later...
    Cron->>Ledger: earnings_available entry
    Cron->>Wallet: -₹360 pending, +₹360 available

    Note over Cron: Daily at 02:00 UTC
    Cron->>API: SettlementService.runSettlementBatch()
    API->>RazorpayX: POST /v1/payouts (UPI transfer)
    API->>Wallet: -₹360 available
    RazorpayX-->>API: Webhook: payout.processed
    API->>Ledger: payout_sent entry
```

## Wallet API (Mechanic)

### Get Wallet Summary

```bash theme={null}
GET /api/v1/wallet/summary
Authorization: Bearer MECHANIC_TOKEN
```

```json theme={null}
{
  "data": {
    "pending": 36000,
    "available": 150000,
    "locked": 0,
    "total": 186000,
    "currency": "INR"
  }
}
```

### Get Transaction History

```bash theme={null}
GET /api/v1/wallet/transactions?limit=20&offset=0
Authorization: Bearer MECHANIC_TOKEN
```

```json theme={null}
{
  "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

```bash theme={null}
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

```mermaid theme={null}
flowchart TD
    A["Mechanic adds UPI ID"] --> B["API creates Contact"]
    B --> C["API creates Fund Account"]
    C --> D["API triggers VPA Validation"]
    D --> E{"Name\nmatch?"}
    E -->|Exact / Partial| F["✅ Auto-verified"]
    E -->|Mismatch| G["⏳ Manual review queue"]
    G --> H{"Admin\napproves?"}
    H -->|Yes| F
    H -->|No| I["❌ Rejected"]
    F --> J["Ready for payouts"]

    style F fill:#dcfce7,stroke:#16a34a
    style I fill:#fee2e2,stroke:#dc2626
    style G fill:#fef3c7,stroke:#d97706
```

### Add UPI Account

```bash theme={null}
POST /api/v1/wallet/upi
Authorization: Bearer MECHANIC_TOKEN
Content-Type: application/json

{
  "vpa": "mechanic@upi",
  "beneficiary_name": "Ravi Kumar"
}
```

**Response (201):**

```json theme={null}
{
  "data": {
    "upiAccountId": "uuid",
    "message": "Verification initiated"
  }
}
```

<Note>
  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.
</Note>

### List UPI Accounts

```bash theme={null}
GET /api/v1/wallet/upi
Authorization: Bearer MECHANIC_TOKEN
```

### Admin: Manually Approve UPI

```bash theme={null}
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

```mermaid theme={null}
stateDiagram-v2
    [*] --> Open: Party raises dispute
    Open --> UnderReview: Admin picks up
    UnderReview --> ResolvedCustomer: Refund issued
    UnderReview --> ResolvedMechanic: Earnings released
    UnderReview --> Rejected: Dispute invalid
    ResolvedCustomer --> [*]
    ResolvedMechanic --> [*]
    Rejected --> [*]
```

### Raise a Dispute

```bash theme={null}
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

```bash theme={null}
GET /api/v1/wallet/disputes/job/{jobId}
Authorization: Bearer TOKEN
```

### Admin: Resolve a Dispute

```bash theme={null}
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:

```mermaid theme={null}
flowchart LR
    A["Admin initiates<br/>refund"] --> B["Razorpay<br/>Refund API"]
    B --> C["Ledger: clawback<br/>entry"]
    C --> D["Wallet: deduct<br/>from mechanic"]
    D --> E["Notify<br/>customer"]

    style A fill:#e0f2fe,stroke:#0284c7
    style E fill:#dcfce7,stroke:#16a34a
```

### Initiate Refund

```bash theme={null}
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

```bash theme={null}
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

```mermaid theme={null}
flowchart TD
    A["Cron trigger<br/>02:00 UTC"] --> B["Find eligible mechanics"]
    B --> C{"Balance ≥ ₹100?<br/>Verified UPI?"}
    C -->|No| D["Skip"]
    C -->|Yes| E["Create settlement row"]
    E --> F["PayoutService.initiatePayout()"]
    F --> G["RazorpayX POST /v1/payouts"]
    G --> H["Deduct from available wallet"]
    H --> I["Notify mechanic"]
    G -.->|"Webhook later"| J["payout.processed → settlement complete"]
    G -.->|"Webhook later"| K["payout.failed → settlement failed"]

    style A fill:#e0f2fe,stroke:#0284c7
    style J fill:#dcfce7,stroke:#16a34a
    style K fill:#fee2e2,stroke:#dc2626
```

### 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:

```mermaid theme={null}
erDiagram
    LEDGER_ACCOUNTS ||--o{ LEDGER_ENTRIES : "debit or credit"
    LEDGER_ENTRIES ||--o| WALLET_TRANSACTIONS : "backs"
    WALLETS ||--o{ WALLET_TRANSACTIONS : "contains"

    LEDGER_ACCOUNTS {
        uuid id PK
        string account_type
        string name
        uuid user_id FK
    }
    LEDGER_ENTRIES {
        uuid id PK
        uuid entry_group_id
        string idempotency_key UK
        string entry_type
        uuid debit_account_id FK
        uuid credit_account_id FK
        bigint amount
        string reference_type
        uuid reference_id
    }
    WALLETS {
        uuid id PK
        uuid user_id FK
        string type
        bigint cached_pending_balance
        bigint cached_available_balance
        bigint cached_locked_balance
    }
    WALLET_TRANSACTIONS {
        uuid id PK
        uuid wallet_id FK
        uuid ledger_entry_id FK
        string transaction_type
        bigint amount
        string balance_type
    }
```

### 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.
