feat: improve operations admin
This commit is contained in:
@@ -85,6 +85,19 @@ class InvalidRechargePayMethodError(RechargeOrderCreateError):
|
||||
super().__init__("Unsupported recharge payment method.")
|
||||
|
||||
|
||||
class WalletAdjustmentError(BillingOperationError):
|
||||
code = "invalid_wallet_adjustment"
|
||||
|
||||
|
||||
class WalletAdjustmentWouldOverdrawError(WalletAdjustmentError):
|
||||
code = "adjustment_would_overdraw"
|
||||
|
||||
def __init__(self, *, points_delta: int, available_points: int):
|
||||
self.points_delta = points_delta
|
||||
self.available_points = available_points
|
||||
super().__init__("Wallet adjustment would make the balance negative.")
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class CallCharge:
|
||||
call_record: CallRecord
|
||||
@@ -126,12 +139,56 @@ class RechargeResult:
|
||||
applied: bool
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class WalletAdjustment:
|
||||
wallet: UserWallet
|
||||
ledger_entry: PointsLedger
|
||||
points_delta: int
|
||||
balance_after: int
|
||||
|
||||
|
||||
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")
|
||||
return points
|
||||
|
||||
|
||||
def _validate_adjustment_delta(points_delta: int) -> int:
|
||||
if (
|
||||
isinstance(points_delta, bool)
|
||||
or not isinstance(points_delta, int)
|
||||
or points_delta == 0
|
||||
):
|
||||
raise WalletAdjustmentError("points_delta must be a non-zero integer.")
|
||||
return points_delta
|
||||
|
||||
|
||||
def _normalize_adjustment_reason(reason: str) -> str:
|
||||
normalized_reason = str(reason or "").strip()
|
||||
if not normalized_reason:
|
||||
raise WalletAdjustmentError("Adjustment reason is required.")
|
||||
return normalized_reason
|
||||
|
||||
|
||||
def _format_operator(actor) -> str:
|
||||
if actor is None:
|
||||
return ""
|
||||
username_getter = getattr(actor, "get_username", None)
|
||||
username = (
|
||||
username_getter()
|
||||
if callable(username_getter)
|
||||
else getattr(actor, "username", "")
|
||||
)
|
||||
actor_pk = getattr(actor, "pk", None)
|
||||
if username and actor_pk:
|
||||
return f"{username}#{actor_pk}"
|
||||
if username:
|
||||
return str(username)
|
||||
if actor_pk:
|
||||
return f"id#{actor_pk}"
|
||||
return str(actor)
|
||||
|
||||
|
||||
def _locked_wallet_for_user(user) -> UserWallet:
|
||||
wallet, _created = UserWallet.objects.select_for_update().get_or_create(user=user)
|
||||
return wallet
|
||||
@@ -234,6 +291,48 @@ def get_balance_snapshot(user) -> BalanceSnapshot:
|
||||
)
|
||||
|
||||
|
||||
def adjust_wallet_points(
|
||||
*,
|
||||
user,
|
||||
points_delta: int,
|
||||
reason: str,
|
||||
actor=None,
|
||||
) -> WalletAdjustment:
|
||||
points_delta = _validate_adjustment_delta(points_delta)
|
||||
normalized_reason = _normalize_adjustment_reason(reason)
|
||||
operator = _format_operator(actor)
|
||||
ledger_reason = (
|
||||
f"{normalized_reason}\n\noperator={operator}" if operator else normalized_reason
|
||||
)
|
||||
|
||||
with transaction.atomic():
|
||||
wallet = _locked_wallet_for_user(user)
|
||||
balance_after = wallet.points_balance + points_delta
|
||||
if balance_after < 0:
|
||||
raise WalletAdjustmentWouldOverdrawError(
|
||||
points_delta=points_delta,
|
||||
available_points=wallet.points_balance,
|
||||
)
|
||||
|
||||
wallet.points_balance = balance_after
|
||||
wallet.save(update_fields=("points_balance", "updated_at"))
|
||||
|
||||
ledger_entry = PointsLedger.objects.create(
|
||||
user=user,
|
||||
change_type=PointsLedger.ChangeType.ADJUST,
|
||||
points_delta=points_delta,
|
||||
balance_after=wallet.points_balance,
|
||||
reason=ledger_reason,
|
||||
)
|
||||
|
||||
return WalletAdjustment(
|
||||
wallet=wallet,
|
||||
ledger_entry=ledger_entry,
|
||||
points_delta=points_delta,
|
||||
balance_after=ledger_entry.balance_after,
|
||||
)
|
||||
|
||||
|
||||
def apply_recharge_payment(payment: RechargePayment) -> RechargeResult:
|
||||
order_no = str(payment.order_no or "").strip()
|
||||
pay_method = _normalize_pay_method(payment.pay_method)
|
||||
|
||||
Reference in New Issue
Block a user