webhook fixes

This commit is contained in:
2025-05-27 17:45:06 +02:00
parent 99dd16bab3
commit df285efb00

View File

@@ -7,7 +7,6 @@ import os
from dotenv import load_dotenv from dotenv import load_dotenv
from enum import Enum from enum import Enum
from nodeless import PaymentHandler from nodeless import PaymentHandler
from shopify.router import router as shopify_router
import logging import logging
import threading import threading
import asyncio import asyncio
@@ -32,30 +31,78 @@ _consecutive_sync_failures = 0
# Webhook configuration # Webhook configuration
WEBHOOK_CONFIG = { WEBHOOK_CONFIG = {
'url': os.getenv('WEBHOOK_URL'), # WooCommerce site URL 'url': os.getenv('WEBHOOK_URL'), # WooCommerce site URL - if set, webhooks will be sent for all payments
'secret': os.getenv('WEBHOOK_SECRET'), # Should match WooCommerce webhook secret
} }
API_KEY = os.getenv("API_SECRET")
# Track payments that have already had successful webhook notifications sent
# Format: {invoice_id: {status: webhook_sent_timestamp}}
_webhook_sent_cache = {}
_webhook_cache_lock = threading.Lock()
def has_webhook_been_sent(invoice_id: str, status: str) -> bool:
"""
Check if a webhook has already been successfully sent for this payment and status.
Args:
invoice_id: The payment invoice ID
status: The payment status
Returns:
True if webhook was already sent, False otherwise
"""
with _webhook_cache_lock:
if invoice_id in _webhook_sent_cache:
return _webhook_sent_cache[invoice_id].get(status) is not None
return False
def mark_webhook_sent(invoice_id: str, status: str):
"""
Mark that a webhook has been successfully sent for this payment and status.
Args:
invoice_id: The payment invoice ID
status: The payment status
"""
with _webhook_cache_lock:
if invoice_id not in _webhook_sent_cache:
_webhook_sent_cache[invoice_id] = {}
_webhook_sent_cache[invoice_id][status] = time.time()
# Keep cache size reasonable - remove entries older than 24 hours
current_time = time.time()
for payment_id, statuses in list(_webhook_sent_cache.items()):
for status_key, timestamp in list(statuses.items()):
if current_time - timestamp > 86400: # 24 hours
del statuses[status_key]
if not statuses: # Remove payment entry if no statuses left
del _webhook_sent_cache[payment_id]
async def send_webhook_notification(invoice_id: str, status: str, payment_details: dict): async def send_webhook_notification(invoice_id: str, status: str, payment_details: dict):
""" """
Send webhook notification to WooCommerce about payment status changes. Send webhook notification to WooCommerce about payment status changes.
Only sends notifications for payments that were created through WooCommerce. Sends notifications for all payments if WEBHOOK_URL is configured.
Only sends once per payment/status combination.
Args: Args:
invoice_id: The payment invoice/destination ID invoice_id: The payment invoice/destination ID
status: The new payment status status: The new payment status
payment_details: Additional payment details (amount, fees, etac) payment_details: Additional payment details (amount, fees, etc)
""" """
if not WEBHOOK_CONFIG['url'] or not WEBHOOK_CONFIG['secret']: if not WEBHOOK_CONFIG['url']:
logger.warning("Webhook configuration missing - notifications disabled") logger.debug("Webhook URL not configured - notifications disabled")
return
if not API_KEY:
logger.warning("API_SECRET not configured - webhook authentication disabled")
return
# Check if webhook was already sent for this payment and status
if has_webhook_been_sent(invoice_id, status):
logger.debug(f"Webhook already sent for {invoice_id[:30]}... status {status}, skipping")
return return
try: try:
# Check if this payment was created through WooCommerce by checking payment details
if not payment_details.get('source') == 'woocommerce':
logger.debug(f"Skipping webhook for non-WooCommerce payment {invoice_id}")
return
webhook_url = f"{WEBHOOK_CONFIG['url'].rstrip('/')}/wp-json/breez-wc/v1/webhook" webhook_url = f"{WEBHOOK_CONFIG['url'].rstrip('/')}/wp-json/breez-wc/v1/webhook"
# Prepare webhook payload with only required fields # Prepare webhook payload with only required fields
@@ -74,9 +121,9 @@ async def send_webhook_notification(invoice_id: str, status: str, payment_detail
# Create signature payload exactly as WooCommerce expects # Create signature payload exactly as WooCommerce expects
signature_payload = f"{timestamp}{nonce}{payload_string}" signature_payload = f"{timestamp}{nonce}{payload_string}"
# Calculate HMAC signature using webhook secret # Calculate HMAC signature using API secret
signature = hmac.new( signature = hmac.new(
WEBHOOK_CONFIG['secret'].encode('utf-8'), API_KEY.encode('utf-8'),
signature_payload.encode('utf-8'), signature_payload.encode('utf-8'),
hashlib.sha256 hashlib.sha256
).hexdigest() ).hexdigest()
@@ -89,7 +136,7 @@ async def send_webhook_notification(invoice_id: str, status: str, payment_detail
'X-Breez-Nonce': nonce 'X-Breez-Nonce': nonce
} }
logger.info(f"Sending webhook notification for invoice {invoice_id}: {status}") logger.info(f"Sending webhook notification for invoice {invoice_id[:30]}...: {status}")
logger.debug(f"Webhook payload: {payload_string}") logger.debug(f"Webhook payload: {payload_string}")
logger.debug(f"Signature components - Timestamp: {timestamp}, Nonce: {nonce}") logger.debug(f"Signature components - Timestamp: {timestamp}, Nonce: {nonce}")
logger.debug(f"Signature payload: {signature_payload}") logger.debug(f"Signature payload: {signature_payload}")
@@ -104,14 +151,18 @@ async def send_webhook_notification(invoice_id: str, status: str, payment_detail
) )
if response.status_code == 200: if response.status_code == 200:
logger.info(f"Webhook notification sent successfully for invoice {invoice_id}") logger.info(f"Webhook notification sent successfully for invoice {invoice_id[:30]}...")
logger.debug(f"Webhook response: {response.text}") logger.debug(f"Webhook response: {response.text}")
# Mark webhook as sent only on successful delivery
mark_webhook_sent(invoice_id, status)
else: else:
logger.error(f"Webhook notification failed for invoice {invoice_id}: {response.status_code}") logger.error(f"Webhook notification failed for invoice {invoice_id[:30]}...: {response.status_code}")
logger.error(f"Response: {response.text}") logger.error(f"Response: {response.text}")
except Exception as e: except Exception as e:
logger.error(f"Error sending webhook notification: {str(e)}") logger.error(f"Error sending webhook notification: {str(e)}")
logger.exception("Full webhook error details:")
async def periodic_sync_check(): async def periodic_sync_check():
"""Background task to periodically check SDK sync status and attempt resync if needed.""" """Background task to periodically check SDK sync status and attempt resync if needed."""
@@ -145,6 +196,8 @@ async def periodic_sync_check():
# After successful sync, check all pending payments # After successful sync, check all pending payments
try: try:
pending_payments = _payment_handler.list_payments({"status": "PENDING"}) pending_payments = _payment_handler.list_payments({"status": "PENDING"})
logger.info(f"Checking {len(pending_payments)} pending payments for status updates")
for payment in pending_payments: for payment in pending_payments:
payment_id = payment.get('destination') payment_id = payment.get('destination')
if not payment_id: if not payment_id:
@@ -154,8 +207,11 @@ async def periodic_sync_check():
current_status = _payment_handler.check_payment_status(payment_id) current_status = _payment_handler.check_payment_status(payment_id)
status = current_status.get('status') status = current_status.get('status')
logger.debug(f"Payment {payment_id[:30]}... status: {status}")
# Send webhook for completed or failed payments # Send webhook for completed or failed payments
if status in ['SUCCEEDED', 'FAILED']: if status in ['SUCCEEDED', 'FAILED']:
logger.info(f"Found completed payment {payment_id[:30]}... with status {status}, sending webhook")
await send_webhook_notification( await send_webhook_notification(
invoice_id=payment_id, invoice_id=payment_id,
status=status, status=status,
@@ -252,11 +308,9 @@ app = FastAPI(
API_KEY_NAME = "x-api-key" API_KEY_NAME = "x-api-key"
api_key_header = APIKeyHeader(name=API_KEY_NAME, auto_error=False) api_key_header = APIKeyHeader(name=API_KEY_NAME, auto_error=False)
API_KEY = os.getenv("API_SECRET")
# Load environment variables # Load environment variables
ln_router = APIRouter(prefix="/v1/lnurl", tags=["lnurl"]) ln_router = APIRouter(prefix="/v1/lnurl", tags=["lnurl"])
app.include_router(shopify_router)
# --- Models --- # --- Models ---
class PaymentMethodEnum(str, Enum): class PaymentMethodEnum(str, Enum):
@@ -295,6 +349,8 @@ class PaymentResponse(BaseModel):
tx_id: Optional[str] = None tx_id: Optional[str] = None
payment_hash: Optional[str] = None payment_hash: Optional[str] = None
swap_id: Optional[str] = None swap_id: Optional[str] = None
metadata: Optional[Dict[str, Any]] = None
source: Optional[str] = None
class PaymentListResponse(BaseModel): class PaymentListResponse(BaseModel):
payments: List[PaymentResponse] payments: List[PaymentResponse]
@@ -302,6 +358,8 @@ class PaymentListResponse(BaseModel):
class ReceiveResponse(BaseModel): class ReceiveResponse(BaseModel):
destination: str destination: str
fees_sat: int fees_sat: int
metadata: Optional[Dict[str, Any]] = None
source: Optional[str] = None
class SendResponse(BaseModel): class SendResponse(BaseModel):
status: str status: str
@@ -391,14 +449,26 @@ async def receive_payment(
handler: PaymentHandler = Depends(get_payment_handler) handler: PaymentHandler = Depends(get_payment_handler)
): ):
try: try:
# Call SDK method with original parameters
result = handler.receive_payment( result = handler.receive_payment(
amount=request.amount, amount=request.amount,
payment_method=request.method.value, payment_method=request.method.value,
description=request.description, description=request.description,
asset_id=request.asset_id, asset_id=request.asset_id
source=request.source
) )
return result
# Add metadata if source is provided
metadata = {}
if request.source:
metadata['source'] = request.source
# Return response with metadata and source
return {
"destination": result["destination"],
"fees_sat": result["fees_sat"],
"metadata": metadata if metadata else None,
"source": request.source
}
except Exception as e: except Exception as e:
raise HTTPException(status_code=500, detail=str(e)) raise HTTPException(status_code=500, detail=str(e))
@@ -459,6 +529,40 @@ async def health():
return {"status": "ok", "sdk_synced": True} return {"status": "ok", "sdk_synced": True}
return {"status": "ok", "sdk_synced": False} return {"status": "ok", "sdk_synced": False}
@app.get("/webhook_status")
async def webhook_status(
api_key: str = Depends(get_api_key)
):
"""
Get webhook configuration and cache status for debugging.
Returns:
Webhook configuration status and recent webhook cache entries
"""
global _webhook_sent_cache
with _webhook_cache_lock:
# Only show recent entries (last hour) for privacy
current_time = time.time()
recent_cache = {}
for payment_id, statuses in _webhook_sent_cache.items():
recent_statuses = {}
for status, timestamp in statuses.items():
if current_time - timestamp < 3600: # Last hour
recent_statuses[status] = {
"timestamp": timestamp,
"age_seconds": int(current_time - timestamp)
}
if recent_statuses:
recent_cache[payment_id[:30] + "..."] = recent_statuses
return {
"webhook_url_configured": bool(WEBHOOK_CONFIG['url']),
"api_secret_configured": bool(API_KEY),
"webhook_cache_size": len(_webhook_sent_cache),
"recent_webhooks_sent": recent_cache
}
@app.get("/check_payment_status/{destination}", response_model=PaymentStatusResponse) @app.get("/check_payment_status/{destination}", response_model=PaymentStatusResponse)
async def check_payment_status( async def check_payment_status(
destination: str, destination: str,