feat: add software subscription orders

This commit is contained in:
QiuSW
2026-07-21 11:52:49 +08:00
parent 341864dc70
commit bc9d1aa0f7
25 changed files with 1179 additions and 47 deletions
+85 -1
View File
@@ -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)