# Razorpay Payments — Vendor Store Orders

Online payment for vendor store orders. COD keeps working exactly as before;
this is the second payment mode next to it.

## Setup

1. Install (already in `package.json`):

   ```bash
   npm install
   ```

2. Add the keys to `.env` (they are read only on the server — see *Security* below):

   ```
   RAZORPAY_KEY_ID=rzp_test_xxxxxxxxxxxxxx
   RAZORPAY_KEY_SECRET=xxxxxxxxxxxxxxxxxxxxxxxx
   RAZORPAY_WEBHOOK_SECRET=xxxxxxxxxxxxxxxxxxxx
   RAZORPAY_CURRENCY=INR
   ```

3. Create the tables (idempotent, safe to re-run):

   ```bash
   node src/scripts/create-payment-tables.js
   ```

4. In the Razorpay dashboard → **Settings → Webhooks**, add
   `https://<your-host>/webhooks/razorpay` with the events
   `payment.captured`, `payment.failed`, `order.paid`, `refund.processed`,
   and use the same secret as `RAZORPAY_WEBHOOK_SECRET`.

Without `RAZORPAY_KEY_ID` / `RAZORPAY_KEY_SECRET` the server still boots and every
other API keeps working — only the payment endpoints answer `503`.

## Flow

```
1. place order      POST /api/vendors/store/buy   or   POST /api/vendors/cart/checkout
                    -> order id(s) + checkout_id
2. create payment   POST /api/payments/orders     { user_id, checkout_id | order_ids }
                    -> key_id + razorpay_order_id + amount (in paise)
3. app opens Razorpay Checkout with those values
4. verify           POST /api/payments/verify     { razorpay_order_id, razorpay_payment_id,
                                                    razorpay_signature }
                    -> payment_status = 'paid' on every order
5. webhook          POST /webhooks/razorpay       (Razorpay -> server, independent of the app)
```

Steps 4 and 5 both settle the order and are safe to run in either order or twice —
whoever gets there second is a no-op.

---

## Endpoints

All `/api/*` routes need the usual `X-API-Key` header.
The webhook does not (Razorpay cannot send it) — it is authenticated by its signature.

### `POST /api/payments/orders`

Creates the Razorpay order to open Checkout with.

```json
{ "user_id": 12, "checkout_id": "9f0c…-uuid" }
```

or, for a single order (`/store/buy`):

```json
{ "user_id": 12, "order_ids": [104] }
```

Response `201`:

```json
{
  "success": true,
  "message": "Payment order created",
  "key_id": "rzp_test_xxxxxxxxxxxxxx",
  "order_ids": [104, 105],
  "checkout_id": "9f0c…-uuid",
  "prefill": { "name": "Rohit", "email": "r@x.com", "contact": "98…" },
  "data": {
    "id": 7,
    "razorpay_order_id": "order_Nd9xY…",
    "amount": 2499.00,
    "amount_paise": 249900,
    "currency": "INR",
    "status": "created",
    "receipt": "chk_9f0c…"
  }
}
```

**The amount is summed from the `vendor_orders` rows on the server.** An amount in the
request body is ignored — a client cannot pay ₹1 for a ₹2499 order.

Errors: `400` no ids / cancelled order / amount below ₹1, `404` order not found for
this user, `409` already paid, `503` Razorpay not configured.

Tapping "Pay" twice returns the *same* Razorpay order (`"reused": true`) instead of
creating a second one.

### `POST /api/payments/verify`

Send back exactly what Razorpay Checkout handed the app.

```json
{
  "razorpay_order_id": "order_Nd9xY…",
  "razorpay_payment_id": "pay_Nd9zK…",
  "razorpay_signature": "9ef4…"
}
```

Response `200`:

```json
{
  "success": true,
  "message": "Payment verified successfully",
  "data": { "status": "paid", "method": "upi", "paid_at": "2026-08-27T…" },
  "orders": [ { "id": 104, "payment_status": "paid", "status": "pending", … } ]
}
```

What it checks, in order:

1. the HMAC-SHA256 of `"<order_id>|<payment_id>"` against the Key Secret
   (constant-time compare) — a mismatch is `400` and changes nothing;
2. a `payments.fetch` straight from Razorpay: the payment really belongs to this
   order, the amount matches to the paisa, and it was captured/authorised;
3. only then are `vendor_payments` and every `vendor_orders` row marked paid, with a
   `payment_received` entry added to `order_status_history`.

If Razorpay is unreachable in step 2 the signature from step 1 is still cryptographic
proof of a genuine handoff, so the order settles and the webhook remains the backstop.

Order `status` stays `pending` after payment — the vendor still has to accept it.

### `GET /api/payments/order/:razorpay_order_id?user_id=12`

Poll this when Checkout closes without a clean callback (app killed, network drop).
Returns the payment plus the orders behind it.

### `GET /api/payments/user/:user_id`

Payment history for a user, each with its `order_ids`.

### `POST /webhooks/razorpay`

Razorpay → server. Handles:

| event | effect |
|---|---|
| `payment.captured`, `payment.authorized`, `order.paid` | payment + orders → `paid` |
| `payment.failed` | payment → `failed`, pending orders → `failed` (the order stays open for a retry) |
| `refund.processed`, `refund.created` | payment + orders → `refunded` / `partially_refunded` |

Anything else is acknowledged and ignored. Returns `400` on a bad signature, `503` if
`RAZORPAY_WEBHOOK_SECRET` is not set — an unverified event is never acted on.

---

## Security

- **The Key Secret never leaves the server.** It is read from `.env` into
  [src/config/razorpay.js](src/config/razorpay.js) and used only to sign and verify.
  The client only ever receives `key_id`, which is public by design.
- **Amounts come from our own tables**, never from the request body.
- **Nothing becomes `paid` without a valid signature.** Signature comparison is
  constant-time (`crypto.timingSafeEqual`), and unequal lengths are rejected before
  the compare.
- **The webhook verifies the raw request body.** It is mounted before
  `express.json()` with `express.raw()` — re-serialised JSON would not match the
  signed bytes.
- A forged verify call cannot flip somebody else's pending payment to `failed`:
  a signature mismatch changes no state at all.
- Settlement is idempotent and row-locked (`SELECT … FOR UPDATE`), so the checkout
  callback and the webhook racing each other cannot double-settle.
- `razorpay_order_id` is `UNIQUE`, so the same gateway order can never be recorded twice.

## Tables

`vendor_payments` — one row per Razorpay order (which may cover several
`vendor_orders`, since a cart checkout splits into one order per item).

| column | note |
|---|---|
| `razorpay_order_id` | unique |
| `razorpay_payment_id`, `razorpay_signature` | filled on success |
| `amount` / `amount_paise` | rupees for us, paise for Razorpay |
| `status` | `created` → `paid` / `failed` / `refunded` / `partially_refunded` |
| `error_code`, `error_description` | from `payment.failed` |

`vendor_orders` gains `payment_id` (→ `vendor_payments`), `gateway_signature` and
`paid_at`. The existing `payment_gateway` / `gateway_order_id` / `gateway_payment_id`
columns are filled in as well, so anything already reading them keeps working.

## Not included

Raising refunds is not wired up — cancelling a paid order still reports
`refund: null`. Refunds started from the Razorpay dashboard *are* picked up by the
webhook and reflected in `payment_status`.
