Files
reseller-platform/backend/app/routers/webhooks.py

78 lines
3.1 KiB
Python

"""BTCPay webhook router — advances orders through the money flow.
POST /api/webhooks/btcpay (signed by BTCPay)
InvoiceSettled -> order paid -> place supplier order -> status supplier_order
InvoiceExpired -> order cancelled (no payment)
InvoiceInvalid -> order cancelled (payment failed)
"""
from fastapi import APIRouter, Depends, Header, HTTPException, Request
from sqlalchemy.orm import Session
from app.database import get_db
from app.engines.payments import btcpay
from app.engines.suppliers.base import build_adapter
from app.models import AuditLog, Order, Supplier
router = APIRouter()
def _get_order_by_invoice(db: Session, invoice_id: str):
return db.query(Order).filter(Order.btcpay_invoice_id == invoice_id).first()
def _place_supplier_order(db: Session, order: Order):
"""Call the supplier's real create_order and store the reference."""
if order.supplier_id is None:
order.status = "supplier_order"
db.add(AuditLog(actor="webhook", action="no_supplier", entity="order",
entity_id=order.order_number))
return
supplier = db.query(Supplier).filter(Supplier.id == order.supplier_id).first()
if supplier is None:
order.status = "supplier_order"
return
try:
adapter = build_adapter(supplier)
items = [
{"supplier_sku": it.get("sku"), "qty": it.get("qty", 1)}
for it in (order.items or [])
]
result = adapter.create_order(items)
order.supplier_order_ref = str(result.get("ref", ""))
order.status = "supplier_order"
db.add(AuditLog(actor="webhook", action="supplier_order_placed", entity="order",
entity_id=order.order_number,
detail={"supplier": supplier.name, "ref": order.supplier_order_ref}))
except Exception as e:
order.status = "paid" # paid but supplier order failed -> needs manual retry
db.add(AuditLog(actor="webhook", action="supplier_order_failed", entity="order",
entity_id=order.order_number, detail={"error": str(e)}))
@router.post("/btcpay")
async def btcpay_webhook(request: Request, db: Session = Depends(get_db)):
raw = await request.body()
sig = request.headers.get("btcpay-sig", "")
if not btcpay.verify_webhook(raw, sig):
raise HTTPException(status_code=401, detail="invalid signature")
payload = await request.json()
event_type = payload.get("type")
invoice_id = payload.get("invoiceId") or payload.get("id")
order = _get_order_by_invoice(db, invoice_id) if invoice_id else None
if order is None:
return {"ok": True, "matched": False}
if event_type == "InvoiceSettled":
order.status = "paid"
_place_supplier_order(db, order)
elif event_type in ("InvoiceExpired", "InvoiceInvalid"):
order.status = "cancelled"
db.commit()
db.add(AuditLog(actor="btcpay", action=event_type, entity="order",
entity_id=order.order_number, detail={"invoice_id": invoice_id}))
db.commit()
return {"ok": True, "order": order.order_number, "status": order.status}