Verify Huihui organization payment callbacks against paid orders
This commit is contained in:
@@ -810,12 +810,10 @@ 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")
|
||||
async def _signed_huihui_notice(request: Request, *, secret_name: str, minimum_length: int) -> dict:
|
||||
secret = os.getenv(secret_name, "").strip()
|
||||
if len(secret) < minimum_length:
|
||||
raise HTTPException(status_code=503, detail="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:
|
||||
@@ -840,6 +838,97 @@ async def huihui_virtual_payment_notify(request: Request, db: Session = Depends(
|
||||
raise HTTPException(status_code=400, detail="invalid callback JSON")
|
||||
if not isinstance(payload, dict):
|
||||
raise HTTPException(status_code=400, detail="invalid callback JSON")
|
||||
return payload
|
||||
|
||||
|
||||
@router.post("/token/payment/huihui/org/notify")
|
||||
async def huihui_org_payment_notify(request: Request, db: Session = Depends(get_db)):
|
||||
"""Huihui organization recharge notice; independently verify payment before crediting."""
|
||||
payload = await _signed_huihui_notice(
|
||||
request, secret_name="HUIHUI_ORG_CALLBACK_SECRET", minimum_length=16,
|
||||
)
|
||||
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.provider == "huihui_org",
|
||||
TokenPaymentOrder.order_no == order_no,
|
||||
).first()
|
||||
if order is None and order_no.isdigit():
|
||||
matches = db.query(TokenPaymentOrder).filter(
|
||||
TokenPaymentOrder.provider == "huihui_org",
|
||||
TokenPaymentOrder.provider_order_no == order_no,
|
||||
).limit(2).all()
|
||||
if len(matches) == 1:
|
||||
order = matches[0]
|
||||
if not order:
|
||||
raise HTTPException(status_code=404, detail="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"))
|
||||
except (TypeError, ValueError, WechatVirtualPaymentError):
|
||||
raise HTTPException(status_code=422, detail="invalid payment details")
|
||||
if (
|
||||
isinstance(payload.get("amountCents"), bool)
|
||||
or not user
|
||||
or not expected_product_id
|
||||
or str(payload.get("productId") or "") != expected_product_id
|
||||
or amount_cents != order.price_cents
|
||||
):
|
||||
raise HTTPException(status_code=422, detail="payment details mismatch")
|
||||
if order.status in {"paid", "refunded"}:
|
||||
return ok({"received": True, "duplicate": True})
|
||||
if not order.provider_order_no or not user.huihui_token or not user.huihui_user_id:
|
||||
raise HTTPException(status_code=503, detail="provider order verification unavailable")
|
||||
try:
|
||||
provider_order = HuihuiOrgPaymentClient().get_order(
|
||||
token=user.huihui_token,
|
||||
user_id=user.huihui_user_id,
|
||||
order_no=order.provider_order_no,
|
||||
)
|
||||
except HuihuiPaymentError as exc:
|
||||
raise HTTPException(status_code=503, detail="provider order verification unavailable") from exc
|
||||
try:
|
||||
provider_status = int(provider_order.get("payStatus"))
|
||||
provider_type = int(provider_order.get("chargeType"))
|
||||
except (TypeError, ValueError):
|
||||
raise HTTPException(status_code=422, detail="invalid provider order details")
|
||||
if (
|
||||
str(provider_order.get("orderNo") or "") != order.provider_order_no
|
||||
or str(provider_order.get("paymentNo") or "") != payment_no
|
||||
or str(provider_order.get("productId") or "") != expected_product_id
|
||||
or str(provider_order.get("userId") or "") != user.huihui_user_id
|
||||
or provider_type != 9
|
||||
or _callback_amount_cents({"chargeAmount": provider_order.get("chargeAmount")}) != order.price_cents
|
||||
):
|
||||
raise HTTPException(status_code=422, detail="provider order details mismatch")
|
||||
if provider_status not in {2, 3, 4}:
|
||||
raise HTTPException(status_code=409, detail="provider payment not confirmed")
|
||||
existing_payment = db.query(PaymentTransaction).filter(
|
||||
PaymentTransaction.provider == "huihui_org",
|
||||
PaymentTransaction.event_type == "payment",
|
||||
PaymentTransaction.transaction_no == payment_no,
|
||||
).first()
|
||||
if existing_payment and existing_payment.order_no != order.order_no:
|
||||
raise HTTPException(status_code=409, detail="payment number belongs to another order")
|
||||
_settle_paid_order(db, order, provider_status="HUIHUI_SUCCESS", transaction_no=payment_no)
|
||||
db.commit()
|
||||
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."""
|
||||
payload = await _signed_huihui_notice(
|
||||
request, secret_name="HUIHUI_VIRTUAL_CALLBACK_SECRET", minimum_length=32,
|
||||
)
|
||||
|
||||
order_no = str(payload.get("orderNo") or "").strip()
|
||||
payment_no = str(payload.get("paymentNo") or "").strip()
|
||||
@@ -877,7 +966,7 @@ async def huihui_virtual_payment_notify(request: Request, db: Session = Depends(
|
||||
PaymentTransaction.event_type == "payment",
|
||||
PaymentTransaction.transaction_no == payment_no,
|
||||
).first()
|
||||
if existing_payment and existing_payment.order_no != order_no:
|
||||
if existing_payment and existing_payment.order_no != order.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})
|
||||
|
||||
@@ -78,6 +78,74 @@ def test_huihui_virtual_callback_requires_signature_and_exact_order_details(auth
|
||||
db.close()
|
||||
|
||||
|
||||
def test_huihui_org_callback_verifies_provider_before_granting_points(authorization_context):
|
||||
seed()
|
||||
context = authorization_context
|
||||
provider_no = "26102010414409000001"
|
||||
payment_no = f"WV-{context['suffix']}"
|
||||
order_no = f"AV-{context['suffix']}"
|
||||
db = SessionLocal()
|
||||
try:
|
||||
user = db.query(User).filter(User.id == context["owner"].id).one()
|
||||
user.huihui_token = "test-huihui-token"
|
||||
db.add(TokenPaymentOrder(
|
||||
order_no=order_no, provider_order_no=provider_no,
|
||||
user_id=user.id, plan_id="Basic", payment_method="wechat",
|
||||
pay_type="WECHAT", pay_way="LITE", provider="huihui_org",
|
||||
points_amount=2_000_000, price_cents=100, status="pending",
|
||||
))
|
||||
db.commit()
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
secret = "abcdefghijklmnop"
|
||||
path = "/api/token/payment/huihui/org/notify"
|
||||
payload = {
|
||||
"orderNo": provider_no, "paymentNo": payment_no,
|
||||
"status": "SUCCESS", "amountCents": 100,
|
||||
"productId": "Basic",
|
||||
}
|
||||
provider_data = {
|
||||
"orderNo": provider_no, "paymentNo": payment_no,
|
||||
"payStatus": 1, "chargeType": 9, "chargeAmount": 1,
|
||||
"productId": "Basic", "userId": context["owner"].huihui_user_id,
|
||||
}
|
||||
|
||||
def send(body_payload, *, signature=True):
|
||||
body = json.dumps(body_payload, separators=(",", ":")).encode()
|
||||
headers = _huihui_virtual_headers(secret, body) if signature else {"X-Huihui-Signature": "bad"}
|
||||
return client.post(path, content=body, headers=headers)
|
||||
|
||||
with patch.dict(os.environ, {"HUIHUI_ORG_CALLBACK_SECRET": secret}), patch(
|
||||
"routers.tokens.HuihuiOrgPaymentClient"
|
||||
) as client_factory:
|
||||
provider = client_factory.return_value
|
||||
provider.get_order.return_value = provider_data
|
||||
assert send(payload, signature=False).status_code == 401
|
||||
assert send({**payload, "amountCents": 99}).status_code == 422
|
||||
assert send(payload).status_code == 409
|
||||
provider.get_order.return_value = {**provider_data, "payStatus": 2}
|
||||
assert send({**payload, "paymentNo": "wrong"}).status_code == 422
|
||||
provider.get_order.return_value = {**provider_data, "payStatus": 2, "chargeAmount": 2}
|
||||
assert send(payload).status_code == 422
|
||||
provider.get_order.return_value = {**provider_data, "payStatus": 2}
|
||||
first = send(payload)
|
||||
second = send(payload)
|
||||
|
||||
assert first.status_code == 200
|
||||
assert first.json()["data"] == {"received": True, "paid": True}
|
||||
assert second.json()["data"] == {"received": True, "duplicate": True}
|
||||
db = SessionLocal()
|
||||
try:
|
||||
order = db.query(TokenPaymentOrder).filter(TokenPaymentOrder.order_no == order_no).one()
|
||||
account = db.query(TokenAccount).filter(TokenAccount.user_id == context["owner"].id).one()
|
||||
assert order.status == "paid"
|
||||
assert account.balance == DEFAULT_TOKEN_GRANT + 2_000_000
|
||||
assert provider.get_order.call_count >= 3
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
def _signature(token, timestamp, nonce):
|
||||
return hashlib.sha1("".join(sorted([token, timestamp, nonce])).encode()).hexdigest()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user