72 lines
2.3 KiB
TypeScript
72 lines
2.3 KiB
TypeScript
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();
|
|
|