This commit is contained in:
2026-05-20 00:09:28 +02:00
parent 968bf7d102
commit 51b5ddc6f2
695 changed files with 999722 additions and 270 deletions

View File

@@ -27,7 +27,7 @@ from decimal import Decimal
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from pathlib import Path
from typing import Any, Dict, Optional
from typing import Any, Dict, List, Optional
import stripe
from fastapi import APIRouter, Header, HTTPException, Request
@@ -118,6 +118,8 @@ def _ensure_storage() -> None:
con.execute("ALTER TABLE licenses ADD COLUMN license_key TEXT")
if "practice_id" not in cols:
con.execute("ALTER TABLE licenses ADD COLUMN practice_id TEXT")
if "current_period_start" not in cols:
con.execute("ALTER TABLE licenses ADD COLUMN current_period_start INTEGER")
con.commit()
@@ -559,6 +561,7 @@ def _upsert_license(
customer_email: Optional[str],
client_reference_id: Optional[str],
current_period_end: Optional[int],
current_period_start: Optional[int] = None,
license_key: Optional[str] = None,
) -> str:
"""Upsert license row. Returns the license_key (generated if not yet set).
@@ -588,9 +591,9 @@ def _upsert_license(
INSERT INTO licenses(
subscription_id, customer_id, status, lookup_key,
allowed_users, devices_per_user, customer_email, client_reference_id,
current_period_end, updated_at, license_key, practice_id
current_period_end, current_period_start, updated_at, license_key, practice_id
)
VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(subscription_id) DO UPDATE SET
customer_id=excluded.customer_id,
status=excluded.status,
@@ -600,6 +603,7 @@ def _upsert_license(
customer_email=COALESCE(excluded.customer_email, customer_email),
client_reference_id=COALESCE(excluded.client_reference_id, client_reference_id),
current_period_end=COALESCE(excluded.current_period_end, current_period_end),
current_period_start=COALESCE(excluded.current_period_start, current_period_start),
updated_at=excluded.updated_at,
license_key=COALESCE(license_key, excluded.license_key),
practice_id=CASE
@@ -618,6 +622,7 @@ def _upsert_license(
customer_email,
client_reference_id,
current_period_end,
current_period_start,
now,
final_key,
final_pid,
@@ -694,6 +699,109 @@ def test_license_email(email: str) -> Dict[str, Any]:
return result
def _subscription_period_bounds(sub: Dict[str, Any]) -> tuple[Optional[int], Optional[int]]:
"""current_period_start/end aus Subscription-Dict (wie sync_subscription / Webhook)."""
current_period_end = sub.get("current_period_end")
if not current_period_end:
try:
current_period_end = sub["items"]["data"][0]["current_period_end"]
except Exception:
current_period_end = None
current_period_start = sub.get("current_period_start")
if not current_period_start:
try:
current_period_start = sub["items"]["data"][0]["current_period_start"]
except Exception:
current_period_start = None
def _ti(x: Any) -> Optional[int]:
if x is None:
return None
try:
return int(x)
except (TypeError, ValueError):
return None
return _ti(current_period_start), _ti(current_period_end)
def sync_active_license_periods_from_stripe_only() -> Dict[str, Any]:
"""
Für jede aktive Lizenz mit subscription_id: Stripe Subscription per API read-only laden
und nur current_period_start, current_period_end und updated_at in SQLite setzen.
Keine Änderung an status, customer_id, lookup_key, practice_id, Beträgen oder Stripe-Objekten.
"""
if not STRIPE_SECRET_KEY:
raise HTTPException(
status_code=500,
detail="Stripe API not configured (STRIPE_SECRET_KEY missing)",
)
_ensure_storage()
_init_stripe()
now = int(time.time())
with sqlite3.connect(DB_PATH) as con:
rows = con.execute(
"""
SELECT subscription_id FROM licenses
WHERE lower(trim(status))='active'
AND subscription_id IS NOT NULL
AND trim(subscription_id) != ''
"""
).fetchall()
checked = 0
updated = 0
failures: List[Dict[str, str]] = []
with sqlite3.connect(DB_PATH) as con:
for (sub_id,) in rows:
sid = str(sub_id).strip()
if not sid:
continue
checked += 1
try:
sub = _stripe_to_dict(stripe.Subscription.retrieve(sid))
cps, cpe = _subscription_period_bounds(sub)
if cps is None and cpe is None:
failures.append(
{
"subscription_id_prefix": sid[:12],
"detail": "stripe_subscription_missing_period_fields",
}
)
continue
cur = con.execute(
"""
UPDATE licenses
SET current_period_start=?,
current_period_end=?,
updated_at=?
WHERE subscription_id=?
AND lower(trim(status))='active'
""",
(cps, cpe, now, sid),
)
if cur.rowcount:
updated += 1
except Exception as exc:
failures.append(
{
"subscription_id_prefix": sid[:12],
"detail": type(exc).__name__,
}
)
con.commit()
return {
"ok": True,
"checked_active_with_subscription": checked,
"rows_updated": updated,
"failed_count": len(failures),
"failures": failures[:20],
}
@router.post("/sync_subscription")
async def sync_subscription(payload: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
_require_env()
@@ -729,16 +837,22 @@ async def sync_subscription(payload: Optional[Dict[str, Any]] = None) -> Dict[st
current_period_end = sub["items"]["data"][0]["current_period_end"]
except Exception:
current_period_end = None
current_period_start = sub.get("current_period_start")
if not current_period_start:
try:
current_period_start = sub["items"]["data"][0]["current_period_start"]
except Exception:
current_period_start = None
now = int(time.time())
with sqlite3.connect(DB_PATH) as con:
con.execute(
"""
UPDATE licenses
SET status=?, current_period_end=?, updated_at=?
SET status=?, current_period_end=?, current_period_start=?, updated_at=?
WHERE subscription_id=?
""",
(status, current_period_end, now, subscription_id),
(status, current_period_end, current_period_start, now, subscription_id),
)
con.commit()
@@ -746,6 +860,7 @@ async def sync_subscription(payload: Optional[Dict[str, Any]] = None) -> Dict[st
"subscription_id": subscription_id,
"status": status,
"current_period_end": current_period_end,
"current_period_start": current_period_start,
}
@@ -822,6 +937,12 @@ async def stripe_webhook(
current_period_end = sub["items"]["data"][0]["current_period_end"]
except Exception:
current_period_end = None
current_period_start = sub.get("current_period_start")
if not current_period_start:
try:
current_period_start = sub["items"]["data"][0]["current_period_start"]
except Exception:
current_period_start = None
md = sub.get("metadata") or {}
lookup_key = (md.get("lookup_key") or "").strip()
allowed_users = md.get("allowed_users")
@@ -862,6 +983,7 @@ async def stripe_webhook(
customer_email=customer_email,
client_reference_id=(client_reference_id or "").strip() or None,
current_period_end=_to_int(current_period_end),
current_period_start=_to_int(current_period_start),
)
_log_event("license_upsert", {
"event_id": event_id,
@@ -898,6 +1020,14 @@ async def stripe_webhook(
current_period_end = items_data[0].get("current_period_end")
except Exception:
current_period_end = None
current_period_start = obj.get("current_period_start")
if not current_period_start:
try:
items_data = (obj.get("items") or {}).get("data") or []
if items_data:
current_period_start = items_data[0].get("current_period_start")
except Exception:
current_period_start = None
md = obj.get("metadata") or {}
lookup_key = (md.get("lookup_key") or "").strip()
allowed_users = md.get("allowed_users")
@@ -936,6 +1066,7 @@ async def stripe_webhook(
customer_email=None,
client_reference_id=None,
current_period_end=_to_int(current_period_end),
current_period_start=_to_int(current_period_start),
)
_log_event("license_upsert", {
"event_id": event_id,