feat: add recharge callback processing

This commit is contained in:
QiuSW
2026-07-03 09:07:21 +08:00
parent 8931960b35
commit c15a07a5c2
20 changed files with 1073 additions and 25 deletions
+160 -1
View File
@@ -1,13 +1,16 @@
from __future__ import annotations
from dataclasses import dataclass
from datetime import datetime
from decimal import Decimal
from django.db import transaction
from django.db.models import Sum
from django.utils import timezone
from apps.users.models import UserWallet
from .models import CallRecord, PointsLedger, normalize_resolution
from .models import CallRecord, PointsLedger, RechargeOrder, normalize_resolution
class BillingOperationError(RuntimeError):
@@ -30,6 +33,44 @@ class InvalidCallStateError(BillingOperationError):
super().__init__(message)
class RechargeCallbackError(BillingOperationError):
code = "recharge_callback_error"
class RechargeOrderNotFoundError(RechargeCallbackError):
code = "order_not_found"
def __init__(self, order_no: str):
self.order_no = order_no
super().__init__("Recharge order was not found.")
class RechargeAmountMismatchError(RechargeCallbackError):
code = "amount_mismatch"
def __init__(self, *, order_amount: Decimal, callback_amount: Decimal):
self.order_amount = order_amount
self.callback_amount = callback_amount
super().__init__("Payment callback amount does not match local recharge order.")
class RechargePayMethodMismatchError(RechargeCallbackError):
code = "bad_request"
def __init__(self, *, order_pay_method: str, callback_pay_method: str):
self.order_pay_method = order_pay_method
self.callback_pay_method = callback_pay_method
super().__init__("Payment callback method does not match local recharge order.")
class InvalidRechargeOrderStateError(RechargeCallbackError):
code = "bad_request"
def __init__(self, *, status: str):
self.status = status
super().__init__("Recharge order cannot be paid from its current state.")
@dataclass(frozen=True)
class CallCharge:
call_record: CallRecord
@@ -53,6 +94,24 @@ 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
@dataclass(frozen=True)
class RechargeResult:
order: RechargeOrder
ledger_entry: PointsLedger | None
points_granted: int
balance_after: int
applied: bool
def _validate_positive_points(points: int) -> int:
if isinstance(points, bool) or not isinstance(points, int) or points <= 0:
raise ValueError("points must be a positive integer")
@@ -64,6 +123,14 @@ def _locked_wallet_for_user(user) -> UserWallet:
return wallet
def _normalize_money(value) -> Decimal:
return Decimal(str(value)).quantize(Decimal("0.01"))
def _normalize_pay_method(value: str) -> str:
return str(value or "").strip().lower()
def get_balance_snapshot(user) -> BalanceSnapshot:
points_balance = (
UserWallet.objects.filter(user=user)
@@ -79,6 +146,98 @@ def get_balance_snapshot(user) -> BalanceSnapshot:
)
def apply_recharge_payment(payment: RechargePayment) -> RechargeResult:
order_no = str(payment.order_no or "").strip()
pay_method = _normalize_pay_method(payment.pay_method)
callback_amount = _normalize_money(payment.amount)
paid_at = payment.paid_at or timezone.now()
with transaction.atomic():
try:
order = (
RechargeOrder.objects.select_for_update()
.select_related("user")
.get(order_no=order_no)
)
except RechargeOrder.DoesNotExist as exc:
raise RechargeOrderNotFoundError(order_no) from exc
if order.status == RechargeOrder.Status.PAID:
ledger_entry = (
PointsLedger.objects.filter(
ref_order_id=order.id,
change_type=PointsLedger.ChangeType.RECHARGE,
)
.order_by("id")
.first()
)
wallet_balance = (
UserWallet.objects.filter(user=order.user)
.values_list("points_balance", flat=True)
.first()
)
return RechargeResult(
order=order,
ledger_entry=ledger_entry,
points_granted=0,
balance_after=int(wallet_balance or 0),
applied=False,
)
if order.status != RechargeOrder.Status.PENDING:
raise InvalidRechargeOrderStateError(status=order.status)
if _normalize_pay_method(order.pay_method) != pay_method:
raise RechargePayMethodMismatchError(
order_pay_method=order.pay_method,
callback_pay_method=pay_method,
)
order_amount = _normalize_money(order.amount_money)
if order_amount != callback_amount:
raise RechargeAmountMismatchError(
order_amount=order_amount,
callback_amount=callback_amount,
)
points_granted = _validate_positive_points(order.points_granted)
wallet = _locked_wallet_for_user(order.user)
wallet.points_balance += points_granted
wallet.save(update_fields=("points_balance", "updated_at"))
order.status = RechargeOrder.Status.PAID
order.payment_txn_no = str(payment.transaction_id or "").strip()
order.paid_at = paid_at
order.save(update_fields=("status", "payment_txn_no", "paid_at", "updated_at"))
ledger_entry = PointsLedger.objects.create(
user=order.user,
change_type=PointsLedger.ChangeType.RECHARGE,
points_delta=points_granted,
balance_after=wallet.points_balance,
ref_order_id=order.id,
reason=f"Recharge paid via {order.pay_method}: {order.order_no}",
)
return RechargeResult(
order=order,
ledger_entry=ledger_entry,
points_granted=points_granted,
balance_after=ledger_entry.balance_after,
applied=True,
)
def query_and_apply_recharge_payment(order_no: str, query_func) -> RechargeResult:
normalized_order_no = str(order_no or "").strip()
try:
order = RechargeOrder.objects.get(order_no=normalized_order_no)
except RechargeOrder.DoesNotExist as exc:
raise RechargeOrderNotFoundError(normalized_order_no) from exc
payment = query_func(order)
return apply_recharge_payment(payment)
def precharge_call(
*,
user,