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

# Payments

> Razorpay integration, double-entry ledger, commission splits, webhook processing, and cancellation fees

## Overview

MechZie uses **Razorpay** for payment collection and **RazorpayX** for mechanic payouts. Every monetary operation is backed by an **immutable double-entry ledger** — the `payments` table records Razorpay state, while `ledger_entries` is the financial source of truth.

All amounts are in **paisa** (₹1 = 100 paisa).

## Architecture

```mermaid theme={null}
graph TD
    subgraph Client
        A["Flutter App"]
    end

    subgraph API["MechZie API"]
        B["PaymentsController"]
        C["PaymentsService"]
        D["WebhookService"]
        E["CommissionService"]
        F["LedgerService"]
        G["WalletService"]
    end

    subgraph External
        H["Razorpay"]
    end

    subgraph Storage
        I[("payments table")]
        J[("ledger_entries")]
        K[("wallets")]
    end

    A -->|"create-order / verify"| B
    B --> C
    H -->|"webhook"| D
    C --> E
    D --> E
    E --> F
    E --> G
    F --> J
    G --> K
    C --> I
    D --> I
```

## Payment Flow

Two paths confirm a payment — the **optimistic verify** (fast UI) and the **webhook** (canonical truth). Both are idempotent; if they race, the first one wins.

```mermaid theme={null}
sequenceDiagram
    participant App as Flutter App
    participant API as MechZie API
    participant RP as Razorpay
    participant Ledger as Ledger + Wallet

    Note over App: Mechanic completes job
    App->>API: POST /payments/create-order
    API->>RP: orders.create(amount, currency)
    RP-->>API: orderId
    API-->>App: { orderId, amount, currency }

    App->>RP: Open Razorpay Checkout
    RP-->>App: { orderId, paymentId, signature }

    par Optimistic Path
        App->>API: POST /payments/verify
        API->>API: HMAC verify signature
        API->>Ledger: CommissionService.processOnlinePaymentEarnings()
        API-->>App: { status: captured }
    and Canonical Path
        RP->>API: POST /payments/webhook
        API->>API: WebhookService.processWebhook()
        API->>Ledger: Idempotent ledger entries
        API-->>RP: 200 OK (always)
    end
```

## Step-by-Step Integration

<Steps>
  <Step title="Create a Razorpay Order">
    After the mechanic marks a job as `completed`, the customer creates a payment order:

    ```bash theme={null}
    curl -X POST https://api.mechzie.com/api/v1/payments/create-order \
      -H "Authorization: Bearer ACCESS_TOKEN" \
      -H "Content-Type: application/json" \
      -d '{ "job_id": "uuid-of-completed-job" }'
    ```

    **Response (200):**

    ```json theme={null}
    {
      "orderId": "order_PqR1s2T3u4V5",
      "amount": 45000,
      "currency": "INR"
    }
    ```

    Amount is in **paisa**. Display as `₹${(amount / 100).toStringAsFixed(0)}`.
  </Step>

  <Step title="Open Razorpay Checkout">
    Use the Razorpay Flutter SDK:

    ```dart theme={null}
    final razorpay = Razorpay();

    razorpay.on(Razorpay.EVENT_PAYMENT_SUCCESS, (PaymentSuccessResponse response) {
      verifyPayment(response.orderId!, response.paymentId!, response.signature!);
    });
    razorpay.on(Razorpay.EVENT_PAYMENT_ERROR, (PaymentFailureResponse response) {
      showPaymentError(response.message);
    });

    razorpay.open({
      'key': 'rzp_live_YOUR_KEY_ID',
      'amount': 45000,
      'currency': 'INR',
      'order_id': 'order_PqR1s2T3u4V5',
      'name': 'MechZie',
      'description': 'Service Payment',
      'prefill': { 'contact': '+919876543210' },
    });
    ```

    <Warning>
      Use the **Razorpay Key ID** (starts with `rzp_`), not the Key Secret. The Key Secret is server-side only.
    </Warning>
  </Step>

  <Step title="Verify Payment (Optimistic)">
    After Razorpay returns success, verify the signature:

    ```bash theme={null}
    curl -X POST https://api.mechzie.com/api/v1/payments/verify \
      -H "Authorization: Bearer ACCESS_TOKEN" \
      -H "Content-Type: application/json" \
      -d '{
        "razorpay_order_id": "order_PqR1s2T3u4V5",
        "razorpay_payment_id": "pay_AbC1d2E3f4G5",
        "razorpay_signature": "hmac-sha256-signature-string"
      }'
    ```

    On success, this also triggers the **commission split** into the ledger (mechanic net earnings + platform commission). The webhook is the canonical fallback if this path fails.

    <Note>
      If verify and webhook disagree, webhook always wins. The ledger uses idempotency keys to prevent double-processing.
    </Note>
  </Step>
</Steps>

## Commission Split

When a payment is captured, `CommissionService` splits the gross amount:

```mermaid theme={null}
graph LR
    A["Gross Payment<br/>₹450"] --> B{"Commission<br/>Engine"}
    B -->|"Net (80%)"| C["Mechanic Earnings<br/>₹360 → pending wallet"]
    B -->|"Commission (20%)"| D["Platform Revenue<br/>₹90"]

    style A fill:#e0f2fe,stroke:#0284c7
    style C fill:#dcfce7,stroke:#16a34a
    style D fill:#fef3c7,stroke:#d97706
```

| Component    | Formula                    | Example (₹450 job) |
| ------------ | -------------------------- | ------------------ |
| Gross        | Payment amount             | 45000 paisa        |
| Commission   | `gross × rate_bps / 10000` | 9000 paisa (20%)   |
| Net earnings | `gross − commission`       | 36000 paisa        |

The commission rate is dynamic — stored in `platform_config` and changeable without redeployment.

## Webhook Processing

The webhook pipeline guarantees **exactly-once** processing via `payment_events.razorpay_event_id`:

```mermaid theme={null}
flowchart TD
    A["Razorpay POST /payments/webhook"] --> B["Log to webhook_logs"]
    B --> C{"HMAC\nvalid?"}
    C -->|No| D["403 + log failure"]
    C -->|Yes| E{"Duplicate\nevent?"}
    E -->|Yes| F["Return 200"]
    E -->|No| G["Insert payment_events row"]
    G --> H{"Route by\nevent type"}
    H -->|"payment.captured"| I["Update payment → captured\nJob → paid\nLedger entries"]
    H -->|"payment.failed"| J["Update payment → failed"]
    H -->|"refund.processed"| K["Update refund status"]
    H -->|"payout.processed"| L["Mark settlement complete"]
    H -->|"payout.failed"| M["Mark settlement failed"]
    I --> N["Mark event processed"]
    J --> N
    K --> N
    L --> N
    M --> N
    N --> F
```

**Handled events:**

| Razorpay Event     | Action                                                    |
| ------------------ | --------------------------------------------------------- |
| `payment.captured` | Payment → captured, job → paid, commission ledger entries |
| `payment.failed`   | Payment → failed                                          |
| `refund.processed` | Refund row → processed, payment → refunded                |
| `payout.processed` | Payout → processed, settlement → completed                |
| `payout.failed`    | Payout → failed, settlement → failed                      |
| `payout.reversed`  | Payout → reversed                                         |

**Endpoint:** `POST /api/v1/payments/webhook`

* No authentication (Razorpay sends directly)
* Verified via HMAC-SHA256 in `X-Razorpay-Signature` header
* Always returns 200 to prevent Razorpay retries

<Note>
  Frontend devs don't need to implement webhook handling — it's server-side only. But you should handle the case where `verify` succeeds but the UI should wait for the job status to update to `paid`.
</Note>

## Checking Payment Status

```bash theme={null}
GET /api/v1/payments/{jobId}
Authorization: Bearer ACCESS_TOKEN
```

**Response:**

```json theme={null}
{
  "id": "uuid",
  "job_id": "uuid",
  "type": "service",
  "razorpay_order_id": "order_PqR1s2T3u4V5",
  "razorpay_payment_id": "pay_AbC1d2E3f4G5",
  "amount": 45000,
  "status": "captured",
  "method": "upi",
  "created_at": "2026-05-21T10:30:00.000Z"
}
```

### Payment Statuses

| Status       | Meaning                                | UI Action                |
| ------------ | -------------------------------------- | ------------------------ |
| `created`    | Order created, waiting for payment     | Show checkout button     |
| `authorized` | Payment authorized (rare intermediate) | Show "Processing..."     |
| `captured`   | Payment successful                     | Show receipt ✅           |
| `failed`     | Payment failed                         | Show retry option        |
| `refunded`   | Refund processed                       | Show refund confirmation |

## Cancellation Fees

Late cancellations incur fees. The fee amount is configurable in `platform_config`.

### Customer cancellation

Cancelling from `en_route`, `arrived`, or `in_progress` → flat ₹50 fee (default). The customer cannot create new jobs until the fee is paid (402 `PAYMENT_REQUIRED`).

### Mechanic cancellation

Consecutive cancellations above a threshold (default: 3 in 30 days) → ₹100 penalty deducted from their wallet automatically.

### Check for Unpaid Fees

```bash theme={null}
GET /api/v1/payments/unpaid-fees
Authorization: Bearer ACCESS_TOKEN
```

```json theme={null}
{ "hasFees": true, "amount": 5000 }
```

Use the same `create-order` → Razorpay checkout → `verify` flow with `type: "cancellation_fee"`.

<Warning>
  Customers with unpaid fees **cannot create new jobs** (402 PAYMENT\_REQUIRED). Always check on app launch and prompt payment if needed.
</Warning>

## Pricing Model

| Component       | Description                         | Example           |
| --------------- | ----------------------------------- | ----------------- |
| `base_price`    | Set per service category (in paisa) | 35000 (₹350)      |
| Line items      | Parts + labor added by mechanic     | 10000 (₹100)      |
| `final_price`   | `base_price + SUM(line items)`      | 45000 (₹450)      |
| Commission      | Platform cut (configurable BPS)     | 9000 (₹90 at 20%) |
| Net to mechanic | `final_price − commission`          | 36000 (₹360)      |

<Warning>
  **All amounts in the API are in paisa.** Always divide by 100 for display: `₹${(amount / 100).toStringAsFixed(0)}`.
</Warning>

## Client SDKs Required

| SDK              | Package            | Purpose             |
| ---------------- | ------------------ | ------------------- |
| Razorpay Flutter | `razorpay_flutter` | Payment checkout UI |

The Razorpay Key ID will be provided via your app's environment configuration. Do not hardcode it.
