From 7bfb786e1545c9b58b5558cf198f701ee9c51ff1 Mon Sep 17 00:00:00 2001 From: KavyaCSH Date: Wed, 19 Aug 2026 15:14:22 +0530 Subject: [PATCH] feat: implement asynchronous webhook service with retries and add environment configuration --- .env.example | 23 ++++++++++ src/modules/webhooks/service.ts | 74 ++++++++++++++++++++++----------- 2 files changed, 72 insertions(+), 25 deletions(-) create mode 100644 .env.example diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..015213e --- /dev/null +++ b/.env.example @@ -0,0 +1,23 @@ +# Server +PORT=8080 + +# Database +DATABASE_URL=postgres://postgres:postgres@localhost:5432/ai_pharmacy + +# JWT Authentication +JWT_SECRET=your_jwt_secret_here + +# OpenAI +OPENAI_API_KEY=your_openai_api_key_here + +# AWS S3 +S3_KEY=your_s3_access_key_here +S3_SECRET=your_s3_secret_key_here +S3_BUCKET=your_bucket_name +S3_REGION=us-east-1 +S3_BASE_PATH= +S3_PUBLIC_BASE_PATH= + +# Webhooks +WEBHOOK_SECRET=your_webhook_secret_here +WEBHOOK_URL=http://localhost:8081/webhooks/pharmacy diff --git a/src/modules/webhooks/service.ts b/src/modules/webhooks/service.ts index 870674a..55daa5b 100644 --- a/src/modules/webhooks/service.ts +++ b/src/modules/webhooks/service.ts @@ -1,15 +1,29 @@ import crypto from 'crypto'; const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET || 'test_secret_for_webhooks'; -// For demo purposes, we log it or hit a dummy URL. -const WEBHOOK_URL = process.env.WEBHOOK_URL || 'http://localhost:8081/webhooks/pharmacy'; +const WEBHOOK_URL = process.env.WEBHOOK_URL || 'http://localhost:8081/webhooks/pharmacy'; +const MAX_RETRIES = 3; export class WebhookService { - async dispatchEvent(event: string, idempotencyKey: string, payload: any) { + /** + * Fire-and-forget: dispatches the webhook in the background. + * Dispensing will NEVER fail because of a webhook issue. + */ + dispatchEvent(event: string, idempotencyKey: string, payload: any) { + // Run in background — no await, no blocking + this._sendWithRetry(event, idempotencyKey, payload).catch((err) => { + console.error(`[WEBHOOK] All retries failed for ${event}: ${err.message}`); + }); + } + + /** + * Internal: send webhook with exponential backoff retry. + */ + private async _sendWithRetry(event: string, idempotencyKey: string, payload: any) { const timestamp = Math.floor(Date.now() / 1000); const bodyString = JSON.stringify(payload); const dataToSign = `${timestamp}.${bodyString}`; - + const signature = crypto .createHmac('sha256', WEBHOOK_SECRET) .update(dataToSign) @@ -17,31 +31,41 @@ export class WebhookService { const signatureHeader = `t=${timestamp},v1=${signature}`; - console.log(`[WEBHOOK] Dispatching event: ${event} (Idempotency: ${idempotencyKey})`); - - try { - // Assuming Node 20+, native fetch is available - const response = await fetch(WEBHOOK_URL, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'X-Pharmacy-Event': event, - 'X-Pharmacy-Signature': signatureHeader, - 'Idempotency-Key': idempotencyKey, - }, - body: bodyString, - }); + for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) { + try { + console.log(`[WEBHOOK] Attempt ${attempt}/${MAX_RETRIES} — ${event} (${idempotencyKey})`); - if (!response.ok) { - console.error(`[WEBHOOK] Dispatch failed with status: ${response.status}`); - } else { - console.log(`[WEBHOOK] Successfully dispatched event: ${event}`); + const response = await fetch(WEBHOOK_URL, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-Pharmacy-Event': event, + 'X-Pharmacy-Signature': signatureHeader, + 'Idempotency-Key': idempotencyKey, + }, + body: bodyString, + }); + + if (response.ok) { + console.log(`[WEBHOOK] ✓ Delivered: ${event}`); + return; // Success — stop retrying + } + + console.warn(`[WEBHOOK] Server responded ${response.status} for ${event}`); + } catch (error: any) { + console.error(`[WEBHOOK] Attempt ${attempt} failed: ${error.message}`); + } + + // Exponential backoff: 1s, 2s, 4s + if (attempt < MAX_RETRIES) { + const delay = Math.pow(2, attempt - 1) * 1000; + await new Promise((resolve) => setTimeout(resolve, delay)); } - } catch (error: any) { - console.error(`[WEBHOOK] Failed to dispatch webhook: ${error.message}`); - // In a real system, this would queue for retry with exponential backoff. } + + console.error(`[WEBHOOK] ✗ Failed after ${MAX_RETRIES} attempts: ${event}`); } } export const webhookService = new WebhookService(); +