portal: budget notices gain mode=test (email one sample, no ledger row)

Lets the letter be read in a mail client before the sender is armed. Uses the member's real numbers when the address matches one, writes nothing to budget_notices so the once-per-crossing latch is not consumed, and works while BUDGET_NOTICES is off.
This commit is contained in:
inference-bot committed 2026-10-31 16:53:22 +00:00
1 parent c21421a89b
commit b998d8d535
1 file changed
+44 -5
+44 -5
View File
@@ -4171,16 +4171,55 @@ async def budget_notice_sweep(send: bool = False) -> dict:
"would_notify": len(preview), "notices": preview}
async def budget_notice_test(dest: str) -> dict:
"""Email one sample low-balance letter to `dest`, using that member's real numbers.
Deliberately does NOT write to budget_notices: a test must not consume the real
once-per-crossing latch, or reviewing the copy would silence the actual notice.
Works while BUDGET_NOTICES is off, so copy can be reviewed before arming anything.
"""
conn = get_db()
row = conn.execute("SELECT slug FROM members WHERE email = ?", (dest,)).fetchone()
name = ((row[0] or "").strip() if row else "") or "there"
conn.close()
budget, spend = 15.0, 13.34 # sample numbers if they aren't a member
if row:
team_id = await litellm_team_id_for_email(dest)
stats = (get_team_budget_from_db(team_id) or {}) if team_id else {}
if stats.get("max_budget"):
budget = float(stats["max_budget"])
spend = float(stats.get("spend") or 0.0)
remaining = budget - spend
subject, text_body, html_body = budget_notice_email(
name, spend, budget, remaining, credit_pack_url()
)
ok = send_email(dest, subject, text_body, html_body)
return {
"mode": "test", "to": dest, "sent": bool(ok), "subject": subject,
"rendered": {"name": name, "spend": round(spend, 2), "budget": round(budget, 2),
"remaining": round(remaining, 2)},
"note": "no ledger row written — the real once-per-crossing latch is untouched",
}
@app.post("/admin/budget-notices")
@limiter.limit("10/minute")
async def admin_budget_notices(request: Request, mode: str = "dryrun"):
"""Preview (default) or send the monthly budget notices.
async def admin_budget_notices(request: Request, mode: str = "dryrun", to: str | None = None):
"""Preview (default), send, or email a single sample.
Authenticated by X-Admin-Token. `mode=dryrun` never emails; `mode=send`
requires BUDGET_NOTICES=on and refuses otherwise, so a preview can be run at
any time without arming the sender.
Authenticated by X-Admin-Token. `mode=dryrun` never emails; `mode=send` requires
BUDGET_NOTICES=on and refuses otherwise; `mode=test&to=<address>` emails one sample
using that member's real numbers so the copy can be read in a mail client first.
A test works while the flag is off and leaves no trace in the ledger.
"""
_verify_admin_token(request)
if mode == "test":
dest = (to or "").strip()
if not dest:
return {"refused": "mode=test needs &to=<address>"}
return await budget_notice_test(dest)
if mode == "send":
if budget_notices_enabled() != "on":
return {"refused": f"BUDGET_NOTICES is '{budget_notices_enabled()}', not 'on'"