import hashlib import json import os from unittest.mock import patch from fastapi.testclient import TestClient from database import SessionLocal from main import app, seed from models import InvoiceApplication, PaymentRefund, TokenAccount, TokenPaymentOrder, TokenPlan, User from services.token_billing import DEFAULT_TOKEN_GRANT client = TestClient(app) def _signature(token, timestamp, nonce): return hashlib.sha1("".join(sorted([token, timestamp, nonce])).encode()).hexdigest() def test_virtual_payment_callback_and_refund_are_idempotent(authorization_context): seed() context = authorization_context db = SessionLocal() try: user = db.query(User).filter(User.id == context["owner"].id).one() user.wechat_mp_openid = "openid-flow" user.wechat_mp_session_key = "session-flow" plan = db.query(TokenPlan).filter(TokenPlan.id == "1").one() plan.virtual_product_id = "points_plan_1" db.commit() finally: db.close() env = { "WECHAT_VIRTUAL_ENV": "sandbox", "WECHAT_VIRTUAL_SANDBOX_APP_KEY": "sandbox-key", "WECHAT_VIRTUAL_OFFER_ID": "offer-1", "WECHAT_VIRTUAL_CALLBACK_TOKEN": "callback-token", "AVATAR_FINANCE_ADMIN_SECRET": "finance-admin-secret-123", } with patch.dict(os.environ, env): created = client.post( "/api/token/charge", headers=context["owner_headers"], json={"planId": "1", "paymentMethod": "wechat", "payScene": "LITE"}, ).json()["data"] assert created["provider"] == "wechat_virtual" params = json.loads(created["payMessage"]) assert params["mode"] == "short_series_goods" assert "session-flow" not in created["payMessage"] notify = { "Event": "xpay_goods_deliver_notify", "OutTradeNo": created["orderNo"], "OpenId": "openid-flow", "Env": 1, "GoodsInfo": json.dumps({"ProductId": "points_plan_1", "ActualPrice": 1000}), "WeChatPayInfo": json.dumps({"TransactionId": "wx-transaction-1"}), } query = {"timestamp": "100", "nonce": "nonce", "signature": _signature("callback-token", "100", "nonce")} assert client.post("/api/token/payment/wechat/virtual/notify", params=query, json=notify).json()["ErrCode"] == 0 assert client.post("/api/token/payment/wechat/virtual/notify", params=query, json=notify).json()["ErrCode"] == 0 invoice = client.post( f"/api/token/orders/{created['orderNo']}/invoice", headers=context["owner_headers"], json={"title": "测试用户", "invoiceType": "personal", "email": "test@example.com"}, ).json()["data"] assert invoice["status"] == "pending" with patch("routers.tokens.request_wechat_virtual_refund", return_value={"errcode": 0}): refund_response = client.post( f"/api/token/admin/orders/{created['orderNo']}/refund", headers={"X-Avatar-Finance-Key": "finance-admin-secret-123"}, json={"reason": "用户申请退款", "operator": "tester"}, ) assert refund_response.json()["data"]["status"] == "processing" refund_no = refund_response.json()["data"]["refundNo"] refund_notify = { "Event": "xpay_refund_notify", "MchOrderId": created["orderNo"], "MchRefundId": refund_no, "WxRefundId": "wx-refund-1", "RefundFee": 1000, "RetCode": 0, } assert client.post("/api/token/payment/wechat/virtual/notify", params=query, json=refund_notify).json()["ErrCode"] == 0 assert client.post("/api/token/payment/wechat/virtual/notify", params=query, json=refund_notify).json()["ErrCode"] == 0 assert client.post("/api/token/payment/wechat/virtual/notify", params=query, json=notify).json()["ErrCode"] == 0 db = SessionLocal() try: order = db.query(TokenPaymentOrder).filter(TokenPaymentOrder.order_no == created["orderNo"]).one() account = db.query(TokenAccount).filter(TokenAccount.user_id == context["owner"].id).one() refund = db.query(PaymentRefund).filter(PaymentRefund.refund_no == refund_no).one() invoice = db.query(InvoiceApplication).filter(InvoiceApplication.order_no == created["orderNo"]).one() assert order.status == "refunded" assert refund.status == "succeeded" assert invoice.status == "cancelled" assert account.balance == DEFAULT_TOKEN_GRANT finally: db.close()