diff --git a/backend/scheduler.py b/backend/scheduler.py index 91e8163..aee77b7 100644 --- a/backend/scheduler.py +++ b/backend/scheduler.py @@ -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"
IBAN: {IBAN} · Verwendungszweck: {rg}
" if IBAN else "" + body = f""" +Hallo {_html.escape(name)},
++ unsere Rechnung {rg} vom {datetime.fromisoformat(inv['created_at'][:10]).strftime('%d.%m.%Y')} + über {amount:.2f} EUR ist leider noch offen. +
++ Bitte überweisen Sie den Betrag bis zum {frist}. + {iban_line} +
++ Sollte die Zahlung bis zu diesem Datum nicht eingehen, sind wir leider gezwungen, + Ihr Abonnement fristlos zu kündigen (§ 314 BGB). +
""" + 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""" +Hallo {_html.escape(name)},
++ da die Zahlung für Rechnung {rg} ({amount:.2f} EUR) + trotz unserer Zahlungserinnerung nicht eingegangen ist, + haben wir Ihr Abonnement gemäß § 314 BGB fristlos gekündigt. +
++ Ihre Daten bleiben vollständig erhalten. Sie können jederzeit ein neues Abonnement abschließen. +
++ Bei Rückfragen antworten Sie einfach auf diese E-Mail. +
""" + 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"Abo von {_html.escape(name)} ({email}) wurde automatisch fristlos gekündigt (§314 BGB). Rechnung {rg} seit 35 Tagen offen.
"), + 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