feat: add recharge callback processing
This commit is contained in:
+166
-1
@@ -13,7 +13,13 @@ from django.test import TestCase, TransactionTestCase, override_settings
|
||||
from django.utils import timezone
|
||||
|
||||
from apps.ai.models import AiModel, ModelAlias
|
||||
from apps.billing.models import CallRecord, ExchangeRate, PointsLedger, PricingRule
|
||||
from apps.billing.models import (
|
||||
CallRecord,
|
||||
ExchangeRate,
|
||||
PointsLedger,
|
||||
PricingRule,
|
||||
RechargeOrder,
|
||||
)
|
||||
from apps.billing.pricing import (
|
||||
NoPricingRuleError,
|
||||
calculate_points_cost,
|
||||
@@ -24,8 +30,12 @@ from apps.billing.pricing import (
|
||||
from apps.billing.services import (
|
||||
InsufficientPointsError,
|
||||
InvalidCallStateError,
|
||||
RechargeAmountMismatchError,
|
||||
RechargePayment,
|
||||
apply_recharge_payment,
|
||||
mark_call_success,
|
||||
precharge_call,
|
||||
query_and_apply_recharge_payment,
|
||||
refund_call_points,
|
||||
)
|
||||
from apps.users.models import ApiKey, UserWallet
|
||||
@@ -178,11 +188,55 @@ class BillingCoreModelTests(TestCase):
|
||||
ref_call=call,
|
||||
)
|
||||
|
||||
def test_points_ledger_rejects_duplicate_recharge_for_same_order(self):
|
||||
order = RechargeOrder.objects.create(
|
||||
user=self.user,
|
||||
order_no=f"R{uuid.uuid4().hex[:12]}",
|
||||
amount_money=Decimal("20.00"),
|
||||
pay_method=RechargeOrder.PayMethod.WEIXIN,
|
||||
exchange_rate=Decimal("10.0000"),
|
||||
points_granted=200,
|
||||
)
|
||||
other_order = RechargeOrder.objects.create(
|
||||
user=self.user,
|
||||
order_no=f"R{uuid.uuid4().hex[:12]}",
|
||||
amount_money=Decimal("30.00"),
|
||||
pay_method=RechargeOrder.PayMethod.WEIXIN,
|
||||
exchange_rate=Decimal("10.0000"),
|
||||
points_granted=300,
|
||||
)
|
||||
|
||||
PointsLedger.objects.create(
|
||||
user=self.user,
|
||||
change_type=PointsLedger.ChangeType.RECHARGE,
|
||||
points_delta=200,
|
||||
balance_after=200,
|
||||
ref_order_id=order.id,
|
||||
)
|
||||
PointsLedger.objects.create(
|
||||
user=self.user,
|
||||
change_type=PointsLedger.ChangeType.RECHARGE,
|
||||
points_delta=300,
|
||||
balance_after=500,
|
||||
ref_order_id=other_order.id,
|
||||
)
|
||||
|
||||
with self.assertRaises(IntegrityError):
|
||||
with transaction.atomic():
|
||||
PointsLedger.objects.create(
|
||||
user=self.user,
|
||||
change_type=PointsLedger.ChangeType.RECHARGE,
|
||||
points_delta=200,
|
||||
balance_after=700,
|
||||
ref_order_id=order.id,
|
||||
)
|
||||
|
||||
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)
|
||||
self.assertIn(RechargeOrder, admin.site._registry)
|
||||
|
||||
|
||||
@override_settings(AI_KEY_ENCRYPTION_KEY=TEST_ENCRYPTION_KEY)
|
||||
@@ -339,6 +393,22 @@ class BillingServiceTests(TestCase):
|
||||
self.wallet = UserWallet.objects.create(user=self.user, points_balance=100)
|
||||
self.api_key, _raw_key = ApiKey.create_for_user(self.user, name="server")
|
||||
|
||||
def create_recharge_order(
|
||||
self,
|
||||
*,
|
||||
amount="20.00",
|
||||
points_granted=200,
|
||||
pay_method=RechargeOrder.PayMethod.WEIXIN,
|
||||
) -> RechargeOrder:
|
||||
return RechargeOrder.objects.create(
|
||||
user=self.user,
|
||||
order_no=f"R{uuid.uuid4().hex[:12]}",
|
||||
amount_money=Decimal(amount),
|
||||
pay_method=pay_method,
|
||||
exchange_rate=Decimal("10.0000"),
|
||||
points_granted=points_granted,
|
||||
)
|
||||
|
||||
def test_precharge_call_debits_wallet_and_writes_pending_call_and_consume_ledger(self):
|
||||
charge = precharge_call(
|
||||
user=self.user,
|
||||
@@ -458,6 +528,101 @@ class BillingServiceTests(TestCase):
|
||||
with self.assertRaises(InvalidCallStateError):
|
||||
refund_call_points(call, error_message="late failure")
|
||||
|
||||
def test_apply_recharge_payment_credits_wallet_writes_ledger_and_marks_paid(self):
|
||||
order = self.create_recharge_order(amount="20.00", points_granted=200)
|
||||
|
||||
result = apply_recharge_payment(
|
||||
RechargePayment(
|
||||
order_no=order.order_no,
|
||||
pay_method=RechargeOrder.PayMethod.WEIXIN,
|
||||
amount=Decimal("20.00"),
|
||||
transaction_id="wx-txn-001",
|
||||
paid_at=timezone.now(),
|
||||
)
|
||||
)
|
||||
|
||||
self.wallet.refresh_from_db()
|
||||
order.refresh_from_db()
|
||||
self.assertTrue(result.applied)
|
||||
self.assertEqual(result.points_granted, 200)
|
||||
self.assertEqual(result.balance_after, 300)
|
||||
self.assertEqual(self.wallet.points_balance, 300)
|
||||
self.assertEqual(order.status, RechargeOrder.Status.PAID)
|
||||
self.assertEqual(order.payment_txn_no, "wx-txn-001")
|
||||
self.assertIsNotNone(order.paid_at)
|
||||
|
||||
ledger = PointsLedger.objects.get(ref_order_id=order.id)
|
||||
self.assertEqual(ledger.change_type, PointsLedger.ChangeType.RECHARGE)
|
||||
self.assertEqual(ledger.points_delta, 200)
|
||||
self.assertEqual(ledger.balance_after, 300)
|
||||
self.assertEqual(ledger.user, self.user)
|
||||
|
||||
def test_apply_recharge_payment_is_idempotent_for_duplicate_callback(self):
|
||||
order = self.create_recharge_order(amount="20.00", points_granted=200)
|
||||
payment = RechargePayment(
|
||||
order_no=order.order_no,
|
||||
pay_method=RechargeOrder.PayMethod.WEIXIN,
|
||||
amount=Decimal("20.00"),
|
||||
transaction_id="wx-txn-duplicate",
|
||||
paid_at=timezone.now(),
|
||||
)
|
||||
|
||||
first = apply_recharge_payment(payment)
|
||||
second = apply_recharge_payment(payment)
|
||||
|
||||
self.wallet.refresh_from_db()
|
||||
self.assertTrue(first.applied)
|
||||
self.assertFalse(second.applied)
|
||||
self.assertEqual(self.wallet.points_balance, 300)
|
||||
self.assertEqual(
|
||||
PointsLedger.objects.filter(
|
||||
ref_order_id=order.id,
|
||||
change_type=PointsLedger.ChangeType.RECHARGE,
|
||||
).count(),
|
||||
1,
|
||||
)
|
||||
|
||||
def test_apply_recharge_payment_rejects_amount_mismatch_without_crediting(self):
|
||||
order = self.create_recharge_order(amount="20.00", points_granted=200)
|
||||
|
||||
with self.assertRaises(RechargeAmountMismatchError):
|
||||
apply_recharge_payment(
|
||||
RechargePayment(
|
||||
order_no=order.order_no,
|
||||
pay_method=RechargeOrder.PayMethod.WEIXIN,
|
||||
amount=Decimal("19.99"),
|
||||
transaction_id="wx-txn-bad-amount",
|
||||
paid_at=timezone.now(),
|
||||
)
|
||||
)
|
||||
|
||||
self.wallet.refresh_from_db()
|
||||
order.refresh_from_db()
|
||||
self.assertEqual(self.wallet.points_balance, 100)
|
||||
self.assertEqual(order.status, RechargeOrder.Status.PENDING)
|
||||
self.assertFalse(PointsLedger.objects.filter(ref_order_id=order.id).exists())
|
||||
|
||||
def test_query_and_apply_recharge_payment_uses_same_idempotent_path(self):
|
||||
order = self.create_recharge_order(amount="30.00", points_granted=300)
|
||||
|
||||
def fake_query(queried_order):
|
||||
return RechargePayment(
|
||||
order_no=queried_order.order_no,
|
||||
pay_method=queried_order.pay_method,
|
||||
amount=queried_order.amount_money,
|
||||
transaction_id="queried-txn-001",
|
||||
paid_at=timezone.now(),
|
||||
)
|
||||
|
||||
result = query_and_apply_recharge_payment(order.order_no, fake_query)
|
||||
|
||||
self.wallet.refresh_from_db()
|
||||
order.refresh_from_db()
|
||||
self.assertTrue(result.applied)
|
||||
self.assertEqual(self.wallet.points_balance, 400)
|
||||
self.assertEqual(order.status, RechargeOrder.Status.PAID)
|
||||
self.assertEqual(order.payment_txn_no, "queried-txn-001")
|
||||
|
||||
|
||||
class ConcurrentDebitTests(TransactionTestCase):
|
||||
def setUp(self):
|
||||
|
||||
Reference in New Issue
Block a user