feat: disclose large auto-collected charges (migration 031)#45
Closed
I-am-nothing wants to merge 3 commits into
Closed
feat: disclose large auto-collected charges (migration 031)#45I-am-nothing wants to merge 3 commits into
I-am-nothing wants to merge 3 commits into
Conversation
Add a post-hoc TRANSPARENCY flag for off-session charges that ALREADY succeeded, once they cross a per-account size threshold — so customers aren't surprised by a large automatic debit. This changes NO charging behaviour; it is complementary to (and distinct from) the spend-ceiling gate, which SKIPS a charge before it fires. - migration 031: accounts.auto_collect_threshold_micros (BIGINT NULL, NULL = $100 default) + invoices.is_large_auto_collect (BOOLEAN NOT NULL DEFAULT false). - collection.DefaultAutoCollectThresholdMicros ($100) + ResolveAutoCollectThreshold + IsLargeAutoCollect (strict-> : exactly-at threshold is NOT flagged, matching ExceedsSpendCeiling's dollar-cap reading). - Server-computed at charge time (threshold resolved when the charge fires, not later) in BOTH off-session charge legs: RunBillingCycle (arrears + advance base) and RegisterApp proration. - Wired through InvoiceMirror/UpsertInvoice and exposed on the wire as InvoiceRow.is_large_auto_collect (additive) for the web billing page. - Tests: collection flag logic (below/above/at/override/NULL) + cycle wiring (large flags, small doesn't, per-account override respected). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Stripe only ever charges whole cents (round-half-up, cycle.centsFromMicros). IsLargeAutoCollect compared the raw pre-rounding micros amount against the threshold, so a charge strictly between the threshold and threshold+5,000 micros (half a cent) rounds DOWN to exactly the threshold's dollar amount when actually charged, yet was still flagged "large" — contradicting the "exactly at threshold is not large" contract. Round both the charged amount and the threshold to cents (the same conversion Stripe applies) before comparing, so the flag can never disagree with the money that actually moved. Corrects one existing test (TestIsLargeAutoCollect_ZeroOverrideFlagsAny...) whose old assertion baked in the same raw-micros-vs-charged-cents mismatch (a 1-micro "charge" rounds to $0.00 actually charged, so it must not be flagged over a $0 threshold either) — documented inline as a judgment call. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…legs RunBillingCycle resolved the account's auto_collect_threshold_micros near the top of the function — before the risk gate, the PM check, and both Stripe HTTP calls — while RegisterApp's proration leg already resolved it AFTER its own Stripe charge succeeded. A concurrent threshold edit landing mid-charge was therefore honored inconsistently between the two call sites, even though both are documented as resolving "at charge time". RunBillingCycle now re-resolves the threshold immediately after its Stripe charge succeeds (the same point RegisterApp already used), so a race lands identically on both legs. Also adds an end-to-end regression for the earlier IsLargeAutoCollect rounding fix, proving the concrete cents Stripe is charged (not just "no error"). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
I-am-nothing
added a commit
that referenced
this pull request
Jul 5, 2026
…number migration 031→034 PR #45 (feat/auto-collect-disclosure) adds collection.IsLargeAutoCollect / AutoCollectThresholdMicros + the invoices.is_large_auto_collect mirror column (migration 034, renumbered from 031 to clear this branch's creation-grace 031_apps_proration_skipped + 032_account_wide_overage). Conflict resolution, preserving all three intents: - apps.go: kept this branch's creation-grace RegisterApp (mirror-only, no synchronous charge). #45's IsLargeAutoCollect wiring targeted the OLD synchronous-charge proration path that no longer lives in RegisterApp, so it is ported to proration.go's ChargeCreationProration callback instead (the threshold is resolved right after the Stripe call succeeds, matching the boundary leg). - models.go / *.sql.go: regenerated via sqlc from the merged migrations (032 account-wide overage + 034 auto-collect both present). - charge_test.go: the #45 RegisterApp post-charge-resolution test targeted the removed synchronous path; rewritten to drive ChargeCreationProration with the unified model's flat $20 base ($10 pre-charge vs $30 mid-charge threshold straddle), preserving the "resolved after Stripe succeeds" assertion. build / vet / test / gofmt all green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Contributor
Author
|
Superseded by #48 — the auto-collect threshold/flag logic carries forward as-is into the unified branch (migration renumbered 031→034), now wired into every charge site via one shared helper instead of just the boundary leg. Branch |
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
A post-hoc transparency disclosure for off-session charges that already succeeded, once they cross a per-account size threshold — so customers aren't surprised by a large automatic debit.
This is complementary to, and deliberately distinct from, the spend-ceiling gate (
internal/account/collectionExceedsSpendCeiling): the ceiling skips a charge that would breach a bill-shock cap before it fires. This feature changes no charging behaviour at all — it only records, after the fact, that a successful charge was "large" so the web billing page can disclose it.Migration number
Uses 031 (
031_auto_collect_disclosure.{up,down}.sql). Latest existing was 028; 029/030 were reserved by sibling worktrees in flight when this was written. If those land elsewhere first, this is still the next free slot after them.Changes
accounts.auto_collect_threshold_micros BIGINT NULL— per-account threshold; NULL = platform default ($100 = 100_000_000 micros).invoices.is_large_auto_collect BOOLEAN NOT NULL DEFAULT false— server-computed verdict, frozen per invoice.collectionpackage:DefaultAutoCollectThresholdMicros($100) +ResolveAutoCollectThreshold+IsLargeAutoCollect.>), so a charge equal to the threshold is not flagged — "large" means above. Consistent withExceedsSpendCeiling's precise-dollar-cap reading and documented in code + tests.RunBillingCycle(arrears + advance base) andRegisterAppproration — set the flag on theInvoiceMirror.InvoiceMirror/UpsertInvoiceand exposed on the wire asInvoiceRow.is_large_auto_collect(additive only).Tests
collectionunit tests (below / above / exactly-at / custom override respected / NULL→default) +cyclewiring tests (large charge flags the mirror, small does not, per-account override governs over the default).make test,go vet ./...,go build ./...all clean.Follow-ups (out of scope here)
api-client-shared: its TypeScriptInvoicetype (billing/types.ts) needs a matchingis_large_auto_collect: booleanfield added so web-account can consume the real server flag. Until then web-account ships against an interim client-side threshold check (separate web-account PR).mirrorstack-docs: companiondb/ms_billing/{tables,migrations}.mdupdate for the two new columns (per this repo's schema/doc cross-repo convention).🤖 Generated with Claude Code