Feat: Automatische Zahlungsmahnung (Tag 21) + fristlose Kündigung (Tag 35) per Scheduler (§314 BGB)
This commit is contained in:
parent
e714580d77
commit
ee280fdaae
1 changed files with 119 additions and 0 deletions
|
|
@ -195,6 +195,13 @@ def start():
|
||||||
replace_existing=True,
|
replace_existing=True,
|
||||||
misfire_grace_time=3600,
|
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()
|
_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).")
|
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']}")
|
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 (§ 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äß § 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():
|
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
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue