feat: add software subscription orders
This commit is contained in:
@@ -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")),
|
||||
|
||||
Reference in New Issue
Block a user