Feat: Kündigung blockt Erneuerungsentwurf; Upgrade storniert alte Rechnungen + legt neuen Entwurf an

This commit is contained in:
rene 2026-05-15 12:00:27 +02:00
parent b1dbde332f
commit a9f7923716
2 changed files with 69 additions and 2 deletions

View file

@ -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
# ------------------------------------------------------------------

View file

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