Feat: Erneuerungsrechnung-Entwurf 30 Tage vor Abo-Ablauf + 7-Tage-Erinnerung an Admin
This commit is contained in:
parent
96030304d4
commit
b1dbde332f
1 changed files with 119 additions and 2 deletions
|
|
@ -207,6 +207,119 @@ def stop():
|
|||
# ------------------------------------------------------------------
|
||||
# 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():
|
||||
"""Abgelaufene Abos auf Standard setzen; Warnmails 30 und 7 Tage vorher."""
|
||||
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,
|
||||
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:
|
||||
body = f"""
|
||||
<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,
|
||||
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:
|
||||
body = f"""
|
||||
<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")
|
||||
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.")
|
||||
await _remind_renewal_invoice(u, expires, tier_label)
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"subscription_check Fehler für {u['email']}: {e}")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue