Files
aza/AzA march 2026/aza_ai_credit.py
2026-05-23 21:31:34 +02:00

1478 lines
51 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- 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