1223 lines
46 KiB
Python
1223 lines
46 KiB
Python
|
|
# -*- 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_INTERNAL_USD,
|
||
|
|
DEFAULT_MONTHLY_LIMIT_CHF,
|
||
|
|
DEFAULT_SETUP_SUCCESS_URL,
|
||
|
|
DEFAULT_TOPUP_SUCCESS_URL,
|
||
|
|
DEFAULT_TRIGGER_PERCENT,
|
||
|
|
apply_auto_topup_user_settings,
|
||
|
|
apply_extra_credit_to_snapshot,
|
||
|
|
auto_topup_is_fully_configured,
|
||
|
|
auto_topup_settings_status,
|
||
|
|
normalize_stripe_id,
|
||
|
|
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,
|
||
|
|
sum_auto_topups_chf_this_month,
|
||
|
|
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_10_usd(self) -> None:
|
||
|
|
self.assertAlmostEqual(chf_to_internal_usd(30), 10.0, places=2)
|
||
|
|
|
||
|
|
def test_chf10_gives_3333_usd(self) -> None:
|
||
|
|
self.assertAlmostEqual(chf_to_internal_usd(10), 3.3333, places=4)
|
||
|
|
|
||
|
|
def test_custom_amount_formula(self) -> None:
|
||
|
|
self.assertAlmostEqual(chf_to_internal_usd(45), 15.0, places=1)
|
||
|
|
self.assertAlmostEqual(chf_to_internal_usd(50), 16.6667, places=4)
|
||
|
|
self.assertAlmostEqual(chf_to_internal_usd(300), 100.0, places=4)
|
||
|
|
|
||
|
|
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=10.0,
|
||
|
|
trigger_below_percent=10,
|
||
|
|
monthly_limit_chf=300.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, 300.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"), "auto_topup_monthly_limit_reached")
|
||
|
|
self.assertEqual(r.get("error_code"), "AUTO_TOPUP_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=10.0,
|
||
|
|
trigger_below_percent=10,
|
||
|
|
monthly_limit_chf=300.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": {"id": "seti_setup_1", "payment_method": "pm_setup_1"},
|
||
|
|
"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, 10.0)
|
||
|
|
self.assertEqual(loaded.trigger_below_percent, 5)
|
||
|
|
self.assertEqual(loaded.monthly_limit_chf, 300.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.stripe_customer_id, "cus_setup3")
|
||
|
|
self.assertEqual(loaded.default_payment_method_id, "pm_embedded")
|
||
|
|
self.assertTrue(loaded.auto_topup_enabled)
|
||
|
|
|
||
|
|
def test_normalize_stripe_id_from_dict_and_json(self) -> None:
|
||
|
|
self.assertEqual(normalize_stripe_id("pm_abc123", "pm_"), "pm_abc123")
|
||
|
|
self.assertEqual(normalize_stripe_id({"id": "pm_from_dict"}, "pm_"), "pm_from_dict")
|
||
|
|
self.assertEqual(
|
||
|
|
normalize_stripe_id('{"id": "pm_from_json", "object": "payment_method"}', "pm_"),
|
||
|
|
"pm_from_json",
|
||
|
|
)
|
||
|
|
|
||
|
|
class _FakePm:
|
||
|
|
id = "pm_from_obj"
|
||
|
|
|
||
|
|
self.assertEqual(normalize_stripe_id(_FakePm(), "pm_"), "pm_from_obj")
|
||
|
|
self.assertIsNone(normalize_stripe_id("not_an_id", "pm_"))
|
||
|
|
|
||
|
|
def test_setup_incomplete_without_customer(self) -> None:
|
||
|
|
session = {
|
||
|
|
"id": "cs_setup_nc",
|
||
|
|
"customer": None,
|
||
|
|
"setup_intent": {"id": "seti_x", "payment_method": "pm_only"},
|
||
|
|
"metadata": {"aza_purpose": "ai_topup_setup", "practice_id": "prac_top"},
|
||
|
|
}
|
||
|
|
r = process_setup_checkout_completed(self.con, session=session)
|
||
|
|
self.assertEqual(r.get("reason"), "AUTO_TOPUP_SETUP_INCOMPLETE")
|
||
|
|
loaded = get_topup_settings(self.con, "prac_top")
|
||
|
|
self.assertFalse(loaded.auto_topup_enabled)
|
||
|
|
self.assertEqual(loaded.default_payment_method_id, "pm_only")
|
||
|
|
self.assertIsNone(loaded.stripe_customer_id)
|
||
|
|
|
||
|
|
def test_setup_incomplete_without_payment_method(self) -> None:
|
||
|
|
session = {
|
||
|
|
"id": "cs_setup_np",
|
||
|
|
"customer": "cus_only",
|
||
|
|
"setup_intent": None,
|
||
|
|
"metadata": {"aza_purpose": "ai_topup_setup", "practice_id": "prac_top"},
|
||
|
|
}
|
||
|
|
r = process_setup_checkout_completed(self.con, session=session)
|
||
|
|
self.assertEqual(r.get("reason"), "AUTO_TOPUP_SETUP_INCOMPLETE")
|
||
|
|
loaded = get_topup_settings(self.con, "prac_top")
|
||
|
|
self.assertFalse(loaded.auto_topup_enabled)
|
||
|
|
self.assertEqual(loaded.stripe_customer_id, "cus_only")
|
||
|
|
|
||
|
|
def test_setup_stripe_object_pm_normalized_not_json(self) -> None:
|
||
|
|
class _FakePm:
|
||
|
|
id = "pm_stripe_obj"
|
||
|
|
|
||
|
|
session = {
|
||
|
|
"id": "cs_setup_obj",
|
||
|
|
"customer": "cus_obj",
|
||
|
|
"setup_intent": {"id": "seti_obj", "payment_method": _FakePm()},
|
||
|
|
"metadata": {"aza_purpose": "ai_topup_setup", "practice_id": "prac_top"},
|
||
|
|
}
|
||
|
|
r = process_setup_checkout_completed(self.con, session=session)
|
||
|
|
self.assertTrue(r.get("auto_topup_enabled"))
|
||
|
|
loaded = get_topup_settings(self.con, "prac_top")
|
||
|
|
self.assertEqual(loaded.default_payment_method_id, "pm_stripe_obj")
|
||
|
|
self.assertFalse(str(loaded.default_payment_method_id or "").startswith("{"))
|
||
|
|
|
||
|
|
def test_payment_method_configured_requires_cus_and_pm(self) -> None:
|
||
|
|
save_topup_settings(
|
||
|
|
self.con,
|
||
|
|
TopupSettings(
|
||
|
|
practice_id="prac_top",
|
||
|
|
auto_topup_enabled=True,
|
||
|
|
topup_amount_chf=30.0,
|
||
|
|
internal_credit_usd=10.0,
|
||
|
|
trigger_below_percent=5,
|
||
|
|
monthly_limit_chf=300.0,
|
||
|
|
stripe_customer_id=None,
|
||
|
|
default_payment_method_id="pm_only",
|
||
|
|
),
|
||
|
|
)
|
||
|
|
self.assertFalse(auto_topup_settings_status(self.con, practice_id="prac_top").get("payment_method_configured"))
|
||
|
|
save_topup_settings(
|
||
|
|
self.con,
|
||
|
|
TopupSettings(
|
||
|
|
practice_id="prac_top",
|
||
|
|
auto_topup_enabled=True,
|
||
|
|
topup_amount_chf=30.0,
|
||
|
|
internal_credit_usd=10.0,
|
||
|
|
trigger_below_percent=5,
|
||
|
|
monthly_limit_chf=300.0,
|
||
|
|
stripe_customer_id="cus_ok",
|
||
|
|
default_payment_method_id="pm_ok",
|
||
|
|
),
|
||
|
|
)
|
||
|
|
self.assertTrue(auto_topup_settings_status(self.con, practice_id="prac_top").get("payment_method_configured"))
|
||
|
|
|
||
|
|
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=10.0,
|
||
|
|
trigger_below_percent=10,
|
||
|
|
monthly_limit_chf=300.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=10.0,
|
||
|
|
trigger_below_percent=10,
|
||
|
|
monthly_limit_chf=300.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=10.0,
|
||
|
|
trigger_below_percent=10,
|
||
|
|
monthly_limit_chf=300.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=10.0,
|
||
|
|
trigger_below_percent=10,
|
||
|
|
monthly_limit_chf=300.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=10.0,
|
||
|
|
trigger_below_percent=10,
|
||
|
|
monthly_limit_chf=300.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], 10.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 test_default_trigger_percent_is_5(self) -> None:
|
||
|
|
self.assertEqual(DEFAULT_TRIGGER_PERCENT, 5)
|
||
|
|
loaded = get_topup_settings(self.con, "prac_new_defaults")
|
||
|
|
self.assertEqual(loaded.trigger_below_percent, 5)
|
||
|
|
self.assertEqual(loaded.topup_amount_chf, 30.0)
|
||
|
|
self.assertEqual(loaded.internal_credit_usd, 10.0)
|
||
|
|
self.assertEqual(loaded.monthly_limit_chf, 300.0)
|
||
|
|
|
||
|
|
def test_setup_success_url_constant(self) -> None:
|
||
|
|
self.assertEqual(
|
||
|
|
DEFAULT_SETUP_SUCCESS_URL,
|
||
|
|
"https://aza-medwork.ch/ki-topup-setup-success/",
|
||
|
|
)
|
||
|
|
|
||
|
|
def test_setup_checkout_uses_setup_success_url(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/setup", id="cs_setup")
|
||
|
|
with patch("stripe.checkout.Session.create", return_value=mock_session) as mock_create:
|
||
|
|
create_setup_checkout_session(
|
||
|
|
practice_id="prac_top",
|
||
|
|
customer_email="cli@example.test",
|
||
|
|
success_url=DEFAULT_SETUP_SUCCESS_URL,
|
||
|
|
cancel_url="https://aza-medwork.ch",
|
||
|
|
)
|
||
|
|
self.assertEqual(
|
||
|
|
mock_create.call_args.kwargs["success_url"],
|
||
|
|
"https://aza-medwork.ch/ki-topup-setup-success/",
|
||
|
|
)
|
||
|
|
self.assertEqual(mock_create.call_args.kwargs["customer_creation"], "always")
|
||
|
|
|
||
|
|
def test_disable_auto_topup_preserves_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=10.0,
|
||
|
|
trigger_below_percent=5,
|
||
|
|
monthly_limit_chf=300.0,
|
||
|
|
stripe_customer_id="cus_keep",
|
||
|
|
default_payment_method_id="pm_keep",
|
||
|
|
),
|
||
|
|
)
|
||
|
|
before = self.con.execute("SELECT COUNT(1) FROM ai_credit_ledger").fetchone()[0]
|
||
|
|
r = apply_auto_topup_user_settings(self.con, practice_id="prac_top", enabled=False)
|
||
|
|
after = self.con.execute("SELECT COUNT(1) FROM ai_credit_ledger").fetchone()[0]
|
||
|
|
self.assertTrue(r.get("ok"))
|
||
|
|
self.assertFalse(r.get("auto_topup_enabled"))
|
||
|
|
loaded = get_topup_settings(self.con, "prac_top")
|
||
|
|
self.assertFalse(loaded.auto_topup_enabled)
|
||
|
|
self.assertEqual(loaded.stripe_customer_id, "cus_keep")
|
||
|
|
self.assertEqual(loaded.default_payment_method_id, "pm_keep")
|
||
|
|
self.assertEqual(before, after)
|
||
|
|
|
||
|
|
def test_enable_auto_topup_without_payment_method_blocked(self) -> None:
|
||
|
|
r = apply_auto_topup_user_settings(self.con, practice_id="prac_top", enabled=True)
|
||
|
|
self.assertFalse(r.get("ok"))
|
||
|
|
self.assertEqual(r.get("error_code"), "AUTO_TOPUP_NO_PAYMENT_METHOD")
|
||
|
|
|
||
|
|
def test_auto_topup_settings_status_masks_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=10.0,
|
||
|
|
trigger_below_percent=5,
|
||
|
|
monthly_limit_chf=300.0,
|
||
|
|
stripe_customer_id="cus_x",
|
||
|
|
default_payment_method_id="pm_secret123",
|
||
|
|
),
|
||
|
|
)
|
||
|
|
status = auto_topup_settings_status(self.con, practice_id="prac_top")
|
||
|
|
self.assertTrue(status.get("ok"))
|
||
|
|
self.assertTrue(status.get("auto_topup_enabled"))
|
||
|
|
self.assertTrue(status.get("payment_method_configured"))
|
||
|
|
self.assertEqual(status.get("trigger_below_percent"), 5)
|
||
|
|
self.assertNotIn("default_payment_method_id", status)
|
||
|
|
self.assertNotIn("pm_secret123", json.dumps(status))
|
||
|
|
|
||
|
|
def test_auto_topup_settings_get_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-auto-topup-token"
|
||
|
|
app = FastAPI()
|
||
|
|
app.include_router(router)
|
||
|
|
with TestClient(app) as client:
|
||
|
|
resp = client.get(
|
||
|
|
"/v1/ai-credit/auto-topup/settings",
|
||
|
|
headers={"X-Device-Id": "dev1", "X-Practice-Id": "prac_top"},
|
||
|
|
)
|
||
|
|
self.assertEqual(resp.status_code, 401)
|
||
|
|
|
||
|
|
def test_default_product_constants(self) -> None:
|
||
|
|
self.assertEqual(DEFAULT_INTERNAL_USD, 10.0)
|
||
|
|
self.assertEqual(DEFAULT_MONTHLY_LIMIT_CHF, 300.0)
|
||
|
|
self.assertEqual(DEFAULT_TRIGGER_PERCENT, 5)
|
||
|
|
|
||
|
|
def test_available_percent_can_exceed_100(self) -> None:
|
||
|
|
base = {
|
||
|
|
"remaining_usd": 20.0,
|
||
|
|
"used_usd": 0.0,
|
||
|
|
"budget_usd": 20.0,
|
||
|
|
}
|
||
|
|
snap = apply_extra_credit_to_snapshot(base, extra_remaining=10.0, monthly_budget=20.0)
|
||
|
|
self.assertEqual(snap["available_percent"], 150)
|
||
|
|
|
||
|
|
def test_available_percent_extra_only_50(self) -> None:
|
||
|
|
base = {"remaining_usd": 0.0, "used_usd": 20.0, "budget_usd": 20.0}
|
||
|
|
snap = apply_extra_credit_to_snapshot(base, extra_remaining=10.0, monthly_budget=20.0)
|
||
|
|
self.assertEqual(snap["available_percent"], 50)
|
||
|
|
|
||
|
|
def test_available_percent_mixed_130(self) -> None:
|
||
|
|
base = {"remaining_usd": 6.0, "used_usd": 14.0, "budget_usd": 20.0}
|
||
|
|
snap = apply_extra_credit_to_snapshot(base, extra_remaining=20.0, monthly_budget=20.0)
|
||
|
|
self.assertEqual(snap["available_percent"], 130)
|
||
|
|
|
||
|
|
def test_auto_monthly_limit_ignores_manual_topups(self) -> None:
|
||
|
|
save_topup_settings(
|
||
|
|
self.con,
|
||
|
|
TopupSettings(
|
||
|
|
practice_id="prac_top",
|
||
|
|
auto_topup_enabled=True,
|
||
|
|
topup_amount_chf=30.0,
|
||
|
|
internal_credit_usd=10.0,
|
||
|
|
trigger_below_percent=50,
|
||
|
|
monthly_limit_chf=300.0,
|
||
|
|
stripe_customer_id="cus_x",
|
||
|
|
default_payment_method_id="pm_test",
|
||
|
|
),
|
||
|
|
)
|
||
|
|
insert_ledger_event(
|
||
|
|
self.con,
|
||
|
|
practice_id="prac_top",
|
||
|
|
subscription_id="sub_top",
|
||
|
|
customer_email=None,
|
||
|
|
event_type="topup_purchase",
|
||
|
|
amount_internal_usd=66.6667,
|
||
|
|
amount_paid_chf=200.0,
|
||
|
|
stripe_checkout_session_id="cs_manual_big",
|
||
|
|
status="succeeded",
|
||
|
|
)
|
||
|
|
self.assertAlmostEqual(sum_auto_topups_chf_this_month(self.con, "prac_top"), 0.0, places=2)
|
||
|
|
r = maybe_run_auto_topup_for_practice(self.con, practice_id="prac_top", available_percent=5, dry_run=True)
|
||
|
|
self.assertTrue(r.get("dry_run"))
|
||
|
|
|
||
|
|
def test_manual_topup_checkout_allowed_when_auto_limit_reached(self) -> None:
|
||
|
|
save_topup_settings(
|
||
|
|
self.con,
|
||
|
|
TopupSettings(
|
||
|
|
practice_id="prac_top",
|
||
|
|
auto_topup_enabled=True,
|
||
|
|
topup_amount_chf=30.0,
|
||
|
|
internal_credit_usd=10.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=10.0,
|
||
|
|
amount_paid_chf=30.0,
|
||
|
|
stripe_payment_intent_id=f"pi_auto_cap_{i}",
|
||
|
|
status="succeeded",
|
||
|
|
)
|
||
|
|
blocked = maybe_run_auto_topup_for_practice(
|
||
|
|
self.con, practice_id="prac_top", available_percent=5, dry_run=True
|
||
|
|
)
|
||
|
|
self.assertEqual(blocked.get("reason"), "auto_topup_monthly_limit_reached")
|
||
|
|
os.environ["AZA_AI_TOPUP_DRY_RUN"] = "1"
|
||
|
|
manual = create_topup_checkout_session(
|
||
|
|
practice_id="prac_top",
|
||
|
|
subscription_id="sub_top",
|
||
|
|
customer_email="cli@example.test",
|
||
|
|
paid_chf=30.0,
|
||
|
|
internal_usd=chf_to_internal_usd(30.0),
|
||
|
|
success_url="https://example/s",
|
||
|
|
cancel_url="https://example/c",
|
||
|
|
)
|
||
|
|
self.assertTrue(manual.get("dry_run"))
|
||
|
|
|
||
|
|
def test_history_shows_ledger_value_not_recalculated(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=6.6667,
|
||
|
|
amount_paid_chf=10.0,
|
||
|
|
stripe_payment_intent_id="pi_old_rate",
|
||
|
|
status="succeeded",
|
||
|
|
)
|
||
|
|
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_old_auto",
|
||
|
|
status="succeeded",
|
||
|
|
)
|
||
|
|
items = list_user_credit_history(self.con, practice_id="prac_top")
|
||
|
|
by_pi = {i.get("stripe_payment_reference") or "": i for i in items}
|
||
|
|
amounts = sorted(i["amount_internal_usd"] for i in items)
|
||
|
|
self.assertEqual(amounts, [6.6667, 20.0])
|
||
|
|
self.assertAlmostEqual(chf_to_internal_usd(10.0), 3.3333, places=4)
|
||
|
|
|
||
|
|
def test_gate_reports_auto_monthly_limit_message(self) -> None:
|
||
|
|
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=40.0,
|
||
|
|
request_id="gate_limit",
|
||
|
|
status="success",
|
||
|
|
)
|
||
|
|
save_topup_settings(
|
||
|
|
self.con,
|
||
|
|
TopupSettings(
|
||
|
|
practice_id="prac_top",
|
||
|
|
auto_topup_enabled=True,
|
||
|
|
topup_amount_chf=30.0,
|
||
|
|
internal_credit_usd=10.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=10.0,
|
||
|
|
amount_paid_chf=30.0,
|
||
|
|
stripe_payment_intent_id=f"pi_gate_cap_{i}",
|
||
|
|
status="succeeded",
|
||
|
|
)
|
||
|
|
os.environ["AZA_AI_AUTO_TOPUP_ENABLED"] = "1"
|
||
|
|
blocked = budget_gate_blocked_payload_or_none(
|
||
|
|
self.con,
|
||
|
|
lic,
|
||
|
|
device_id=None,
|
||
|
|
request_id="req_limit",
|
||
|
|
operation_type="chat",
|
||
|
|
model="gpt-4o-mini",
|
||
|
|
)
|
||
|
|
self.assertIsNotNone(blocked)
|
||
|
|
self.assertEqual(blocked.get("error_code"), "AUTO_TOPUP_MONTHLY_LIMIT_REACHED")
|
||
|
|
self.assertIn("manuell", (blocked.get("message_user") or "").lower())
|
||
|
|
|
||
|
|
|
||
|
|
def _now_delta(ts: int | None) -> int:
|
||
|
|
if not ts:
|
||
|
|
return 999999
|
||
|
|
return int(__import__("time").time()) - ts
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
unittest.main()
|