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)
|
# 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}")
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue