mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-03 01:40:21 +08:00
feat(wallet): 钱包系统替代配额系统,新增支付与退款机制
- 新增钱包余额管理、充值、扣费、退款完整流程 - 新增支付网关抽象层(支持手动/支付宝/微信) - 用量计费从配额系统迁移到钱包余额扣费 - 新增管理员钱包管理与支付订单管理页面 - 新增用户钱包中心页面 - 移除独立 Key 锁定机制,统一由钱包余额控制 - 新增相关 API 路由、序列化器与数据库迁移 - 新增钱包、支付、退款相关测试
This commit is contained in:
586
tests/services/test_wallet_service_rules.py
Normal file
586
tests/services/test_wallet_service_rules.py
Normal file
@@ -0,0 +1,586 @@
|
||||
from decimal import Decimal
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
|
||||
from src.services.wallet.service import WalletService
|
||||
|
||||
|
||||
def _build_wallet(
|
||||
*,
|
||||
wallet_id: str = "wallet-1",
|
||||
recharge: str = "0",
|
||||
gift: str = "0",
|
||||
limit_mode: str = "finite",
|
||||
status: str = "active",
|
||||
api_key_id: str | None = None,
|
||||
) -> SimpleNamespace:
|
||||
return SimpleNamespace(
|
||||
id=wallet_id,
|
||||
balance=Decimal(recharge),
|
||||
gift_balance=Decimal(gift),
|
||||
total_recharged=Decimal("0"),
|
||||
total_consumed=Decimal("0"),
|
||||
total_refunded=Decimal("0"),
|
||||
total_adjusted=Decimal("0"),
|
||||
limit_mode=limit_mode,
|
||||
currency="USD",
|
||||
status=status,
|
||||
api_key_id=api_key_id,
|
||||
updated_at=None,
|
||||
)
|
||||
|
||||
|
||||
def _build_locked_db(wallet: SimpleNamespace) -> MagicMock:
|
||||
db = MagicMock()
|
||||
query = MagicMock()
|
||||
query.filter.return_value = query
|
||||
query.with_for_update.return_value = query
|
||||
query.one_or_none.return_value = wallet
|
||||
db.query.return_value = query
|
||||
return db
|
||||
|
||||
|
||||
def test_get_or_create_wallet_prefers_user_owner_for_non_standalone_key() -> None:
|
||||
db = MagicMock()
|
||||
db.query.return_value.filter.return_value.first.return_value = None
|
||||
db.begin_nested.return_value.__enter__.return_value = None
|
||||
db.begin_nested.return_value.__exit__.return_value = None
|
||||
|
||||
user = SimpleNamespace(id="user-1")
|
||||
api_key = SimpleNamespace(id="key-1", is_standalone=False)
|
||||
|
||||
wallet = WalletService.get_or_create_wallet(db, user=user, api_key=api_key)
|
||||
|
||||
assert wallet is not None
|
||||
assert wallet.user_id == "user-1"
|
||||
assert wallet.api_key_id is None
|
||||
|
||||
|
||||
def test_get_or_create_wallet_uses_api_key_owner_for_standalone_key() -> None:
|
||||
db = MagicMock()
|
||||
db.query.return_value.filter.return_value.first.return_value = None
|
||||
db.begin_nested.return_value.__enter__.return_value = None
|
||||
db.begin_nested.return_value.__exit__.return_value = None
|
||||
|
||||
user = SimpleNamespace(id="user-1")
|
||||
api_key = SimpleNamespace(id="key-1", is_standalone=True)
|
||||
|
||||
wallet = WalletService.get_or_create_wallet(db, user=user, api_key=api_key)
|
||||
|
||||
assert wallet is not None
|
||||
assert wallet.user_id is None
|
||||
assert wallet.api_key_id == "key-1"
|
||||
|
||||
|
||||
def test_check_request_allowed_denies_when_recharge_negative_even_total_positive() -> None:
|
||||
wallet = _build_wallet(recharge="-1", gift="10", limit_mode="finite")
|
||||
db = MagicMock()
|
||||
api_key = MagicMock()
|
||||
|
||||
with patch.object(WalletService, "get_or_create_wallet", return_value=wallet):
|
||||
result = WalletService.check_request_allowed(db, user=None, api_key=api_key)
|
||||
|
||||
assert result.allowed is False
|
||||
assert result.message == "钱包欠费,请先充值"
|
||||
assert result.remaining == Decimal("-1.00000000")
|
||||
|
||||
|
||||
def test_get_balance_snapshot_returns_negative_recharge_for_unlimited_wallet() -> None:
|
||||
wallet = _build_wallet(recharge="-2", gift="100", limit_mode="unlimited")
|
||||
db = MagicMock()
|
||||
|
||||
with patch.object(WalletService, "get_or_create_wallet", return_value=wallet):
|
||||
snapshot = WalletService.get_balance_snapshot(db, user=None, api_key=MagicMock())
|
||||
|
||||
assert snapshot == Decimal("-2.00000000")
|
||||
|
||||
|
||||
def test_admin_adjust_balance_negative_from_gift_spills_to_recharge() -> None:
|
||||
wallet = _build_wallet(recharge="2", gift="3")
|
||||
db = _build_locked_db(wallet)
|
||||
|
||||
tx = WalletService.admin_adjust_balance(
|
||||
db,
|
||||
wallet=wallet,
|
||||
amount_usd=Decimal("-10"),
|
||||
balance_type="gift",
|
||||
operator_id="admin-1",
|
||||
description="test",
|
||||
)
|
||||
|
||||
assert wallet.gift_balance == Decimal("0E-8")
|
||||
assert wallet.balance == Decimal("-5.00000000")
|
||||
assert wallet.total_adjusted == Decimal("-10.00000000")
|
||||
assert tx.recharge_balance_after == Decimal("-5.00000000")
|
||||
assert tx.gift_balance_after == Decimal("0E-8")
|
||||
assert tx.amount == Decimal("-10.00000000")
|
||||
assert tx.category == "adjust"
|
||||
assert tx.reason_code == "adjust_admin"
|
||||
|
||||
|
||||
def test_admin_adjust_balance_negative_from_recharge_then_gift() -> None:
|
||||
wallet = _build_wallet(recharge="2", gift="3")
|
||||
db = _build_locked_db(wallet)
|
||||
|
||||
tx = WalletService.admin_adjust_balance(
|
||||
db,
|
||||
wallet=wallet,
|
||||
amount_usd=Decimal("-4"),
|
||||
balance_type="recharge",
|
||||
operator_id="admin-1",
|
||||
description="test",
|
||||
)
|
||||
|
||||
assert wallet.balance == Decimal("0E-8")
|
||||
assert wallet.gift_balance == Decimal("1.00000000")
|
||||
assert wallet.total_adjusted == Decimal("-4.00000000")
|
||||
assert tx.recharge_balance_after == Decimal("0E-8")
|
||||
assert tx.gift_balance_after == Decimal("1.00000000")
|
||||
assert tx.amount == Decimal("-4.00000000")
|
||||
|
||||
|
||||
def test_admin_adjust_balance_positive_adds_to_selected_bucket_without_offset() -> None:
|
||||
wallet = _build_wallet(recharge="-2", gift="5")
|
||||
db = _build_locked_db(wallet)
|
||||
|
||||
tx = WalletService.admin_adjust_balance(
|
||||
db,
|
||||
wallet=wallet,
|
||||
amount_usd=Decimal("1"),
|
||||
balance_type="gift",
|
||||
operator_id="admin-1",
|
||||
description="test",
|
||||
)
|
||||
|
||||
assert wallet.balance == Decimal("-2.00000000")
|
||||
assert wallet.gift_balance == Decimal("6.00000000")
|
||||
assert wallet.total_adjusted == Decimal("1.00000000")
|
||||
assert tx.recharge_balance_after == Decimal("-2.00000000")
|
||||
assert tx.gift_balance_after == Decimal("6.00000000")
|
||||
assert tx.amount == Decimal("1.00000000")
|
||||
assert tx.category == "adjust"
|
||||
assert tx.reason_code == "adjust_admin"
|
||||
|
||||
|
||||
def test_apply_usage_charge_prefers_gift_then_recharge() -> None:
|
||||
wallet = _build_wallet(recharge="5", gift="3", limit_mode="finite")
|
||||
usage = SimpleNamespace(
|
||||
wallet_id=None,
|
||||
api_key_id=None,
|
||||
user_id="user-1",
|
||||
wallet_balance_before=None,
|
||||
wallet_balance_after=None,
|
||||
wallet_recharge_balance_before=None,
|
||||
wallet_recharge_balance_after=None,
|
||||
wallet_gift_balance_before=None,
|
||||
wallet_gift_balance_after=None,
|
||||
)
|
||||
db = _build_locked_db(wallet)
|
||||
|
||||
with patch.object(WalletService, "_resolve_wallet_for_usage", return_value=wallet):
|
||||
before, after = WalletService.apply_usage_charge(db, usage=usage, amount_usd=Decimal("6"))
|
||||
|
||||
assert before == Decimal("8.00000000")
|
||||
assert after == Decimal("2.00000000")
|
||||
assert wallet.gift_balance == Decimal("0E-8")
|
||||
assert wallet.balance == Decimal("2.00000000")
|
||||
assert wallet.total_consumed == Decimal("6.00000000")
|
||||
assert usage.wallet_balance_before == Decimal("8.00000000")
|
||||
assert usage.wallet_balance_after == Decimal("2.00000000")
|
||||
assert usage.wallet_recharge_balance_before == Decimal("5.00000000")
|
||||
assert usage.wallet_recharge_balance_after == Decimal("2.00000000")
|
||||
assert usage.wallet_gift_balance_before == Decimal("3.00000000")
|
||||
assert usage.wallet_gift_balance_after == Decimal("0E-8")
|
||||
|
||||
|
||||
def test_apply_usage_charge_unlimited_wallet_keeps_balances() -> None:
|
||||
wallet = _build_wallet(recharge="5", gift="3", limit_mode="unlimited")
|
||||
usage = SimpleNamespace(
|
||||
wallet_id=None,
|
||||
api_key_id=None,
|
||||
user_id="user-1",
|
||||
wallet_balance_before=None,
|
||||
wallet_balance_after=None,
|
||||
wallet_recharge_balance_before=None,
|
||||
wallet_recharge_balance_after=None,
|
||||
wallet_gift_balance_before=None,
|
||||
wallet_gift_balance_after=None,
|
||||
)
|
||||
db = _build_locked_db(wallet)
|
||||
|
||||
with patch.object(WalletService, "_resolve_wallet_for_usage", return_value=wallet):
|
||||
before, after = WalletService.apply_usage_charge(db, usage=usage, amount_usd=Decimal("4"))
|
||||
|
||||
assert before == Decimal("8.00000000")
|
||||
assert after == Decimal("8.00000000")
|
||||
assert wallet.balance == Decimal("5")
|
||||
assert wallet.gift_balance == Decimal("3")
|
||||
assert wallet.total_consumed == Decimal("4.00000000")
|
||||
assert usage.wallet_balance_before == Decimal("8.00000000")
|
||||
assert usage.wallet_balance_after == Decimal("8.00000000")
|
||||
assert usage.wallet_recharge_balance_before == Decimal("5")
|
||||
assert usage.wallet_recharge_balance_after == Decimal("5")
|
||||
assert usage.wallet_gift_balance_before == Decimal("3")
|
||||
assert usage.wallet_gift_balance_after == Decimal("3")
|
||||
|
||||
|
||||
def test_complete_refund_requires_processing_status() -> None:
|
||||
refund = SimpleNamespace(id="refund-1", status="failed")
|
||||
db = MagicMock()
|
||||
query = MagicMock()
|
||||
query.filter.return_value = query
|
||||
query.with_for_update.return_value = query
|
||||
query.one_or_none.return_value = refund
|
||||
db.query.return_value = query
|
||||
|
||||
with pytest.raises(ValueError, match="processing"):
|
||||
WalletService.complete_refund(db, refund=refund)
|
||||
|
||||
|
||||
def test_get_or_create_wallet_reuses_existing_after_integrity_error() -> None:
|
||||
existing_wallet = _build_wallet(wallet_id="wallet-existing", recharge="1", gift="0")
|
||||
db = MagicMock()
|
||||
nested = MagicMock()
|
||||
nested.__enter__.return_value = None
|
||||
nested.__exit__.return_value = None
|
||||
db.begin_nested.return_value = nested
|
||||
db.flush.side_effect = IntegrityError("insert", {}, Exception("duplicate key"))
|
||||
|
||||
with patch.object(WalletService, "get_wallet", side_effect=[None, existing_wallet]):
|
||||
wallet = WalletService.get_or_create_wallet(
|
||||
db,
|
||||
user=SimpleNamespace(id="user-1"),
|
||||
api_key=None,
|
||||
)
|
||||
|
||||
assert wallet is existing_wallet
|
||||
|
||||
|
||||
def test_create_refund_request_rejects_uncredited_payment_order() -> None:
|
||||
wallet = _build_wallet(wallet_id="wallet-1", recharge="10", gift="0")
|
||||
payment_order = SimpleNamespace(
|
||||
id="order-1",
|
||||
wallet_id="wallet-1",
|
||||
status="pending",
|
||||
refundable_amount_usd=Decimal("10"),
|
||||
)
|
||||
wallet_query = MagicMock()
|
||||
wallet_query.filter.return_value = wallet_query
|
||||
wallet_query.with_for_update.return_value = wallet_query
|
||||
wallet_query.one_or_none.return_value = wallet
|
||||
order_query = MagicMock()
|
||||
order_query.filter.return_value = order_query
|
||||
order_query.with_for_update.return_value = order_query
|
||||
order_query.one_or_none.return_value = payment_order
|
||||
db = MagicMock()
|
||||
|
||||
def _query(model: object) -> MagicMock:
|
||||
name = getattr(model, "__name__", "")
|
||||
if name == "Wallet":
|
||||
return wallet_query
|
||||
if name == "PaymentOrder":
|
||||
return order_query
|
||||
raise AssertionError(f"unexpected model query: {name}")
|
||||
|
||||
db.query.side_effect = _query
|
||||
|
||||
with patch.object(WalletService, "_get_pending_refund_reserved_amount", return_value=Decimal("0")):
|
||||
with pytest.raises(ValueError, match="payment order is not refundable"):
|
||||
WalletService.create_refund_request(
|
||||
db,
|
||||
wallet=wallet,
|
||||
user_id="user-1",
|
||||
amount_usd=Decimal("2"),
|
||||
refund_no="rf-1",
|
||||
source_type="payment_order",
|
||||
source_id="order-1",
|
||||
refund_mode="original_channel",
|
||||
payment_order=payment_order,
|
||||
)
|
||||
|
||||
|
||||
def test_create_refund_request_reserves_pending_wallet_amount() -> None:
|
||||
wallet = _build_wallet(wallet_id="wallet-1", recharge="10", gift="0")
|
||||
wallet_query = MagicMock()
|
||||
wallet_query.filter.return_value = wallet_query
|
||||
wallet_query.with_for_update.return_value = wallet_query
|
||||
wallet_query.one_or_none.return_value = wallet
|
||||
db = MagicMock()
|
||||
|
||||
def _query(model: object) -> MagicMock:
|
||||
name = getattr(model, "__name__", "")
|
||||
if name == "Wallet":
|
||||
return wallet_query
|
||||
raise AssertionError(f"unexpected model query: {name}")
|
||||
|
||||
db.query.side_effect = _query
|
||||
|
||||
with patch.object(
|
||||
WalletService,
|
||||
"_get_pending_refund_reserved_amount",
|
||||
return_value=Decimal("9"),
|
||||
):
|
||||
with pytest.raises(ValueError, match="available refundable recharge balance"):
|
||||
WalletService.create_refund_request(
|
||||
db,
|
||||
wallet=wallet,
|
||||
user_id="user-1",
|
||||
amount_usd=Decimal("2"),
|
||||
refund_no="rf-2",
|
||||
source_type="wallet_balance",
|
||||
source_id=None,
|
||||
refund_mode="offline_payout",
|
||||
)
|
||||
|
||||
|
||||
def test_create_refund_request_reserves_pending_order_amount() -> None:
|
||||
wallet = _build_wallet(wallet_id="wallet-1", recharge="10", gift="0")
|
||||
payment_order = SimpleNamespace(
|
||||
id="order-1",
|
||||
wallet_id="wallet-1",
|
||||
status="credited",
|
||||
refundable_amount_usd=Decimal("5"),
|
||||
)
|
||||
wallet_query = MagicMock()
|
||||
wallet_query.filter.return_value = wallet_query
|
||||
wallet_query.with_for_update.return_value = wallet_query
|
||||
wallet_query.one_or_none.return_value = wallet
|
||||
order_query = MagicMock()
|
||||
order_query.filter.return_value = order_query
|
||||
order_query.with_for_update.return_value = order_query
|
||||
order_query.one_or_none.return_value = payment_order
|
||||
db = MagicMock()
|
||||
|
||||
def _query(model: object) -> MagicMock:
|
||||
name = getattr(model, "__name__", "")
|
||||
if name == "Wallet":
|
||||
return wallet_query
|
||||
if name == "PaymentOrder":
|
||||
return order_query
|
||||
raise AssertionError(f"unexpected model query: {name}")
|
||||
|
||||
db.query.side_effect = _query
|
||||
|
||||
with patch.object(
|
||||
WalletService,
|
||||
"_get_pending_refund_reserved_amount",
|
||||
side_effect=[Decimal("0"), Decimal("4")],
|
||||
):
|
||||
with pytest.raises(ValueError, match="available refundable amount"):
|
||||
WalletService.create_refund_request(
|
||||
db,
|
||||
wallet=wallet,
|
||||
user_id="user-1",
|
||||
amount_usd=Decimal("2"),
|
||||
refund_no="rf-3",
|
||||
source_type="payment_order",
|
||||
source_id="order-1",
|
||||
refund_mode="original_channel",
|
||||
payment_order=payment_order,
|
||||
)
|
||||
|
||||
|
||||
def test_move_refund_to_processing_rejects_double_transition() -> None:
|
||||
refund = SimpleNamespace(
|
||||
id="refund-1",
|
||||
wallet_id="wallet-1",
|
||||
payment_order_id="order-1",
|
||||
amount_usd=Decimal("2"),
|
||||
status="pending_approval",
|
||||
approved_by=None,
|
||||
processed_by=None,
|
||||
processed_at=None,
|
||||
updated_at=None,
|
||||
)
|
||||
wallet = _build_wallet(wallet_id="wallet-1", recharge="10", gift="0")
|
||||
payment_order = SimpleNamespace(
|
||||
id="order-1",
|
||||
refunded_amount_usd=Decimal("0"),
|
||||
refundable_amount_usd=Decimal("10"),
|
||||
)
|
||||
refund_query = MagicMock()
|
||||
refund_query.filter.return_value = refund_query
|
||||
refund_query.with_for_update.return_value = refund_query
|
||||
refund_query.one_or_none.return_value = refund
|
||||
wallet_query = MagicMock()
|
||||
wallet_query.filter.return_value = wallet_query
|
||||
wallet_query.with_for_update.return_value = wallet_query
|
||||
wallet_query.one_or_none.return_value = wallet
|
||||
order_query = MagicMock()
|
||||
order_query.filter.return_value = order_query
|
||||
order_query.with_for_update.return_value = order_query
|
||||
order_query.one_or_none.return_value = payment_order
|
||||
|
||||
db = MagicMock()
|
||||
|
||||
def _query(model: object) -> MagicMock:
|
||||
name = getattr(model, "__name__", "")
|
||||
if name == "RefundRequest":
|
||||
return refund_query
|
||||
if name == "Wallet":
|
||||
return wallet_query
|
||||
if name == "PaymentOrder":
|
||||
return order_query
|
||||
raise AssertionError(f"unexpected model query: {name}")
|
||||
|
||||
db.query.side_effect = _query
|
||||
tx = SimpleNamespace(id="tx-1")
|
||||
|
||||
with patch.object(WalletService, "create_wallet_transaction", return_value=tx) as create_tx:
|
||||
first_tx = WalletService.move_refund_to_processing(db, refund=refund, operator_id="admin-1")
|
||||
with pytest.raises(ValueError, match="not approvable"):
|
||||
WalletService.move_refund_to_processing(db, refund=refund, operator_id="admin-1")
|
||||
|
||||
assert first_tx is tx
|
||||
assert create_tx.call_count == 1
|
||||
assert refund.status == "processing"
|
||||
assert payment_order.refunded_amount_usd == Decimal("2.00000000")
|
||||
assert payment_order.refundable_amount_usd == Decimal("8.00000000")
|
||||
|
||||
|
||||
def test_move_refund_to_processing_rechecks_payment_order_refundable_amount() -> None:
|
||||
refund = SimpleNamespace(
|
||||
id="refund-1",
|
||||
wallet_id="wallet-1",
|
||||
payment_order_id="order-1",
|
||||
amount_usd=Decimal("2"),
|
||||
status="pending_approval",
|
||||
approved_by=None,
|
||||
processed_by=None,
|
||||
processed_at=None,
|
||||
updated_at=None,
|
||||
)
|
||||
wallet = _build_wallet(wallet_id="wallet-1", recharge="10", gift="0")
|
||||
payment_order = SimpleNamespace(
|
||||
id="order-1",
|
||||
refunded_amount_usd=Decimal("0"),
|
||||
refundable_amount_usd=Decimal("1"),
|
||||
)
|
||||
refund_query = MagicMock()
|
||||
refund_query.filter.return_value = refund_query
|
||||
refund_query.with_for_update.return_value = refund_query
|
||||
refund_query.one_or_none.return_value = refund
|
||||
wallet_query = MagicMock()
|
||||
wallet_query.filter.return_value = wallet_query
|
||||
wallet_query.with_for_update.return_value = wallet_query
|
||||
wallet_query.one_or_none.return_value = wallet
|
||||
order_query = MagicMock()
|
||||
order_query.filter.return_value = order_query
|
||||
order_query.with_for_update.return_value = order_query
|
||||
order_query.one_or_none.return_value = payment_order
|
||||
|
||||
db = MagicMock()
|
||||
|
||||
def _query(model: object) -> MagicMock:
|
||||
name = getattr(model, "__name__", "")
|
||||
if name == "RefundRequest":
|
||||
return refund_query
|
||||
if name == "Wallet":
|
||||
return wallet_query
|
||||
if name == "PaymentOrder":
|
||||
return order_query
|
||||
raise AssertionError(f"unexpected model query: {name}")
|
||||
|
||||
db.query.side_effect = _query
|
||||
|
||||
with patch.object(WalletService, "create_wallet_transaction") as create_tx:
|
||||
with pytest.raises(ValueError, match="refund amount exceeds refundable amount"):
|
||||
WalletService.move_refund_to_processing(db, refund=refund, operator_id="admin-1")
|
||||
|
||||
create_tx.assert_not_called()
|
||||
assert refund.status == "pending_approval"
|
||||
assert payment_order.refunded_amount_usd == Decimal("0")
|
||||
assert payment_order.refundable_amount_usd == Decimal("1")
|
||||
|
||||
|
||||
def test_fail_refund_rejects_invalid_status_after_first_failure() -> None:
|
||||
refund = SimpleNamespace(
|
||||
id="refund-2",
|
||||
wallet_id="wallet-1",
|
||||
payment_order_id=None,
|
||||
amount_usd=Decimal("1"),
|
||||
status="processing",
|
||||
failure_reason=None,
|
||||
updated_at=None,
|
||||
)
|
||||
wallet = _build_wallet(wallet_id="wallet-1", recharge="10", gift="0")
|
||||
refund_query = MagicMock()
|
||||
refund_query.filter.return_value = refund_query
|
||||
refund_query.with_for_update.return_value = refund_query
|
||||
refund_query.one_or_none.return_value = refund
|
||||
wallet_query = MagicMock()
|
||||
wallet_query.filter.return_value = wallet_query
|
||||
wallet_query.first.return_value = wallet
|
||||
|
||||
db = MagicMock()
|
||||
|
||||
def _query(model: object) -> MagicMock:
|
||||
name = getattr(model, "__name__", "")
|
||||
if name == "RefundRequest":
|
||||
return refund_query
|
||||
if name == "Wallet":
|
||||
return wallet_query
|
||||
raise AssertionError(f"unexpected model query: {name}")
|
||||
|
||||
db.query.side_effect = _query
|
||||
tx = SimpleNamespace(id="tx-revert")
|
||||
|
||||
with patch.object(WalletService, "create_wallet_transaction", return_value=tx) as create_tx:
|
||||
first_tx = WalletService.fail_refund(
|
||||
db,
|
||||
refund=refund,
|
||||
reason="first-failure",
|
||||
operator_id="admin-1",
|
||||
)
|
||||
with pytest.raises(ValueError, match="cannot fail refund in status: failed"):
|
||||
WalletService.fail_refund(
|
||||
db,
|
||||
refund=refund,
|
||||
reason="retry-failure",
|
||||
operator_id="admin-1",
|
||||
)
|
||||
|
||||
assert first_tx is tx
|
||||
assert create_tx.call_count == 1
|
||||
assert refund.status == "failed"
|
||||
assert refund.failure_reason == "first-failure"
|
||||
|
||||
|
||||
def test_fail_refund_rejects_succeeded_status() -> None:
|
||||
refund = SimpleNamespace(
|
||||
id="refund-succeeded",
|
||||
wallet_id="wallet-1",
|
||||
payment_order_id=None,
|
||||
amount_usd=Decimal("1"),
|
||||
status="succeeded",
|
||||
failure_reason=None,
|
||||
updated_at=None,
|
||||
)
|
||||
refund_query = MagicMock()
|
||||
refund_query.filter.return_value = refund_query
|
||||
refund_query.with_for_update.return_value = refund_query
|
||||
refund_query.one_or_none.return_value = refund
|
||||
|
||||
db = MagicMock()
|
||||
|
||||
def _query(model: object) -> MagicMock:
|
||||
name = getattr(model, "__name__", "")
|
||||
if name == "RefundRequest":
|
||||
return refund_query
|
||||
raise AssertionError(f"unexpected model query: {name}")
|
||||
|
||||
db.query.side_effect = _query
|
||||
|
||||
with pytest.raises(ValueError, match="cannot fail refund in status: succeeded"):
|
||||
WalletService.fail_refund(
|
||||
db,
|
||||
refund=refund,
|
||||
reason="should-not-override",
|
||||
operator_id="admin-1",
|
||||
)
|
||||
|
||||
assert refund.status == "succeeded"
|
||||
assert refund.failure_reason is None
|
||||
Reference in New Issue
Block a user