# Stripe billing setup — cloud-itonami (hr-management / isco-1212 flow)

Owner-run checklist to take the billing rail (`cloud-itonami.edge.billing`)
from "code complete, no live Stripe" to **live**. Each step is either a Stripe
dashboard action or a `wrangler pages secret put`. **No secret value is ever
committed to the repo** — Cloudflare Pages secrets only (see
`docs/deployment-secrets.md`). This doc is the missing source the docstrings in
`edge/billing.cljc` and the cockpit (`site/isco_1212.cljc`) already point at.

**Referenced by**: `edge/billing.cljc` ns docstring & `create-checkout-session!`;
 `site/isco_1212.cljc`; `docs/deployment-secrets.md`.

## What you are creating

`create-checkout-session!` opens a Stripe **Subscription** carrying two
**metered** line-item prices. Usage is reported fire-and-forget by
`report-llm-proposal!` (per HR-advisor proposal) and `report-storage-usage!`
(per tenant GiB stored) via Stripe `/billing/meter_events`.

| Secret | Stripe object | kind | meter event_name | aggregate |
|---|---|---|---|---|
| `STRIPE_PRICE_LLM_PROPOSAL` | Price | metered, USD, per proposal | `hr_management_llm_proposal` | sum |
| `STRIPE_PRICE_STORAGE_GB` | Price | metered, USD, per GiB-month | `hr_management_storage_gb` | last (gauge) |
| `STRIPE_SECRET_KEY` | live key | `sk_test_… or sk_live_…` | — | — |
| `STRIPE_WEBHOOK_SECRET` | webhook signing secret | `whsec_…` | subscribes to `checkout.session.completed` | — |

**Governor cap.** Each `unit_amount` must be ≤
`kernels.billing-cap/default-max-unit-amount` (= `50000` minor units = $500.00 /
¥50,000). A price above the cap is refused at propose time
(`billing/propose-billing-change!` → `business-governor`), so pick the per-unit
figures under it. The figures below are illustrative placeholders; set the real
ones to the pricing strategy.


## Test mode is enough to clear `stripe-configured`

`GET /api/billing/status` only checks **presence** of secrets. A Stripe
**test** key (`sk_test_…`) plus two test Price ids and a test webhook secret
is enough for `readyForLiveCheckout: true` and for `/api/status` checklist
item `stripe-configured`. Use test cards for the first paid checkout; switch
to `sk_test_… or sk_live_…` when ready for real money.

All `wrangler` secret commands must target this Pages project:

```bash
npx wrangler pages secret put STRIPE_SECRET_KEY --project-name=cloud-itonami
npx wrangler pages secret put STRIPE_PRICE_LLM_PROPOSAL --project-name=cloud-itonami
npx wrangler pages secret put STRIPE_PRICE_STORAGE_GB --project-name=cloud-itonami
npx wrangler pages secret put STRIPE_WEBHOOK_SECRET --project-name=cloud-itonami
npx wrangler pages secret list --project-name=cloud-itonami
```

Verify (no secret values returned):

```bash
curl -sS https://itonami.cloud/api/billing/status | jq '{readyForLiveCheckout,readyForEntitlement,mode,missing,components}'
curl -sS https://itonami.cloud/api/status | jq '.checklist[] | select(.id=="stripe-configured" or .id=="paid-tenant")'
```

## Steps

### 1. Product
Create a product "cloud-itonami hr-management" in the Stripe dashboard. Note
the `prod_…` id (one product is enough; both prices hang off it).

### 2. Price — LLM proposal (metered, sum)
Dashboard: Products → Add product → **Recurring** → usage = **metered**;
currency USD; `unit_amount` per proposal (e.g. 50 = $0.50); interval month;
**aggregate usage = sum**. Then create a **Billing Meter** with
`event_name = hr_management_llm_proposal` and link this price to it (this is
what makes `report-llm-proposal!`'s `/billing/meter_events` posts land on this
price). Note the `price_…` id.

Reference API shape (run with the owner-held `STRIPE_SECRET_KEY`; see Stripe's
current Billing Meter docs for the exact meter-link step):
```bash
curl https://api.stripe.com/v1/prices -u "$STRIPE_SECRET_KEY:" \
  -d currency=usd -d unit_amount=50 -d product=prod_… \
  -d 'recurring[interval]=month' \
  -d 'recurring[usage_type]=metered' \
  -d 'recurring[aggregate_usage]=sum' \
  -d nickname=hr_management_llm_proposal
```

### 3. Price — storage GiB (metered, gauge/last)
Same shape; `unit_amount` per GiB-month (e.g. 20 = $0.20); **aggregate usage =
last**; meter `event_name = hr_management_storage_gb`. Note the `price_…` id.

### 4–5. Publish the two price ids
```bash
npx wrangler pages secret put STRIPE_PRICE_LLM_PROPOSAL --project-name=cloud-itonami  # paste price_… from step 2
npx wrangler pages secret put STRIPE_PRICE_STORAGE_GB --project-name=cloud-itonami  # paste price_… from step 3
```

### 6. Webhook endpoint
Stripe → Developers → Endpoints → Add endpoint:
- URL: `https://itonami.cloud/api/stripe/webhook`
- Events to send: `checkout.session.completed`

Copy the signing secret (`whsec_…`):
```bash
npx wrangler pages secret put STRIPE_WEBHOOK_SECRET --project-name=cloud-itonami
```

### 7. Secret key
```bash
npx wrangler pages secret put STRIPE_SECRET_KEY --project-name=cloud-itonami  # sk_test_… or sk_live_…
```
(Also `WEBAUTHN_KEK`, the `ITONAMI_DATA` KV binding, and any mail/Resend
secrets per `docs/deployment-secrets.md`.)

### 8. Verify live (end-to-end)
1. As a tenant admin in the cockpit, start a subscription. The cockpit POSTs
   `/api/{org}/{repo}/billing/checkout` and must get `200 {:ok true :url …}`
   — a real Stripe Checkout URL (success/cancel redirect to `itonami.cloud`,
   the only origin on `allowed-base-urls`).
2. Complete the checkout in Stripe (test card first, then live).
3. Stripe fires `checkout.session.completed` → `/api/stripe/webhook`. The
   `Stripe-Signature` HMAC is verified constant-time (`verify-webhook-signature!`),
   and `ITONAMI_DATA` gains `stripe-customer:{org}/{repo}` = the customer id
   (`handle-checkout-completed!`).
4. `GET /api/{org}/{repo}/metrics` then reflects the subscription under
   `:stripe.active-subscriptions` (`count-active-subscriptions`, scoped by the
   price-id allowlist so the ¥0 legacy subs on the account never count).

A missing secret fails closed — `500 server misconfigured` — never a silent
skip, except usage metering itself which is deliberately best-effort (a Stripe
outage must never block an HR action; see `edge.billing` ns docstring).

## After it is live
`pricing 2→3` (live Stripe) and `revenue 0→1` (billing wired) become honest in
the superproject `90-docs/business/maturity-facts.edn`. Re-run
`nbb 70-tools/bmc/bin/gftd.cljs score md` to regenerate `maturity-scores.md`.
The first real external paid tenant then moves `:revenue 1→3` and the
`:hyp/itonami-smb-pay` gate from `untested` to `measuring`.

## Owner-action prerequisites (not closed by this doc)
Per `docs/launch-blockers.md`, SaaS (external self-registration) additionally
needs: B1 Durable Object (true atomic tenant claim), legal counsel sign-off on
the 6 `[CONFIRM]` items in `legal/*.md`, and the kotoba-server prod backend.
Self-hosted dogfood (gftdcojp operating its own ventures) is unblocked on the
code side once the secrets above are set and a cron/launchd host runs
`clojure -M:tick-loop`.
