feat: add recharge create and status APIs
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import secrets
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
from decimal import Decimal, InvalidOperation
|
||||
|
||||
from django.db import transaction
|
||||
from django.db.models import Sum
|
||||
@@ -11,6 +12,7 @@ from django.utils import timezone
|
||||
from apps.users.models import UserWallet
|
||||
|
||||
from .models import CallRecord, PointsLedger, RechargeOrder, normalize_resolution
|
||||
from .pricing import quote_recharge_points
|
||||
|
||||
|
||||
class BillingOperationError(RuntimeError):
|
||||
@@ -71,6 +73,18 @@ class InvalidRechargeOrderStateError(RechargeCallbackError):
|
||||
super().__init__("Recharge order cannot be paid from its current state.")
|
||||
|
||||
|
||||
class RechargeOrderCreateError(BillingOperationError):
|
||||
code = "bad_request"
|
||||
|
||||
|
||||
class InvalidRechargePayMethodError(RechargeOrderCreateError):
|
||||
code = "bad_request"
|
||||
|
||||
def __init__(self, pay_method: str):
|
||||
self.pay_method = pay_method
|
||||
super().__init__("Unsupported recharge payment method.")
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class CallCharge:
|
||||
call_record: CallRecord
|
||||
@@ -124,13 +138,87 @@ def _locked_wallet_for_user(user) -> UserWallet:
|
||||
|
||||
|
||||
def _normalize_money(value) -> Decimal:
|
||||
return Decimal(str(value)).quantize(Decimal("0.01"))
|
||||
try:
|
||||
return Decimal(str(value)).quantize(Decimal("0.01"))
|
||||
except (InvalidOperation, TypeError, ValueError) as exc:
|
||||
raise RechargeOrderCreateError("Invalid recharge amount.") from exc
|
||||
|
||||
|
||||
def _normalize_pay_method(value: str) -> str:
|
||||
return str(value or "").strip().lower()
|
||||
|
||||
|
||||
def _validate_recharge_pay_method(value: str) -> str:
|
||||
pay_method = _normalize_pay_method(value)
|
||||
if pay_method not in {
|
||||
RechargeOrder.PayMethod.WEIXIN,
|
||||
RechargeOrder.PayMethod.ALIPAY,
|
||||
}:
|
||||
raise InvalidRechargePayMethodError(pay_method)
|
||||
return pay_method
|
||||
|
||||
|
||||
def _generate_recharge_order_no() -> str:
|
||||
for _attempt in range(10):
|
||||
timestamp = timezone.now().strftime("%Y%m%d%H%M%S")
|
||||
order_no = f"R{timestamp}{secrets.token_hex(4).upper()}"
|
||||
if not RechargeOrder.objects.filter(order_no=order_no).exists():
|
||||
return order_no
|
||||
raise RechargeOrderCreateError("Could not generate unique recharge order number.")
|
||||
|
||||
|
||||
def create_recharge_order(
|
||||
*,
|
||||
user,
|
||||
amount,
|
||||
pay_method: str,
|
||||
currency: str = "CNY",
|
||||
payment_order_func=None,
|
||||
) -> RechargeOrder:
|
||||
normalized_amount = _normalize_money(amount)
|
||||
if normalized_amount <= 0:
|
||||
raise RechargeOrderCreateError("Recharge amount must be greater than zero.")
|
||||
|
||||
normalized_pay_method = _validate_recharge_pay_method(pay_method)
|
||||
quote = quote_recharge_points(normalized_amount, currency=currency)
|
||||
if quote.points_granted <= 0:
|
||||
raise RechargeOrderCreateError(
|
||||
"Recharge amount is too small for the current exchange rate."
|
||||
)
|
||||
|
||||
order = RechargeOrder.objects.create(
|
||||
user=user,
|
||||
order_no=_generate_recharge_order_no(),
|
||||
amount_money=quote.amount,
|
||||
currency=quote.currency,
|
||||
pay_method=normalized_pay_method,
|
||||
exchange_rate=quote.points_per_unit,
|
||||
points_granted=quote.points_granted,
|
||||
status=RechargeOrder.Status.PENDING,
|
||||
)
|
||||
|
||||
if payment_order_func is None:
|
||||
from .payment_gateways import create_payment_order
|
||||
|
||||
payment_order_func = create_payment_order
|
||||
|
||||
try:
|
||||
payment_order = payment_order_func(order)
|
||||
code_url = str(getattr(payment_order, "code_url", "") or "").strip()
|
||||
expires_at = getattr(payment_order, "expires_at", None)
|
||||
if not code_url:
|
||||
raise RechargeOrderCreateError("Payment gateway did not return a QR code URL.")
|
||||
except Exception:
|
||||
order.status = RechargeOrder.Status.FAILED
|
||||
order.save(update_fields=("status", "updated_at"))
|
||||
raise
|
||||
|
||||
order.code_url = code_url
|
||||
order.expires_at = expires_at
|
||||
order.save(update_fields=("code_url", "expires_at", "updated_at"))
|
||||
return order
|
||||
|
||||
|
||||
def get_balance_snapshot(user) -> BalanceSnapshot:
|
||||
points_balance = (
|
||||
UserWallet.objects.filter(user=user)
|
||||
|
||||
Reference in New Issue
Block a user