update
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user