Skip to content

feat(lit-payments): auto-send the monthly enterprise invoice - #603

Merged
glitch003 merged 2 commits into
mainfrom
glitch003/missing-monthly-invoice
Jul 25, 2026
Merged

feat(lit-payments): auto-send the monthly enterprise invoice#603
glitch003 merged 2 commits into
mainfrom
glitch003/missing-monthly-invoice

Conversation

@clawdbot-glitch003

@clawdbot-glitch003 clawdbot-glitch003 commented Jul 24, 2026

Copy link
Copy Markdown
Collaborator

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_send existed (default false, "flip to true to finalize+send automatically (future)") and finalize_and_send was sitting unused in lit-billing-core.

Changes

  • billing.rs: after the draft-invoice and buffer-regrant steps, auto_send accounts get finalize_and_send (Stripe finalizes and emails the invoice to the customer, net-30) followed by an FYI email to notify_email, and the row is marked sent. Manual accounts keep the existing draft + review-email path.
    • The finalize call uses idempotency key 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.
    • Anomaly hold: a 0-unit cycle (metering identity suspect — external credit or usage stopped) is always held as a draft for review, even with auto_send on. The review email says so explicitly.
  • email.rs: the review email and new sent-FYI email share one breakdown body; only the subject, lead line, and CTA differ. Review copy is unchanged.
  • db.rs: set_invoice_sent (status sent + notified_at); sent was already a valid status in the check constraint.
  • migration: flips Uneven Labs to auto_send = true.
  • Doc updates in lit-billing-core/invoice.rs and types.rs to stop calling this "future".

Behavior notes

  • Failure ordering is unchanged: any step failing leaves the row pending and the hourly tick resumes it, with each Stripe side effect deduped inside the idempotency window and refused past it.
  • If the FYI email fails after finalize succeeds, the next tick replays finalize (deduped) and retries the email — worst case is a duplicate internal FYI, same as the existing draft-flow behavior.
  • Rows already in 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.
  • First live exercise will be the 2026-08-17 anchor tick.

🤖 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):

  1. Finalize ≠ send: finalize_and_send now explicitly POSTs invoices/{id}/send after 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 say sent while the customer never received the invoice.
  2. Drained-buffer hold: auto-send is also held when consumed >= target_credit_cents (calc::hold_for_review, unit-tested). Since consumed = 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.
  3. Durable finalize marker: new finalized_at column, 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).
  4. Stricter missed-cycle guard: the previous period's row must be in a resolved status (draft/sent/paid/manual), not merely exist — an error/pending prior 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.

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>
@clawdbot-glitch003
clawdbot-glitch003 requested a review from a team July 24, 2026 20:31
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>
@glitch003
glitch003 merged commit cf7655a into main Jul 25, 2026
14 checks passed
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.

2 participants