from __future__ import annotations import hashlib import hmac import json from decimal import Decimal from decimal import InvalidOperation from pathlib import Path from django.conf import settings from django.utils import timezone from django.utils.dateparse import parse_datetime from .models import RechargeOrder from .services import RechargePayment class PaymentVerificationError(RuntimeError): code = "signature_invalid" class PaymentQueryUnavailableError(RuntimeError): code = "payment_query_unavailable" def payment_callback_mode() -> str: return str(getattr(settings, "PAYMENT_CALLBACK_MODE", "") or "").strip().lower() def _mock_secret() -> str: return str(getattr(settings, "PAYMENT_MOCK_CALLBACK_SECRET", "") or "") def _require_mock_secret() -> bytes: secret = _mock_secret() if not secret: raise PaymentVerificationError("Mock payment callback secret is not configured.") return secret.encode("utf-8") def build_mock_body_signature(body: bytes) -> str: return hmac.new(_require_mock_secret(), body, hashlib.sha256).hexdigest() def build_mock_alipay_signature(data: dict) -> str: canonical = "&".join( f"{key}={data[key]}" for key in sorted(data) if key not in {"sign", "sign_type"} ) return hmac.new( _require_mock_secret(), canonical.encode("utf-8"), hashlib.sha256, ).hexdigest() def _parse_paid_at(value: str | None): if not value: return timezone.now() parsed = parse_datetime(str(value)) if parsed is None: return timezone.now() if timezone.is_naive(parsed): return timezone.make_aware(parsed, timezone.get_current_timezone()) return parsed def _decimal_money(value) -> Decimal: try: return Decimal(str(value)).quantize(Decimal("0.01")) except (InvalidOperation, TypeError, ValueError) as exc: raise PaymentVerificationError("Invalid payment amount.") from exc def _verify_mock_wechat_signature(headers, body: bytes) -> None: expected = build_mock_body_signature(body) provided = headers.get("Wechatpay-Signature") or headers.get("X-Cmhub-Mock-Signature") if not provided or not hmac.compare_digest(str(provided), expected): raise PaymentVerificationError("Invalid mock WeChat callback signature.") def _verify_mock_alipay_signature(data: dict) -> None: expected = build_mock_alipay_signature(data) provided = data.get("sign") if not provided or not hmac.compare_digest(str(provided), expected): raise PaymentVerificationError("Invalid mock Alipay callback signature.") def verify_wechat_callback(headers, body: bytes) -> RechargePayment: if payment_callback_mode() == "mock": _verify_mock_wechat_signature(headers, body) try: payload = json.loads(body.decode("utf-8")) except (UnicodeDecodeError, json.JSONDecodeError) as exc: raise PaymentVerificationError("Invalid mock WeChat callback payload.") from exc resource = payload.get("resource") or {} if payload.get("event_type") != "TRANSACTION.SUCCESS": raise PaymentVerificationError("Unsupported WeChat callback event.") if resource.get("trade_state") != "SUCCESS": raise PaymentVerificationError("WeChat transaction is not successful.") try: 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( order_no=str(resource.get("out_trade_no") or ""), pay_method=RechargeOrder.PayMethod.WEIXIN, amount=(total_cents / Decimal("100")).quantize(Decimal("0.01")), transaction_id=str(resource.get("transaction_id") or ""), paid_at=_parse_paid_at(resource.get("success_time")), ) return _verify_wechat_callback_with_sdk(headers, body) def verify_alipay_callback(data: dict) -> RechargePayment: callback_data = {key: str(value) for key, value in data.items()} if payment_callback_mode() == "mock": _verify_mock_alipay_signature(callback_data) else: _verify_alipay_callback_with_sdk(callback_data) if callback_data.get("trade_status") not in {"TRADE_SUCCESS", "TRADE_FINISHED"}: raise PaymentVerificationError("Alipay trade is not successful.") return RechargePayment( order_no=str(callback_data.get("out_trade_no") or ""), pay_method=RechargeOrder.PayMethod.ALIPAY, amount=_decimal_money(callback_data.get("total_amount")), transaction_id=str(callback_data.get("trade_no") or ""), paid_at=_parse_paid_at(callback_data.get("gmt_payment")), ) def _verify_wechat_callback_with_sdk(headers, body: bytes) -> RechargePayment: try: from wechatpayv3 import WeChatPay # type: ignore except ImportError as exc: raise PaymentVerificationError("wechatpayv3 is not installed.") from exc private_key_path = getattr(settings, "WECHAT_PAY_PRIVATE_KEY_PATH", "") if not private_key_path: raise PaymentVerificationError("WeChat Pay private key path is not configured.") private_key = Path(private_key_path).read_text(encoding="utf-8") client = WeChatPay( wechatpay_type="NATIVE", mchid=settings.WECHAT_PAY_MCHID, private_key=private_key, 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, ) resource = client.callback(headers, body) if resource.get("trade_state") != "SUCCESS": raise PaymentVerificationError("WeChat transaction is not successful.") try: 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( order_no=str(resource.get("out_trade_no") or ""), pay_method=RechargeOrder.PayMethod.WEIXIN, amount=(total_cents / Decimal("100")).quantize(Decimal("0.01")), transaction_id=str(resource.get("transaction_id") or ""), paid_at=_parse_paid_at(resource.get("success_time")), ) def _verify_alipay_callback_with_sdk(data: dict) -> None: try: from alipay import AliPay # type: ignore except ImportError as exc: raise PaymentVerificationError("python-alipay-sdk is not installed.") from exc app_private_key = Path(settings.ALIPAY_APP_PRIVATE_KEY_PATH).read_text( encoding="utf-8" ) alipay_public_key = Path(settings.ALIPAY_PUBLIC_KEY_PATH).read_text( encoding="utf-8" ) sign = data.pop("sign", "") client = AliPay( appid=settings.ALIPAY_APPID, app_notify_url=settings.ALIPAY_NOTIFY_URL, app_private_key_string=app_private_key, alipay_public_key_string=alipay_public_key, sign_type="RSA2", debug=settings.ALIPAY_DEBUG, ) if not client.verify(data, sign): raise PaymentVerificationError("Invalid Alipay callback signature.") def query_payment_order(order: RechargeOrder) -> RechargePayment: raise PaymentQueryUnavailableError( f"Active payment query for {order.pay_method} is not configured." )