diff --git a/backend/routes/admin.py b/backend/routes/admin.py index 8eb07ba..4972bb6 100644 --- a/backend/routes/admin.py +++ b/backend/routes/admin.py @@ -1150,7 +1150,7 @@ async def list_upgrade_requests(user=Depends(require_admin)): async def fulfill_upgrade_request(req_id: int, user=Depends(require_admin)): with db() as conn: req = conn.execute( - "SELECT r.*, u.name, u.email FROM upgrade_requests r JOIN users u ON u.id=r.user_id WHERE r.id=?", + "SELECT r.*, u.name, u.email, u.subscription_tier AS old_tier FROM upgrade_requests r JOIN users u ON u.id=r.user_id WHERE r.id=?", (req_id,) ).fetchone() if not req: @@ -1259,9 +1259,70 @@ async def fulfill_upgrade_request(req_id: int, user=Depends(require_admin)): import logging logging.getLogger(__name__).warning(f"Bestätigungsmail fehlgeschlagen: {e}") + # Offene Rechnungen (sent/draft) des alten Tiers stornieren + neuen Entwurf anlegen + try: + await _handle_upgrade_invoices(req, tier_label) + except Exception as e: + logger.warning(f"Upgrade-Rechnungslogik fehlgeschlagen für {req['name']}: {e}") + return {"ok": True, "tier": req["tier"], "user": req["name"]} +async def _handle_upgrade_invoices(req: dict, new_tier_label: str): + """Storniert offene Rechnungen des alten Tiers und legt neuen Entwurf an.""" + from routes.invoices import _next_invoice_number + from datetime import timedelta + + with db() as conn: + # Offene Rechnungen (draft + sent) dieses Users finden + open_invoices = conn.execute( + "SELECT * FROM invoices WHERE user_id=? AND status IN ('draft','sent')", + (req["user_id"],) + ).fetchall() + + for inv in open_invoices: + cancel_num = _next_invoice_number(conn, "ST") + conn.execute( + """UPDATE invoices SET status='cancelled', cancelled_at=strftime('%Y-%m-%dT%H:%M:%SZ','now'), + cancellation_reason=?, cancellation_number=? WHERE id=?""", + (f"Tarif-Upgrade auf {new_tier_label}", cancel_num, inv["id"]) + ) + logger.info(f"Rechnung {inv['invoice_number']} storniert ({cancel_num}) — Upgrade auf {new_tier_label}") + + # Neuen Entwurf für den neuen Tier anlegen + tier = req["tier"] + price = {"pro": 29.00, "breeder": 49.00}.get(tier, 29.00) + today = datetime.now(_TZ).date() + end_date = today.replace(year=today.year + 1) - timedelta(days=1) + period = f"{today.strftime('%d.%m.%Y')} – {end_date.strftime('%d.%m.%Y')}" + description = f"{new_tier_label} Jahresabo" + + billing = conn.execute( + "SELECT billing_address FROM users WHERE id=?", (req["user_id"],) + ).fetchone() + billing_address = billing["billing_address"] if billing else None + + inv_number = _next_invoice_number(conn) + 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,?,?) + """, ( + inv_number, req["user_id"], req["name"], req["email"], billing_address, + description, period, price, price, price, + f"Automatisch bei Upgrade von {req.get('old_tier','Standard')} auf {new_tier_label}.", + )) + invoice_id = conn.execute("SELECT last_insert_rowid()").fetchone()[0] + conn.execute( + "INSERT INTO invoice_items (invoice_id, description, quantity, unit_price, total) VALUES (?,?,1,?,?)", + (invoice_id, description, price, price) + ) + + logger.info(f"Neuer Rechnungsentwurf {inv_number} für {req['email']} nach Upgrade auf {new_tier_label}") + + # ------------------------------------------------------------------ # Helpers: Quartalsdaten # ------------------------------------------------------------------ diff --git a/backend/scheduler.py b/backend/scheduler.py index a1edb8a..8a7b24d 100644 --- a/backend/scheduler.py +++ b/backend/scheduler.py @@ -216,6 +216,11 @@ async def _create_renewal_invoice_draft(user: dict, expires: date, tier_label: s from mailer import send_email, email_html from routes.invoices import _next_invoice_number + # Gekündigte Abos bekommen keine Erneuerungsrechnung + if user.get("subscription_cancelled_at"): + logger.info(f"Kein Erneuerungsentwurf für {user['email']} — Abo ist gekündigt.") + return + tier = user["subscription_tier"] price = _TIER_PRICE.get(tier, 29.00) # Verlängerungszeitraum: Folgetag nach Ablauf bis +1 Jahr @@ -330,7 +335,8 @@ async def _job_subscription_check(): with _db() as conn: users = conn.execute( - """SELECT id, name, email, subscription_tier, subscription_expires_at + """SELECT id, name, email, subscription_tier, subscription_expires_at, + subscription_cancelled_at FROM users WHERE subscription_tier IN ('pro','breeder') AND subscription_expires_at IS NOT NULL"""