feat: add avatar payments and finance management
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
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()
|
||||
Reference in New Issue
Block a user