feat: add recharge callback processing
This commit is contained in:
+185
-1
@@ -1,6 +1,8 @@
|
||||
import uuid
|
||||
import base64
|
||||
import json
|
||||
import tempfile
|
||||
from decimal import Decimal
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
@@ -20,7 +22,11 @@ from apps.ai.providers import (
|
||||
ImageGenerationResult,
|
||||
TextGenerationResult,
|
||||
)
|
||||
from apps.billing.models import CallRecord, PointsLedger, PricingRule
|
||||
from apps.billing.models import CallRecord, PointsLedger, PricingRule, RechargeOrder
|
||||
from apps.billing.payment_gateways import (
|
||||
build_mock_alipay_signature,
|
||||
build_mock_body_signature,
|
||||
)
|
||||
from apps.users.models import ApiKey
|
||||
from apps.users.models import UserWallet
|
||||
|
||||
@@ -191,6 +197,184 @@ class BalanceApiTests(TestCase):
|
||||
self.assertEqual(response.data["error"]["code"], "unauthorized")
|
||||
|
||||
|
||||
@override_settings(
|
||||
PAYMENT_CALLBACK_MODE="mock",
|
||||
PAYMENT_MOCK_CALLBACK_SECRET="test-payment-callback-secret",
|
||||
)
|
||||
class RechargeCallbackApiTests(TestCase):
|
||||
wechat_url = "/api/v1/recharge/callback/wechat"
|
||||
alipay_url = "/api/v1/recharge/callback/alipay"
|
||||
|
||||
def setUp(self):
|
||||
suffix = uuid.uuid4().hex[:8]
|
||||
self.user = get_user_model().objects.create_user(
|
||||
username=f"recharge-user-{suffix}",
|
||||
email=f"recharge-user-{suffix}@example.com",
|
||||
password="password",
|
||||
)
|
||||
self.wallet = UserWallet.objects.create(user=self.user, points_balance=100)
|
||||
self.client = APIClient(enforce_csrf_checks=True)
|
||||
|
||||
def create_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 signed_wechat_body(self, order, *, total_cents=2000):
|
||||
payload = {
|
||||
"event_type": "TRANSACTION.SUCCESS",
|
||||
"resource": {
|
||||
"trade_state": "SUCCESS",
|
||||
"out_trade_no": order.order_no,
|
||||
"transaction_id": "wx-txn-001",
|
||||
"success_time": "2026-07-03T00:00:00+08:00",
|
||||
"amount": {"total": total_cents},
|
||||
},
|
||||
}
|
||||
body = json.dumps(payload, separators=(",", ":")).encode("utf-8")
|
||||
return body, build_mock_body_signature(body)
|
||||
|
||||
def signed_alipay_payload(self, order, *, total_amount="20.00"):
|
||||
payload = {
|
||||
"trade_status": "TRADE_SUCCESS",
|
||||
"out_trade_no": order.order_no,
|
||||
"trade_no": "ali-txn-001",
|
||||
"total_amount": total_amount,
|
||||
"gmt_payment": "2026-07-03 00:00:00",
|
||||
}
|
||||
payload["sign"] = build_mock_alipay_signature(payload)
|
||||
return payload
|
||||
|
||||
def test_wechat_callback_credits_once_and_is_csrf_exempt(self):
|
||||
order = self.create_order(amount="20.00", points_granted=200)
|
||||
body, signature = self.signed_wechat_body(order)
|
||||
|
||||
first = self.client.post(
|
||||
self.wechat_url,
|
||||
data=body,
|
||||
content_type="application/json",
|
||||
HTTP_WECHATPAY_SIGNATURE=signature,
|
||||
)
|
||||
second = self.client.post(
|
||||
self.wechat_url,
|
||||
data=body,
|
||||
content_type="application/json",
|
||||
HTTP_WECHATPAY_SIGNATURE=signature,
|
||||
)
|
||||
|
||||
self.assertEqual(first.status_code, 200)
|
||||
self.assertEqual(second.status_code, 200)
|
||||
self.assertEqual(first.data["code"], "SUCCESS")
|
||||
self.wallet.refresh_from_db()
|
||||
order.refresh_from_db()
|
||||
self.assertEqual(self.wallet.points_balance, 300)
|
||||
self.assertEqual(order.status, RechargeOrder.Status.PAID)
|
||||
self.assertEqual(order.payment_txn_no, "wx-txn-001")
|
||||
self.assertEqual(
|
||||
PointsLedger.objects.filter(
|
||||
user=self.user,
|
||||
ref_order_id=order.id,
|
||||
change_type=PointsLedger.ChangeType.RECHARGE,
|
||||
).count(),
|
||||
1,
|
||||
)
|
||||
|
||||
def test_wechat_callback_rejects_bad_signature_without_crediting(self):
|
||||
order = self.create_order(amount="20.00", points_granted=200)
|
||||
body, _signature = self.signed_wechat_body(order)
|
||||
|
||||
response = self.client.post(
|
||||
self.wechat_url,
|
||||
data=body,
|
||||
content_type="application/json",
|
||||
HTTP_WECHATPAY_SIGNATURE="bad-signature",
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertEqual(response.data["error"]["code"], "signature_invalid")
|
||||
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_wechat_callback_rejects_amount_mismatch_without_crediting(self):
|
||||
order = self.create_order(amount="20.00", points_granted=200)
|
||||
body, signature = self.signed_wechat_body(order, total_cents=1999)
|
||||
|
||||
response = self.client.post(
|
||||
self.wechat_url,
|
||||
data=body,
|
||||
content_type="application/json",
|
||||
HTTP_WECHATPAY_SIGNATURE=signature,
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertEqual(response.data["error"]["code"], "amount_mismatch")
|
||||
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_alipay_callback_credits_once_returns_success_and_is_csrf_exempt(self):
|
||||
order = self.create_order(
|
||||
amount="20.00",
|
||||
points_granted=200,
|
||||
pay_method=RechargeOrder.PayMethod.ALIPAY,
|
||||
)
|
||||
payload = self.signed_alipay_payload(order)
|
||||
|
||||
first = self.client.post(self.alipay_url, data=payload)
|
||||
second = self.client.post(self.alipay_url, data=payload)
|
||||
|
||||
self.assertEqual(first.status_code, 200)
|
||||
self.assertEqual(second.status_code, 200)
|
||||
self.assertEqual(first.content, b"success")
|
||||
self.wallet.refresh_from_db()
|
||||
order.refresh_from_db()
|
||||
self.assertEqual(self.wallet.points_balance, 300)
|
||||
self.assertEqual(order.status, RechargeOrder.Status.PAID)
|
||||
self.assertEqual(order.payment_txn_no, "ali-txn-001")
|
||||
self.assertEqual(
|
||||
PointsLedger.objects.filter(
|
||||
ref_order_id=order.id,
|
||||
change_type=PointsLedger.ChangeType.RECHARGE,
|
||||
).count(),
|
||||
1,
|
||||
)
|
||||
|
||||
def test_alipay_callback_rejects_bad_signature_without_crediting(self):
|
||||
order = self.create_order(
|
||||
amount="20.00",
|
||||
points_granted=200,
|
||||
pay_method=RechargeOrder.PayMethod.ALIPAY,
|
||||
)
|
||||
payload = self.signed_alipay_payload(order)
|
||||
payload["sign"] = "bad-signature"
|
||||
|
||||
response = self.client.post(self.alipay_url, data=payload)
|
||||
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertEqual(response.content, b"fail")
|
||||
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())
|
||||
|
||||
|
||||
class FakeGenerationProvider:
|
||||
def __init__(self, *, capabilities=None):
|
||||
self._capabilities = set(capabilities or {"text", "image", "vision"})
|
||||
|
||||
Reference in New Issue
Block a user