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,
|
||||
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 (§ 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():
|
||||
"""Abgelaufene Abos auf Standard setzen; Warnmails 30 und 7 Tage vorher."""
|
||||
from database import db as _db
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue