# Billing (M6)

Money is always integer **paise**. Prices in `plan_prices` are **exclusive of GST**.

## Status machine (`subscriptions.status`)

```
trialing ──(trial ends, no paid plan)──▶ grace ──(grace_ends_at passes)──▶ suspended
   │                                       ▲
   └─▶ active ⇄ past_due ──(retries exhausted: subscription.halted)──┘
active/trialing/past_due/grace ──(cancel_at reached)──▶ suspended
incomplete = checkout started, not paid (not the current subscription)
replaced   = superseded by a newer confirmed subscription
```

- **past_due**: Razorpay is retrying (`subscription.pending`). Full access; the owner is e-mailed.
- **grace**: `BILLING_GRACE_DAYS` (7) of full access with a banner and e-mail.
- **suspended**: login, Security, Billing, notifications and data export stay available. Every other page redirects owners to Billing; other members see a "workspace paused" page (HTTP 402). The publisher **holds** due posts (`retry_scheduled`, code `tenant_suspended`, re-checked every 15 min) and `SchedulePost` refuses new schedules. Nothing is dropped; paying again resumes delivery.
- Admin suspension (`tenants.status = suspended`) is separate: the whole workspace is blocked.

`billing:enforce` (hourly) applies the time-based transitions. `BillingGate` decides restriction; tenants without a subscription row (custom contracts) are never billing-restricted.

## GST

`GstCalculator` (property-tested):

- Tax = round half-up (taxable × 18%), computed **once**. Intra-state supply (buyer state = `BILLING_SELLER_STATE_CODE`) splits it into CGST + SGST (the odd paisa goes to SGST); inter-state supply charges IGST. Because of this the gross is identical under both regimes, so one Razorpay plan amount works for every buyer.
- Invoices are derived **from the amount paid** (`fromGross`). The invoice total always equals the payment, and any difference from the list price appears as a discount line.
- Invoice numbers are gap-free per Indian financial year (April–March, IST): `{BILLING_INVOICE_PREFIX}/{yy-yy}/{000001}`, allocated under a row lock in `invoice_sequences`. Invoices are immutable; refunds are corrected with credit notes (below).
- Place of supply = the billing profile state. GSTINs are validated (format, checksum, state code must match the address).
- Invoice PDFs (SAC 998314) are rendered on demand from the stored row with DomPDF.

## Razorpay

`RazorpayGateway` (REST API, Basic auth):

1. **Checkout**: `ensurePlan` creates a Razorpay plan once per price with the **gross** amount, and `createSubscription` creates a subscription and returns `short_url` (Razorpay's hosted page; card and UPI data never reach us).
   - A running trial is kept by setting `start_at` to the trial end.
   - A plan change starts when the current paid period ends; the old subscription is cancelled at cycle end once the new one is confirmed.
2. **Webhooks**: `POST /webhooks/razorpay`, verified with `X-Razorpay-Signature` (HMAC-SHA256 of the raw body with `RAZORPAY_WEBHOOK_SECRET`) and deduplicated by `X-Razorpay-Event-Id` in `billing_webhook_events`. A replay returns `duplicate`. Payments are also unique by Razorpay payment id, so a retried `charged` event never creates a second invoice. Events handled: `subscription.authenticated|activated|charged|pending|halted|cancelled|completed`, `refund.processed|failed`.
3. **Coupons**: validated server-side (active, dates, max redemptions, once per tenant). A coupon needs a Razorpay `offer_id` so the charged amount includes the discount. Redemption is recorded when the subscription is confirmed.

Configure the webhook in the Razorpay dashboard for the events above. Use test keys outside production.

`BILLING_GATEWAY=fake` (local/testing only; production always uses Razorpay) sends checkout to a local page that feeds Razorpay-shaped events through the same processor.

## Refunds and credit notes

- **Requesting**: super admin → Billing → Recent payments → Refund (amount incl. GST, reason). `RefundService::request` checks the amount against what is still refundable (paid minus processed and pending refunds), calls `POST /payments/{id}/refund`, and records a `refunds` row. Refunding does not cancel the subscription.
- **Settling**: Razorpay answers `processed` (credit note issued at once) or `pending`; `refund.processed` / `refund.failed` webhooks settle it. Refunds made in the Razorpay dashboard arrive only as webhooks and are recorded the same way. Subscribe the webhook to `refund.processed` and `refund.failed` too.
- **Credit notes** (`CreditNoteIssuer`): one per processed refund, against the payment's invoice, with the invoice's parties and tax split (CGST+SGST or IGST). A partial refund is split with `fromGross`; the refund that uses up the invoice takes exactly what is left of each component, so credit notes always sum to the invoice. Numbers have their own gap-free series: `{BILLING_CREDIT_NOTE_PREFIX}/{yy-yy}/{000001}` (`credit_note_sequences`). PDFs are listed under the invoice on the workspace's Billing page.
- Payments move to `partially_refunded` / `refunded` with `refunded_paise`; the admin "collected" figure nets refunds out.

## Plan limits

`EntitlementService` (plan features + per-tenant overrides; NULL = unlimited) is enforced at creation time: clients, users, connected accounts, storage, AI credits, **scheduled posts per month** (distinct posts scheduled in the UTC month; rescheduling is free), inbox and white-label flags. Every over-limit message ends with "Upgrade your plan under Billing".

## Screens

- `/billing` (owner manages; admins view): plan, status, checkout with a GST preview, coupon, cancel, GST billing details, invoice PDFs.
- Super admin → Billing: prices (editing a price detaches its Razorpay plan; running subscriptions keep theirs), coupons, status counts, overdue tenants, payments, webhook errors.
