feat(lit-payments): auto-send the monthly enterprise invoice - #603
Merged
Conversation
Wire the existing (but dormant) auto_send account flag through the billing
job: after the draft + regrant steps, auto_send accounts now get
finalize_and_send (idempotency key ent_send:{account}:{period}, guarded by
the same 23h retry-age window as the other Stripe side effects) and an FYI
email instead of a review request. 0-unit cycles are still held as drafts
for human review even with auto_send on, since a 0 reading means the
metering identity is suspect.
Includes a migration flipping Uneven Labs to auto_send = true — the July
2026 cycle validated the draft flow end-to-end.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Four fixes from the Codex adversarial review of the auto-send flow:
- Explicitly POST invoices/{id}/send after finalize. Finalization alone
only emails the customer when the Stripe account's 'email finalized
invoices' dashboard setting is on; without this the DB could say 'sent'
while the customer never got the invoice.
- Hold auto-send when the consumed reading is at/above the full buffer
target (calc::hold_for_review). Since consumed = target + balance, such
a reading means the payer's balance went zero/positive mid-cycle and
the overage figure is unreliable — previously this would have
auto-sent a wrongly-huge invoice; now it's held as a draft like the
0-unit case, with its own caution in the review email.
- Record finalized_at (new column) after Stripe confirms finalize+send
and before the FYI email, so a crash between them resumes at the email
step instead of replaying Stripe calls past the idempotency window.
- Missed-cycle guard now requires the previous period's row to be in a
resolved status (draft/sent/paid/manual), not merely to exist — an
error/pending prior cycle blocks auto-billing the next one.
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.
Context
The July 2026 Uneven Labs invoice run surfaced a Stripe key permission gap (fixed out-of-band); once unblocked, the draft + review-email flow worked end-to-end. We're now comfortable dropping the human-in-the-loop send step, which the code was already built for:
enterprise_accounts.auto_sendexisted (default false, "flip to true to finalize+send automatically (future)") andfinalize_and_sendwas sitting unused in lit-billing-core.Changes
auto_sendaccounts getfinalize_and_send(Stripe finalizes and emails the invoice to the customer, net-30) followed by an FYI email tonotify_email, and the row is markedsent. Manual accounts keep the existing draft + review-email path.ent_send:{account}:{period}and the same 23h retry-age guard as the create/regrant steps: past the window it refuses and surfaces for a human instead of guessing whether the customer was already emailed.auto_sendon. The review email says so explicitly.set_invoice_sent(statussent+notified_at);sentwas already a valid status in the check constraint.auto_send = true.Behavior notes
pendingand the hourly tick resumes it, with each Stripe side effect deduped inside the idempotency window and refused past it.draft(e.g. July 2026) stay terminal — they still need a manual send in the dashboard.Testing
cargo clippy --all-targets: no warnings in touched files.cargo test(lit-payments): 120 passed.🤖 Generated with Claude Code
Adversarial review fixes (2nd commit)
A Codex adversarial review flagged 6 issues; 4 were addressed here (the other 2 — duplicate internal FYI emails under multi-instance overlap, and the by-design past-window human hand-off — are accepted as low-risk/pre-existing):
finalize_and_sendnow explicitly POSTsinvoices/{id}/sendafter finalize (separate idempotency keys). Finalization alone only emails the customer if the Stripe dashboard's "email finalized invoices" setting is on — otherwise the DB would saysentwhile the customer never received the invoice.consumed >= target_credit_cents(calc::hold_for_review, unit-tested). Sinceconsumed = target + balance, that reading means the payer balance went zero/positive mid-cycle and the overage figure is unreliable — previously this exact case was logged as "verify before the invoice is sent" but would have been auto-sent anyway. The review email explains the hold.finalized_atcolumn, written after Stripe confirms finalize+send and before the FYI email. A crash between them now resumes at the email step instead of replaying Stripe calls (whose idempotency keys expire after ~24h).draft/sent/paid/manual), not merely exist — anerror/pendingprior cycle now blocks auto-billing the next one.Note on the migration-flips-in-flight-rows concern: verified in prod that the only existing rows are terminal (
manual,draft), so the flag flip resumes nothing retroactively.