Files
aza/AzA march 2026/aza_ai_credit.py

1478 lines
51 KiB
Python
Raw Normal View History

2026-05-23 21:31:34 +02:00
# -*- coding: utf-8 -*-
"""
KI-Zusatzguthaben: Ledger, Stripe-Checkout (Phase 1), Auto-Aufladung (vorbereitet, nicht produktiv).
Keine Secrets loggen. Keine Live-Charges ohne AZA_AI_TOPUP_ALLOW_LIVE=1.
"""
from __future__ import annotations
import json
import os
import sqlite3
import time
import uuid
from dataclasses import dataclass
from typing import Any, Dict, List, Optional, Tuple
DEFAULT_TOPUP_CHF = 30.0
DEFAULT_INTERNAL_USD = 10.0
DEFAULT_TOPUP_SUCCESS_URL = "https://aza-medwork.ch/succes2"
DEFAULT_TOPUP_CANCEL_URL = "https://aza-medwork.ch"
DEFAULT_SETUP_SUCCESS_URL = "https://aza-medwork.ch/ki-topup-setup-success/"
DEFAULT_SETUP_CANCEL_URL = "https://aza-medwork.ch"
CHF_TO_INTERNAL_RATIO = 1.0 / 3.0 # CHF 30 → USD 10 (+50 % Monatskontingent)
MIN_TOPUP_CHF = 10.0
MAX_TOPUP_CHF = 300.0
DEFAULT_TRIGGER_PERCENT = 5
DEFAULT_MONTHLY_LIMIT_CHF = 300.0
AUTO_TOPUP_MONTHLY_LIMIT_MESSAGE = (
"Das Monatslimit für automatische Aufladungen ist erreicht. "
"Sie können weiterhin manuell Zusatzguthaben kaufen."
)
AUTO_TOPUP_COOLDOWN_SEC = 24 * 3600
AUTO_TOPUP_PENDING_STALE_SEC = 15 * 60
AUTO_TOPUP_CONSENT_TEXT = (
"Automatische Aufladung: CHF 30 (+10 USD Zusatzguthaben), sobald KI-Guthaben unter 5 % fällt, "
"maximal CHF 300 automatische Aufladungen pro Monat. Jederzeit deaktivierbar."
)
LEDGER_TOPUP_TYPES = ("topup_purchase", "auto_topup_purchase", "adjustment", "refund")
LEDGER_DEBIT_TYPES = ("usage",)
USER_HISTORY_EVENT_TYPES = ("topup_purchase", "auto_topup_purchase", "refund")
def _auto_topup_enabled_env() -> bool:
return os.environ.get("AZA_AI_AUTO_TOPUP_ENABLED", "0").strip() == "1"
def _topup_allow_live_env() -> bool:
return os.environ.get("AZA_AI_TOPUP_ALLOW_LIVE", "0").strip() == "1"
def _topup_dry_run_env() -> bool:
return os.environ.get("AZA_AI_TOPUP_DRY_RUN", "1").strip() == "1"
AUTO_TOPUP_ENABLED_ENV = _auto_topup_enabled_env()
TOPUP_ALLOW_LIVE = _topup_allow_live_env()
def _format_date_de(ts: int) -> str:
from datetime import datetime, timezone
return datetime.fromtimestamp(int(ts), tz=timezone.utc).strftime("%d.%m.%Y")
def mask_stripe_reference(ref: Optional[str]) -> Optional[str]:
rid = (ref or "").strip()
if not rid:
return None
if len(rid) <= 8:
return rid[:2] + ""
return rid[:3] + "" + rid[-4:]
def _fetch_stripe_receipt_url(payment_intent_id: Optional[str]) -> Optional[str]:
"""Read-only: receipt_url vom Charge, falls verfügbar."""
pid = (payment_intent_id or "").strip()
if not pid:
return None
try:
import stripe
stripe.api_key = os.environ.get("STRIPE_SECRET_KEY", "")
pi = stripe.PaymentIntent.retrieve(pid, expand=["latest_charge"])
charge = getattr(pi, "latest_charge", None)
if charge is None:
return None
if isinstance(charge, str):
charge = stripe.Charge.retrieve(charge)
url = getattr(charge, "receipt_url", None)
if url is None and isinstance(charge, dict):
url = charge.get("receipt_url")
cleaned = (str(url).strip() if url else "") or None
if cleaned and cleaned.startswith("https://"):
return cleaned
except Exception:
return None
return None
def _meta_with_receipt(
base: Optional[Dict[str, Any]],
*,
payment_intent_id: Optional[str],
) -> Dict[str, Any]:
meta = dict(base or {})
receipt = _fetch_stripe_receipt_url(payment_intent_id)
if receipt:
meta["receipt_url"] = receipt
return meta
def chf_to_internal_usd(paid_chf: float) -> float:
"""Internes Zusatzguthaben: paid_chf / 3 (CHF 30 → 10 USD)."""
chf = max(0.0, float(paid_chf))
return round(chf * CHF_TO_INTERNAL_RATIO, 4)
def _now() -> int:
return int(time.time())
def _month_start_ts(ts: Optional[int] = None) -> int:
from datetime import datetime, timezone
t = ts or _now()
dt = datetime.fromtimestamp(t, tz=timezone.utc)
start = dt.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
return int(start.timestamp())
def _sanitize_meta(meta: Optional[Dict[str, Any]]) -> str:
clean: Dict[str, Any] = {}
if meta:
for k, v in meta.items():
kl = str(k).lower()
if any(x in kl for x in ("secret", "token", "password", "key", "payment_method")):
continue
clean[str(k)[:64]] = v
return json.dumps(clean, ensure_ascii=False)[:4000]
def ensure_ai_credit_schema(con: sqlite3.Connection) -> None:
con.execute(
"""
CREATE TABLE IF NOT EXISTS ai_credit_ledger (
id TEXT PRIMARY KEY,
practice_id TEXT,
subscription_id TEXT,
customer_email TEXT,
event_type TEXT NOT NULL,
amount_internal_usd REAL NOT NULL DEFAULT 0,
amount_paid_chf REAL,
stripe_checkout_session_id TEXT,
stripe_payment_intent_id TEXT,
stripe_customer_id TEXT,
stripe_subscription_id TEXT,
status TEXT NOT NULL DEFAULT 'pending',
period_start INTEGER,
period_end INTEGER,
meta_json TEXT,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
)
"""
)
con.execute(
"CREATE INDEX IF NOT EXISTS idx_ai_credit_ledger_practice "
"ON ai_credit_ledger(practice_id, status, event_type)"
)
con.execute(
"CREATE UNIQUE INDEX IF NOT EXISTS idx_ai_credit_checkout_session "
"ON ai_credit_ledger(stripe_checkout_session_id) "
"WHERE stripe_checkout_session_id IS NOT NULL AND stripe_checkout_session_id != ''"
)
con.execute(
"CREATE UNIQUE INDEX IF NOT EXISTS idx_ai_credit_payment_intent "
"ON ai_credit_ledger(stripe_payment_intent_id) "
"WHERE stripe_payment_intent_id IS NOT NULL AND stripe_payment_intent_id != ''"
)
con.execute(
"""
CREATE TABLE IF NOT EXISTS ai_topup_settings (
practice_id TEXT PRIMARY KEY,
auto_topup_enabled INTEGER NOT NULL DEFAULT 0,
topup_amount_chf REAL NOT NULL DEFAULT 30,
internal_credit_usd REAL NOT NULL DEFAULT 10,
trigger_below_percent INTEGER NOT NULL DEFAULT 5,
monthly_limit_chf REAL NOT NULL DEFAULT 300,
stripe_customer_id TEXT,
default_payment_method_id TEXT,
updated_at INTEGER NOT NULL
)
"""
)
con.commit()
def sum_extra_credit_topups(con: sqlite3.Connection, *, practice_id: str) -> float:
pid = (practice_id or "").strip()
if not pid:
return 0.0
row = con.execute(
"""
SELECT COALESCE(SUM(amount_internal_usd), 0)
FROM ai_credit_ledger
WHERE practice_id = ?
AND status = 'succeeded'
AND event_type IN ('topup_purchase', 'auto_topup_purchase', 'adjustment', 'refund')
""",
(pid,),
).fetchone()
return max(0.0, float(row[0] or 0.0))
def _count_usage_periods(con: sqlite3.Connection, subscription_id: str) -> int:
row = con.execute(
"""
SELECT COUNT(DISTINCT period_start || ':' || period_end)
FROM ai_usage_events
WHERE subscription_id = ? AND status = 'success'
""",
(subscription_id,),
).fetchone()
n = int(row[0] or 0)
return max(1, n)
def sum_total_usage_usd(con: sqlite3.Connection, subscription_id: str) -> float:
row = con.execute(
"""
SELECT COALESCE(SUM(estimated_cost_usd), 0)
FROM ai_usage_events
WHERE subscription_id = ? AND status = 'success'
""",
(subscription_id,),
).fetchone()
return float(row[0] or 0.0)
def compute_extra_credit_remaining(
con: sqlite3.Connection,
*,
practice_id: str,
subscription_id: str,
monthly_budget_usd: float,
) -> float:
"""Zusatzguthaben = Topups minus Verbrauch über kumuliertes Monatskontingent."""
topups = sum_extra_credit_topups(con, practice_id=practice_id)
if topups <= 0:
return 0.0
total_used = sum_total_usage_usd(con, subscription_id)
periods = _count_usage_periods(con, subscription_id)
allocated = max(0.0, float(monthly_budget_usd)) * periods
consumed_from_extra = max(0.0, total_used - allocated)
return max(0.0, round(topups - consumed_from_extra, 4))
def apply_extra_credit_to_snapshot(
snap: Dict[str, Any],
*,
extra_remaining: float,
monthly_budget: float,
) -> Dict[str, Any]:
monthly_remaining = max(0.0, float(snap.get("remaining_usd", 0) or 0))
extra = max(0.0, float(extra_remaining))
total_available = monthly_remaining + extra
budget = max(0.01, float(monthly_budget))
pct = int((total_available / budget) * 100.0)
pct = max(0, pct)
label = f"KI-Kontingent: {pct} % verfügbar"
out = dict(snap)
out["available_percent"] = pct
out["remaining_usd"] = round(monthly_remaining, 4)
out["extra_credit_remaining_usd"] = round(extra, 4)
out["total_available_usd"] = round(total_available, 4)
out["show_warning"] = pct <= 20 and pct > 0
out["user_label"] = label
out["extra_credit_active"] = extra > 0 and monthly_remaining <= 0
return out
def budget_allows_with_extra(
snap: Dict[str, Any],
*,
extra_remaining: float,
) -> bool:
monthly_remaining = max(0.0, float(snap.get("remaining_usd", 0) or 0))
extra = max(0.0, float(extra_remaining))
return (monthly_remaining + extra) > 1e-9
@dataclass
class TopupSettings:
practice_id: str
auto_topup_enabled: bool
topup_amount_chf: float
internal_credit_usd: float
trigger_below_percent: int
monthly_limit_chf: float
stripe_customer_id: Optional[str]
default_payment_method_id: Optional[str]
def get_topup_settings(con: sqlite3.Connection, practice_id: str) -> TopupSettings:
pid = (practice_id or "").strip()
row = con.execute(
"SELECT * FROM ai_topup_settings WHERE practice_id = ?",
(pid,),
).fetchone()
if not row:
return TopupSettings(
practice_id=pid,
auto_topup_enabled=False,
topup_amount_chf=DEFAULT_TOPUP_CHF,
internal_credit_usd=DEFAULT_INTERNAL_USD,
trigger_below_percent=DEFAULT_TRIGGER_PERCENT,
monthly_limit_chf=DEFAULT_MONTHLY_LIMIT_CHF,
stripe_customer_id=None,
default_payment_method_id=None,
)
cols = [d[1] for d in con.execute("PRAGMA table_info(ai_topup_settings)").fetchall()]
d = dict(zip(cols, row))
return TopupSettings(
practice_id=pid,
auto_topup_enabled=bool(int(d.get("auto_topup_enabled") or 0)),
topup_amount_chf=float(d.get("topup_amount_chf") or DEFAULT_TOPUP_CHF),
internal_credit_usd=float(d.get("internal_credit_usd") or DEFAULT_INTERNAL_USD),
trigger_below_percent=int(d.get("trigger_below_percent") or DEFAULT_TRIGGER_PERCENT),
monthly_limit_chf=float(d.get("monthly_limit_chf") or DEFAULT_MONTHLY_LIMIT_CHF),
stripe_customer_id=(d.get("stripe_customer_id") or None),
default_payment_method_id=(d.get("default_payment_method_id") or None),
)
def save_topup_settings(con: sqlite3.Connection, settings: TopupSettings, *, commit: bool = True) -> None:
now = _now()
con.execute(
"""
INSERT INTO ai_topup_settings(
practice_id, auto_topup_enabled, topup_amount_chf, internal_credit_usd,
trigger_below_percent, monthly_limit_chf, stripe_customer_id,
default_payment_method_id, updated_at
) VALUES (?,?,?,?,?,?,?,?,?)
ON CONFLICT(practice_id) DO UPDATE SET
auto_topup_enabled=excluded.auto_topup_enabled,
topup_amount_chf=excluded.topup_amount_chf,
internal_credit_usd=excluded.internal_credit_usd,
trigger_below_percent=excluded.trigger_below_percent,
monthly_limit_chf=excluded.monthly_limit_chf,
stripe_customer_id=COALESCE(excluded.stripe_customer_id, ai_topup_settings.stripe_customer_id),
default_payment_method_id=COALESCE(excluded.default_payment_method_id, ai_topup_settings.default_payment_method_id),
updated_at=excluded.updated_at
""",
(
settings.practice_id,
1 if settings.auto_topup_enabled else 0,
settings.topup_amount_chf,
settings.internal_credit_usd,
settings.trigger_below_percent,
settings.monthly_limit_chf,
settings.stripe_customer_id,
settings.default_payment_method_id,
now,
),
)
if commit:
con.commit()
def apply_auto_topup_user_settings(
con: sqlite3.Connection,
*,
practice_id: str,
enabled: bool,
amount_chf: Optional[float] = None,
trigger_below_percent: Optional[int] = None,
monthly_limit_chf: Optional[float] = None,
) -> Dict[str, Any]:
existing = get_topup_settings(con, practice_id)
if enabled and not auto_topup_is_fully_configured(existing):
return {"ok": False, "error_code": "AUTO_TOPUP_NO_PAYMENT_METHOD"}
amt = amount_chf if amount_chf is not None else existing.topup_amount_chf
trig = trigger_below_percent if trigger_below_percent is not None else existing.trigger_below_percent
monthly = monthly_limit_chf if monthly_limit_chf is not None else existing.monthly_limit_chf
settings = TopupSettings(
practice_id=practice_id,
auto_topup_enabled=enabled,
topup_amount_chf=amt,
internal_credit_usd=chf_to_internal_usd(amt),
trigger_below_percent=trig,
monthly_limit_chf=monthly,
stripe_customer_id=existing.stripe_customer_id,
default_payment_method_id=existing.default_payment_method_id,
)
save_topup_settings(con, settings)
return {"ok": True, "auto_topup_enabled": settings.auto_topup_enabled}
def auto_topup_settings_status(con: sqlite3.Connection, *, practice_id: str) -> Dict[str, Any]:
settings = get_topup_settings(con, practice_id)
return {
"ok": True,
"auto_topup_enabled": settings.auto_topup_enabled,
"topup_amount_chf": settings.topup_amount_chf,
"trigger_below_percent": settings.trigger_below_percent,
"monthly_limit_chf": settings.monthly_limit_chf,
"payment_method_configured": auto_topup_is_fully_configured(settings),
}
def insert_ledger_event(
con: sqlite3.Connection,
*,
practice_id: Optional[str],
subscription_id: Optional[str],
customer_email: Optional[str],
event_type: str,
amount_internal_usd: float,
amount_paid_chf: Optional[float] = None,
stripe_checkout_session_id: Optional[str] = None,
stripe_payment_intent_id: Optional[str] = None,
stripe_customer_id: Optional[str] = None,
stripe_subscription_id: Optional[str] = None,
status: str = "pending",
period_start: Optional[int] = None,
period_end: Optional[int] = None,
meta: Optional[Dict[str, Any]] = None,
ledger_id: Optional[str] = None,
commit: bool = True,
) -> str:
eid = ledger_id or str(uuid.uuid4())
now = _now()
con.execute(
"""
INSERT INTO ai_credit_ledger(
id, practice_id, subscription_id, customer_email, event_type,
amount_internal_usd, amount_paid_chf, stripe_checkout_session_id,
stripe_payment_intent_id, stripe_customer_id, stripe_subscription_id,
status, period_start, period_end, meta_json, created_at, updated_at
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
""",
(
eid,
practice_id,
subscription_id,
customer_email,
event_type,
float(amount_internal_usd),
amount_paid_chf,
stripe_checkout_session_id,
stripe_payment_intent_id,
stripe_customer_id,
stripe_subscription_id,
status,
period_start,
period_end,
_sanitize_meta(meta),
now,
now,
),
)
if commit:
con.commit()
return eid
def update_ledger_event(
con: sqlite3.Connection,
ledger_id: str,
*,
status: Optional[str] = None,
stripe_payment_intent_id: Optional[str] = None,
meta: Optional[Dict[str, Any]] = None,
commit: bool = True,
) -> None:
parts: List[str] = ["updated_at = ?"]
vals: List[Any] = [_now()]
if status is not None:
parts.append("status = ?")
vals.append(status)
if stripe_payment_intent_id is not None:
parts.append("stripe_payment_intent_id = ?")
vals.append(stripe_payment_intent_id)
if meta is not None:
parts.append("meta_json = ?")
vals.append(_sanitize_meta(meta))
vals.append(ledger_id)
con.execute(
f"UPDATE ai_credit_ledger SET {', '.join(parts)} WHERE id = ?",
tuple(vals),
)
if commit:
con.commit()
def ledger_exists_for_checkout(con: sqlite3.Connection, session_id: str) -> bool:
sid = (session_id or "").strip()
if not sid:
return False
row = con.execute(
"SELECT 1 FROM ai_credit_ledger WHERE stripe_checkout_session_id = ? LIMIT 1",
(sid,),
).fetchone()
return bool(row)
def ledger_exists_for_payment_intent(con: sqlite3.Connection, payment_intent_id: str) -> bool:
pid = (payment_intent_id or "").strip()
if not pid:
return False
row = con.execute(
"""
SELECT 1 FROM ai_credit_ledger
WHERE stripe_payment_intent_id = ?
AND status = 'succeeded'
LIMIT 1
""",
(pid,),
).fetchone()
return bool(row)
def has_pending_auto_topup(con: sqlite3.Connection, practice_id: str) -> bool:
pid = (practice_id or "").strip()
if not pid:
return False
cutoff = _now() - AUTO_TOPUP_PENDING_STALE_SEC
row = con.execute(
"""
SELECT 1 FROM ai_credit_ledger
WHERE practice_id = ?
AND event_type = 'auto_topup_purchase'
AND status = 'pending'
AND created_at >= ?
LIMIT 1
""",
(pid, cutoff),
).fetchone()
return bool(row)
def last_successful_auto_topup_ts(con: sqlite3.Connection, practice_id: str) -> Optional[int]:
pid = (practice_id or "").strip()
if not pid:
return None
row = con.execute(
"""
SELECT MAX(created_at) FROM ai_credit_ledger
WHERE practice_id = ?
AND event_type = 'auto_topup_purchase'
AND status = 'succeeded'
""",
(pid,),
).fetchone()
if not row or row[0] is None:
return None
return int(row[0])
def _auto_topup_idempotency_key(practice_id: str) -> str:
from datetime import datetime, timezone
dt = datetime.fromtimestamp(_now(), tz=timezone.utc)
return f"aza_auto_topup_{practice_id}_{dt.strftime('%Y%m')}_{dt.strftime('%d')}"
def _pause_auto_topup_on_failure(
con: sqlite3.Connection,
settings: TopupSettings,
*,
reason: str,
payment_intent_id: Optional[str] = None,
commit: bool = True,
) -> None:
settings.auto_topup_enabled = False
save_topup_settings(con, settings, commit=commit)
insert_ledger_event(
con,
practice_id=settings.practice_id,
subscription_id=None,
customer_email=None,
event_type="failed_auto_topup",
amount_internal_usd=0,
amount_paid_chf=settings.topup_amount_chf,
stripe_payment_intent_id=payment_intent_id,
stripe_customer_id=settings.stripe_customer_id,
status="failed",
meta={"reason": reason},
commit=commit,
)
def process_topup_checkout_completed(
con: sqlite3.Connection,
*,
session: Dict[str, Any],
) -> Dict[str, Any]:
"""Idempotent: checkout.session.completed für aza_purpose=ai_topup."""
md = session.get("metadata") or {}
if (md.get("aza_purpose") or "").strip() != "ai_topup":
return {"handled": False, "reason": "not_ai_topup"}
sid = str(session.get("id") or "")
if ledger_exists_for_checkout(con, sid):
return {"handled": True, "duplicate": True, "session_id": sid}
pi_from_session = str(session.get("payment_intent") or "") or None
if pi_from_session and ledger_exists_for_payment_intent(con, pi_from_session):
return {"handled": True, "duplicate": True, "payment_intent_id": pi_from_session}
practice_id = (md.get("practice_id") or "").strip() or None
subscription_id = (md.get("subscription_id") or "").strip() or None
try:
internal_usd = float(md.get("internal_credit_usd") or 0)
paid_chf = float(md.get("paid_chf") or 0)
except (TypeError, ValueError):
return {"handled": False, "reason": "invalid_metadata_amounts"}
if internal_usd <= 0 or paid_chf <= 0:
return {"handled": False, "reason": "zero_amounts"}
payment_status = (session.get("payment_status") or "").strip()
if payment_status != "paid":
insert_ledger_event(
con,
practice_id=practice_id,
subscription_id=subscription_id,
customer_email=(session.get("customer_email") or None),
event_type="topup_purchase",
amount_internal_usd=0,
amount_paid_chf=paid_chf,
stripe_checkout_session_id=sid,
stripe_payment_intent_id=str(session.get("payment_intent") or "") or None,
stripe_customer_id=str(session.get("customer") or "") or None,
status="failed",
meta={"payment_status": payment_status},
)
return {"handled": True, "credited": False, "reason": "not_paid"}
pi_id = str(session.get("payment_intent") or "") or None
insert_ledger_event(
con,
practice_id=practice_id,
subscription_id=subscription_id,
customer_email=(session.get("customer_email") or None),
event_type="topup_purchase",
amount_internal_usd=internal_usd,
amount_paid_chf=paid_chf,
stripe_checkout_session_id=sid,
stripe_payment_intent_id=pi_id,
stripe_customer_id=str(session.get("customer") or "") or None,
status="succeeded",
meta=_meta_with_receipt({"source": "checkout.session.completed"}, payment_intent_id=pi_id),
)
return {"handled": True, "credited": True, "session_id": sid}
def normalize_stripe_id(value: Any, prefix: str) -> Optional[str]:
"""Extrahiert eine Stripe-ID (z. B. pm_..., cus_...) aus String, Dict oder StripeObject."""
if value is None:
return None
pfx = (prefix or "").strip()
if isinstance(value, str):
raw = value.strip()
if not raw:
return None
if raw.startswith(pfx):
return raw
if raw.startswith("{"):
try:
obj = json.loads(raw)
except Exception:
return None
if isinstance(obj, dict):
return normalize_stripe_id(obj.get("id"), pfx)
return None
return None
if isinstance(value, dict):
if value.get("object") and value.get("id"):
return normalize_stripe_id(value.get("id"), pfx)
return normalize_stripe_id(value.get("id"), pfx)
obj_id = getattr(value, "id", None)
if obj_id:
return normalize_stripe_id(str(obj_id), pfx)
return None
def auto_topup_is_fully_configured(settings: TopupSettings) -> bool:
cus = normalize_stripe_id(settings.stripe_customer_id, "cus_")
pm = normalize_stripe_id(settings.default_payment_method_id, "pm_")
return bool(cus and pm)
def _retrieve_setup_intent_obj(setup_intent_ref: Any) -> Any:
si_id = normalize_stripe_id(setup_intent_ref, "seti_")
if not si_id and isinstance(setup_intent_ref, dict):
si_id = normalize_stripe_id(setup_intent_ref.get("id"), "seti_")
if not si_id:
return None
try:
import stripe
return stripe.SetupIntent.retrieve(si_id, expand=["payment_method", "customer"])
except Exception:
return None
def resolve_auto_topup_setup_ids(
*,
session: Optional[Dict[str, Any]] = None,
setup_intent: Optional[Dict[str, Any]] = None,
) -> Tuple[Optional[str], Optional[str]]:
customer_id: Optional[str] = None
pm_id: Optional[str] = None
def _from_setup_dict(si: Any) -> Tuple[Optional[str], Optional[str]]:
if not isinstance(si, dict):
return None, None
c = normalize_stripe_id(si.get("customer"), "cus_")
p = normalize_stripe_id(si.get("payment_method"), "pm_")
return c, p
if session:
customer_id = normalize_stripe_id(session.get("customer"), "cus_")
si_ref = session.get("setup_intent")
c2, p2 = _from_setup_dict(si_ref)
customer_id = customer_id or c2
pm_id = pm_id or p2
si = _retrieve_setup_intent_obj(si_ref)
if si is not None:
pm_id = pm_id or normalize_stripe_id(getattr(si, "payment_method", None), "pm_")
customer_id = customer_id or normalize_stripe_id(getattr(si, "customer", None), "cus_")
if pm_id and not customer_id:
customer_id = normalize_stripe_id(getattr(getattr(si, "payment_method", None), "customer", None), "cus_")
cs_id = normalize_stripe_id(session.get("id"), "cs_")
if cs_id and (not customer_id or not pm_id):
try:
import stripe
cs = stripe.checkout.Session.retrieve(
cs_id,
expand=["setup_intent", "setup_intent.payment_method", "customer"],
)
if not customer_id:
customer_id = normalize_stripe_id(getattr(cs, "customer", None), "cus_")
si2 = getattr(cs, "setup_intent", None)
if si2 is not None:
if not pm_id:
pm_id = normalize_stripe_id(getattr(si2, "payment_method", None), "pm_")
if not customer_id:
customer_id = normalize_stripe_id(getattr(si2, "customer", None), "cus_")
if pm_id and not customer_id:
customer_id = normalize_stripe_id(
getattr(getattr(si2, "payment_method", None), "customer", None),
"cus_",
)
except Exception:
pass
if setup_intent:
if not customer_id:
customer_id = normalize_stripe_id(setup_intent.get("customer"), "cus_")
if not pm_id:
pm_id = normalize_stripe_id(setup_intent.get("payment_method"), "pm_")
c3, p3 = _from_setup_dict(setup_intent)
customer_id = customer_id or c3
pm_id = pm_id or p3
if not customer_id or not pm_id:
si = _retrieve_setup_intent_obj(setup_intent)
if si is not None:
if not pm_id:
pm_id = normalize_stripe_id(getattr(si, "payment_method", None), "pm_")
if not customer_id:
customer_id = normalize_stripe_id(getattr(si, "customer", None), "cus_")
if pm_id and not customer_id:
customer_id = normalize_stripe_id(
getattr(getattr(si, "payment_method", None), "customer", None),
"cus_",
)
return (
normalize_stripe_id(customer_id, "cus_"),
normalize_stripe_id(pm_id, "pm_"),
)
def _apply_auto_topup_setup_settings(
con: sqlite3.Connection,
*,
practice_id: str,
customer_id: Optional[str],
payment_method_id: Optional[str],
) -> Tuple[TopupSettings, bool]:
cus = normalize_stripe_id(customer_id, "cus_")
pm = normalize_stripe_id(payment_method_id, "pm_")
settings = get_topup_settings(con, practice_id)
if cus:
settings.stripe_customer_id = cus
if pm:
settings.default_payment_method_id = pm
complete = bool(cus and pm)
if complete:
settings.auto_topup_enabled = True
settings.topup_amount_chf = DEFAULT_TOPUP_CHF
settings.internal_credit_usd = DEFAULT_INTERNAL_USD
settings.trigger_below_percent = DEFAULT_TRIGGER_PERCENT
settings.monthly_limit_chf = DEFAULT_MONTHLY_LIMIT_CHF
else:
settings.auto_topup_enabled = False
save_topup_settings(con, settings)
return settings, complete
def process_setup_checkout_completed(
con: sqlite3.Connection,
*,
session: Dict[str, Any],
) -> Dict[str, Any]:
md = session.get("metadata") or {}
if (md.get("aza_purpose") or "").strip() != "ai_topup_setup":
return {"handled": False}
practice_id = (md.get("practice_id") or "").strip()
if not practice_id:
return {"handled": False, "reason": "no_practice_id"}
customer_id, pm_id = resolve_auto_topup_setup_ids(session=session)
_, complete = _apply_auto_topup_setup_settings(
con,
practice_id=practice_id,
customer_id=customer_id,
payment_method_id=pm_id,
)
if not complete:
return {
"handled": True,
"practice_id": practice_id,
"auto_topup_enabled": False,
"reason": "AUTO_TOPUP_SETUP_INCOMPLETE",
"has_customer": bool(customer_id),
"has_payment_method": bool(pm_id),
}
return {
"handled": True,
"practice_id": practice_id,
"auto_topup_enabled": True,
"default_payment_method_id": pm_id,
"stripe_customer_id": customer_id,
}
def process_setup_intent_succeeded(
con: sqlite3.Connection,
*,
setup_intent: Dict[str, Any],
) -> Dict[str, Any]:
md = setup_intent.get("metadata") or {}
if (md.get("aza_purpose") or "").strip() != "ai_topup_setup":
return {"handled": False}
practice_id = (md.get("practice_id") or "").strip()
if not practice_id:
return {"handled": False, "reason": "no_practice_id"}
customer_id, pm_id = resolve_auto_topup_setup_ids(setup_intent=setup_intent)
_, complete = _apply_auto_topup_setup_settings(
con,
practice_id=practice_id,
customer_id=customer_id,
payment_method_id=pm_id,
)
if not complete:
return {
"handled": True,
"practice_id": practice_id,
"auto_topup_enabled": False,
"reason": "AUTO_TOPUP_SETUP_INCOMPLETE",
"has_customer": bool(customer_id),
"has_payment_method": bool(pm_id),
}
return {
"handled": True,
"practice_id": practice_id,
"auto_topup_enabled": True,
"default_payment_method_id": pm_id,
"stripe_customer_id": customer_id,
}
def repair_auto_topup_settings_from_checkout_session(
con: sqlite3.Connection,
*,
practice_id: str,
checkout_session_id: str,
) -> Dict[str, Any]:
"""Read-only Stripe-Abfrage + lokales Settings-Update (keine Zahlung)."""
key = (os.environ.get("STRIPE_SECRET_KEY") or "").strip()
if not key:
return {"ok": False, "reason": "no_stripe_key"}
import stripe
stripe.api_key = key
cs_id = normalize_stripe_id(checkout_session_id, "cs_")
if not cs_id:
return {"ok": False, "reason": "invalid_checkout_session_id"}
try:
cs = stripe.checkout.Session.retrieve(
cs_id,
expand=["setup_intent", "setup_intent.payment_method", "customer"],
)
except Exception:
return {"ok": False, "reason": "session_retrieve_failed"}
session = {
"id": getattr(cs, "id", None),
"customer": getattr(cs, "customer", None),
"setup_intent": getattr(cs, "setup_intent", None),
"metadata": getattr(cs, "metadata", None) or {},
}
customer_id, pm_id = resolve_auto_topup_setup_ids(session=session)
if pm_id and not customer_id:
try:
pm_obj = stripe.PaymentMethod.retrieve(pm_id)
customer_id = normalize_stripe_id(getattr(pm_obj, "customer", None), "cus_")
except Exception:
pass
_, complete = _apply_auto_topup_setup_settings(
con,
practice_id=practice_id,
customer_id=customer_id,
payment_method_id=pm_id,
)
if not complete:
return {
"ok": False,
"reason": "AUTO_TOPUP_SETUP_INCOMPLETE",
"has_customer": bool(customer_id),
"has_payment_method": bool(pm_id),
}
return {
"ok": True,
"auto_topup_enabled": True,
"has_customer": True,
"has_payment_method": True,
}
def process_topup_payment_intent_succeeded(
con: sqlite3.Connection,
*,
payment_intent: Dict[str, Any],
) -> Dict[str, Any]:
"""Idempotent backup wenn checkout.session.completed noch nicht lief."""
md = payment_intent.get("metadata") or {}
if (md.get("aza_purpose") or "").strip() != "ai_topup":
return {"handled": False, "reason": "not_ai_topup"}
pi_id = str(payment_intent.get("id") or "")
if ledger_exists_for_payment_intent(con, pi_id):
return {"handled": True, "duplicate": True, "payment_intent_id": pi_id}
practice_id = (md.get("practice_id") or "").strip() or None
subscription_id = (md.get("subscription_id") or "").strip() or None
try:
internal_usd = float(md.get("internal_credit_usd") or 0)
paid_chf = float(md.get("paid_chf") or 0)
except (TypeError, ValueError):
return {"handled": False, "reason": "invalid_metadata_amounts"}
if internal_usd <= 0 or paid_chf <= 0:
return {"handled": False, "reason": "zero_amounts"}
insert_ledger_event(
con,
practice_id=practice_id,
subscription_id=subscription_id,
customer_email=None,
event_type="topup_purchase",
amount_internal_usd=internal_usd,
amount_paid_chf=paid_chf,
stripe_payment_intent_id=pi_id,
stripe_customer_id=str(payment_intent.get("customer") or "") or None,
status="succeeded",
meta=_meta_with_receipt({"source": "payment_intent.succeeded"}, payment_intent_id=pi_id),
)
return {"handled": True, "credited": True, "payment_intent_id": pi_id}
def process_auto_topup_payment_intent_succeeded(
con: sqlite3.Connection,
*,
payment_intent: Dict[str, Any],
) -> Dict[str, Any]:
"""Idempotent: payment_intent.succeeded für aza_purpose=ai_auto_topup."""
md = payment_intent.get("metadata") or {}
if (md.get("aza_purpose") or "").strip() != "ai_auto_topup":
return {"handled": False, "reason": "not_ai_auto_topup"}
pi_id = str(payment_intent.get("id") or "")
if ledger_exists_for_payment_intent(con, pi_id):
return {"handled": True, "duplicate": True, "payment_intent_id": pi_id}
practice_id = (md.get("practice_id") or "").strip() or None
subscription_id = (md.get("subscription_id") or "").strip() or None
try:
internal_usd = float(md.get("internal_credit_usd") or 0)
paid_chf = float(md.get("paid_chf") or 0)
except (TypeError, ValueError):
return {"handled": False, "reason": "invalid_metadata_amounts"}
if internal_usd <= 0 or paid_chf <= 0:
return {"handled": False, "reason": "zero_amounts"}
insert_ledger_event(
con,
practice_id=practice_id,
subscription_id=subscription_id,
customer_email=None,
event_type="auto_topup_purchase",
amount_internal_usd=internal_usd,
amount_paid_chf=paid_chf,
stripe_payment_intent_id=pi_id,
stripe_customer_id=str(payment_intent.get("customer") or "") or None,
status="succeeded",
meta=_meta_with_receipt({"source": "payment_intent.succeeded"}, payment_intent_id=pi_id),
)
return {"handled": True, "credited": True, "payment_intent_id": pi_id}
def sum_topups_chf_this_month(con: sqlite3.Connection, practice_id: str) -> float:
"""Summe aller erfolgreichen Topups (manuell + auto) im Kalendermonat."""
start = _month_start_ts()
row = con.execute(
"""
SELECT COALESCE(SUM(amount_paid_chf), 0)
FROM ai_credit_ledger
WHERE practice_id = ?
AND status = 'succeeded'
AND event_type IN ('topup_purchase', 'auto_topup_purchase')
AND created_at >= ?
""",
(practice_id, start),
).fetchone()
return float(row[0] or 0.0)
def sum_auto_topups_chf_this_month(con: sqlite3.Connection, practice_id: str) -> float:
"""Nur automatische Aufladungen für Auto-Monatslimit CHF 300."""
start = _month_start_ts()
row = con.execute(
"""
SELECT COALESCE(SUM(amount_paid_chf), 0)
FROM ai_credit_ledger
WHERE practice_id = ?
AND status = 'succeeded'
AND event_type = 'auto_topup_purchase'
AND created_at >= ?
""",
(practice_id, start),
).fetchone()
return float(row[0] or 0.0)
def maybe_run_auto_topup_for_practice(
con: sqlite3.Connection,
*,
practice_id: str,
available_percent: int,
dry_run: bool = True,
subscription_id: Optional[str] = None,
customer_email: Optional[str] = None,
) -> Dict[str, Any]:
"""
Auto-Aufladung nur wenn alle Schutzbedingungen erfüllt sind.
Live-Charge nur bei AZA_AI_AUTO_TOPUP_ENABLED=1, AZA_AI_TOPUP_ALLOW_LIVE=1,
AZA_AI_TOPUP_DRY_RUN=0.
"""
pid = (practice_id or "").strip()
if not pid:
return {"ok": False, "reason": "no_practice_id"}
settings = get_topup_settings(con, pid)
if not settings.auto_topup_enabled:
return {"ok": False, "reason": "auto_topup_not_enabled"}
if available_percent >= settings.trigger_below_percent:
return {"ok": False, "reason": "above_trigger"}
if not normalize_stripe_id(settings.stripe_customer_id, "cus_") or not normalize_stripe_id(
settings.default_payment_method_id, "pm_"
):
return {"ok": False, "reason": "no_payment_method"}
if settings.topup_amount_chf < MIN_TOPUP_CHF or settings.topup_amount_chf > MAX_TOPUP_CHF:
return {"ok": False, "reason": "invalid_amount"}
spent = sum_auto_topups_chf_this_month(con, pid)
if spent + settings.topup_amount_chf > settings.monthly_limit_chf + 1e-9:
return {
"ok": False,
"reason": "auto_topup_monthly_limit_reached",
"error_code": "AUTO_TOPUP_MONTHLY_LIMIT_REACHED",
"message_user": AUTO_TOPUP_MONTHLY_LIMIT_MESSAGE,
}
last_ts = last_successful_auto_topup_ts(con, pid)
if last_ts and (_now() - last_ts) < AUTO_TOPUP_COOLDOWN_SEC:
return {"ok": False, "reason": "cooldown_24h"}
if has_pending_auto_topup(con, pid):
return {"ok": False, "reason": "pending_auto_topup"}
if dry_run or not _topup_allow_live_env() or not _auto_topup_enabled_env():
return {
"ok": True,
"dry_run": True,
"would_charge_chf": settings.topup_amount_chf,
"would_credit_usd": settings.internal_credit_usd,
}
if _topup_dry_run_env():
return {
"ok": True,
"dry_run": True,
"would_charge_chf": settings.topup_amount_chf,
"would_credit_usd": settings.internal_credit_usd,
}
return _execute_live_auto_topup(
con,
settings=settings,
subscription_id=subscription_id,
customer_email=customer_email,
)
def _execute_live_auto_topup(
con: sqlite3.Connection,
*,
settings: TopupSettings,
subscription_id: Optional[str],
customer_email: Optional[str],
) -> Dict[str, Any]:
pid = settings.practice_id
try:
con.execute("BEGIN IMMEDIATE")
if has_pending_auto_topup(con, pid):
con.execute("ROLLBACK")
return {"ok": False, "reason": "pending_auto_topup"}
last_ts = last_successful_auto_topup_ts(con, pid)
if last_ts and (_now() - last_ts) < AUTO_TOPUP_COOLDOWN_SEC:
con.execute("ROLLBACK")
return {"ok": False, "reason": "cooldown_24h"}
spent = sum_auto_topups_chf_this_month(con, pid)
if spent + settings.topup_amount_chf > settings.monthly_limit_chf + 1e-9:
con.execute("ROLLBACK")
return {
"ok": False,
"reason": "auto_topup_monthly_limit_reached",
"error_code": "AUTO_TOPUP_MONTHLY_LIMIT_REACHED",
"message_user": AUTO_TOPUP_MONTHLY_LIMIT_MESSAGE,
}
ledger_id = insert_ledger_event(
con,
practice_id=pid,
subscription_id=subscription_id,
customer_email=customer_email,
event_type="auto_topup_purchase",
amount_internal_usd=settings.internal_credit_usd,
amount_paid_chf=settings.topup_amount_chf,
stripe_customer_id=settings.stripe_customer_id,
status="pending",
meta={"source": "auto_topup_off_session"},
commit=False,
)
import stripe
stripe.api_key = os.environ.get("STRIPE_SECRET_KEY", "")
amount_rappen = int(round(settings.topup_amount_chf * 100))
idempotency_key = _auto_topup_idempotency_key(pid)
try:
pi = stripe.PaymentIntent.create(
amount=amount_rappen,
currency="chf",
customer=normalize_stripe_id(settings.stripe_customer_id, "cus_"),
payment_method=normalize_stripe_id(settings.default_payment_method_id, "pm_"),
off_session=True,
confirm=True,
metadata={
"aza_purpose": "ai_auto_topup",
"practice_id": pid,
"subscription_id": subscription_id or "",
"internal_credit_usd": str(settings.internal_credit_usd),
"paid_chf": str(settings.topup_amount_chf),
},
idempotency_key=idempotency_key,
)
except stripe.error.CardError as exc:
pi_obj = getattr(exc, "error", None)
pi_id = None
if pi_obj is not None:
pi_id = getattr(getattr(pi_obj, "payment_intent", None), "id", None)
update_ledger_event(con, ledger_id, status="failed", commit=False)
_pause_auto_topup_on_failure(
con,
settings,
reason="card_error",
payment_intent_id=None,
commit=False,
)
con.commit()
return {"ok": False, "reason": "payment_failed", "charged": False, "paused": True}
except Exception:
update_ledger_event(con, ledger_id, status="failed", commit=False)
_pause_auto_topup_on_failure(
con,
settings,
reason="stripe_error",
commit=False,
)
con.commit()
return {"ok": False, "reason": "payment_failed", "charged": False, "paused": True}
pi_id = str(getattr(pi, "id", "") or "")
pi_status = str(getattr(pi, "status", "") or "")
if pi_status == "succeeded":
update_ledger_event(
con,
ledger_id,
status="succeeded",
stripe_payment_intent_id=pi_id or None,
meta=_meta_with_receipt({"source": "auto_topup_off_session"}, payment_intent_id=pi_id or None),
commit=False,
)
con.commit()
return {
"ok": True,
"charged": True,
"amount_chf": settings.topup_amount_chf,
"internal_credit_usd": settings.internal_credit_usd,
"payment_intent_id": pi_id,
}
update_ledger_event(
con,
ledger_id,
status="failed",
stripe_payment_intent_id=pi_id or None,
meta={"stripe_status": pi_status},
commit=False,
)
_pause_auto_topup_on_failure(
con,
settings,
reason=pi_status or "requires_action",
payment_intent_id=None,
commit=False,
)
con.commit()
return {"ok": False, "reason": "payment_failed", "charged": False, "paused": True}
except Exception:
try:
con.execute("ROLLBACK")
except Exception:
pass
return {"ok": False, "reason": "internal_error", "charged": False}
def _stripe_live_key() -> bool:
key = (os.environ.get("STRIPE_SECRET_KEY") or "").strip()
return key.startswith("sk_live")
def create_topup_checkout_session(
*,
practice_id: str,
subscription_id: str,
customer_email: str,
paid_chf: float,
internal_usd: float,
save_payment_method: bool = False,
success_url: str,
cancel_url: str,
) -> Dict[str, Any]:
if _stripe_live_key() and not _topup_allow_live_env():
return {"ok": False, "error_code": "LIVE_TOPUP_BLOCKED", "dry_run": True}
if _topup_dry_run_env():
pending_id = f"dry_chk_{uuid.uuid4().hex[:16]}"
return {
"ok": True,
"dry_run": True,
"checkout_url": None,
"session_id": pending_id,
"paid_chf": paid_chf,
"internal_credit_usd": internal_usd,
}
import stripe
stripe.api_key = os.environ.get("STRIPE_SECRET_KEY", "")
amount_rappen = int(round(paid_chf * 100))
params: Dict[str, Any] = {
"mode": "payment",
"success_url": success_url,
"cancel_url": cancel_url,
"customer_email": customer_email,
"line_items": [
{
"price_data": {
"currency": "chf",
"product_data": {"name": "AzA KI-Zusatzguthaben"},
"unit_amount": amount_rappen,
},
"quantity": 1,
}
],
"metadata": {
"aza_purpose": "ai_topup",
"practice_id": practice_id,
"subscription_id": subscription_id,
"internal_credit_usd": str(internal_usd),
"paid_chf": str(paid_chf),
},
}
if save_payment_method:
params["payment_intent_data"] = {"setup_future_usage": "off_session"}
session = stripe.checkout.Session.create(**params)
return {
"ok": True,
"dry_run": False,
"checkout_url": session.url,
"session_id": session.id,
"paid_chf": paid_chf,
"internal_credit_usd": internal_usd,
}
def create_setup_checkout_session(
*,
practice_id: str,
customer_email: str,
success_url: str,
cancel_url: str,
) -> Dict[str, Any]:
if _topup_dry_run_env():
return {
"ok": True,
"dry_run": True,
"checkout_url": None,
"session_id": f"dry_setup_{uuid.uuid4().hex[:12]}",
}
if _stripe_live_key() and not _topup_allow_live_env():
return {"ok": False, "error_code": "LIVE_SETUP_BLOCKED", "dry_run": True}
import stripe
stripe.api_key = os.environ.get("STRIPE_SECRET_KEY", "")
session = stripe.checkout.Session.create(
mode="setup",
currency="chf",
payment_method_types=["card"],
customer_creation="always",
success_url=success_url,
cancel_url=cancel_url,
customer_email=customer_email,
custom_text={
"submit": {"message": AUTO_TOPUP_CONSENT_TEXT},
},
metadata={
"aza_purpose": "ai_topup_setup",
"practice_id": practice_id,
"consent_text": AUTO_TOPUP_CONSENT_TEXT[:500],
},
setup_intent_data={
"metadata": {
"aza_purpose": "ai_topup_setup",
"practice_id": practice_id,
},
},
)
return {
"ok": True,
"dry_run": False,
"checkout_url": session.url,
"session_id": session.id,
}
def admin_credit_overview_row(
con: sqlite3.Connection,
*,
practice_id: str,
subscription_id: str,
monthly_budget: float,
snap: Dict[str, Any],
) -> Dict[str, Any]:
settings = get_topup_settings(con, practice_id)
extra = compute_extra_credit_remaining(
con,
practice_id=practice_id,
subscription_id=subscription_id,
monthly_budget_usd=monthly_budget,
)
topups_month = sum_topups_chf_this_month(con, practice_id)
failed = con.execute(
"""
SELECT COUNT(*) FROM ai_credit_ledger
WHERE practice_id = ? AND status = 'failed'
AND event_type IN ('topup_purchase', 'auto_topup_purchase', 'failed_auto_topup')
""",
(practice_id,),
).fetchone()[0]
sub_m = subscription_id[:10] + "" if len(subscription_id) > 12 else subscription_id
return {
"practice_id": practice_id,
"subscription_id_masked": sub_m,
"monthly_budget_usd": round(monthly_budget, 2),
"used_current_period_usd": snap.get("used_usd"),
"monthly_remaining_usd": snap.get("remaining_usd"),
"extra_credit_remaining_usd": extra,
"available_percent": snap.get("available_percent"),
"auto_topup_enabled": settings.auto_topup_enabled,
"topups_chf_this_month": round(topups_month, 2),
"failed_topup_events": int(failed or 0),
}
def list_ledger_for_practice(
con: sqlite3.Connection,
practice_id: str,
*,
limit: int = 100,
) -> List[Dict[str, Any]]:
rows = con.execute(
"""
SELECT id, event_type, amount_internal_usd, amount_paid_chf, status,
stripe_checkout_session_id, created_at, updated_at
FROM ai_credit_ledger
WHERE practice_id = ?
ORDER BY created_at DESC
LIMIT ?
""",
(practice_id, limit),
).fetchall()
out = []
for r in rows:
sid = r[5] or ""
out.append(
{
"id": r[0],
"event_type": r[1],
"amount_internal_usd": r[2],
"amount_paid_chf": r[3],
"status": r[4],
"checkout_session_masked": (sid[:12] + "") if len(sid) > 14 else sid,
"created_at": r[6],
}
)
return out
def list_user_credit_history(
con: sqlite3.Connection,
*,
practice_id: str,
limit: int = 100,
) -> List[Dict[str, Any]]:
"""Benutzer-Historie: nur eigene practice_id, maskierte Stripe-Referenzen."""
pid = (practice_id or "").strip()
placeholders = ",".join("?" for _ in USER_HISTORY_EVENT_TYPES)
rows = con.execute(
f"""
SELECT event_type, amount_internal_usd, amount_paid_chf, status,
stripe_payment_intent_id, stripe_checkout_session_id, meta_json, created_at
FROM ai_credit_ledger
WHERE practice_id = ?
AND event_type IN ({placeholders})
ORDER BY created_at DESC
LIMIT ?
""",
(pid, *USER_HISTORY_EVENT_TYPES, limit),
).fetchall()
out: List[Dict[str, Any]] = []
for r in rows:
meta: Dict[str, Any] = {}
if r[6]:
try:
parsed = json.loads(r[6])
if isinstance(parsed, dict):
meta = parsed
except (TypeError, ValueError, json.JSONDecodeError):
meta = {}
stripe_ref = mask_stripe_reference(r[4]) or mask_stripe_reference(r[5])
item: Dict[str, Any] = {
"created_at": int(r[7]),
"date_de": _format_date_de(int(r[7])),
"event_type": r[0],
"amount_paid_chf": r[2],
"amount_internal_usd": r[1],
"status": r[3],
"stripe_payment_reference": stripe_ref,
}
receipt = meta.get("receipt_url")
if isinstance(receipt, str) and receipt.startswith("https://"):
item["receipt_url"] = receipt
out.append(item)
return out