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:
1 parent
c21421a89b
commit
b998d8d535
1 file changed
+44
-5
+44
-5
@@ -4171,16 +4171,55 @@ async def budget_notice_sweep(send: bool = False) -> dict:
|
|||||||
"would_notify": len(preview), "notices": preview}
|
"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")
|
@app.post("/admin/budget-notices")
|
||||||
@limiter.limit("10/minute")
|
@limiter.limit("10/minute")
|
||||||
async def admin_budget_notices(request: Request, mode: str = "dryrun"):
|
async def admin_budget_notices(request: Request, mode: str = "dryrun", to: str | None = None):
|
||||||
"""Preview (default) or send the monthly budget notices.
|
"""Preview (default), send, or email a single sample.
|
||||||
|
|
||||||
Authenticated by X-Admin-Token. `mode=dryrun` never emails; `mode=send`
|
Authenticated by X-Admin-Token. `mode=dryrun` never emails; `mode=send` requires
|
||||||
requires BUDGET_NOTICES=on and refuses otherwise, so a preview can be run at
|
BUDGET_NOTICES=on and refuses otherwise; `mode=test&to=<address>` emails one sample
|
||||||
any time without arming the sender.
|
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)
|
_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 mode == "send":
|
||||||
if budget_notices_enabled() != "on":
|
if budget_notices_enabled() != "on":
|
||||||
return {"refused": f"BUDGET_NOTICES is '{budget_notices_enabled()}', not 'on'"
|
return {"refused": f"BUDGET_NOTICES is '{budget_notices_enabled()}', not 'on'"
|
||||||
|
|||||||
Reference in new issue
Block a user