105 lines
4.5 KiB
Python
105 lines
4.5 KiB
Python
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()
|