mirror of
https://github.com/aljazceru/payments-rest-api.git
synced 2025-12-20 15:04:23 +01:00
webhook fixes
This commit is contained in:
148
fly/main.py
148
fly/main.py
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user