from django.contrib import admin from django.contrib.auth import get_user_model from django.core.exceptions import ValidationError from django.db import IntegrityError, transaction from django.test import TestCase from apps.billing.models import CallRecord, PointsLedger from apps.users.models import ApiKey, UserWallet class BillingCoreModelTests(TestCase): def setUp(self): self.user = get_user_model().objects.create_user( username="client", email="client@example.com", password="password", ) def test_user_wallet_defaults_to_zero_and_rejects_negative_balance(self): wallet = UserWallet.objects.create(user=self.user) self.assertEqual(wallet.points_balance, 0) other_user = get_user_model().objects.create_user( username="negative", email="negative@example.com", password="password", ) with self.assertRaises(IntegrityError): with transaction.atomic(): UserWallet.objects.create(user=other_user, points_balance=-1) def test_api_key_hashes_plaintext_and_matches_only_raw_key(self): api_key, raw_key = ApiKey.create_for_user(self.user, name="desktop") self.assertTrue(raw_key.startswith("sk_cmhub_")) self.assertEqual(api_key.key_prefix, raw_key[: ApiKey.KEY_PREFIX_LENGTH]) self.assertEqual(len(api_key.key_hash), 64) self.assertNotEqual(api_key.key_hash, raw_key) self.assertNotIn(raw_key, str(api_key.__dict__)) self.assertTrue(api_key.matches_key(raw_key)) self.assertFalse(api_key.matches_key(raw_key + "-wrong")) self.assertEqual(api_key.status, ApiKey.Status.ACTIVE) def test_call_record_stores_summary_reference_but_no_provider_raw_field(self): api_key, _raw_key = ApiKey.create_for_user(self.user) call = CallRecord.objects.create( user=self.user, api_key=api_key, operation_type=CallRecord.OperationType.IMAGE, alias="image-hd", model_used="gpt-image-2", resolution="1K", prompt="Generate an image", points_cost=10, status=CallRecord.Status.SUCCESS, upstream_latency_ms=1234, result_ref="https://cdn.example.test/result.png", result_summary="stored image result", ) field_names = {field.name for field in CallRecord._meta.fields} self.assertNotIn("raw", field_names) self.assertNotIn("provider_raw", field_names) self.assertEqual(call.user, self.user) self.assertEqual(call.api_key, api_key) self.assertEqual(call.alias, "image-hd") self.assertEqual(call.model_used, "gpt-image-2") self.assertEqual(call.result_ref, "https://cdn.example.test/result.png") self.assertEqual(call.result_summary, "stored image result") def test_points_ledger_records_balance_and_requires_adjust_reason(self): call = CallRecord.objects.create( user=self.user, operation_type=CallRecord.OperationType.TITLE, alias="title-standard", model_used="gpt-5.5", points_cost=2, status=CallRecord.Status.SUCCESS, ) ledger = PointsLedger.objects.create( user=self.user, change_type=PointsLedger.ChangeType.CONSUME, points_delta=-2, balance_after=98, ref_call=call, ) self.assertEqual(ledger.ref_call, call) self.assertEqual(ledger.balance_after, 98) adjustment = PointsLedger( user=self.user, change_type=PointsLedger.ChangeType.ADJUST, points_delta=10, balance_after=108, ) with self.assertRaises(ValidationError): adjustment.full_clean() def test_points_ledger_rejects_zero_delta_and_negative_balance_after(self): with self.assertRaises(IntegrityError): with transaction.atomic(): PointsLedger.objects.create( user=self.user, change_type=PointsLedger.ChangeType.RECHARGE, points_delta=0, balance_after=100, ) with self.assertRaises(IntegrityError): with transaction.atomic(): PointsLedger.objects.create( user=self.user, change_type=PointsLedger.ChangeType.CONSUME, points_delta=-1, balance_after=-1, ) def test_billing_models_are_registered_in_admin(self): self.assertIn(UserWallet, admin.site._registry) self.assertIn(ApiKey, admin.site._registry) self.assertIn(PointsLedger, admin.site._registry) self.assertIn(CallRecord, admin.site._registry)