feat(avatar): add signed Huihui virtual payment callback
This commit is contained in:
@@ -2,6 +2,7 @@ import hashlib
|
||||
import hmac
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from decimal import Decimal, InvalidOperation, ROUND_HALF_UP
|
||||
@@ -753,6 +754,83 @@ async def payment_callback(
|
||||
return ok({"received": True, "paid": True})
|
||||
|
||||
|
||||
@router.post("/token/payment/huihui/virtual/notify")
|
||||
async def huihui_virtual_payment_notify(request: Request, db: Session = Depends(get_db)):
|
||||
"""Server-to-server virtual-payment notice using the Huihui HMAC contract."""
|
||||
secret = os.getenv("HUIHUI_VIRTUAL_CALLBACK_SECRET", "").strip()
|
||||
if len(secret) < 32:
|
||||
raise HTTPException(status_code=503, detail="virtual payment callback is not configured")
|
||||
timestamp = request.headers.get("x-huihui-timestamp", "").strip()
|
||||
signature = request.headers.get("x-huihui-signature", "").strip().lower()
|
||||
if not timestamp.isascii() or not timestamp.isdecimal() or len(timestamp) > 12:
|
||||
raise HTTPException(status_code=401, detail="invalid callback signature")
|
||||
try:
|
||||
issued_at = int(timestamp)
|
||||
except ValueError:
|
||||
raise HTTPException(status_code=401, detail="invalid callback signature")
|
||||
if abs(int(time.time()) - issued_at) > 300:
|
||||
raise HTTPException(status_code=401, detail="callback timestamp expired")
|
||||
body = await request.body()
|
||||
if not body or len(body) > 65536:
|
||||
raise HTTPException(status_code=400, detail="invalid callback body")
|
||||
expected = hmac.new(
|
||||
secret.encode("utf-8"), timestamp.encode("ascii") + b"\n" + body, hashlib.sha256
|
||||
).hexdigest()
|
||||
if not hmac.compare_digest(signature, expected):
|
||||
raise HTTPException(status_code=401, detail="invalid callback signature")
|
||||
try:
|
||||
payload = json.loads(body)
|
||||
except (UnicodeDecodeError, ValueError):
|
||||
raise HTTPException(status_code=400, detail="invalid callback JSON")
|
||||
if not isinstance(payload, dict):
|
||||
raise HTTPException(status_code=400, detail="invalid callback JSON")
|
||||
|
||||
order_no = str(payload.get("orderNo") or "").strip()
|
||||
payment_no = str(payload.get("paymentNo") or "").strip()
|
||||
status = str(payload.get("status") or "").upper()
|
||||
if not order_no or not payment_no or status not in {"SUCCESS", "FAILED"}:
|
||||
raise HTTPException(status_code=400, detail="missing callback fields")
|
||||
order = db.query(TokenPaymentOrder).filter(TokenPaymentOrder.order_no == order_no).first()
|
||||
if not order or order.provider != "wechat_virtual":
|
||||
raise HTTPException(status_code=404, detail="virtual payment order not found")
|
||||
if status == "FAILED":
|
||||
return ok({"received": True, "paid": False})
|
||||
|
||||
user = db.query(User).filter(User.id == order.user_id).first()
|
||||
plan = db.query(TokenPlan).filter(TokenPlan.id == order.plan_id).first()
|
||||
try:
|
||||
expected_product_id = product_id_for_plan(plan) if plan else ""
|
||||
amount_cents = int(payload.get("amountCents"))
|
||||
callback_env = int(payload.get("env"))
|
||||
except (TypeError, ValueError, WechatVirtualPaymentError):
|
||||
raise HTTPException(status_code=422, detail="invalid virtual payment details")
|
||||
if (
|
||||
isinstance(payload.get("amountCents"), bool)
|
||||
or isinstance(payload.get("env"), bool)
|
||||
or not user
|
||||
or not user.wechat_mp_openid
|
||||
or str(payload.get("openId") or "") != user.wechat_mp_openid
|
||||
or not expected_product_id
|
||||
or str(payload.get("productId") or "") != expected_product_id
|
||||
or amount_cents != order.price_cents
|
||||
or callback_env != wechat_virtual_env()
|
||||
):
|
||||
raise HTTPException(status_code=422, detail="virtual payment details mismatch")
|
||||
existing_payment = db.query(PaymentTransaction).filter(
|
||||
PaymentTransaction.provider == "wechat_virtual",
|
||||
PaymentTransaction.event_type == "payment",
|
||||
PaymentTransaction.transaction_no == payment_no,
|
||||
).first()
|
||||
if existing_payment and existing_payment.order_no != order_no:
|
||||
raise HTTPException(status_code=409, detail="payment number belongs to another order")
|
||||
if order.status in {"paid", "refunded"}:
|
||||
return ok({"received": True, "duplicate": True})
|
||||
|
||||
_settle_paid_order(db, order, provider_status="HUIHUI_SUCCESS", transaction_no=payment_no)
|
||||
db.commit()
|
||||
return ok({"received": True, "paid": True})
|
||||
|
||||
|
||||
def _wechat_notify_response(request: Request, *, success: bool, message: str = ""):
|
||||
code = 0 if success else 1
|
||||
text = "success" if success else (message or "fail")[:200].replace("]]>", "")
|
||||
|
||||
Reference in New Issue
Block a user