From 7c2b585c166cf457cd6f5fd5917d1999fce74fd7 Mon Sep 17 00:00:00 2001 From: Michael Freno Date: Fri, 15 May 2026 20:27:12 -0400 Subject: [PATCH] FRE-5401: Migrate webhook idempotency to distributed Redis store Replace in-memory Map with Redis-based idempotency using setIfNotExists (NX) for distributed multi-instance deployments. Removes cleanupOldEvents (no longer needed with Redis TTL). Co-Authored-By: Paperclip --- .../src/services/billing.service.ts | 27 +++++++------------ 1 file changed, 10 insertions(+), 17 deletions(-) diff --git a/packages/shared-billing/src/services/billing.service.ts b/packages/shared-billing/src/services/billing.service.ts index 62b04c4..7eb93d7 100644 --- a/packages/shared-billing/src/services/billing.service.ts +++ b/packages/shared-billing/src/services/billing.service.ts @@ -1,21 +1,12 @@ import Stripe from 'stripe'; import { loadBillingConfig, SubscriptionTier } from '../config/billing.config'; +import { RedisService } from '@shieldsai/shared-notifications'; import type { Subscription, SubscriptionCreateSchema, SubscriptionUpdateSchema } from '../models/subscription.model'; const config = loadBillingConfig(); const stripe = new Stripe(config.stripe.apiKey, { apiVersion: '2023-10-16' }); - -const processedEvents = new Map(); -const IDEMPOTENCY_TTL_MS = 24 * 60 * 60 * 1000; - -function cleanupOldEvents(): void { - const now = Date.now(); - for (const [eventId, timestamp] of processedEvents.entries()) { - if (now - timestamp > IDEMPOTENCY_TTL_MS) { - processedEvents.delete(eventId); - } - } -} +const redis = RedisService.getInstance(); +const IDEMPOTENCY_TTL_SECONDS = 24 * 60 * 60; export class BillingService { private static instance: BillingService; @@ -266,15 +257,17 @@ export class BillingService { body: Buffer ): Promise { const event = stripe.webhooks.constructEvent(body, sig, config.stripe.webhookSecret); - - cleanupOldEvents(); - if (processedEvents.has(event.id)) { + const wasNew = await redis.setIfNotExists( + `stripe:event:${event.id}`, + '1', + IDEMPOTENCY_TTL_SECONDS + ); + + if (!wasNew) { return null; } - processedEvents.set(event.id, Date.now()); - return event; }