8da8c3cfae
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
196 lines
7.8 KiB
Python
196 lines
7.8 KiB
Python
"""邀请奖励金提现账户隔离测试:提现扣 invite_cash 账户、退款退回 invite_cash、与金币现金不串、
|
|
/invite/me 返回奖励金战绩、提现单 source 过滤。复用 test_withdraw 的 wxpay monkeypatch 模式。
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from sqlalchemy import select
|
|
|
|
from app.db.session import SessionLocal
|
|
from app.models.user import User
|
|
from app.models.wallet import CoinAccount, InviteCashTransaction
|
|
from app.repositories import wallet as crud_wallet
|
|
|
|
|
|
def _login(client, phone: str) -> str:
|
|
client.post("/api/v1/auth/sms/send", json={"phone": phone})
|
|
r = client.post("/api/v1/auth/sms/login", json={"phone": phone, "code": "123456"})
|
|
assert r.status_code == 200, r.text
|
|
return r.json()["access_token"]
|
|
|
|
|
|
def _auth(token: str) -> dict[str, str]:
|
|
return {"Authorization": f"Bearer {token}"}
|
|
|
|
|
|
def _patch_userinfo(monkeypatch, openid: str) -> None:
|
|
monkeypatch.setattr(
|
|
"app.integrations.wxpay.code_to_userinfo",
|
|
lambda code: {"openid": openid, "nickname": None, "avatar_url": None, "raw": {}},
|
|
)
|
|
|
|
|
|
def _seed_balances(client, token: str, phone: str, *, cash: int = 0, invite_cash: int = 0) -> None:
|
|
"""访问 /account 触发建账户,再 DB 直接灌两个账户余额(没有"加钱"接口,正常靠兑换/发奖)。"""
|
|
client.get("/api/v1/wallet/account", headers=_auth(token))
|
|
db = SessionLocal()
|
|
try:
|
|
user = db.execute(select(User).where(User.phone == phone)).scalar_one()
|
|
acc = db.get(CoinAccount, user.id)
|
|
acc.cash_balance_cents = cash
|
|
acc.invite_cash_balance_cents = invite_cash
|
|
db.commit()
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
def _balances(client, token: str) -> tuple[int, int]:
|
|
j = client.get("/api/v1/wallet/account", headers=_auth(token)).json()
|
|
return j["cash_balance_cents"], j["invite_cash_balance_cents"]
|
|
|
|
|
|
def _reject(bill: str, reason: str = "测试拒绝") -> None:
|
|
db = SessionLocal()
|
|
try:
|
|
crud_wallet.reject_withdraw(db, bill, reason)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
def test_invite_cash_withdraw_deducts_invite_account(client, monkeypatch) -> None:
|
|
"""source=invite_cash 提现 → 扣 invite_cash_balance_cents,不动 cash;流水落 invite_cash_transaction。"""
|
|
_patch_userinfo(monkeypatch, "openid_ic_1")
|
|
token = _login(client, "13800004001")
|
|
_seed_balances(client, token, "13800004001", cash=300, invite_cash=500)
|
|
client.post("/api/v1/wallet/bind-wechat", json={"code": "c"}, headers=_auth(token))
|
|
|
|
r = client.post(
|
|
"/api/v1/wallet/withdraw",
|
|
json={"amount_cents": 200, "source": "invite_cash"},
|
|
headers=_auth(token),
|
|
)
|
|
assert r.status_code == 200, r.text
|
|
assert r.json()["status"] == "reviewing"
|
|
|
|
cash, invite_cash = _balances(client, token)
|
|
assert invite_cash == 300 # 扣了邀请奖励金
|
|
assert cash == 300 # 金币现金没动
|
|
|
|
db = SessionLocal()
|
|
try:
|
|
user = db.execute(select(User).where(User.phone == "13800004001")).scalar_one()
|
|
txns = db.execute(
|
|
select(InviteCashTransaction).where(
|
|
InviteCashTransaction.user_id == user.id,
|
|
InviteCashTransaction.biz_type == "invite_withdraw",
|
|
)
|
|
).scalars().all()
|
|
assert len(txns) == 1 and txns[0].amount_cents == -200
|
|
finally:
|
|
db.close()
|
|
orders = client.get("/api/v1/wallet/withdraw-orders", headers=_auth(token)).json()["items"]
|
|
assert orders[0]["source"] == "invite_cash"
|
|
|
|
|
|
def test_invite_cash_reject_refunds_invite_account(client, monkeypatch) -> None:
|
|
"""拒绝 invite_cash 提现 → 退回 invite_cash,不串金币现金;退款流水落 invite_cash_transaction。"""
|
|
_patch_userinfo(monkeypatch, "openid_ic_2")
|
|
token = _login(client, "13800004002")
|
|
_seed_balances(client, token, "13800004002", cash=0, invite_cash=500)
|
|
client.post("/api/v1/wallet/bind-wechat", json={"code": "c"}, headers=_auth(token))
|
|
|
|
r = client.post(
|
|
"/api/v1/wallet/withdraw",
|
|
json={"amount_cents": 200, "source": "invite_cash"},
|
|
headers=_auth(token),
|
|
)
|
|
bill = r.json()["out_bill_no"]
|
|
cash, invite_cash = _balances(client, token)
|
|
assert invite_cash == 300 and cash == 0 # 扣后
|
|
|
|
_reject(bill)
|
|
|
|
cash, invite_cash = _balances(client, token)
|
|
assert invite_cash == 500 # 退回邀请奖励金
|
|
assert cash == 0 # 没串进现金
|
|
|
|
db = SessionLocal()
|
|
try:
|
|
user = db.execute(select(User).where(User.phone == "13800004002")).scalar_one()
|
|
refunds = db.execute(
|
|
select(InviteCashTransaction).where(
|
|
InviteCashTransaction.user_id == user.id,
|
|
InviteCashTransaction.biz_type == "invite_withdraw_refund",
|
|
)
|
|
).scalars().all()
|
|
assert len(refunds) == 1 and refunds[0].amount_cents == 200
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
def test_two_accounts_withdraw_independent(client, monkeypatch) -> None:
|
|
"""两账户各提各的不串:先提 invite_cash(拒绝结清),再提 cash,各扣各账户。
|
|
注:一个用户同一时间只能一个活跃提现单(跨账户),故第二笔需先结清第一笔。"""
|
|
_patch_userinfo(monkeypatch, "openid_ic_3")
|
|
token = _login(client, "13800004003")
|
|
_seed_balances(client, token, "13800004003", cash=400, invite_cash=500)
|
|
client.post("/api/v1/wallet/bind-wechat", json={"code": "c"}, headers=_auth(token))
|
|
|
|
r1 = client.post(
|
|
"/api/v1/wallet/withdraw",
|
|
json={"amount_cents": 200, "source": "invite_cash"},
|
|
headers=_auth(token),
|
|
)
|
|
_reject(r1.json()["out_bill_no"]) # 退回 invite_cash + 结清活跃单
|
|
r2 = client.post(
|
|
"/api/v1/wallet/withdraw",
|
|
# 50 分 = 0.5 元档(7-9 起 coin_cash 只能提预设档位)
|
|
json={"amount_cents": 50, "source": "coin_cash"},
|
|
headers=_auth(token),
|
|
)
|
|
assert r2.json()["status"] == "reviewing"
|
|
|
|
cash, invite_cash = _balances(client, token)
|
|
assert invite_cash == 500 # 已退回
|
|
assert cash == 350 # 扣了 cash 50
|
|
|
|
|
|
def test_invite_me_returns_reward_stats(client) -> None:
|
|
"""/invite/me 返回 reward_balance_cents(可提现奖励金)。"""
|
|
token = _login(client, "13800004004")
|
|
_seed_balances(client, token, "13800004004", invite_cash=350)
|
|
j = client.get("/api/v1/invite/me", headers=_auth(token)).json()
|
|
assert j["reward_balance_cents"] == 350
|
|
assert j["reward_withdrawn_cents"] == 0
|
|
|
|
|
|
def test_withdraw_orders_source_filter(client, monkeypatch) -> None:
|
|
"""/wallet/withdraw-orders?source=invite_cash 只返回邀请奖励金提现单。"""
|
|
_patch_userinfo(monkeypatch, "openid_ic_5")
|
|
token = _login(client, "13800004005")
|
|
_seed_balances(client, token, "13800004005", cash=400, invite_cash=500)
|
|
client.post("/api/v1/wallet/bind-wechat", json={"code": "c"}, headers=_auth(token))
|
|
|
|
r1 = client.post(
|
|
"/api/v1/wallet/withdraw",
|
|
json={"amount_cents": 200, "source": "invite_cash"},
|
|
headers=_auth(token),
|
|
)
|
|
_reject(r1.json()["out_bill_no"]) # 结清,才能提第二笔
|
|
client.post(
|
|
"/api/v1/wallet/withdraw",
|
|
# 50 分 = 0.5 元档(7-9 起 coin_cash 只能提预设档位)
|
|
json={"amount_cents": 50, "source": "coin_cash"},
|
|
headers=_auth(token),
|
|
)
|
|
|
|
all_orders = client.get("/api/v1/wallet/withdraw-orders", headers=_auth(token)).json()["items"]
|
|
invite_orders = client.get(
|
|
"/api/v1/wallet/withdraw-orders", params={"source": "invite_cash"}, headers=_auth(token)
|
|
).json()["items"]
|
|
coin_orders = client.get(
|
|
"/api/v1/wallet/withdraw-orders", params={"source": "coin_cash"}, headers=_auth(token)
|
|
).json()["items"]
|
|
assert len(all_orders) == 2
|
|
assert len(invite_orders) == 1 and invite_orders[0]["source"] == "invite_cash"
|
|
assert len(coin_orders) == 1 and coin_orders[0]["source"] == "coin_cash"
|