"""邀请奖励金提现账户隔离测试:提现扣 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"