mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 17:30:23 +08:00
feat(wallet): 钱包系统替代配额系统,新增支付与退款机制
- 新增钱包余额管理、充值、扣费、退款完整流程 - 新增支付网关抽象层(支持手动/支付宝/微信) - 用量计费从配额系统迁移到钱包余额扣费 - 新增管理员钱包管理与支付订单管理页面 - 新增用户钱包中心页面 - 移除独立 Key 锁定机制,统一由钱包余额控制 - 新增相关 API 路由、序列化器与数据库迁移 - 新增钱包、支付、退款相关测试
This commit is contained in:
202
tests/api/test_wallet_refund_routes.py
Normal file
202
tests/api/test_wallet_refund_routes.py
Normal file
@@ -0,0 +1,202 @@
|
||||
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"
|
||||
Reference in New Issue
Block a user