feat: add software subscription orders
This commit is contained in:
+85
-1
@@ -57,12 +57,14 @@ from apps.billing.models import (
|
||||
from apps.billing.payment_gateways import (
|
||||
build_mock_alipay_signature,
|
||||
build_mock_body_signature,
|
||||
PaymentOrderCode,
|
||||
)
|
||||
from apps.billing.services import RechargePayment
|
||||
from apps.moderation.models import SensitiveWord
|
||||
from apps.moderation.providers.keyword import reset_keyword_matcher_cache
|
||||
from apps.portal.models import DownloadRelease
|
||||
from apps.licensing.services import register_device
|
||||
from apps.licensing.models import ClientDevice, SoftwareOrder, SoftwarePlan
|
||||
from apps.licensing.services import create_software_order, register_device
|
||||
from apps.users.models import ApiKey
|
||||
from apps.users.models import UserWallet
|
||||
|
||||
@@ -3099,3 +3101,85 @@ class GenerateApiTests(TestCase):
|
||||
).count(),
|
||||
1,
|
||||
)
|
||||
|
||||
|
||||
@override_settings(
|
||||
PAYMENT_CALLBACK_MODE="mock",
|
||||
PAYMENT_MOCK_CALLBACK_SECRET="test-payment-callback-secret",
|
||||
)
|
||||
class SoftwareOrderCallbackApiTests(TestCase):
|
||||
callback_url = "/api/v1/software-orders/callback/wechat"
|
||||
|
||||
def setUp(self):
|
||||
self.user = get_user_model().objects.create_user(
|
||||
username="software-callback-user",
|
||||
email="software-callback@example.com",
|
||||
password="test-password",
|
||||
)
|
||||
self.plan = SoftwarePlan.objects.create(
|
||||
product_code=ClientDevice.ProductCode.CMSHOPEE,
|
||||
name="软件月度套餐",
|
||||
duration_days=30,
|
||||
price=Decimal("19.90"),
|
||||
device_limit=1,
|
||||
)
|
||||
self.order = create_software_order(
|
||||
user=self.user,
|
||||
plan=self.plan,
|
||||
pay_method=SoftwareOrder.PayMethod.WEIXIN,
|
||||
payment_order_func=lambda _order: PaymentOrderCode(
|
||||
code_url="weixin://software-order-test",
|
||||
expires_at=timezone.now() + timedelta(minutes=10),
|
||||
),
|
||||
)
|
||||
|
||||
def signed_body(self, *, amount_cents=1990, transaction_id="wx-software-callback-001"):
|
||||
payload = {
|
||||
"event_type": "TRANSACTION.SUCCESS",
|
||||
"resource": {
|
||||
"trade_state": "SUCCESS",
|
||||
"out_trade_no": self.order.order_no,
|
||||
"transaction_id": transaction_id,
|
||||
"success_time": "2026-07-21T12:00:00+08:00",
|
||||
"amount": {"total": amount_cents},
|
||||
},
|
||||
}
|
||||
body = json.dumps(payload, separators=(",", ":")).encode("utf-8")
|
||||
return body, build_mock_body_signature(body)
|
||||
|
||||
def test_callback_fulfills_once_without_writing_points_ledger(self):
|
||||
body, signature = self.signed_body()
|
||||
first = self.client.post(
|
||||
self.callback_url,
|
||||
data=body,
|
||||
content_type="application/json",
|
||||
HTTP_WECHATPAY_SIGNATURE=signature,
|
||||
)
|
||||
second = self.client.post(
|
||||
self.callback_url,
|
||||
data=body,
|
||||
content_type="application/json",
|
||||
HTTP_WECHATPAY_SIGNATURE=signature,
|
||||
)
|
||||
|
||||
self.assertEqual(first.status_code, 200)
|
||||
self.assertEqual(second.status_code, 200)
|
||||
self.order.refresh_from_db()
|
||||
self.assertEqual(self.order.status, SoftwareOrder.Status.PAID)
|
||||
self.assertIsNotNone(self.order.entitlement_id)
|
||||
self.assertEqual(PointsLedger.objects.filter(user=self.user).count(), 0)
|
||||
|
||||
def test_callback_rejects_amount_mismatch_without_fulfilling(self):
|
||||
body, signature = self.signed_body(amount_cents=1989)
|
||||
response = self.client.post(
|
||||
self.callback_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.order.refresh_from_db()
|
||||
self.assertEqual(self.order.status, SoftwareOrder.Status.PENDING)
|
||||
self.assertEqual(PointsLedger.objects.filter(user=self.user).count(), 0)
|
||||
|
||||
Reference in New Issue
Block a user