feat(avatar): integrate Huihui payments
This commit is contained in:
@@ -1,14 +1,17 @@
|
||||
import uuid
|
||||
import os
|
||||
import hashlib
|
||||
import hmac
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from threading import Barrier
|
||||
from unittest.mock import patch
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from database import SessionLocal
|
||||
from main import app, seed
|
||||
from models import Avatar, TokenAccount, TokenPlan, TokenUsage, User
|
||||
from models import Avatar, TokenAccount, TokenPaymentOrder, TokenPlan, TokenUsage, User
|
||||
from routers.chat import _resolve_reply, _stream_reply
|
||||
from services.token_billing import (
|
||||
DEFAULT_TOKEN_GRANT,
|
||||
@@ -22,6 +25,16 @@ from services.token_billing import (
|
||||
client = TestClient(app)
|
||||
|
||||
|
||||
def _enable_huihui_payment_login(context):
|
||||
db = SessionLocal()
|
||||
try:
|
||||
user = db.query(User).filter(User.id == context["owner"].id).one()
|
||||
user.huihui_token = f"huihui-payment-{context['suffix']}"
|
||||
db.commit()
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
def test_balance_is_user_scoped_and_defaults_to_one_million(authorization_context):
|
||||
context = authorization_context
|
||||
owner = client.get("/api/token/balance", headers=context["owner_headers"])
|
||||
@@ -52,6 +65,171 @@ def test_seed_synchronizes_requested_recharge_plans():
|
||||
db.close()
|
||||
|
||||
|
||||
def test_charge_creates_huihui_order_without_early_points(authorization_context):
|
||||
context = authorization_context
|
||||
_enable_huihui_payment_login(context)
|
||||
payment_client = Mock()
|
||||
payment_client.create_payment.return_value = {
|
||||
"orderId": "huihui-payment-id",
|
||||
"orderNo": "huihui-payment-no",
|
||||
"payMessage": {"mock": "payment-params"},
|
||||
"payType": "WECHAT",
|
||||
"paySubType": "APP",
|
||||
"status": "pending",
|
||||
}
|
||||
env = {
|
||||
"HUIHUI_PAYMENT_CALLBACK_BASE_URL": "https://digital.example",
|
||||
"HUIHUI_PAYMENT_CALLBACK_SECRET": "test-callback-secret-123456",
|
||||
}
|
||||
with patch.dict(os.environ, env), patch("routers.tokens._payment_client", return_value=payment_client):
|
||||
response = client.post(
|
||||
"/api/token/charge",
|
||||
headers=context["owner_headers"],
|
||||
json={"planId": "1", "paymentMethod": "wechat", "payScene": "APP"},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
result = response.json()["data"]
|
||||
assert result["status"] == "pending"
|
||||
assert result["payType"] == "WECHAT"
|
||||
assert result["payWay"] == "APP"
|
||||
assert result["balance"] == DEFAULT_TOKEN_GRANT
|
||||
assert payment_client.create_payment.call_args.kwargs["amount"] == "10.00"
|
||||
callback_url = payment_client.create_payment.call_args.kwargs["callback_url"]
|
||||
assert callback_url.startswith("https://digital.example/api/token/payment/callback/AV")
|
||||
assert "test-callback-secret-123456" not in callback_url
|
||||
|
||||
|
||||
def test_success_callback_credits_once_and_status_is_user_scoped(authorization_context):
|
||||
context = authorization_context
|
||||
_enable_huihui_payment_login(context)
|
||||
payment_client = Mock()
|
||||
payment_client.create_payment.return_value = {
|
||||
"orderId": "huihui-payment-id",
|
||||
"orderNo": "huihui-payment-no",
|
||||
"payMessage": "payment-message",
|
||||
"status": "pending",
|
||||
}
|
||||
secret = "test-callback-secret-123456"
|
||||
env = {
|
||||
"HUIHUI_PAYMENT_CALLBACK_BASE_URL": "https://digital.example",
|
||||
"HUIHUI_PAYMENT_CALLBACK_SECRET": secret,
|
||||
}
|
||||
with patch.dict(os.environ, env), patch("routers.tokens._payment_client", return_value=payment_client):
|
||||
created = client.post(
|
||||
"/api/token/charge",
|
||||
headers=context["owner_headers"],
|
||||
json={"planId": "1", "paymentMethod": "alipay", "payScene": "APP"},
|
||||
).json()["data"]
|
||||
callback_body = {
|
||||
"data": {
|
||||
"masterOrderNo": created["orderNo"],
|
||||
"status": "succeeded",
|
||||
"payAmt": "10.00",
|
||||
}
|
||||
}
|
||||
signature = hmac.new(
|
||||
secret.encode(), created["orderNo"].encode(), hashlib.sha256
|
||||
).hexdigest()
|
||||
callback_path = f"/api/token/payment/callback/{created['orderNo']}/{signature}"
|
||||
first = client.post(callback_path, json=callback_body)
|
||||
second = client.post(callback_path, json=callback_body)
|
||||
|
||||
assert first.json()["data"] == {"received": True, "paid": True}
|
||||
assert second.json()["data"] == {"received": True, "duplicate": True}
|
||||
status = client.get(
|
||||
f"/api/token/payment/{created['id']}", headers=context["owner_headers"]
|
||||
).json()["data"]
|
||||
assert status["status"] == "paid"
|
||||
assert status["balance"] == DEFAULT_TOKEN_GRANT + 2_000_000
|
||||
assert client.get(
|
||||
f"/api/token/payment/{created['id']}", headers=context["other_headers"]
|
||||
).json()["code"] == 404
|
||||
|
||||
|
||||
def test_callback_amount_mismatch_never_credits_points(authorization_context):
|
||||
context = authorization_context
|
||||
_enable_huihui_payment_login(context)
|
||||
payment_client = Mock()
|
||||
payment_client.create_payment.return_value = {"status": "pending", "payMessage": "mock"}
|
||||
secret = "test-callback-secret-123456"
|
||||
env = {
|
||||
"HUIHUI_PAYMENT_CALLBACK_BASE_URL": "https://digital.example",
|
||||
"HUIHUI_PAYMENT_CALLBACK_SECRET": secret,
|
||||
}
|
||||
with patch.dict(os.environ, env), patch("routers.tokens._payment_client", return_value=payment_client):
|
||||
created = client.post(
|
||||
"/api/token/charge",
|
||||
headers=context["owner_headers"],
|
||||
json={"planId": "1", "paymentMethod": "wechat", "payScene": "APP"},
|
||||
).json()["data"]
|
||||
signature = hmac.new(
|
||||
secret.encode(), created["orderNo"].encode(), hashlib.sha256
|
||||
).hexdigest()
|
||||
callback = client.post(
|
||||
f"/api/token/payment/callback/{created['orderNo']}/{signature}",
|
||||
json={
|
||||
"masterOrderNo": created["orderNo"],
|
||||
"status": "success",
|
||||
"actualAmt": "9.99",
|
||||
},
|
||||
)
|
||||
|
||||
assert callback.json()["code"] == 422
|
||||
db = SessionLocal()
|
||||
try:
|
||||
order = db.query(TokenPaymentOrder).filter(TokenPaymentOrder.id == created["id"]).one()
|
||||
account = get_or_create_account(db, context["owner"].id)
|
||||
assert order.status == "pending"
|
||||
assert account.balance == DEFAULT_TOKEN_GRANT
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
def test_payment_callback_creates_missing_account_in_same_settlement(authorization_context):
|
||||
context = authorization_context
|
||||
_enable_huihui_payment_login(context)
|
||||
payment_client = Mock()
|
||||
payment_client.create_payment.return_value = {"status": "pending", "payMessage": "mock"}
|
||||
secret = "test-callback-secret-123456"
|
||||
env = {
|
||||
"HUIHUI_PAYMENT_CALLBACK_BASE_URL": "https://digital.example",
|
||||
"HUIHUI_PAYMENT_CALLBACK_SECRET": secret,
|
||||
}
|
||||
with patch.dict(os.environ, env), patch("routers.tokens._payment_client", return_value=payment_client):
|
||||
created = client.post(
|
||||
"/api/token/charge",
|
||||
headers=context["owner_headers"],
|
||||
json={"planId": "1", "paymentMethod": "alipay", "payScene": "APP"},
|
||||
).json()["data"]
|
||||
db = SessionLocal()
|
||||
try:
|
||||
db.query(TokenAccount).filter(TokenAccount.user_id == context["owner"].id).delete()
|
||||
db.commit()
|
||||
finally:
|
||||
db.close()
|
||||
signature = hmac.new(
|
||||
secret.encode(), created["orderNo"].encode(), hashlib.sha256
|
||||
).hexdigest()
|
||||
callback = client.post(
|
||||
f"/api/token/payment/callback/{created['orderNo']}/{signature}",
|
||||
json={
|
||||
"masterOrderNo": created["orderNo"],
|
||||
"status": "success",
|
||||
"payAmt": "10.00",
|
||||
},
|
||||
)
|
||||
|
||||
assert callback.json()["data"] == {"received": True, "paid": True}
|
||||
db = SessionLocal()
|
||||
try:
|
||||
account = db.query(TokenAccount).filter(TokenAccount.user_id == context["owner"].id).one()
|
||||
assert account.balance == DEFAULT_TOKEN_GRANT + 2_000_000
|
||||
assert account.total_granted == DEFAULT_TOKEN_GRANT + 2_000_000
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
def test_multiple_avatars_share_owner_balance_and_usage_is_itemized():
|
||||
suffix = uuid.uuid4().hex
|
||||
db = SessionLocal()
|
||||
|
||||
Reference in New Issue
Block a user