Feat: Erneuerungsrechnung-Entwurf 30 Tage vor Abo-Ablauf + 7-Tage-Erinnerung an Admin

This commit is contained in:
rene 2026-05-15 11:53:29 +02:00
parent 96030304d4
commit b1dbde332f

View file

@ -207,6 +207,119 @@ def stop():
# ------------------------------------------------------------------ # ------------------------------------------------------------------
# JOB: Abo-Ablauf prüfen (täglich 03:00) # JOB: Abo-Ablauf prüfen (täglich 03:00)
# ------------------------------------------------------------------ # ------------------------------------------------------------------
_TIER_PRICE = {"pro": 29.00, "breeder": 49.00}
async def _create_renewal_invoice_draft(user: dict, expires: date, tier_label: str):
"""Legt einen Rechnungs-Entwurf für die Abo-Verlängerung an, sofern noch keiner existiert."""
import os
from mailer import send_email, email_html
from routes.invoices import _next_invoice_number
tier = user["subscription_tier"]
price = _TIER_PRICE.get(tier, 29.00)
# Verlängerungszeitraum: Folgetag nach Ablauf bis +1 Jahr
start = expires + timedelta(days=1)
end = start.replace(year=start.year + 1) - timedelta(days=1)
period = f"{start.strftime('%d.%m.%Y')} {end.strftime('%d.%m.%Y')}"
with db() as conn:
# Nur anlegen wenn noch kein Entwurf/offener Eintrag für diesen User + Zeitraum
existing = conn.execute(
"""SELECT id FROM invoices
WHERE user_id=? AND status IN ('draft','sent')
AND service_period=?""",
(user["id"], period)
).fetchone()
if existing:
logger.info(f"Erneuerungsrechnung bereits vorhanden für user {user['id']}")
return
# Billing-Adresse des Users laden
row = conn.execute(
"SELECT billing_address FROM users WHERE id=?", (user["id"],)
).fetchone()
billing_address = row["billing_address"] if row else None
invoice_number = _next_invoice_number(conn)
description = f"{tier_label} Jahresabo (Verlängerung)"
conn.execute("""
INSERT INTO invoices
(invoice_number, user_id, recipient_name, recipient_email, recipient_address,
description, service_period, amount_net, discount_pct, discount_amount,
amount_after_discount, tax_rate, tax_amount, amount_gross, notes)
VALUES (?,?,?,?,?,?,?,?,0,0,?,0,0,?,?)
""", (
invoice_number, user["id"], user["name"], user["email"], billing_address,
description, period,
price, price, price,
f"Automatisch erstellt — Abo läuft am {expires.strftime('%d.%m.%Y')} ab.",
))
conn.execute(
"INSERT INTO invoice_items (invoice_id, description, quantity, unit_price, total) VALUES (?,?,1,?,?)",
(conn.execute("SELECT last_insert_rowid()").fetchone()[0], description, price, price)
)
logger.info(f"Erneuerungsrechnung {invoice_number} als Entwurf angelegt für {user['email']}")
# Admin-Benachrichtigung
admin_email = os.getenv("ADMIN_EMAIL", "")
if admin_email:
app_url = os.getenv("APP_URL", "https://banyaro.app")
body = f"""
<p>Für <strong>{user['name']}</strong> ({user['email']}) wurde automatisch ein
Rechnungsentwurf für die Abo-Verlängerung erstellt.</p>
<table style="border-collapse:collapse;font-size:14px;margin:12px 0">
<tr><td style="padding:4px 12px 4px 0;color:#888">Rechnung:</td><td><strong>{invoice_number}</strong></td></tr>
<tr><td style="padding:4px 12px 4px 0;color:#888">Tarif:</td><td>{tier_label}</td></tr>
<tr><td style="padding:4px 12px 4px 0;color:#888">Betrag:</td><td>{price:.2f} EUR</td></tr>
<tr><td style="padding:4px 12px 4px 0;color:#888">Zeitraum:</td><td>{period}</td></tr>
<tr><td style="padding:4px 12px 4px 0;color:#888">Abo läuft ab:</td><td>{expires.strftime('%d.%m.%Y')} (in 30 Tagen)</td></tr>
</table>
<p>Bitte prüfen, ggf. anpassen und rechtzeitig versenden.</p>"""
html = email_html(body, cta_url=f"{app_url}/#admin", cta_label="Zur Rechnung im Admin")
await send_email(
admin_email,
f"Erneuerungsrechnung {invoice_number} bereit — {user['name']}",
html,
f"Entwurf {invoice_number} für {user['name']} ({tier_label}, {price:.2f} EUR, {period}) bereit."
)
async def _remind_renewal_invoice(user: dict, expires: date, tier_label: str):
"""7-Tage-Erinnerung an René: Entwurf noch nicht versendet."""
import os
from mailer import send_email, email_html
with db() as conn:
draft = conn.execute(
"SELECT invoice_number FROM invoices WHERE user_id=? AND status='draft' LIMIT 1",
(user["id"],)
).fetchone()
if not draft:
return # kein offener Entwurf, nichts zu erinnern
admin_email = os.getenv("ADMIN_EMAIL", "")
if not admin_email:
return
app_url = os.getenv("APP_URL", "https://banyaro.app")
body = f"""
<p><strong>Achtung:</strong> Das Abo von <strong>{user['name']}</strong> ({user['email']})
läuft in <strong>7 Tagen</strong> (am {expires.strftime('%d.%m.%Y')}) ab.</p>
<p>Rechnungsentwurf <strong>{draft['invoice_number']}</strong> wurde noch nicht versendet.
Bitte jetzt versenden damit der Kunde rechtzeitig bezahlen kann.</p>"""
html = email_html(body, cta_url=f"{app_url}/#admin", cta_label="Rechnung jetzt senden")
await send_email(
admin_email,
f"⚠ Noch 7 Tage — Erneuerungsrechnung {draft['invoice_number']} nicht versendet",
html,
f"Entwurf {draft['invoice_number']} für {user['name']} noch nicht versendet. Abo läuft in 7 Tagen ab."
)
logger.info(f"7-Tage-Erinnerung an Admin für {user['email']}: {draft['invoice_number']}")
async def _job_subscription_check(): async def _job_subscription_check():
"""Abgelaufene Abos auf Standard setzen; Warnmails 30 und 7 Tage vorher.""" """Abgelaufene Abos auf Standard setzen; Warnmails 30 und 7 Tage vorher."""
from database import db as _db from database import db as _db
@ -253,7 +366,7 @@ async def _job_subscription_check():
await send_email(u["email"], f"Dein {tier_label}-Abo ist abgelaufen", html, await send_email(u["email"], f"Dein {tier_label}-Abo ist abgelaufen", html,
f"Hallo {u['name']},\ndein {tier_label}-Abo ist abgelaufen. Daten bleiben erhalten.") f"Hallo {u['name']},\ndein {tier_label}-Abo ist abgelaufen. Daten bleiben erhalten.")
# 30 Tage Warnung # 30 Tage Warnung + Erneuerungsrechnung als Entwurf anlegen
elif days_left == 30: elif days_left == 30:
body = f""" body = f"""
<p>Hallo {_html.escape(u['name'])},</p> <p>Hallo {_html.escape(u['name'])},</p>
@ -265,7 +378,10 @@ async def _job_subscription_check():
await send_email(u["email"], f"Dein {tier_label}-Abo läuft in 30 Tagen ab", html, await send_email(u["email"], f"Dein {tier_label}-Abo läuft in 30 Tagen ab", html,
f"Hallo {u['name']},\ndein {tier_label}-Abo läuft in 30 Tagen ab ({expires}).") f"Hallo {u['name']},\ndein {tier_label}-Abo läuft in 30 Tagen ab ({expires}).")
# 7 Tage Warnung # Erneuerungsrechnung als Entwurf anlegen (nur wenn noch keine existiert)
await _create_renewal_invoice_draft(u, expires, tier_label)
# 7 Tage — Warnung an User + Erinnerung an René falls Entwurf noch nicht versendet
elif days_left == 7: elif days_left == 7:
body = f""" body = f"""
<p>Hallo {_html.escape(u['name'])},</p> <p>Hallo {_html.escape(u['name'])},</p>
@ -275,6 +391,7 @@ async def _job_subscription_check():
html = email_html(body, cta_url="https://banyaro.app", cta_label="Abo verlängern") html = email_html(body, cta_url="https://banyaro.app", cta_label="Abo verlängern")
await send_email(u["email"], f"Nur noch 7 Tage — {tier_label}-Abo läuft ab", html, await send_email(u["email"], f"Nur noch 7 Tage — {tier_label}-Abo läuft ab", html,
f"Hallo {u['name']},\nnur noch 7 Tage für dein {tier_label}-Abo.") f"Hallo {u['name']},\nnur noch 7 Tage für dein {tier_label}-Abo.")
await _remind_renewal_invoice(u, expires, tier_label)
except Exception as e: except Exception as e:
logger.warning(f"subscription_check Fehler für {u['email']}: {e}") logger.warning(f"subscription_check Fehler für {u['email']}: {e}")