"""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}