A pure cap_gate compares a firm’s metered overage debt against its configured
ceiling (firm_subscription.extra_usage_cap_inr), warns at cap_warn_pct of it
(config, S1), hard-stops AT it with the distinct terminal reason cap_reached, and is
lifted for a firm whenever a staff 'adjustment' row carries a live expires_at
— the whole thing wired into the clerk worker right after the credit gate and reading only from
config, never a hardcoded number.
Picture the firm’s overage as a car on a prepaid toll road. The credit gate is the entry barrier: no balance, no entry. But an overage firm is waved through on credit — and without S4 the road has no end-of-tether. This Story installs a fuel gauge with two marks. At 60% of the tank (the config warn line) a dashboard light comes on — the car keeps driving, we just note it. At the ceiling the barrier drops: new trips are refused, and the refusal slip says exactly “spend cap”, not “out of credits” or “our wallet is dry” — so support reads one word and knows the remedy. And there is a manager’s key: staff can grant the firm headroom that lifts the barrier until a stamped expiry. Turn the key, the firm drives tonight; the key expires on its own, the barrier comes back — no one has to remember to re-lower it.
Two numbers that already existed, paired for the first time. The ceiling is
business.firm_subscription.extra_usage_cap_inr — per-firm, RLS-scoped, in ₹
(1 credit = ₹1). The spend is the firm’s metered overage debt: how far its
balance has gone below zero (D-13). overage_debt_millicredits(balance) is
max(0, -balance) — zero while the firm is still in credit, positive only once
overage has pushed the top-up bucket negative. So the cap bites only in overage territory,
exactly where the credit gate stops caring.
cap_gate in credits.py, no I/OFour inputs, one CapDecision, unit-tested without a database:
cap_credits <= 0 → allow, no warn (no ceiling configured — gate INERT) override_active → allow, no warn (staff lifted the cap; see part 4) overage_debt >= ceiling → BLOCK, reason "cap_reached" overage_debt >= warn_pct% → allow, warn=true (non-blocking signal) otherwise → allow
The inert-when-unset rule is the safety property: a firm with no positive
extra_usage_cap_inr (the server default is 0) is never newly blocked by this
Story — the cap only ever bites a firm someone deliberately gave a ceiling. cap_reached
is a named constant, deliberately distinct from no_credits / cost_cap /
consent / platform_wallet_low so the block is attributable to the firm’s
own spend and never confused with the platform wallet.
_cap_gate in the clerk worker opens one firm-scoped session and does a small,
near-constant set of reads (DR-1): the firm’s balance (from the projection the credit gate just
granted — no second lazy grant), its ceiling (firm_subscription), the config warn %
(billing.pricing_config.cap_warn_pct, falling back to the module default when un-seeded,
the same fallback-only posture as the wallet floor), and whether a live override exists. It runs
only for firms that cleared the credit gate — i.e. positive balance or paid
overage — so it is precisely the thing that stops a runaway overage firm. The warn is a
non-blocking signal emitted whenever metering is on (pramaan.billing.credit_cap_warning);
the block honours the same credit_gate_enforced flag as the credit gate, and
stamps a terminal run_failed with stop_reason=cap_reached plus a distinct
retry-false user message pointing an admin at the remedy. Reads are never touched — only agent
runs.
“Lift the cap for that firm until expiry” reuses the S3 record_adjustment
writer rather than inventing a new one. It gains one optional argument, expires_at, written
straight onto the credit_ledger.expires_at column that S6 already added. A staff headroom
grant carrying a future expiry does two reinforcing things: the grant itself raises the
balance (real, append-only credits — grants never un-happen), and has_active_cap_override
— a single bounded WHERE expires_at > now() read — reports the cap as lifted
until that instant. This is the first runtime consumer of expires_at:
the expiry compares server-side, so the lift ends exactly on time with no sweep job to run and
nothing to remember to undo. The admin route echoes expires_at back so the grant form
confirms what it set.
| Skipped | Why it’s correct to skip |
|---|---|
| A new migration | Every column this Story needs already ships: pricing_config.cap_warn_pct (0158/S1)
and credit_ledger.expires_at (0160/S6). alembic check stays clean —
no schema change, so none was invented. |
| An expiry sweep that decrements the projection when an override lapses | The override’s runtime effect is a live expires_at > now() read, not a
balance mutation, so the cap re-applies the instant the row expires with no background job. The
granted credits are real and stay (append-only); only the cap lift is time-boxed. |
| Entangling with the platform-wallet guard (S5) | S5 is platform-level (our OpenRouter balance); S4 is per-firm (the firm’s own ceiling). They stay orthogonal gates with distinct reasons so a block is never mis-attributed. |
| A dedicated manual span for the cap gate | The gate’s reads are covered by the existing SQLAlchemy DB spans (T1) inside the clerk worker’s run span; the block/warn emit low-cardinality counters. Same instrumentation posture as the sibling credit/wallet gates — no new ADR-124 registry span needed. |
cap_gate / overage_debt_millicredits: unit-tested across
below-warn, at-warn (warn, no block), at-ceiling (block with cap_reached), live-override
(lifted), no-ceiling (inert), and positive-balance (never bites)._cap_gate clerk-worker helper: exercised against a fake session that answers the four
reads — config-sourced warn %, distinct block reason, override lift, and fallback when
pricing_config is un-seeded — mirroring the platform-wallet gate-helper tests.expiresAt writes the ledger row, echoes the expiry, and makes
has_active_cap_override return true; a plain grant is not an override. Existing
billing route + integration credit suites stay green (9/9).ruff clean; alembic upgrade/downgrade/upgrade roundtrip + alembic check
report no new operations; admin-api OpenAPI snapshot regenerated for the DTO change.