feat: add recharge create and status APIs
This commit is contained in:
+171
-1
@@ -10,6 +10,7 @@ from cryptography.fernet import Fernet
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.test import TestCase, override_settings
|
||||
from django.urls import path
|
||||
from django.utils import timezone
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
@@ -22,11 +23,18 @@ from apps.ai.providers import (
|
||||
ImageGenerationResult,
|
||||
TextGenerationResult,
|
||||
)
|
||||
from apps.billing.models import CallRecord, PointsLedger, PricingRule, RechargeOrder
|
||||
from apps.billing.models import (
|
||||
CallRecord,
|
||||
ExchangeRate,
|
||||
PointsLedger,
|
||||
PricingRule,
|
||||
RechargeOrder,
|
||||
)
|
||||
from apps.billing.payment_gateways import (
|
||||
build_mock_alipay_signature,
|
||||
build_mock_body_signature,
|
||||
)
|
||||
from apps.billing.services import RechargePayment
|
||||
from apps.users.models import ApiKey
|
||||
from apps.users.models import UserWallet
|
||||
|
||||
@@ -375,6 +383,168 @@ class RechargeCallbackApiTests(TestCase):
|
||||
self.assertFalse(PointsLedger.objects.filter(ref_order_id=order.id).exists())
|
||||
|
||||
|
||||
@override_settings(
|
||||
PAYMENT_CALLBACK_MODE="mock",
|
||||
PAYMENT_MOCK_CALLBACK_SECRET="test-payment-callback-secret",
|
||||
PAYMENT_QR_EXPIRES_MINUTES=15,
|
||||
)
|
||||
class RechargeCreateStatusApiTests(TestCase):
|
||||
create_url = "/api/v1/recharge/create"
|
||||
status_url = "/api/v1/recharge/status"
|
||||
|
||||
def setUp(self):
|
||||
suffix = uuid.uuid4().hex[:8]
|
||||
self.user = get_user_model().objects.create_user(
|
||||
username=f"recharge-create-{suffix}",
|
||||
email=f"recharge-create-{suffix}@example.com",
|
||||
password="password",
|
||||
)
|
||||
self.other_user = get_user_model().objects.create_user(
|
||||
username=f"recharge-other-{suffix}",
|
||||
email=f"recharge-other-{suffix}@example.com",
|
||||
password="password",
|
||||
)
|
||||
self.wallet = UserWallet.objects.create(user=self.user, points_balance=100)
|
||||
self.api_key, self.raw_key = ApiKey.create_for_user(self.user, name="recharge")
|
||||
self.exchange_rate = ExchangeRate.objects.create(
|
||||
currency="CNY",
|
||||
points_per_unit=Decimal("10.0000"),
|
||||
effective_from=timezone.now(),
|
||||
)
|
||||
self.client = APIClient()
|
||||
|
||||
def create_order(
|
||||
self,
|
||||
*,
|
||||
user=None,
|
||||
amount="20.00",
|
||||
points_granted=200,
|
||||
pay_method=RechargeOrder.PayMethod.WEIXIN,
|
||||
):
|
||||
return RechargeOrder.objects.create(
|
||||
user=user or 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,
|
||||
code_url=f"mockpay://{pay_method}/existing",
|
||||
)
|
||||
|
||||
def test_recharge_create_requires_web_session_not_api_key(self):
|
||||
response = self.client.post(
|
||||
self.create_url,
|
||||
{"amount": "20.00", "pay_method": "weixin"},
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"Bearer {self.raw_key}",
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 403)
|
||||
self.assertFalse(RechargeOrder.objects.filter(user=self.user).exists())
|
||||
|
||||
def test_recharge_create_is_session_authenticated_and_locks_quote(self):
|
||||
self.client.force_login(self.user)
|
||||
|
||||
response = self.client.post(
|
||||
self.create_url,
|
||||
{"amount": "20.00", "pay_method": "weixin"},
|
||||
format="json",
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 201)
|
||||
self.assertEqual(response.data["amount"], "20.00")
|
||||
self.assertEqual(response.data["exchange_rate"], "10.0000")
|
||||
self.assertEqual(response.data["points_granted"], 200)
|
||||
self.assertEqual(response.data["pay_method"], RechargeOrder.PayMethod.WEIXIN)
|
||||
self.assertEqual(response.data["status"], RechargeOrder.Status.PENDING)
|
||||
self.assertTrue(response.data["code_url"].startswith("weixin://wxpay/cmhub-mock"))
|
||||
self.assertIsNotNone(response.data["expires_at"])
|
||||
|
||||
order = RechargeOrder.objects.get(order_no=response.data["order_no"])
|
||||
self.assertEqual(order.user, self.user)
|
||||
self.assertEqual(order.exchange_rate, Decimal("10.0000"))
|
||||
self.assertEqual(order.points_granted, 200)
|
||||
self.wallet.refresh_from_db()
|
||||
self.assertEqual(self.wallet.points_balance, 100)
|
||||
self.assertFalse(PointsLedger.objects.filter(ref_order_id=order.id).exists())
|
||||
|
||||
def test_recharge_create_supports_alipay_mock_qr_code(self):
|
||||
self.client.force_login(self.user)
|
||||
|
||||
response = self.client.post(
|
||||
self.create_url,
|
||||
{"amount": "30.00", "pay_method": "alipay"},
|
||||
format="json",
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 201)
|
||||
self.assertEqual(response.data["pay_method"], RechargeOrder.PayMethod.ALIPAY)
|
||||
self.assertTrue(response.data["code_url"].startswith("https://qr.alipay.com/cmhub-mock"))
|
||||
|
||||
def test_recharge_create_enforces_csrf_for_real_session_clients(self):
|
||||
csrf_client = APIClient(enforce_csrf_checks=True)
|
||||
csrf_client.force_login(self.user)
|
||||
|
||||
response = csrf_client.post(
|
||||
self.create_url,
|
||||
{"amount": "20.00", "pay_method": "weixin"},
|
||||
format="json",
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 403)
|
||||
self.assertFalse(RechargeOrder.objects.filter(user=self.user).exists())
|
||||
|
||||
def test_recharge_status_returns_pending_order_for_owner_only(self):
|
||||
order = self.create_order()
|
||||
self.client.force_login(self.user)
|
||||
|
||||
response = self.client.get(self.status_url, {"order_no": order.order_no})
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.data["order_no"], order.order_no)
|
||||
self.assertEqual(response.data["status"], RechargeOrder.Status.PENDING)
|
||||
|
||||
self.client.force_login(self.other_user)
|
||||
denied = self.client.get(self.status_url, {"order_no": order.order_no})
|
||||
|
||||
self.assertEqual(denied.status_code, 404)
|
||||
self.assertEqual(denied.data["error"]["code"], "order_not_found")
|
||||
|
||||
def test_recharge_status_active_query_can_apply_paid_order_once(self):
|
||||
order = self.create_order(amount="20.00", points_granted=200)
|
||||
self.client.force_login(self.user)
|
||||
|
||||
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(),
|
||||
)
|
||||
|
||||
with patch("apps.api.views.query_payment_order", side_effect=fake_query) as query:
|
||||
first = self.client.get(self.status_url, {"order_no": order.order_no})
|
||||
second = self.client.get(self.status_url, {"order_no": order.order_no})
|
||||
|
||||
self.assertEqual(first.status_code, 200)
|
||||
self.assertEqual(second.status_code, 200)
|
||||
self.assertEqual(first.data["status"], RechargeOrder.Status.PAID)
|
||||
self.assertEqual(second.data["status"], RechargeOrder.Status.PAID)
|
||||
self.assertEqual(query.call_count, 1)
|
||||
self.wallet.refresh_from_db()
|
||||
order.refresh_from_db()
|
||||
self.assertEqual(self.wallet.points_balance, 300)
|
||||
self.assertEqual(order.payment_txn_no, "queried-txn-001")
|
||||
self.assertEqual(
|
||||
PointsLedger.objects.filter(
|
||||
ref_order_id=order.id,
|
||||
change_type=PointsLedger.ChangeType.RECHARGE,
|
||||
).count(),
|
||||
1,
|
||||
)
|
||||
|
||||
|
||||
class FakeGenerationProvider:
|
||||
def __init__(self, *, capabilities=None):
|
||||
self._capabilities = set(capabilities or {"text", "image", "vision"})
|
||||
|
||||
Reference in New Issue
Block a user