# Pathrule Pattern: Subscriptions & Usage Billing (1.0.0)
# ::pathrule:package:subscriptions-usage-billing

### [RULE] Report usage through Billing Meters with idempotent meter events  (path: /src/usage)
<!-- scope: folder | priority: high | strict -->

Meter all usage through Stripe Billing Meters and meter events. The legacy usage-based billing APIs (subscription_items.create_usage_record / usage_records) were removed in API version 2025-03-31.basil and only exist on pinned versions on or before 2024-09-30.acacia. Do not use them in new code.

- Set a unique `identifier` on every meter event. Stripe enforces uniqueness within a rolling window of at least 24 hours, so reusing the same identifier on a retry dedupes the event. Use a UUID-like value derived from your own ledger row, not a random per-call value, so retries actually collide.
- Keep the event `timestamp` within the past 35 calendar days and no more than 5 minutes in the future, or Stripe rejects the event. The event is bucketed into the window containing `timestamp`, not the time you sent it, so backfills land in the correct period.
- Above roughly 1,000 events/sec, switch from the synchronous v1 `POST /v1/billing/meter_events` to the v2 Meter Event Stream: create a meter event session, use its session token (valid 15 minutes, refresh before expiry) to stream up to 10,000 events/sec. The high-throughput stream is live-mode only.
- Write to your own `usage_events` ledger first and treat it as the source of truth. Stripe meter event summaries are for billing aggregation, not for showing customers their live usage.

---

### [RULE] Verify, dedupe, and acknowledge Stripe webhooks before doing work  (path: /src/api/webhooks)
<!-- scope: folder | priority: high | strict -->

Drive all billing state transitions from verified webhooks, never from browser redirects, because the user can close the tab before the redirect fires.

- Verify the signature with `stripe.webhooks.constructEvent` using the unparsed raw request body and the endpoint secret. JSON body-parser middleware re-serializes the payload and silently breaks HMAC verification, so exempt the webhook route from global JSON parsing and read the raw buffer.
- Make handlers idempotent on `event.id`: a UNIQUE constraint on a processed-events table, checked before mutating state. Stripe can deliver the same event more than once.
- Record the processed `event.id` and the business side effect in the SAME database transaction. If you commit the side effect but crash before recording the id, the next retry double-applies it.
- Return 2xx within seconds and offload slow work (emails, ERP sync, provisioning) to a queue. Long handlers trip Stripe's retry timeout and cause duplicate deliveries.
- Subscribe to `v1.billing.meter.error_report_triggered` and alert on `meter_event_customer_not_found` spikes, which mean a usage integration is silently dropping events.

---

### [RULE] Never hand-roll proration, prices, or dunning math  (path: /src/billing)
<!-- scope: folder | priority: high | advisory -->

Plan changes, seat counts, and failed-payment recovery are owned by Stripe Billing. Computing any of this yourself mis-charges customers, which is a money-correctness bug.

- For upgrades and downgrades, update the subscription item and let Stripe compute proration. Choose `proration_behavior` deliberately: `always_invoice` creates prorations and immediately invoices and collects (use for upgrades you want paid now); `create_prorations` creates the line items but does not invoice until the next cycle; `none` skips proration entirely. Never compute partial-period charges by hand.
- Set `proration_behavior: 'none'` when the subscription's latest invoice is unpaid, so you do not credit a customer for time they have not paid for.
- Model seats as a licensed (per-seat) subscription item `quantity`; update the quantity on team-membership changes so Stripe prorates automatically. Do not create one subscription per seat.
- Never store prices or amounts locally. Always reference Stripe price IDs so the charged amount stays authoritative and packaging changes do not require a deploy.
- Configure dunning with Smart Retries rather than custom retry loops, and react to `invoice.payment_failed`, `invoice.paid`, and `customer.subscription.updated` to flip account status.

---

### [MEMORY] Entitlements are derived from subscription webhooks, with paginated refetch  (path: /src/billing)

Stripe Entitlements expose what each customer can access based on their active subscription. We mirror them into a local `entitlements` table rather than hardcoding plan-to-feature maps, because Stripe recommends persisting them for read performance instead of calling the list API on every gate check.

- On `customer.subscription.created`, `customer.subscription.updated`, `customer.subscription.deleted`, and `entitlements.active_entitlement_summary.updated`, refetch the customer's active entitlements via the list API and upsert them. Do NOT read the entitlement list out of the webhook payload: the `active_entitlement_summary.updated` event is a known footgun that only carries the first 10 entitlements, so payloads with more than 10 silently drop the rest. List with pagination to get the full set.
- Gate every paid feature on the stored entitlement `lookup_key`, not on the price ID or product name, so packaging changes need no code edits.
- Treat `customer.subscription.deleted` and `past_due`/`unpaid` states as access removal, but drive a short grace window off `status` rather than deleting rows immediately.
- Reconcile nightly by listing live subscriptions and entitlements to heal any webhook that was missed or truncated.

See /src/api/webhooks for signature verification and idempotency, and /src/usage for metering.

---

### [MEMORY] Stripe owns proration and dunning; we configure and react to events  (path: /src/billing)

We deliberately do not own billing math. Stripe Billing computes proration, runs the retry schedule, and emits events; our job is configuration plus reaction. This keeps our database as the source of truth for usage and entitlements while treating Stripe events as a queue we reconcile against.

- Dunning uses Smart Retries (AI-timed retry attempts), not a fixed schedule we maintain. The current Stripe-recommended default is 8 attempts within 2 weeks; the policy is configurable from 1 week up to 2 months. Older docs and tutorials cite a fixed 7-retries-over-21-days schedule, which is the legacy default and no longer the recommendation.
- Track recovery off `invoice.payment_failed` (read `attempt_count` and `next_payment_attempt`) and `invoice.paid`. Hard declines keep incrementing `attempt_count` but only actually retry once a new payment method is attached, so do not assume a scheduled retry will run.
- Flip account status (active / past_due / canceled) from subscription and invoice events, never from a redirect or a client call.
- Because Stripe is authoritative, a nightly reconciliation job that diffs Stripe subscriptions/entitlements against our tables is the safety net for any dropped webhook.

See /src/billing entitlement memory for feature gating, and the billing rule for the hard constraints on proration_behavior and price IDs.

---

### [SKILL] subscriptions-usage-billing-review  (path: /)

---
name: subscriptions-usage-billing-review
description: Review checklist for subscription and usage-based billing changes on Stripe Billing. Use before merging any code that records usage, handles billing webhooks, syncs entitlements, changes plans or seats, or touches dunning.
---

# Subscriptions and usage billing review

## Metering
- [ ] Usage is reported via Billing Meters and meter events, not the removed `usage_records` API.
- [ ] Every meter event sets a unique `identifier` derived from the local ledger row so retries dedupe inside Stripe's 24h+ window.
- [ ] Meter event timestamps fall within the past 35 days and under 5 minutes in the future.
- [ ] Volume above ~1,000 events/sec uses the v2 Meter Event Stream with a 15-minute session token (live mode only).
- [ ] A local `usage_events` ledger is written first and treated as the source of truth.

## Webhooks
- [ ] Signatures are verified against the unparsed raw body and endpoint secret; the route is exempt from global JSON parsing.
- [ ] Handlers are idempotent on `event.id` via a UNIQUE constraint.
- [ ] The `event.id` record and the business side effect commit in the same transaction.
- [ ] Handlers return 2xx quickly and push slow work to a queue.
- [ ] `v1.billing.meter.error_report_triggered` is subscribed and alerts on `meter_event_customer_not_found`.

## Entitlements
- [ ] Entitlements are synced from subscription and `active_entitlement_summary.updated` webhooks by refetching via the list API, not by reading the (10-item-truncated) webhook payload.
- [ ] Feature gates read the stored entitlement `lookup_key`, not the price ID or product name.
- [ ] A nightly reconciliation job re-lists subscriptions and entitlements to heal missed or truncated events.

## Proration, seats, dunning
- [ ] Plan changes update the subscription item and let Stripe handle proration; no hand-rolled proration math.
- [ ] `proration_behavior` is chosen deliberately (`always_invoice` vs `create_prorations` vs `none`), and is `none` when the latest invoice is unpaid.
- [ ] Seats are modeled as a per-seat item `quantity` that updates on membership changes.
- [ ] No prices or amounts are stored locally; code references Stripe price IDs.
- [ ] Dunning relies on Smart Retries plus reactions to `invoice.payment_failed` (checking `attempt_count`) and `invoice.paid`.
