Feat: Automatische Zahlungsmahnung (Tag 21) + fristlose Kündigung (Tag 35) per Scheduler (§314 BGB)

This commit is contained in:
rene 2026-05-15 16:10:53 +02:00
parent e714580d77
commit ee280fdaae

View file

@ -195,6 +195,13 @@ def start():
replace_existing=True,
misfire_grace_time=3600,
)
_scheduler.add_job(
_job_invoice_reminder,
CronTrigger(hour=8, minute=30), # täglich 08:30 Uhr
id="invoice_reminder",
replace_existing=True,
misfire_grace_time=3600,
)
_scheduler.start()
logger.info("Scheduler gestartet — Health-Reminder 08:00, Giftköder-Archiv 03:00, Wetter-Alert 07:30, Meilenstein-Check 00:05, Event-Import So 02:00, Rassen-Seed monatlich 1. des Monats, Status-Report täglich 06:00, Moderation-Overdue 12:00, Quartalsbericht 1. Feb/Mai/Aug/Nov 07:00, Streak-Reminder 19:00, Rückruf-Check 08:00, Goldene-Gassi-Stunde 07:00, Jahrestags-Erinnerungen 09:00, Monatlicher-Rückblick 1. des Monats 10:00, Foto-Challenge Mo 08:00, Abo-Check 03:00. OSM-Cache: on-demand (kein Prewarm).")
@ -367,6 +374,118 @@ async def _remind_renewal_invoice(user: dict, expires: date, tier_label: str):
logger.info(f"7-Tage-Erinnerung an Admin für {user['email']}: {draft['invoice_number']}")
async def _job_invoice_reminder():
"""
Unbezahlte Rechnungen (status='sent'):
- Nach 21 Tagen: Zahlungsmahnung mit 14-Tage-Frist (§286/314 BGB)
- Nach 35 Tagen (21+14): Fristlose Abo-Kündigung
"""
from database import db as _db
from mailer import send_email, email_html
from routes.invoices import _next_invoice_number
import html as _html
import os
APP_URL = os.getenv("APP_URL", "https://banyaro.app")
IBAN = os.getenv("RECHNUNG_IBAN", "")
ADMIN_MAIL = os.getenv("ADMIN_EMAIL", "")
today = datetime.now(_TZ).date()
with db() as conn:
open_invoices = conn.execute(
"""SELECT i.*, u.name AS user_name, u.subscription_tier, u.id AS uid
FROM invoices i
LEFT JOIN users u ON u.id = i.user_id
WHERE i.status = 'sent'
AND i.sent_at IS NOT NULL"""
).fetchall()
for inv in open_invoices:
try:
sent_date = datetime.fromisoformat(inv["sent_at"].replace("Z", "+00:00")).date()
days_open = (today - sent_date).days
rg = inv["invoice_number"]
name = inv["recipient_name"]
email = inv["recipient_email"]
amount = inv["amount_gross"]
frist = (today + timedelta(days=14)).strftime("%d.%m.%Y")
# ── 21 Tage: Zahlungsmahnung mit 14-Tage-Frist ───────────
if days_open == 21:
iban_line = f"<p style='margin:0 0 8px;font-size:13px'>IBAN: <strong>{IBAN}</strong> · Verwendungszweck: {rg}</p>" if IBAN else ""
body = f"""
<p style='margin:0 0 12px'>Hallo <b>{_html.escape(name)}</b>,</p>
<p style='margin:0 0 12px'>
unsere Rechnung <b>{rg}</b> vom {datetime.fromisoformat(inv['created_at'][:10]).strftime('%d.%m.%Y')}
über <b>{amount:.2f} EUR</b> ist leider noch offen.
</p>
<p style='margin:0 0 12px'>
Bitte überweisen Sie den Betrag bis zum <b>{frist}</b>.
{iban_line}
</p>
<p style='margin:0;font-size:13px;color:#888'>
Sollte die Zahlung bis zu diesem Datum nicht eingehen, sind wir leider gezwungen,
Ihr Abonnement fristlos zu kündigen (§&nbsp;314 BGB).
</p>"""
html = email_html(body, cta_url=APP_URL, cta_label="Ban Yaro öffnen")
await send_email(
email,
f"Zahlungserinnerung: Rechnung {rg} — Ban Yaro",
html,
f"Hallo {name},\n\nRechnung {rg} über {amount:.2f} EUR ist noch offen.\n"
f"Bitte bis {frist} überweisen. Andernfalls kündigen wir fristlos.\n"
+ (f"IBAN: {IBAN}, Verwendungszweck: {rg}\n" if IBAN else "")
)
logger.info(f"Zahlungsmahnung gesendet: {rg} an {email} (21 Tage offen)")
# ── 35 Tage: Fristlose Kündigung ─────────────────────────
elif days_open == 35:
# Abo kündigen wenn Nutzer zugeordnet und aktives Abo
if inv["uid"] and inv["subscription_tier"] not in (None, "standard", "standard_test"):
with db() as conn2:
conn2.execute(
"""UPDATE users SET subscription_tier='standard',
subscription_expires_at=NULL,
subscription_cancelled_at=strftime('%Y-%m-%dT%H:%M:%SZ','now')
WHERE id=?""",
(inv["uid"],)
)
logger.info(f"Fristlose Kündigung: user {inv['uid']} wegen unbezahlter Rechnung {rg}")
body = f"""
<p style='margin:0 0 12px'>Hallo <b>{_html.escape(name)}</b>,</p>
<p style='margin:0 0 12px'>
da die Zahlung für Rechnung <b>{rg}</b> ({amount:.2f} EUR)
trotz unserer Zahlungserinnerung nicht eingegangen ist,
haben wir Ihr Abonnement gemäß §&nbsp;314 BGB fristlos gekündigt.
</p>
<p style='margin:0 0 12px'>
Ihre Daten bleiben vollständig erhalten. Sie können jederzeit ein neues Abonnement abschließen.
</p>
<p style='margin:0;font-size:13px;color:#888'>
Bei Rückfragen antworten Sie einfach auf diese E-Mail.
</p>"""
html = email_html(body, cta_url=APP_URL, cta_label="Ban Yaro öffnen")
await send_email(
email,
f"Ihr Ban Yaro Abonnement wurde gekündigt — Rechnung {rg}",
html,
f"Hallo {name},\n\nIhr Abo wurde wegen unbezahlter Rechnung {rg} fristlos gekündigt.\n"
f"Ihre Daten sind erhalten. Neue Buchung jederzeit möglich.\n"
)
if ADMIN_MAIL:
await send_email(
ADMIN_MAIL,
f"Fristlose Kündigung: {name}{rg} ({amount:.2f} EUR unbezahlt)",
email_html(f"<p>Abo von <b>{_html.escape(name)}</b> ({email}) wurde automatisch fristlos gekündigt (§314 BGB). Rechnung {rg} seit 35 Tagen offen.</p>"),
f"Abo {name} gekündigt wegen unbezahlter Rechnung {rg}."
)
except Exception as e:
logger.warning(f"Invoice-Reminder Fehler für {inv.get('invoice_number','?')}: {e}")
async def _job_subscription_check():
"""Abgelaufene Abos auf Standard setzen; Warnmails 30 und 7 Tage vorher."""
from database import db as _db