Skip to content

feat: account-wide pooled module overage (migration 030)#47

Closed
I-am-nothing wants to merge 2 commits into
mainfrom
feat/account-wide-module-overage
Closed

feat: account-wide pooled module overage (migration 030)#47
I-am-nothing wants to merge 2 commits into
mainfrom
feat/account-wide-module-overage

Conversation

@I-am-nothing

Copy link
Copy Markdown
Contributor

What

Moves module overage from per-app to a single account-wide pool of 5 included modules, per the owner spec 2026-07-05 (confirmed reversal of the base-fee v1 per-app tier).

  • One pool of 5 for the whole account — sum module_count across all live apps; $3/month for each module beyond the pooled 5, charged once at the account level.
  • The flat $20/app base fee is unchanged and stays per-app. Only the $3/module overage math moved.
  • One 3-day grace timer per account.

How

Schema — migration 030 (030 was free in this worktree; 028 was the prior latest):

  • accounts.overage_since TIMESTAMPTZ NULL — the one grace-timer anchor.
  • account_overage_snapshots — per-(account, period_start) charge ledger; the double-charge guard, mirroring app_base_snapshots.

Pricing (usage/basefee.go): removed the per-app AppBaseFeeMicros overage term; added AccountOverageMicros(pooledCount) + ProratedOverageMicros. Per-app base is now flat.

Timer: RegisterApp / SyncAppModules recompute the pool after every module_count write and arm/clear overage_since (first-cross-wins; idempotent clear; no refund on drop, D1e).

Mid-period grace charge: a new sweep in cmd/billing-cycle charges the pooled overage prorated from grace-end → period-end once the 3-day window elapses — a deliberate mid-period charge (the D1b "no mid-period charges" rule now excludes the overage leg; base-fee behavior is unchanged). Deterministic per-(account, period) Stripe idem keys + the snapshot ledger make it money-safe.

Boundary advance leg (charge.go): each app contributes only its flat base; a separate pooled-overage term bills the closing period once, skipped when the sweep already billed it (source='advance').

Display: GetAccountBillResponse gains AccountOverageMicros (additive — no field removed/renamed; included in TotalMicros), snapshot-first else a live pooled estimate. Per-app displayed base is now flat.

Tests

Pool crossing 5 arms the timer; dropping under clears it; grace holds for exactly 3 days; the mid-period charge fires once and is excluded from the next boundary (no double-charge — the key test); interleaved installs sum to the account pool; uninstall never refunds. make test / go vet / go build ./... all clean.

Follow-up / notes

  • Docs: mirrorstack-docs/db/ms_billing/ needs a companion update for the new account_overage_snapshots table + accounts.overage_since column — flagging, not doing it here (someone will consolidate docs across the three related billing PRs).
  • Frontend not wired — the AccountOverageMicros wire field is additive so existing consumers are unaffected; the web line is a separate PR.
  • Edge case I made a judgment call on: when grace hasn't elapsed by the boundary cutover (or overage started very late in the period), the boundary charges the full pooled overage for the closing period per the spec's literal wording. This slightly tensions with "grace protects for 3 days," but the snapshot + idem-key machinery keeps it money-safe (each period billed exactly once), so I followed the spec as written. In the common daily-cron path the sweep bills each period and the boundary always sees the snapshot and skips.

🤖 Generated with Claude Code

I-am-nothing and others added 2 commits July 5, 2026 06:17
Move module overage from PER-APP to a single ACCOUNT-WIDE POOL of 5
included modules, per the owner spec 2026-07-05 (a deliberate reversal of
the base-fee v1 per-app tier).

- Schema (030): accounts.overage_since (one grace timer per account) +
  account_overage_snapshots (per-(account, period) charge ledger, the
  double-charge guard mirroring app_base_snapshots).
- Pricing: per-app base is now FLAT $20; overage = $3 x max(0,
  SUM(live-app module_count) - 5) at the account level
  (usage.AccountOverageMicros / ProratedOverageMicros).
- Timer: RegisterApp / SyncAppModules recompute the pool after every
  module_count write and arm/clear accounts.overage_since (first-cross-wins
  / idempotent clear, no refund on drop).
- Mid-period grace charge: a new cmd/billing-cycle sweep charges the pooled
  overage prorated from grace-end to the period end once the 3-day grace
  window elapses (a deliberate mid-period charge; base-fee mid-period
  behavior is unchanged). Deterministic per-(account, period) Stripe idem
  keys + the snapshot ledger make it money-safe.
- Boundary advance leg: each app now contributes only its flat base; a
  SEPARATE pooled-overage term bills the closing period ONCE, skipped when
  the sweep already billed it (snapshot guard), source='advance'.
- Display: GetAccountBillResponse gains AccountOverageMicros (additive,
  snapshot-first else live pooled estimate); per-app base display is flat.

Tests cover: pool crossing 5 arms the timer, dropping under clears it,
grace holds for exactly 3 days, the mid-period charge fires once and is
excluded from the boundary (no double-charge), interleaved installs sum to
the account pool, and uninstall never refunds.

mirrorstack-docs/db/ms_billing/ needs a companion update for the new
account_overage_snapshots table + accounts.overage_since column.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… findings)

PR #47 adversarial review found the grace sweep and the boundary leg could
both charge the same period's pooled overage if a crash landed between a
successful Stripe call and the account_overage_snapshots write (disjoint
Idempotency-Key namespaces, so Stripe never deduped it). Adds a status
column ('pending'/'charged') to account_overage_snapshots: the claim is now
written BEFORE either leg calls Stripe, so a crash anywhere leaves durable
evidence the other leg must respect.

Also fixes the boundary reclaim livelock (a reclaimed run recomputed the
overage to 0 instead of reusing the frozen amount, colliding with its own
stable Idempotency-Key) and threads the genuine Stripe invoice item id back
from the boundary's combined charge instead of storing the idempotency-key
string.

Judgment call (no product decision available): pooled-overage growth after
a period's grace charge now gets an incremental top-up, conservatively
prorated from the sweep's own instant forward (never retroactive) — flagged
in cycle/overage.go's topUpGraceOverage doc for owner review.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@I-am-nothing

Copy link
Copy Markdown
Contributor Author

Superseded by #48 — the overage concept (5 included modules, $3/module beyond) carries forward, but the mechanism is fully replaced: single account-wide timer → one independent 3-day timer per module instance, anchored to that module's own install date with live FIFO re-evaluation. Branch feat/account-wide-module-overage kept on origin.

I-am-nothing added a commit that referenced this pull request Jul 6, 2026
feat: unified base-fee + per-module overage timer model (supersedes #45, #46, #47)
I-am-nothing added a commit that referenced this pull request Jul 6, 2026
Replaces the account-wide pooled overage model with per-module-instance
overage timers (migration 033), merges in the app-creation grace period
and the auto-collect disclosure flag, and unifies base fee + module
overage into one charge concept. Supersedes #45, #46, #47.

Key mechanics: 3-day creation grace (deleted WITHIN grace = never
charged; after = still owes); 5 included modules account-wide with
$3/module beyond, one install-anchored timer per module instance with
live FIFO re-evaluation (over->included only); coverage contract —
every grace charge covers install/creation day through the END of the
period its grace elapses into, boundary legs count only entities whose
(immutable) install + grace-expiry predate the new period; all Stripe
charges via draft->pinned-items->finalize with ms_charge_ref metadata;
migration 035/036 markers + FindInvoiceByRef recovery for retries past
Stripe's idempotency-key window (gates bypassed only when a finalized
invoice actually exists); per-app advisory-locked timer synthesis and
deletion. Migrations 029-036.

Squash of the 32-commit PR branch, including two adversarial-review fix
waves (23 + 12 confirmed findings fixed; full record on PR #48 and in
docs-temp/pr48-review/findings.md). go build/vet/gofmt clean; full unit
+ integration suites green against real Postgres (migrations 001->036).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant