Files
Aether/tests/api/test_wallet_refund_routes.py
LewisPen 783f654953 feat(wallet): 钱包系统替代配额系统,新增支付与退款机制
- 新增钱包余额管理、充值、扣费、退款完整流程
- 新增支付网关抽象层(支持手动/支付宝/微信)
- 用量计费从配额系统迁移到钱包余额扣费
- 新增管理员钱包管理与支付订单管理页面
- 新增用户钱包中心页面
- 移除独立 Key 锁定机制,统一由钱包余额控制
- 新增相关 API 路由、序列化器与数据库迁移
- 新增钱包、支付、退款相关测试
2026-03-08 00:05:48 +08:00

203 lines
6.7 KiB
Python

from __future__ import annotations
from decimal import Decimal
from types import SimpleNamespace
from unittest.mock import MagicMock
import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient
from src.api.wallet.routes import router as wallet_router
from src.database import get_db
def _build_wallet_app(
db: MagicMock,
monkeypatch: pytest.MonkeyPatch,
*,
payload: dict[str, object],
user_id: str = "user-1",
) -> TestClient:
app = FastAPI()
app.include_router(wallet_router)
app.dependency_overrides[get_db] = lambda: db
async def _fake_pipeline_run(*, adapter: object, http_request: object, db: MagicMock, mode: object) -> object:
_ = http_request, mode
context = SimpleNamespace(
db=db,
user=SimpleNamespace(id=user_id),
ensure_json_body=lambda: payload,
add_audit_metadata=lambda **_: None,
)
return await adapter.handle(context)
monkeypatch.setattr("src.api.wallet.routes.pipeline.run", _fake_pipeline_run)
return TestClient(app)
def test_create_refund_route_maps_uncredited_order_to_400(
monkeypatch: pytest.MonkeyPatch,
) -> None:
db = MagicMock()
payload = {"amount_usd": 2.0, "payment_order_id": "order-1"}
client = _build_wallet_app(db, monkeypatch, payload=payload)
wallet = SimpleNamespace(id="wallet-1")
payment_order = SimpleNamespace(id="order-1", wallet_id="wallet-1", payment_method="alipay")
monkeypatch.setattr(
"src.api.wallet.routes.WalletService.get_or_create_wallet",
lambda _db, user: wallet,
)
db.query.return_value.filter.return_value.first.return_value = payment_order
def _raise(*args: object, **kwargs: object) -> object:
raise ValueError("payment order is not refundable")
monkeypatch.setattr("src.api.wallet.routes.WalletService.create_refund_request", _raise)
response = client.post("/api/wallet/refunds", json=payload)
assert response.status_code == 400
assert "not refundable" in response.json()["detail"]
db.rollback.assert_called_once()
db.commit.assert_not_called()
def test_create_refund_route_maps_reserved_wallet_amount_to_400(
monkeypatch: pytest.MonkeyPatch,
) -> None:
db = MagicMock()
payload = {"amount_usd": 2.0}
client = _build_wallet_app(db, monkeypatch, payload=payload)
wallet = SimpleNamespace(id="wallet-1")
monkeypatch.setattr(
"src.api.wallet.routes.WalletService.get_or_create_wallet",
lambda _db, user: wallet,
)
def _raise(*args: object, **kwargs: object) -> object:
raise ValueError("refund amount exceeds available refundable recharge balance")
monkeypatch.setattr("src.api.wallet.routes.WalletService.create_refund_request", _raise)
response = client.post("/api/wallet/refunds", json=payload)
assert response.status_code == 400
assert "available refundable recharge balance" in response.json()["detail"]
db.rollback.assert_called_once()
db.commit.assert_not_called()
def test_create_refund_route_passes_default_order_refund_mode_and_commits(
monkeypatch: pytest.MonkeyPatch,
) -> None:
db = MagicMock()
payload = {"amount_usd": 2.0, "payment_order_id": "order-1", "reason": "test"}
client = _build_wallet_app(db, monkeypatch, payload=payload)
wallet = SimpleNamespace(id="wallet-1")
payment_order = SimpleNamespace(id="order-1", wallet_id="wallet-1", payment_method="alipay")
refund = SimpleNamespace(
id="refund-1",
refund_no="rf-1",
payment_order_id="order-1",
source_type="payment_order",
source_id="order-1",
refund_mode="original_channel",
amount_usd=Decimal("2.00000000"),
status="pending_approval",
reason="test",
failure_reason=None,
gateway_refund_id=None,
payout_method=None,
payout_reference=None,
payout_proof=None,
created_at="2026-03-07T00:00:00Z",
updated_at="2026-03-07T00:00:00Z",
processed_at=None,
completed_at=None,
)
monkeypatch.setattr(
"src.api.wallet.routes.WalletService.get_or_create_wallet",
lambda _db, user: wallet,
)
db.query.return_value.filter.return_value.first.return_value = payment_order
captured: dict[str, object] = {}
def _create_refund_request(_db: MagicMock, **kwargs: object) -> object:
captured.update(kwargs)
return refund
monkeypatch.setattr(
"src.api.wallet.routes.WalletService.create_refund_request",
_create_refund_request,
)
response = client.post("/api/wallet/refunds", json=payload)
assert response.status_code == 200
body = response.json()
assert body["id"] == "refund-1"
assert body["status"] == "pending_approval"
assert captured["refund_mode"] == "original_channel"
assert captured["source_type"] == "payment_order"
assert captured["source_id"] == "order-1"
assert captured["payment_order"] is payment_order
db.commit.assert_called_once()
db.refresh.assert_called_once_with(refund)
db.rollback.assert_not_called()
def test_create_refund_route_uses_offline_payout_for_manual_recharge(
monkeypatch: pytest.MonkeyPatch,
) -> None:
db = MagicMock()
payload = {"amount_usd": 2.0, "payment_order_id": "order-2"}
client = _build_wallet_app(db, monkeypatch, payload=payload)
wallet = SimpleNamespace(id="wallet-1")
payment_order = SimpleNamespace(id="order-2", wallet_id="wallet-1", payment_method="admin_manual")
refund = SimpleNamespace(
id="refund-2",
refund_no="rf-2",
payment_order_id="order-2",
source_type="payment_order",
source_id="order-2",
refund_mode="offline_payout",
amount_usd=Decimal("2.00000000"),
status="pending_approval",
reason=None,
failure_reason=None,
gateway_refund_id=None,
payout_method=None,
payout_reference=None,
payout_proof=None,
created_at="2026-03-07T00:00:00Z",
updated_at="2026-03-07T00:00:00Z",
processed_at=None,
completed_at=None,
)
monkeypatch.setattr(
"src.api.wallet.routes.WalletService.get_or_create_wallet",
lambda _db, user: wallet,
)
db.query.return_value.filter.return_value.first.return_value = payment_order
captured: dict[str, object] = {}
def _create_refund_request(_db: MagicMock, **kwargs: object) -> object:
captured.update(kwargs)
return refund
monkeypatch.setattr(
"src.api.wallet.routes.WalletService.create_refund_request",
_create_refund_request,
)
response = client.post("/api/wallet/refunds", json=payload)
assert response.status_code == 200
assert captured["refund_mode"] == "offline_payout"