# Billing: plans, invoices, payments

BitCall bills each **account** (not each app) a monthly platform fee plus overage
for relayed minutes above the plan's included minutes. The logic lives in
`internal/billing` (tier catalog, invoice computation, Razorpay client); the API is
`internal/api/ext_billing.go`.

- Public: `GET /v1/billing/plans`, `POST /v1/billing/webhook/razorpay`
- Admin (staff with `billing.read` / `billing.write`): plans per account, month close,
  invoice lifecycle, invoice list
- Customer portal (member permission `billing.read`): own plan and invoices

Accounts **without** a plan keep the legacy per-app statement
(`GET /v1/admin/invoices`, `GET /v1/account/apps/{id}/invoice`): nothing changes for
them until finance puts them on a tier.

## Tiers

The catalog is a code constant (`billing.Catalog`), returned by `GET /v1/billing/plans`:

| Tier | USD / month | INR / month | Included relayed minutes | Overage per minute (USD / INR) |
|------|------------:|------------:|-------------------------:|-------------------------------:|
| `sandbox` | 0 | 0 | 0 (sandbox apps use their own quota) | – |
| `starter` | 99 | 7,999 | 5,000 | 0.004 / 0.35 |
| `growth`  | 299 | 24,999 | 25,000 | 0.004 / 0.35 |
| `scale`   | 499 | 41,999 | 100,000 | 0.003 / 0.25 |
| `custom`  | set per account | | | |

Included minutes are shared by **all live apps** of the account. Sandbox (`test`) apps
are never invoiced; they are limited by `TEST_MONTHLY_MINUTES`.

### Putting an account on a plan

```
PUT /v1/admin/accounts/{id}/plan            (billing.write)
{ "tier": "growth", "currency": "INR", "notes": "signed 2026-10-01" }
```

- `currency` is `USD` or `INR` and chooses the catalog column (defaults to `CURRENCY`).
- `custom` **requires** `fee_monthly`, `included_minutes` and `overage_per_minute`.
  For the other tiers those fields are optional overrides (negotiated discounts); omit
  them to use the catalog.
- The plan records `started_at` and `set_by` (staff id, or `admin` for the admin key).
- `GET /v1/admin/accounts/{id}/plan` and `GET /v1/account/plan` return `{plan}` (`null`
  when none).

## Closing a month

```
POST /v1/admin/billing/close?month=2026-09[&tax_pct=18]      (billing.write)
```

creates **one draft invoice per account** that has a plan and at least one live app
(`created`), recomputes existing **drafts** of that month (`updated`, keeping their
number, credits and notes) and leaves sent/paid/overdue/void invoices alone
(`skipped`). Run it any time after the 1st; re-running is safe.

Computation (`billing.Compute`):

```
fee      = plan fee
used     = ceil( sum of live apps' relayed seconds in the month / 60 )
overage  = max(0, used - included) x overage_per_minute
tax      = (fee + overage - credits) x tax_pct / 100      (0 unless tax_pct is sent)
total    = fee + overage - credits + tax
```

Lines: one `fee`, one `usage` per live app (quantity = minutes, amount 0, for
transparency), one `overage`, plus `credit` and `tax` when non-zero. Numbers are
`BC-YYYY-MM-NNNN`, a sequence per month. All money is rounded to 2 decimals.

### Invoice lifecycle

| Status | Set by |
|--------|--------|
| `draft` | close month. Editable: `PATCH /v1/admin/invoices/{id} {credits?, notes?}` recomputes the total. |
| `sent` | `POST /v1/admin/invoices/{id}/send`: sets `issued_at` = now, `due_at` = +14 days, creates a Razorpay payment link when configured, and emails the account owner ("BitCall invoice BC-… for YYYY-MM": lines, total, due date, payment link or bank-transfer details). Calling `send` again on a sent/overdue invoice re-sends the email without changing the dates. |
| `overdue` | automatic: `sent` invoices past `due_at` flip hourly and whenever a list is requested. |
| `paid` | `POST /v1/admin/invoices/{id}/mark-paid {payment_ref?, paid_at?}` (bank transfer) or the Razorpay webhook (`payment_ref` = Razorpay payment id). |
| `void` | `POST /v1/admin/invoices/{id}/void {reason}`; the reason is appended to `notes`. |

Paid and void invoices are final: `send`, `mark-paid`, `void` and `PATCH` answer
`409 {"error":{"code":"invalid_state"}}`.

### Lists

- `GET /v1/admin/billing/invoices?month=&status=&account_id=` (`billing.read`) →
  `{invoices, totals, counts, month}`. Each row carries `account_email`; `totals` sums
  `total` per status (`draft, sent, paid, overdue, void`) over the month/account filter,
  `counts` is the number of invoices per status.
- `GET /v1/admin/invoices/{id}` (`billing.read`).
- Customer: `GET /v1/account/invoices` → `{invoices, plan}` and
  `GET /v1/account/invoices/{id}` (member permission `billing.read`: owner, admin,
  finance, support, viewer). Customers only see invoices of their own account and never
  see drafts (another account's or a draft id is `404`).

### Roles

`finance` and `superadmin` hold `billing.write`; `sales` and `viewer` have `billing.read`;
`ops` and `support` have neither (403 `forbidden`). See `docs/ROLES.md`.

## Razorpay (optional)

Set in the environment:

| Variable | Meaning |
|----------|---------|
| `RAZORPAY_KEY_ID`, `RAZORPAY_KEY_SECRET` | API keys (Dashboard → Settings → API Keys). When both are set, `send` creates a Payment Link. |
| `RAZORPAY_WEBHOOK_SECRET` | Secret entered when creating the webhook in the Dashboard. Without it the webhook endpoint answers 404. |
| `RAZORPAY_API_URL` | Override of `https://api.razorpay.com` (tests / sandboxes). |
| `BILLING_BANK_DETAILS` | Free text printed in the invoice email when there is no payment link (bank, account, IFSC, SWIFT). |
| `BILLING_INR_PER_USD` | Fixed rate used only by the MRR metric to express both currencies in `CURRENCY` (default 84). |

On `send`, with keys set, the server calls `POST /v1/payment_links` (basic auth) with
the amount in paise (INR) or cents (USD), `reference_id` = invoice id, the customer's
email, `notify.email=false` (we send our own email) and
`callback_url = <PUBLIC_URL>/portal/billing.html`. The link's `short_url` and id are
stored on the invoice (`payment_link`, `payment_link_id`) and included in the email.
If Razorpay rejects the call, `send` answers `502 {"error":{"code":"razorpay_error"}}`
and the invoice stays a draft.

Webhook: in the Razorpay Dashboard add `https://<your host>/v1/billing/webhook/razorpay`
with the event **`payment_link.paid`** and the secret from `RAZORPAY_WEBHOOK_SECRET`.
The server verifies `X-Razorpay-Signature` (hex HMAC-SHA256 of the raw body) and marks
the invoice whose id equals `payload.payment_link.entity.reference_id` as paid with
`payment_ref` = `payload.payment.entity.id`. Bad or missing signatures are `401
bad_signature`; replays and other events are acknowledged with 200 and ignored. A paid
amount that differs from the invoice total is logged, not rejected.

Local test of the webhook:

```sh
BODY='{"event":"payment_link.paid","payload":{"payment_link":{"entity":{"id":"plink_1","reference_id":"inv_…"}},"payment":{"entity":{"id":"pay_1","amount":799900,"currency":"INR"}}}}'
SIG=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$RAZORPAY_WEBHOOK_SECRET" | awk '{print $NF}')
curl -X POST localhost:8080/v1/billing/webhook/razorpay -H "X-Razorpay-Signature: $SIG" -d "$BODY"
```

## GST note (India)

Indian customers are charged 18 % GST on the platform fee and overage. Close the month
with `tax_pct=18` for the Indian billing run (`POST /v1/admin/billing/close?month=…&tax_pct=18`);
the tax is a separate line and is recomputed when a credit is applied to the draft. Keep
the company's GSTIN and the customer's GSTIN in `BILLING_BANK_DETAILS` / plan `notes`
until a dedicated field exists; export to the accountant comes from the invoice list.
Export customers billed in USD carry no GST (`tax_pct` omitted) — confirm the
place-of-supply rules with the accountant before the first run.

## Metrics hook

`func (s *Server) BillingMRR(month string) (mrr float64, paying int)`:
MRR = sum of monthly fees of every enabled account on a non-sandbox plan, plus the overage
of the last closed month at or before `month`, expressed in `CURRENCY` (the other
currency is converted with `BILLING_INR_PER_USD`); `paying` = distinct accounts with a
`paid` invoice in `month`.

## Operations checklist (monthly)

1. 1st of the month: `POST /v1/admin/billing/close?month=<previous month>` (India run
   with `tax_pct=18`), review drafts in the admin panel, apply credits.
2. `send` each draft (Razorpay link or bank details go out by email).
3. Bank transfers: `mark-paid` with the NEFT/SWIFT reference. Razorpay payments mark
   themselves through the webhook.
4. Overdue invoices show up in `GET /v1/admin/billing/invoices?status=overdue`.
