This commit is contained in:
2026-05-23 21:31:34 +02:00
parent 51b5ddc6f2
commit 641bb10479
6155 changed files with 3775717 additions and 291 deletions

View File

@@ -0,0 +1,3 @@
Backup Auto-Topup URL/Trigger/Disable
Timestamp: 20260521_185350
Restore: copy files back to project root

View File

@@ -0,0 +1,234 @@
# -*- coding: utf-8 -*-
"""API-Routen KI-Zusatzguthaben (Phase 1, keine Live-Charges ohne Freigabe)."""
from __future__ import annotations
import sqlite3
from pathlib import Path
from typing import Any, Dict, Optional
from fastapi import APIRouter, Depends, Header, HTTPException, Query, Request
from fastapi.responses import JSONResponse
from pydantic import BaseModel, Field
from aza_ai_budget import resolve_license_for_empfang
from aza_ai_credit import (
DEFAULT_TOPUP_CANCEL_URL,
DEFAULT_TOPUP_CHF,
DEFAULT_TOPUP_SUCCESS_URL,
MAX_TOPUP_CHF,
MIN_TOPUP_CHF,
TopupSettings,
chf_to_internal_usd,
create_setup_checkout_session,
create_topup_checkout_session,
ensure_ai_credit_schema,
get_topup_settings,
list_user_credit_history,
save_topup_settings,
)
from aza_security import require_api_token
router = APIRouter(tags=["ai-credit"], dependencies=[Depends(require_api_token)])
def _stripe_db_path() -> Path:
import os
base = Path(__file__).resolve().parent
return Path(os.environ.get("STRIPE_DB_PATH", str(base / "data" / "stripe_webhook.sqlite")))
def _resolve_practice_license(
*,
device_id: Optional[str],
practice_id: Optional[str],
):
db_path = _stripe_db_path()
if not db_path.exists():
raise HTTPException(status_code=503, detail="Billing database unavailable")
import backend_main as bm
bm.ensure_license_schema(db_path)
with sqlite3.connect(str(db_path)) as con:
ensure_ai_credit_schema(con)
lic = resolve_license_for_empfang(
con,
x_device_id=device_id,
session_practice_id=practice_id,
)
if not lic:
raise HTTPException(status_code=402, detail="No active license mapping")
if not (lic.practice_id or "").strip():
raise HTTPException(status_code=400, detail="practice_id missing on license")
return con, lic
class TopupCheckoutIn(BaseModel):
package_chf: Optional[float] = Field(default=30.0)
custom_amount_chf: Optional[float] = None
save_payment_method: bool = False
success_url: str = Field(default=DEFAULT_TOPUP_SUCCESS_URL)
cancel_url: str = Field(default=DEFAULT_TOPUP_CANCEL_URL)
class AutoTopupSettingsIn(BaseModel):
enabled: bool = False
amount_chf: float = Field(default=30.0, ge=MIN_TOPUP_CHF, le=MAX_TOPUP_CHF)
trigger_below_percent: int = Field(default=10, ge=1, le=50)
monthly_limit_chf: float = Field(default=90.0, ge=MIN_TOPUP_CHF, le=900.0)
class SetupSessionIn(BaseModel):
success_url: str = Field(default="https://aza-medwork.ch/ki-topup/setup-success")
cancel_url: str = Field(default="https://aza-medwork.ch/ki-topup/setup-cancel")
@router.post("/v1/ai-credit/topup/checkout")
def ai_credit_topup_checkout(
body: TopupCheckoutIn,
request: Request,
x_device_id: Optional[str] = Header(default=None, alias="X-Device-Id"),
x_practice_id: Optional[str] = Header(default=None, alias="X-Practice-Id"),
) -> JSONResponse:
device_id = (x_device_id or "").strip() or None
practice_hdr = (x_practice_id or "").strip() or None
db_path = _stripe_db_path()
import backend_main as bm
bm.ensure_license_schema(db_path)
with sqlite3.connect(str(db_path)) as con:
ensure_ai_credit_schema(con)
lic = resolve_license_for_empfang(
con,
x_device_id=device_id,
session_practice_id=practice_hdr,
)
if not lic or not (lic.practice_id or "").strip():
return JSONResponse(status_code=402, content={"ok": False, "error_code": "NO_LICENSE"})
paid_chf = float(body.custom_amount_chf or body.package_chf or DEFAULT_TOPUP_CHF)
if paid_chf < MIN_TOPUP_CHF or paid_chf > MAX_TOPUP_CHF:
return JSONResponse(
status_code=400,
content={"ok": False, "error_code": "INVALID_AMOUNT", "min_chf": MIN_TOPUP_CHF, "max_chf": MAX_TOPUP_CHF},
)
internal_usd = chf_to_internal_usd(paid_chf)
result = create_topup_checkout_session(
practice_id=lic.practice_id or "",
subscription_id=lic.subscription_id,
customer_email=lic.customer_email,
paid_chf=paid_chf,
internal_usd=internal_usd,
save_payment_method=body.save_payment_method,
success_url=DEFAULT_TOPUP_SUCCESS_URL,
cancel_url=(body.cancel_url or DEFAULT_TOPUP_CANCEL_URL).strip() or DEFAULT_TOPUP_CANCEL_URL,
)
return JSONResponse(content=result)
@router.get("/v1/ai-credit/history")
def ai_credit_history(
x_device_id: Optional[str] = Header(default=None, alias="X-Device-Id"),
x_practice_id: Optional[str] = Header(default=None, alias="X-Practice-Id"),
limit: int = Query(default=100, ge=1, le=200),
) -> JSONResponse:
device_id = (x_device_id or "").strip() or None
practice_hdr = (x_practice_id or "").strip() or None
db_path = _stripe_db_path()
import backend_main as bm
bm.ensure_license_schema(db_path)
with sqlite3.connect(str(db_path)) as con:
ensure_ai_credit_schema(con)
lic = resolve_license_for_empfang(
con,
x_device_id=device_id,
session_practice_id=practice_hdr,
)
if not lic or not (lic.practice_id or "").strip():
return JSONResponse(status_code=402, content={"ok": False, "error_code": "NO_LICENSE"})
pid = lic.practice_id or ""
items = list_user_credit_history(con, practice_id=pid, limit=limit)
return JSONResponse(
content={
"ok": True,
"practice_id": pid,
"subscription_id": lic.subscription_id,
"items": items,
}
)
@router.post("/v1/ai-credit/auto-topup/settings")
def ai_credit_auto_topup_settings(
body: AutoTopupSettingsIn,
x_device_id: Optional[str] = Header(default=None, alias="X-Device-Id"),
x_practice_id: Optional[str] = Header(default=None, alias="X-Practice-Id"),
) -> JSONResponse:
device_id = (x_device_id or "").strip() or None
practice_hdr = (x_practice_id or "").strip() or None
db_path = _stripe_db_path()
import backend_main as bm
bm.ensure_license_schema(db_path)
with sqlite3.connect(str(db_path)) as con:
ensure_ai_credit_schema(con)
lic = resolve_license_for_empfang(
con,
x_device_id=device_id,
session_practice_id=practice_hdr,
)
if not lic or not (lic.practice_id or "").strip():
return JSONResponse(status_code=402, content={"ok": False, "error_code": "NO_LICENSE"})
existing = get_topup_settings(con, lic.practice_id or "")
settings = TopupSettings(
practice_id=lic.practice_id or "",
auto_topup_enabled=body.enabled,
topup_amount_chf=body.amount_chf,
internal_credit_usd=chf_to_internal_usd(body.amount_chf),
trigger_below_percent=body.trigger_below_percent,
monthly_limit_chf=body.monthly_limit_chf,
stripe_customer_id=existing.stripe_customer_id,
default_payment_method_id=existing.default_payment_method_id,
)
save_topup_settings(con, settings)
return JSONResponse(
content={
"ok": True,
"practice_id": settings.practice_id,
"auto_topup_enabled": settings.auto_topup_enabled,
"amount_chf": settings.topup_amount_chf,
"trigger_below_percent": settings.trigger_below_percent,
"monthly_limit_chf": settings.monthly_limit_chf,
}
)
@router.post("/v1/ai-credit/auto-topup/setup-session")
def ai_credit_auto_topup_setup_session(
body: SetupSessionIn,
x_device_id: Optional[str] = Header(default=None, alias="X-Device-Id"),
x_practice_id: Optional[str] = Header(default=None, alias="X-Practice-Id"),
) -> JSONResponse:
device_id = (x_device_id or "").strip() or None
practice_hdr = (x_practice_id or "").strip() or None
db_path = _stripe_db_path()
import backend_main as bm
bm.ensure_license_schema(db_path)
with sqlite3.connect(str(db_path)) as con:
ensure_ai_credit_schema(con)
lic = resolve_license_for_empfang(
con,
x_device_id=device_id,
session_practice_id=practice_hdr,
)
if not lic or not (lic.practice_id or "").strip():
return JSONResponse(status_code=402, content={"ok": False, "error_code": "NO_LICENSE"})
result = create_setup_checkout_session(
practice_id=lic.practice_id or "",
customer_email=lic.customer_email,
success_url=body.success_url,
cancel_url=body.cancel_url,
)
return JSONResponse(content=result)

View File

@@ -0,0 +1,834 @@
# -*- coding: utf-8 -*-
"""Tests KI-Zusatzguthaben / Ledger (Mocks, keine Stripe-Charges)."""
from __future__ import annotations
import json
import os
import sqlite3
import tempfile
import unittest
from pathlib import Path
from aza_ai_budget import (
LicenseBudgetRow,
budget_gate_blocked_payload_or_none,
check_allows_openai_call,
compute_budget_snapshot,
ensure_ai_budget_schema,
insert_usage_event,
)
from aza_ai_credit import (
AUTO_TOPUP_COOLDOWN_SEC,
DEFAULT_TOPUP_SUCCESS_URL,
chf_to_internal_usd,
compute_extra_credit_remaining,
create_setup_checkout_session,
create_topup_checkout_session,
ensure_ai_credit_schema,
get_topup_settings,
has_pending_auto_topup,
insert_ledger_event,
last_successful_auto_topup_ts,
ledger_exists_for_checkout,
ledger_exists_for_payment_intent,
list_ledger_for_practice,
list_user_credit_history,
mask_stripe_reference,
maybe_run_auto_topup_for_practice,
process_auto_topup_payment_intent_succeeded,
process_setup_checkout_completed,
process_setup_intent_succeeded,
process_topup_checkout_completed,
process_topup_payment_intent_succeeded,
save_topup_settings,
TopupSettings,
)
def _mk_db(path: Path) -> None:
con = sqlite3.connect(str(path))
con.execute(
"""
CREATE TABLE licenses (
subscription_id TEXT PRIMARY KEY,
customer_id TEXT,
status TEXT,
lookup_key TEXT,
allowed_users INTEGER,
devices_per_user INTEGER,
customer_email TEXT,
client_reference_id TEXT,
current_period_end INTEGER,
current_period_start INTEGER,
updated_at INTEGER NOT NULL,
license_key TEXT,
practice_id TEXT
)
"""
)
now = 1_700_000_000
ps = now
pe = now + 86400 * 30
con.execute(
"""
INSERT INTO licenses(subscription_id, customer_id, status, lookup_key, allowed_users, devices_per_user,
customer_email, client_reference_id, current_period_end, current_period_start, updated_at, license_key, practice_id)
VALUES ('sub_top', 'cus_x', 'active', 'aza_basic_monthly', 1, 2,
'cli@example.test', NULL, ?, ?, ?, 'KEY', 'prac_top')
""",
(pe, ps, now),
)
con.commit()
con.close()
def _lic() -> LicenseBudgetRow:
now = 1_700_000_000
return LicenseBudgetRow(
subscription_id="sub_top",
customer_email="cli@example.test",
customer_id="cus_x",
practice_id="prac_top",
lookup_key="aza_basic_monthly",
status="active",
period_start=now,
period_end=now + 86400 * 30,
)
class TestAiCreditTopup(unittest.TestCase):
def setUp(self) -> None:
self.tmp = tempfile.NamedTemporaryFile(suffix=".sqlite", delete=False)
self.tmp.close()
self.db = Path(self.tmp.name)
_mk_db(self.db)
self.con = sqlite3.connect(str(self.db))
ensure_ai_budget_schema(self.con)
ensure_ai_credit_schema(self.con)
def tearDown(self) -> None:
self.con.close()
self.db.unlink(missing_ok=True)
def test_chf30_gives_20_usd(self) -> None:
self.assertAlmostEqual(chf_to_internal_usd(30), 20.0, places=2)
def test_custom_amount_formula(self) -> None:
self.assertAlmostEqual(chf_to_internal_usd(45), 30.0, places=1)
def test_ledger_schema_idempotent(self) -> None:
ensure_ai_credit_schema(self.con)
ensure_ai_credit_schema(self.con)
cols = [r[1] for r in self.con.execute("PRAGMA table_info(ai_credit_ledger)").fetchall()]
self.assertIn("event_type", cols)
def test_webhook_writes_single_credit(self) -> None:
session = {
"id": "cs_test_001",
"payment_status": "paid",
"metadata": {
"aza_purpose": "ai_topup",
"practice_id": "prac_top",
"subscription_id": "sub_top",
"internal_credit_usd": "20",
"paid_chf": "30",
},
}
r1 = process_topup_checkout_completed(self.con, session=session)
r2 = process_topup_checkout_completed(self.con, session=session)
self.assertTrue(r1.get("credited"))
self.assertTrue(r2.get("duplicate"))
cnt = self.con.execute(
"SELECT COUNT(*) FROM ai_credit_ledger WHERE stripe_checkout_session_id='cs_test_001'"
).fetchone()[0]
self.assertEqual(cnt, 1)
def test_failed_payment_no_credit(self) -> None:
session = {
"id": "cs_test_fail",
"payment_status": "unpaid",
"metadata": {
"aza_purpose": "ai_topup",
"practice_id": "prac_top",
"subscription_id": "sub_top",
"internal_credit_usd": "20",
"paid_chf": "30",
},
}
r = process_topup_checkout_completed(self.con, session=session)
self.assertFalse(r.get("credited"))
row = self.con.execute(
"SELECT status, amount_internal_usd FROM ai_credit_ledger WHERE stripe_checkout_session_id='cs_test_fail'"
).fetchone()
self.assertEqual(row[0], "failed")
self.assertEqual(row[1], 0.0)
def test_budget_includes_extra_when_monthly_exhausted(self) -> None:
lic = _lic()
insert_ledger_event(
self.con,
practice_id="prac_top",
subscription_id="sub_top",
customer_email=None,
event_type="topup_purchase",
amount_internal_usd=20.0,
amount_paid_chf=30.0,
stripe_checkout_session_id="cs_paid",
status="succeeded",
)
ps, pe = lic.period_start, lic.period_end
insert_usage_event(
self.con,
lic=lic,
device_id=None,
period_start=ps,
period_end=pe,
operation_type="chat",
model="gpt-4o-mini",
input_tokens=1,
output_tokens=1,
total_tokens=2,
audio_seconds=0.0,
estimated_cost_usd=20.0,
request_id="u1",
status="success",
)
snap = compute_budget_snapshot(self.con, lic)
self.assertEqual(snap["remaining_usd"], 0.0)
self.assertGreater(snap.get("extra_credit_remaining_usd", 0), 0)
ok, _ = check_allows_openai_call(self.con, lic)
self.assertTrue(ok)
def test_month_rollover_extra_persists(self) -> None:
lic = _lic()
insert_ledger_event(
self.con,
practice_id="prac_top",
subscription_id="sub_top",
customer_email=None,
event_type="topup_purchase",
amount_internal_usd=15.0,
amount_paid_chf=22.5,
stripe_checkout_session_id="cs_roll",
status="succeeded",
)
extra_before = compute_extra_credit_remaining(
self.con,
practice_id="prac_top",
subscription_id="sub_top",
monthly_budget_usd=20.0,
)
lic_new = LicenseBudgetRow(
subscription_id="sub_top",
customer_email="cli@example.test",
customer_id="cus_x",
practice_id="prac_top",
lookup_key="aza_basic_monthly",
status="active",
period_start=lic.period_end + 1,
period_end=lic.period_end + 86400 * 30,
)
extra_after = compute_extra_credit_remaining(
self.con,
practice_id="prac_top",
subscription_id="sub_top",
monthly_budget_usd=20.0,
)
self.assertAlmostEqual(extra_before, extra_after, places=2)
snap = compute_budget_snapshot(self.con, lic_new)
self.assertEqual(snap["used_usd"], 0.0)
self.assertAlmostEqual(snap.get("extra_credit_remaining_usd", 0), 15.0, places=2)
def test_auto_topup_settings_save(self) -> None:
settings = TopupSettings(
practice_id="prac_top",
auto_topup_enabled=True,
topup_amount_chf=30.0,
internal_credit_usd=20.0,
trigger_below_percent=10,
monthly_limit_chf=90.0,
stripe_customer_id=None,
default_payment_method_id=None,
)
save_topup_settings(self.con, settings)
loaded = get_topup_settings(self.con, "prac_top")
self.assertTrue(loaded.auto_topup_enabled)
self.assertEqual(loaded.monthly_limit_chf, 90.0)
def test_setup_session_dry_run_no_charge(self) -> None:
os.environ["AZA_AI_TOPUP_DRY_RUN"] = "1"
r = create_setup_checkout_session(
practice_id="prac_top",
customer_email="cli@example.test",
success_url="https://example/s",
cancel_url="https://example/c",
)
self.assertTrue(r.get("dry_run"))
self.assertIsNone(r.get("checkout_url"))
def test_topup_checkout_dry_run(self) -> None:
os.environ["AZA_AI_TOPUP_DRY_RUN"] = "1"
r = create_topup_checkout_session(
practice_id="prac_top",
subscription_id="sub_top",
customer_email="cli@example.test",
paid_chf=30.0,
internal_usd=20.0,
success_url="https://example/s",
cancel_url="https://example/c",
)
self.assertTrue(r.get("dry_run"))
self.assertIsNone(r.get("checkout_url"))
def test_auto_topup_respects_monthly_limit(self) -> None:
save_topup_settings(
self.con,
TopupSettings(
practice_id="prac_top",
auto_topup_enabled=True,
topup_amount_chf=30.0,
internal_credit_usd=20.0,
trigger_below_percent=50,
monthly_limit_chf=60.0,
stripe_customer_id="cus_x",
default_payment_method_id="pm_test",
),
)
for i in range(2):
insert_ledger_event(
self.con,
practice_id="prac_top",
subscription_id="sub_top",
customer_email=None,
event_type="auto_topup_purchase",
amount_internal_usd=20.0,
amount_paid_chf=30.0,
stripe_checkout_session_id=f"cs_auto_{i}",
status="succeeded",
)
r = maybe_run_auto_topup_for_practice(self.con, practice_id="prac_top", available_percent=5, dry_run=True)
self.assertEqual(r.get("reason"), "monthly_limit_reached")
def test_no_auto_charge_without_payment_method(self) -> None:
save_topup_settings(
self.con,
TopupSettings(
practice_id="prac_top",
auto_topup_enabled=True,
topup_amount_chf=30.0,
internal_credit_usd=20.0,
trigger_below_percent=10,
monthly_limit_chf=90.0,
stripe_customer_id="cus_x",
default_payment_method_id=None,
),
)
r = maybe_run_auto_topup_for_practice(self.con, practice_id="prac_top", available_percent=5, dry_run=False)
self.assertEqual(r.get("reason"), "no_payment_method")
def test_meta_json_no_secrets(self) -> None:
eid = insert_ledger_event(
self.con,
practice_id="prac_top",
subscription_id="sub_top",
customer_email=None,
event_type="topup_purchase",
amount_internal_usd=1.0,
meta={"note": "ok", "api_secret_key": "must_not_store", "default_payment_method_id": "pm_x"},
)
row = self.con.execute("SELECT meta_json FROM ai_credit_ledger WHERE id=?", (eid,)).fetchone()
meta = json.loads(row[0])
self.assertNotIn("api_secret_key", meta)
self.assertNotIn("default_payment_method_id", meta)
def test_usage_events_unchanged_schema(self) -> None:
cols = [r[1] for r in self.con.execute("PRAGMA table_info(ai_usage_events)").fetchall()]
self.assertIn("estimated_cost_usd", cols)
self.assertIn("subscription_id", cols)
def test_ledger_exists_for_checkout(self) -> None:
self.assertFalse(ledger_exists_for_checkout(self.con, "cs_new"))
insert_ledger_event(
self.con,
practice_id="prac_top",
subscription_id="sub_top",
customer_email=None,
event_type="topup_purchase",
amount_internal_usd=1.0,
stripe_checkout_session_id="cs_new",
status="succeeded",
)
self.assertTrue(ledger_exists_for_checkout(self.con, "cs_new"))
def test_setup_webhook_enables_auto_with_defaults(self) -> None:
session = {
"id": "cs_setup_1",
"customer": "cus_setup",
"setup_intent": None,
"metadata": {"aza_purpose": "ai_topup_setup", "practice_id": "prac_top"},
}
r = process_setup_checkout_completed(self.con, session=session)
self.assertTrue(r.get("handled"))
self.assertTrue(r.get("auto_topup_enabled"))
loaded = get_topup_settings(self.con, "prac_top")
self.assertTrue(loaded.auto_topup_enabled)
self.assertEqual(loaded.stripe_customer_id, "cus_setup")
self.assertEqual(loaded.topup_amount_chf, 30.0)
self.assertEqual(loaded.internal_credit_usd, 20.0)
self.assertEqual(loaded.trigger_below_percent, 10)
self.assertEqual(loaded.monthly_limit_chf, 90.0)
ledger_count = self.con.execute("SELECT COUNT(*) FROM ai_credit_ledger").fetchone()[0]
self.assertEqual(ledger_count, 0)
def test_setup_intent_succeeded_saves_payment_method(self) -> None:
setup_intent = {
"id": "seti_test_1",
"customer": "cus_setup2",
"payment_method": "pm_setup_test",
"metadata": {"aza_purpose": "ai_topup_setup", "practice_id": "prac_top"},
}
r = process_setup_intent_succeeded(self.con, setup_intent=setup_intent)
self.assertTrue(r.get("handled"))
loaded = get_topup_settings(self.con, "prac_top")
self.assertTrue(loaded.auto_topup_enabled)
self.assertEqual(loaded.stripe_customer_id, "cus_setup2")
self.assertEqual(loaded.default_payment_method_id, "pm_setup_test")
def test_setup_checkout_with_embedded_setup_intent_pm(self) -> None:
session = {
"id": "cs_setup_2",
"customer": "cus_setup3",
"setup_intent": {
"id": "seti_embedded",
"payment_method": {"id": "pm_embedded"},
},
"metadata": {"aza_purpose": "ai_topup_setup", "practice_id": "prac_top"},
}
r = process_setup_checkout_completed(self.con, session=session)
self.assertTrue(r.get("handled"))
loaded = get_topup_settings(self.con, "prac_top")
self.assertEqual(loaded.default_payment_method_id, "pm_embedded")
def test_setup_session_response_has_checkout_url_field(self) -> None:
os.environ["AZA_AI_TOPUP_DRY_RUN"] = "1"
r = create_setup_checkout_session(
practice_id="prac_top",
customer_email="cli@example.test",
success_url="https://example/s",
cancel_url="https://example/c",
)
self.assertIn("checkout_url", r)
self.assertTrue(r.get("ok"))
def test_setup_url_field_aliases_accepted(self) -> None:
for key in ("checkout_url", "setup_url", "url"):
payload = {key: "https://checkout.stripe.test/setup"}
picked = (
payload.get("checkout_url")
or payload.get("setup_url")
or payload.get("url")
or ""
).strip()
self.assertEqual(picked, "https://checkout.stripe.test/setup")
def test_24h_cooldown_blocks_second_auto_topup(self) -> None:
save_topup_settings(
self.con,
TopupSettings(
practice_id="prac_top",
auto_topup_enabled=True,
topup_amount_chf=30.0,
internal_credit_usd=20.0,
trigger_below_percent=10,
monthly_limit_chf=90.0,
stripe_customer_id="cus_x",
default_payment_method_id="pm_test",
),
)
now = int(__import__("time").time())
insert_ledger_event(
self.con,
practice_id="prac_top",
subscription_id="sub_top",
customer_email=None,
event_type="auto_topup_purchase",
amount_internal_usd=20.0,
amount_paid_chf=30.0,
stripe_payment_intent_id="pi_recent",
status="succeeded",
)
self.con.execute(
"UPDATE ai_credit_ledger SET created_at=? WHERE stripe_payment_intent_id='pi_recent'",
(now - 3600,),
)
self.con.commit()
self.assertLess(_now_delta(last_successful_auto_topup_ts(self.con, "prac_top")), AUTO_TOPUP_COOLDOWN_SEC)
r = maybe_run_auto_topup_for_practice(self.con, practice_id="prac_top", available_percent=5, dry_run=False)
self.assertEqual(r.get("reason"), "cooldown_24h")
def test_pending_auto_topup_blocks_parallel(self) -> None:
insert_ledger_event(
self.con,
practice_id="prac_top",
subscription_id="sub_top",
customer_email=None,
event_type="auto_topup_purchase",
amount_internal_usd=20.0,
amount_paid_chf=30.0,
status="pending",
)
self.assertTrue(has_pending_auto_topup(self.con, "prac_top"))
save_topup_settings(
self.con,
TopupSettings(
practice_id="prac_top",
auto_topup_enabled=True,
topup_amount_chf=30.0,
internal_credit_usd=20.0,
trigger_below_percent=10,
monthly_limit_chf=90.0,
stripe_customer_id="cus_x",
default_payment_method_id="pm_test",
),
)
r = maybe_run_auto_topup_for_practice(self.con, practice_id="prac_top", available_percent=5, dry_run=False)
self.assertEqual(r.get("reason"), "pending_auto_topup")
def test_double_payment_intent_no_double_credit(self) -> None:
pi = {
"id": "pi_manual_1",
"customer": "cus_x",
"metadata": {
"aza_purpose": "ai_topup",
"practice_id": "prac_top",
"subscription_id": "sub_top",
"internal_credit_usd": "20",
"paid_chf": "30",
},
}
r1 = process_topup_payment_intent_succeeded(self.con, payment_intent=pi)
r2 = process_topup_payment_intent_succeeded(self.con, payment_intent=pi)
self.assertTrue(r1.get("credited"))
self.assertTrue(r2.get("duplicate"))
self.assertTrue(ledger_exists_for_payment_intent(self.con, "pi_manual_1"))
cnt = self.con.execute(
"SELECT COUNT(*) FROM ai_credit_ledger WHERE stripe_payment_intent_id='pi_manual_1'"
).fetchone()[0]
self.assertEqual(cnt, 1)
def test_auto_topup_disabled_when_env_off(self) -> None:
os.environ["AZA_AI_AUTO_TOPUP_ENABLED"] = "0"
os.environ["AZA_AI_TOPUP_ALLOW_LIVE"] = "1"
os.environ["AZA_AI_TOPUP_DRY_RUN"] = "0"
save_topup_settings(
self.con,
TopupSettings(
practice_id="prac_top",
auto_topup_enabled=True,
topup_amount_chf=30.0,
internal_credit_usd=20.0,
trigger_below_percent=10,
monthly_limit_chf=90.0,
stripe_customer_id="cus_x",
default_payment_method_id="pm_test",
),
)
r = maybe_run_auto_topup_for_practice(self.con, practice_id="prac_top", available_percent=5, dry_run=False)
self.assertTrue(r.get("dry_run"))
def test_no_live_topup_when_allow_live_off(self) -> None:
os.environ["AZA_AI_TOPUP_ALLOW_LIVE"] = "0"
os.environ["AZA_AI_TOPUP_DRY_RUN"] = "0"
os.environ["STRIPE_SECRET_KEY"] = "sk_live_test_only"
r = create_topup_checkout_session(
practice_id="prac_top",
subscription_id="sub_top",
customer_email="cli@example.test",
paid_chf=30.0,
internal_usd=20.0,
success_url="https://example/s",
cancel_url="https://example/c",
)
self.assertFalse(r.get("ok", True))
self.assertEqual(r.get("error_code"), "LIVE_TOPUP_BLOCKED")
def test_gate_retries_after_mock_auto_topup_credit(self) -> None:
from unittest.mock import patch
lic = _lic()
ps, pe = lic.period_start, lic.period_end
insert_usage_event(
self.con,
lic=lic,
device_id=None,
period_start=ps,
period_end=pe,
operation_type="chat",
model="gpt-4o-mini",
input_tokens=1,
output_tokens=1,
total_tokens=2,
audio_seconds=0.0,
estimated_cost_usd=20.0,
request_id="gate_block",
status="success",
)
os.environ["AZA_AI_AUTO_TOPUP_ENABLED"] = "1"
def _fake_auto(*_a, **_k):
insert_ledger_event(
self.con,
practice_id="prac_top",
subscription_id="sub_top",
customer_email=None,
event_type="auto_topup_purchase",
amount_internal_usd=20.0,
amount_paid_chf=30.0,
stripe_payment_intent_id="pi_gate",
status="succeeded",
)
return {"ok": True, "charged": True}
with patch("aza_ai_credit.maybe_run_auto_topup_for_practice", side_effect=_fake_auto):
blocked = budget_gate_blocked_payload_or_none(
self.con,
lic,
device_id=None,
request_id="req_gate",
operation_type="chat",
model="gpt-4o-mini",
)
self.assertIsNone(blocked)
def test_failed_auto_topup_pauses_settings(self) -> None:
from unittest.mock import MagicMock, patch
os.environ["AZA_AI_AUTO_TOPUP_ENABLED"] = "1"
os.environ["AZA_AI_TOPUP_ALLOW_LIVE"] = "1"
os.environ["AZA_AI_TOPUP_DRY_RUN"] = "0"
os.environ["STRIPE_SECRET_KEY"] = "sk_live_test_only"
save_topup_settings(
self.con,
TopupSettings(
practice_id="prac_top",
auto_topup_enabled=True,
topup_amount_chf=30.0,
internal_credit_usd=20.0,
trigger_below_percent=10,
monthly_limit_chf=90.0,
stripe_customer_id="cus_x",
default_payment_method_id="pm_test",
),
)
mock_pi = MagicMock()
mock_pi.id = "pi_fail"
mock_pi.status = "requires_action"
with patch("stripe.PaymentIntent.create", return_value=mock_pi):
r = maybe_run_auto_topup_for_practice(
self.con, practice_id="prac_top", available_percent=5, dry_run=False
)
self.assertFalse(r.get("charged", True))
self.assertTrue(r.get("paused"))
loaded = get_topup_settings(self.con, "prac_top")
self.assertFalse(loaded.auto_topup_enabled)
failed_cnt = self.con.execute(
"SELECT COUNT(*) FROM ai_credit_ledger WHERE event_type='failed_auto_topup'"
).fetchone()[0]
self.assertEqual(failed_cnt, 1)
def test_live_auto_topup_mock_success(self) -> None:
from unittest.mock import MagicMock, patch
os.environ["AZA_AI_AUTO_TOPUP_ENABLED"] = "1"
os.environ["AZA_AI_TOPUP_ALLOW_LIVE"] = "1"
os.environ["AZA_AI_TOPUP_DRY_RUN"] = "0"
os.environ["STRIPE_SECRET_KEY"] = "sk_live_test_only"
save_topup_settings(
self.con,
TopupSettings(
practice_id="prac_top",
auto_topup_enabled=True,
topup_amount_chf=30.0,
internal_credit_usd=20.0,
trigger_below_percent=10,
monthly_limit_chf=90.0,
stripe_customer_id="cus_x",
default_payment_method_id="pm_test",
),
)
mock_pi = MagicMock()
mock_pi.id = "pi_ok"
mock_pi.status = "succeeded"
with patch("stripe.PaymentIntent.create", return_value=mock_pi):
r = maybe_run_auto_topup_for_practice(
self.con,
practice_id="prac_top",
available_percent=5,
dry_run=False,
subscription_id="sub_top",
)
self.assertTrue(r.get("charged"))
row = self.con.execute(
"SELECT status, amount_internal_usd FROM ai_credit_ledger WHERE stripe_payment_intent_id='pi_ok'"
).fetchone()
self.assertEqual(row[0], "succeeded")
self.assertEqual(row[1], 20.0)
def test_auto_pi_webhook_idempotent(self) -> None:
pi = {
"id": "pi_auto_wh",
"customer": "cus_x",
"metadata": {
"aza_purpose": "ai_auto_topup",
"practice_id": "prac_top",
"subscription_id": "sub_top",
"internal_credit_usd": "20",
"paid_chf": "30",
},
}
r1 = process_auto_topup_payment_intent_succeeded(self.con, payment_intent=pi)
r2 = process_auto_topup_payment_intent_succeeded(self.con, payment_intent=pi)
self.assertTrue(r1.get("credited"))
self.assertTrue(r2.get("duplicate"))
def test_topup_success_url_is_succes2(self) -> None:
from unittest.mock import MagicMock, patch
os.environ["AZA_AI_TOPUP_DRY_RUN"] = "0"
os.environ["AZA_AI_TOPUP_ALLOW_LIVE"] = "1"
os.environ["STRIPE_SECRET_KEY"] = "sk_live_test_only"
mock_session = MagicMock(url="https://checkout.stripe.com/test", id="cs_test")
with patch("stripe.checkout.Session.create", return_value=mock_session) as mock_create:
create_topup_checkout_session(
practice_id="prac_top",
subscription_id="sub_top",
customer_email="cli@example.test",
paid_chf=30.0,
internal_usd=20.0,
save_payment_method=False,
success_url=DEFAULT_TOPUP_SUCCESS_URL,
cancel_url="https://aza-medwork.ch",
)
self.assertEqual(mock_create.call_args.kwargs["success_url"], "https://aza-medwork.ch/succes2")
def test_abo_checkout_success_url_unchanged(self) -> None:
import stripe_routes
self.assertNotEqual(
stripe_routes.STRIPE_SUCCESS_URL or "",
DEFAULT_TOPUP_SUCCESS_URL,
)
def test_history_no_token_401(self) -> None:
from fastapi import FastAPI
from fastapi.testclient import TestClient
from aza_ai_credit_routes import router
os.environ["AZA_API_TOKEN"] = "test-history-token"
app = FastAPI()
app.include_router(router)
with TestClient(app) as client:
resp = client.get(
"/v1/ai-credit/history",
headers={"X-Device-Id": "dev1", "X-Practice-Id": "prac_top"},
)
self.assertEqual(resp.status_code, 401)
def test_history_only_own_practice(self) -> None:
insert_ledger_event(
self.con,
practice_id="prac_top",
subscription_id="sub_top",
customer_email=None,
event_type="topup_purchase",
amount_internal_usd=20.0,
amount_paid_chf=30.0,
stripe_payment_intent_id="pi_prac_top_secret",
status="succeeded",
)
insert_ledger_event(
self.con,
practice_id="prac_other",
subscription_id="sub_other",
customer_email=None,
event_type="topup_purchase",
amount_internal_usd=20.0,
amount_paid_chf=30.0,
stripe_payment_intent_id="pi_other_secret",
status="succeeded",
)
items = list_user_credit_history(self.con, practice_id="prac_top")
self.assertEqual(len(items), 1)
self.assertEqual(items[0]["amount_paid_chf"], 30.0)
self.assertNotIn("pi_other_secret", json.dumps(items))
def test_history_masks_stripe_ids(self) -> None:
masked = mask_stripe_reference("pi_1234567890abcdef")
self.assertIn("", masked or "")
self.assertNotIn("abcdef", masked or "abcdef")
insert_ledger_event(
self.con,
practice_id="prac_top",
subscription_id="sub_top",
customer_email=None,
event_type="topup_purchase",
amount_internal_usd=20.0,
amount_paid_chf=30.0,
stripe_payment_intent_id="pi_1234567890abcdef",
status="succeeded",
)
items = list_user_credit_history(self.con, practice_id="prac_top")
self.assertEqual(items[0]["stripe_payment_reference"], masked)
def test_history_shows_failed_and_refund(self) -> None:
insert_ledger_event(
self.con,
practice_id="prac_top",
subscription_id="sub_top",
customer_email=None,
event_type="topup_purchase",
amount_internal_usd=0.0,
amount_paid_chf=30.0,
status="failed",
)
insert_ledger_event(
self.con,
practice_id="prac_top",
subscription_id="sub_top",
customer_email=None,
event_type="refund",
amount_internal_usd=-10.0,
amount_paid_chf=-15.0,
status="succeeded",
)
items = list_user_credit_history(self.con, practice_id="prac_top")
statuses = {i["status"] for i in items}
types = {i["event_type"] for i in items}
self.assertIn("failed", statuses)
self.assertIn("refund", types)
def test_admin_ledger_helper_unchanged(self) -> None:
insert_ledger_event(
self.con,
practice_id="prac_top",
subscription_id="sub_top",
customer_email=None,
event_type="topup_purchase",
amount_internal_usd=1.0,
stripe_checkout_session_id="cs_admin_check",
status="succeeded",
)
entries = list_ledger_for_practice(self.con, "prac_top")
self.assertEqual(len(entries), 1)
self.assertIn("checkout_session_masked", entries[0])
def _now_delta(ts: int | None) -> int:
if not ts:
return 999999
return int(__import__("time").time()) - ts
if __name__ == "__main__":
unittest.main()