feat: add billing core models

This commit is contained in:
QiuSW
2026-07-02 15:06:00 +08:00
parent 2f93374193
commit cbc56ee15d
16 changed files with 660 additions and 24 deletions
+122 -1
View File
@@ -1,3 +1,124 @@
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
# Create your tests here.
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)