import crypto from 'crypto'; const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET || 'test_secret_for_webhooks'; const WEBHOOK_URL = process.env.WEBHOOK_URL || 'http://localhost:8081/webhooks/pharmacy'; const MAX_RETRIES = 3; export class WebhookService { /** * 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) .digest('hex'); const signatureHeader = `t=${timestamp},v1=${signature}`; for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) { try { console.log(`[WEBHOOK] Attempt ${attempt}/${MAX_RETRIES} — ${event} (${idempotencyKey})`); 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)); } } console.error(`[WEBHOOK] ✗ Failed after ${MAX_RETRIES} attempts: ${event}`); } } export const webhookService = new WebhookService();