Overview
MechZie uses Razorpay for payment collection and RazorpayX for mechanic payouts. Every monetary operation is backed by an immutable double-entry ledger — thepayments table records Razorpay state, while ledger_entries is the financial source of truth.
All amounts are in paisa (₹1 = 100 paisa).
Architecture
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.Step-by-Step Integration
Create a Razorpay Order
After the mechanic marks a job as Response (200):Amount is in paisa. Display as
completed, the customer creates a payment order:₹${(amount / 100).toStringAsFixed(0)}.Verify Payment (Optimistic)
After Razorpay returns success, verify the signature: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.
If verify and webhook disagree, webhook always wins. The ledger uses idempotency keys to prevent double-processing.
Commission Split
When a payment is captured,CommissionService splits the gross amount:
| Component | Formula | Example (₹450 job) |
|---|---|---|
| Gross | Payment amount | 45000 paisa |
| Commission | gross × rate_bps / 10000 | 9000 paisa (20%) |
| Net earnings | gross − commission | 36000 paisa |
platform_config and changeable without redeployment.
Webhook Processing
The webhook pipeline guarantees exactly-once processing viapayment_events.razorpay_event_id:
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 |
POST /api/v1/payments/webhook
- No authentication (Razorpay sends directly)
- Verified via HMAC-SHA256 in
X-Razorpay-Signatureheader - Always returns 200 to prevent Razorpay retries
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.Checking Payment Status
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 inplatform_config.
Customer cancellation
Cancelling fromen_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
create-order → Razorpay checkout → verify flow with type: "cancellation_fee".
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) |
Client SDKs Required
| SDK | Package | Purpose |
|---|---|---|
| Razorpay Flutter | razorpay_flutter | Payment checkout UI |