"""提现现金账本校验(admin ledger-check)测试:两本物理隔离的账各自对账。 历史盲区:`withdraw_ledger_check` 曾拿全部提现单去和**普通现金流水**(cash_transaction)比对, 而 source=invite_cash 的提现单流水其实在 invite_cash_transaction 表,导致每笔邀请提现单都被 误报「缺扣款/缺退款流水」。这里用真实提现 API 造单 + before/after 差值断言锁定修复: 1) 邀请提现单不再污染普通现金账的缺流水计数; 2) 邀请账户已被纳入对账(能抓到它自己的缺流水); 3) 普通现金账的原有对账未被改坏。 conftest 的库是 session 级共享、测试间不清,故一律用 before/after 差值,只反映本用例造的数据。 """ from __future__ import annotations from sqlalchemy import delete, select from app.admin.repositories.queries import withdraw_ledger_check 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: 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 _reject(bill: str, reason: str = "测试拒绝") -> None: db = SessionLocal() try: crud_wallet.reject_withdraw(db, bill, reason) finally: db.close() def _ledger() -> dict: db = SessionLocal() try: return withdraw_ledger_check(db) finally: db.close() def test_rejected_invite_withdraw_not_flagged_missing(client, monkeypatch) -> None: """核心回归:一笔被拒绝的 invite_cash 提现单,扣款/退款流水都在 invite_cash_transaction, 不应让普通现金账的缺扣款/缺退款计数增加(修复前每笔会各 +1)。""" before = _ledger() _patch_userinfo(monkeypatch, "openid_lc_1") token = _login(client, "13800005001") _seed_balances(client, token, "13800005001", 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), ) assert r.status_code == 200, r.text _reject(r.json()["out_bill_no"]) # rejected + 退款流水落 invite_cash_transaction after = _ledger() # 普通现金账不该因这笔 invite 单产生缺流水(修复前会各 +1 → 就是页面上看到的误报) assert after["missing_withdraw_txn_count"] == before["missing_withdraw_txn_count"] assert after["missing_refund_txn_count"] == before["missing_refund_txn_count"] # 邀请账扣款 + 退款流水齐全,邀请账自身也不该缺 assert after["invite_missing_withdraw_txn_count"] == before["invite_missing_withdraw_txn_count"] assert after["invite_missing_refund_txn_count"] == before["invite_missing_refund_txn_count"] def test_invite_ledger_detects_missing_withdraw_txn(client, monkeypatch) -> None: """删掉一笔 invite 提现单的扣款流水 → 邀请账缺扣款计数 +1、ok=False, 证明邀请账户已真正纳入对账(修复前邀请账完全不校验、永远报不出问题)。""" _patch_userinfo(monkeypatch, "openid_lc_2") token = _login(client, "13800005002") _seed_balances(client, token, "13800005002", 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"] before = _ledger() db = SessionLocal() try: db.execute( delete(InviteCashTransaction).where( InviteCashTransaction.ref_id == bill, InviteCashTransaction.biz_type == "invite_withdraw", ) ) db.commit() finally: db.close() after = _ledger() assert ( after["invite_missing_withdraw_txn_count"] == before["invite_missing_withdraw_txn_count"] + 1 ) assert after["ok"] is False def test_coin_cash_withdraw_still_reconciled(client, monkeypatch) -> None: """普通现金 coin_cash 提现单齐全时不新增缺流水(确保分账改造没弄坏原有普通现金对账)。""" _patch_userinfo(monkeypatch, "openid_lc_3") token = _login(client, "13800005003") _seed_balances(client, token, "13800005003", cash=500, invite_cash=0) client.post("/api/v1/wallet/bind-wechat", json={"code": "c"}, headers=_auth(token)) before = _ledger() r = client.post( "/api/v1/wallet/withdraw", json={"amount_cents": 200, "source": "coin_cash"}, headers=_auth(token), ) assert r.status_code == 200, r.text after = _ledger() # 普通现金提现扣款流水随单写入 cash_transaction,缺扣款计数不变;邀请账更不受影响 assert after["missing_withdraw_txn_count"] == before["missing_withdraw_txn_count"] assert after["invite_missing_withdraw_txn_count"] == before["invite_missing_withdraw_txn_count"]