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
+58 -24
View File
@@ -15,7 +15,6 @@ from django.utils import timezone
from django.utils.dateparse import parse_datetime
from .models import RechargeOrder
from .services import RechargePayment
class PaymentVerificationError(RuntimeError):
@@ -36,6 +35,15 @@ class PaymentOrderCode:
expires_at: object
@dataclass(frozen=True)
class PaymentReceipt:
order_no: str
pay_method: str
amount: Decimal
transaction_id: str
paid_at: object | None = None
def payment_callback_mode() -> str:
return str(getattr(settings, "PAYMENT_CALLBACK_MODE", "") or "").strip().lower()
@@ -140,7 +148,7 @@ def _verify_mock_alipay_signature(data: dict) -> None:
raise PaymentVerificationError("Invalid mock Alipay callback signature.")
def verify_wechat_callback(headers, body: bytes) -> RechargePayment:
def verify_wechat_callback(headers, body: bytes) -> PaymentReceipt:
if payment_callback_mode() == "mock":
_verify_mock_wechat_signature(headers, body)
try:
@@ -158,7 +166,7 @@ def verify_wechat_callback(headers, body: bytes) -> RechargePayment:
total_cents = Decimal(str((resource.get("amount") or {}).get("total")))
except (InvalidOperation, TypeError, ValueError) as exc:
raise PaymentVerificationError("Invalid WeChat payment amount.") from exc
return RechargePayment(
return PaymentReceipt(
order_no=str(resource.get("out_trade_no") or ""),
pay_method=RechargeOrder.PayMethod.WEIXIN,
amount=(total_cents / Decimal("100")).quantize(Decimal("0.01")),
@@ -169,7 +177,7 @@ def verify_wechat_callback(headers, body: bytes) -> RechargePayment:
return _verify_wechat_callback_with_sdk(headers, body)
def verify_alipay_callback(data: dict) -> RechargePayment:
def verify_alipay_callback(data: dict) -> PaymentReceipt:
callback_data = {key: str(value) for key, value in data.items()}
if payment_callback_mode() == "mock":
_verify_mock_alipay_signature(callback_data)
@@ -179,7 +187,7 @@ def verify_alipay_callback(data: dict) -> RechargePayment:
if callback_data.get("trade_status") not in {"TRADE_SUCCESS", "TRADE_FINISHED"}:
raise PaymentVerificationError("Alipay trade is not successful.")
return RechargePayment(
return PaymentReceipt(
order_no=str(callback_data.get("out_trade_no") or ""),
pay_method=RechargeOrder.PayMethod.ALIPAY,
amount=_decimal_money(callback_data.get("total_amount")),
@@ -188,13 +196,27 @@ def verify_alipay_callback(data: dict) -> RechargePayment:
)
def create_payment_order(order: RechargeOrder) -> PaymentOrderCode:
def create_payment_order(
order,
*,
description: str = "cmhub points recharge",
wechat_notify_url: str | None = None,
alipay_notify_url: str | None = None,
) -> PaymentOrderCode:
if payment_callback_mode() == "mock":
return _create_mock_payment_order(order)
if order.pay_method == RechargeOrder.PayMethod.WEIXIN:
return _create_wechat_payment_order_with_sdk(order)
return _create_wechat_payment_order_with_sdk(
order,
description=description,
notify_url=wechat_notify_url,
)
if order.pay_method == RechargeOrder.PayMethod.ALIPAY:
return _create_alipay_payment_order_with_sdk(order)
return _create_alipay_payment_order_with_sdk(
order,
description=description,
notify_url=alipay_notify_url,
)
raise PaymentOrderCreateError("Unsupported payment method.")
@@ -215,12 +237,18 @@ def _create_mock_payment_order(order: RechargeOrder) -> PaymentOrderCode:
return PaymentOrderCode(code_url=code_url, expires_at=_default_expires_at())
def _create_wechat_payment_order_with_sdk(order: RechargeOrder) -> PaymentOrderCode:
def _create_wechat_payment_order_with_sdk(
order,
*,
description: str = "cmhub points recharge",
notify_url: str | None = None,
) -> PaymentOrderCode:
try:
from wechatpayv3 import WeChatPay, WeChatPayType # type: ignore
except ImportError as exc:
raise PaymentOrderCreateError("wechatpayv3 is not installed.") from exc
effective_notify_url = str(notify_url or settings.WECHAT_PAY_NOTIFY_URL or "").strip()
_require_payment_config(
{
"WECHAT_PAY_APPID": settings.WECHAT_PAY_APPID,
@@ -228,7 +256,7 @@ def _create_wechat_payment_order_with_sdk(order: RechargeOrder) -> PaymentOrderC
"WECHAT_PAY_API_V3_KEY": settings.WECHAT_PAY_API_V3_KEY,
"WECHAT_PAY_CERT_SERIAL_NO": settings.WECHAT_PAY_CERT_SERIAL_NO,
"WECHAT_PAY_PRIVATE_KEY_PATH": settings.WECHAT_PAY_PRIVATE_KEY_PATH,
"WECHAT_PAY_NOTIFY_URL": settings.WECHAT_PAY_NOTIFY_URL,
"WECHAT_PAY_NOTIFY_URL": effective_notify_url,
},
"WeChat",
)
@@ -240,11 +268,11 @@ def _create_wechat_payment_order_with_sdk(order: RechargeOrder) -> PaymentOrderC
cert_serial_no=settings.WECHAT_PAY_CERT_SERIAL_NO,
apiv3_key=settings.WECHAT_PAY_API_V3_KEY,
appid=settings.WECHAT_PAY_APPID,
notify_url=settings.WECHAT_PAY_NOTIFY_URL,
notify_url=effective_notify_url,
)
try:
response = client.pay(
description="cmhub points recharge",
description=description,
out_trade_no=order.order_no,
amount={
"total": _wechat_amount_cents(order.amount_money),
@@ -265,18 +293,24 @@ def _create_wechat_payment_order_with_sdk(order: RechargeOrder) -> PaymentOrderC
return PaymentOrderCode(code_url=str(code_url), expires_at=_default_expires_at())
def _create_alipay_payment_order_with_sdk(order: RechargeOrder) -> PaymentOrderCode:
def _create_alipay_payment_order_with_sdk(
order,
*,
description: str = "cmhub points recharge",
notify_url: str | None = None,
) -> PaymentOrderCode:
try:
from alipay import AliPay # type: ignore
except ImportError as exc:
raise PaymentOrderCreateError("python-alipay-sdk is not installed.") from exc
effective_notify_url = str(notify_url or settings.ALIPAY_NOTIFY_URL or "").strip()
_require_payment_config(
{
"ALIPAY_APPID": settings.ALIPAY_APPID,
"ALIPAY_APP_PRIVATE_KEY_PATH": settings.ALIPAY_APP_PRIVATE_KEY_PATH,
"ALIPAY_PUBLIC_KEY_PATH": settings.ALIPAY_PUBLIC_KEY_PATH,
"ALIPAY_NOTIFY_URL": settings.ALIPAY_NOTIFY_URL,
"ALIPAY_NOTIFY_URL": effective_notify_url,
},
"Alipay",
)
@@ -288,7 +322,7 @@ def _create_alipay_payment_order_with_sdk(order: RechargeOrder) -> PaymentOrderC
)
client = AliPay(
appid=settings.ALIPAY_APPID,
app_notify_url=settings.ALIPAY_NOTIFY_URL,
app_notify_url=effective_notify_url,
app_private_key_string=app_private_key,
alipay_public_key_string=alipay_public_key,
sign_type="RSA2",
@@ -296,10 +330,10 @@ def _create_alipay_payment_order_with_sdk(order: RechargeOrder) -> PaymentOrderC
)
try:
response = client.api_alipay_trade_precreate(
subject="cmhub points recharge",
subject=description,
out_trade_no=order.order_no,
total_amount=_format_money(order.amount_money),
notify_url=settings.ALIPAY_NOTIFY_URL,
notify_url=effective_notify_url,
)
except Exception as exc: # pragma: no cover - depends on merchant SDK/runtime.
raise PaymentOrderCreateError("Alipay precreate order failed.") from exc
@@ -310,7 +344,7 @@ def _create_alipay_payment_order_with_sdk(order: RechargeOrder) -> PaymentOrderC
return PaymentOrderCode(code_url=str(qr_code), expires_at=_default_expires_at())
def _verify_wechat_callback_with_sdk(headers, body: bytes) -> RechargePayment:
def _verify_wechat_callback_with_sdk(headers, body: bytes) -> PaymentReceipt:
try:
from wechatpayv3 import WeChatPay # type: ignore
except ImportError as exc:
@@ -338,7 +372,7 @@ def _verify_wechat_callback_with_sdk(headers, body: bytes) -> RechargePayment:
total_cents = Decimal(str((resource.get("amount") or {}).get("total")))
except (InvalidOperation, TypeError, ValueError) as exc:
raise PaymentVerificationError("Invalid WeChat payment amount.") from exc
return RechargePayment(
return PaymentReceipt(
order_no=str(resource.get("out_trade_no") or ""),
pay_method=RechargeOrder.PayMethod.WEIXIN,
amount=(total_cents / Decimal("100")).quantize(Decimal("0.01")),
@@ -372,7 +406,7 @@ def _verify_alipay_callback_with_sdk(data: dict) -> None:
raise PaymentVerificationError("Invalid Alipay callback signature.")
def query_payment_order(order: RechargeOrder) -> RechargePayment:
def query_payment_order(order) -> PaymentReceipt:
if payment_callback_mode() == "mock":
raise PaymentQueryUnavailableError(
f"Mock payment query for {order.order_no} is not configured."
@@ -386,7 +420,7 @@ def query_payment_order(order: RechargeOrder) -> RechargePayment:
)
def _query_wechat_payment_order_with_sdk(order: RechargeOrder) -> RechargePayment:
def _query_wechat_payment_order_with_sdk(order) -> PaymentReceipt:
try:
from wechatpayv3 import WeChatPay # type: ignore
except ImportError as exc:
@@ -432,7 +466,7 @@ def _query_wechat_payment_order_with_sdk(order: RechargeOrder) -> RechargePaymen
total_cents = Decimal(str((resource.get("amount") or {}).get("total")))
except (InvalidOperation, TypeError, ValueError) as exc:
raise PaymentQueryUnavailableError("Invalid WeChat payment amount.") from exc
return RechargePayment(
return PaymentReceipt(
order_no=str(resource.get("out_trade_no") or order.order_no),
pay_method=RechargeOrder.PayMethod.WEIXIN,
amount=(total_cents / Decimal("100")).quantize(Decimal("0.01")),
@@ -441,7 +475,7 @@ def _query_wechat_payment_order_with_sdk(order: RechargeOrder) -> RechargePaymen
)
def _query_alipay_payment_order_with_sdk(order: RechargeOrder) -> RechargePayment:
def _query_alipay_payment_order_with_sdk(order) -> PaymentReceipt:
try:
from alipay import AliPay # type: ignore
except ImportError as exc:
@@ -477,7 +511,7 @@ def _query_alipay_payment_order_with_sdk(order: RechargeOrder) -> RechargePaymen
trade_status = response.get("trade_status") if isinstance(response, dict) else None
if trade_status not in {"TRADE_SUCCESS", "TRADE_FINISHED"}:
raise PaymentQueryUnavailableError("Alipay trade is not paid yet.")
return RechargePayment(
return PaymentReceipt(
order_no=str(response.get("out_trade_no") or order.order_no),
pay_method=RechargeOrder.PayMethod.ALIPAY,
amount=_decimal_money(response.get("total_amount")),
+2 -8
View File
@@ -2,7 +2,6 @@ from __future__ import annotations
import secrets
from dataclasses import dataclass
from datetime import datetime
from decimal import Decimal, InvalidOperation
from django.db import transaction
@@ -18,6 +17,7 @@ from .models import (
SignupBonusGrant,
normalize_resolution,
)
from .payment_gateways import PaymentReceipt
from .pricing import quote_recharge_points
@@ -127,13 +127,7 @@ class BalanceSnapshot:
ledger_balance: int
@dataclass(frozen=True)
class RechargePayment:
order_no: str
pay_method: str
amount: Decimal
transaction_id: str
paid_at: datetime | None = None
RechargePayment = PaymentReceipt
@dataclass(frozen=True)