diff --git a/backend/scheduler.py b/backend/scheduler.py index 510b348..a1edb8a 100644 --- a/backend/scheduler.py +++ b/backend/scheduler.py @@ -207,6 +207,119 @@ def stop(): # ------------------------------------------------------------------ # 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""" +

Für {user['name']} ({user['email']}) wurde automatisch ein + Rechnungsentwurf für die Abo-Verlängerung erstellt.

+ + + + + + +
Rechnung:{invoice_number}
Tarif:{tier_label}
Betrag:{price:.2f} EUR
Zeitraum:{period}
Abo läuft ab:{expires.strftime('%d.%m.%Y')} (in 30 Tagen)
+

Bitte prüfen, ggf. anpassen und rechtzeitig versenden.

""" + 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""" +

Achtung: Das Abo von {user['name']} ({user['email']}) + läuft in 7 Tagen (am {expires.strftime('%d.%m.%Y')}) ab.

+

Rechnungsentwurf {draft['invoice_number']} wurde noch nicht versendet. + Bitte jetzt versenden damit der Kunde rechtzeitig bezahlen kann.

""" + 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(): """Abgelaufene Abos auf Standard setzen; Warnmails 30 und 7 Tage vorher.""" 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, 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: body = f"""

Hallo {_html.escape(u['name'])},

@@ -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, 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: body = f"""

Hallo {_html.escape(u['name'])},

@@ -275,6 +391,7 @@ async def _job_subscription_check(): 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, 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: logger.warning(f"subscription_check Fehler für {u['email']}: {e}")