diff --git a/apps/bff/src/integrations/freebit/services/freebit-operations.service.ts b/apps/bff/src/integrations/freebit/services/freebit-operations.service.ts index 9b86114c..984f1a85 100644 --- a/apps/bff/src/integrations/freebit/services/freebit-operations.service.ts +++ b/apps/bff/src/integrations/freebit/services/freebit-operations.service.ts @@ -55,7 +55,51 @@ export class FreebitOperationsService { } >(); + // Rate limit cleanup to avoid running on every operation + private lastCleanupTime = 0; + private readonly cleanupIntervalMs = 5 * 60 * 1000; // Run cleanup at most every 5 minutes + private readonly staleThresholdMs = 35 * 60 * 1000; // Remove entries older than 35 minutes (beyond 30-min window) + + /** + * Cleanup stale entries from operationTimestamps to prevent memory leak. + * Entries are considered stale when all their timestamps are older than staleThresholdMs. + */ + private cleanupStaleEntries(): void { + const now = Date.now(); + + // Rate limit cleanup to avoid performance overhead + if (now - this.lastCleanupTime < this.cleanupIntervalMs) { + return; + } + this.lastCleanupTime = now; + + const staleThreshold = now - this.staleThresholdMs; + const accountsToRemove: string[] = []; + + for (const [account, entry] of this.operationTimestamps) { + const timestamps = [entry.voice, entry.network, entry.plan, entry.cancellation].filter( + (t): t is number => typeof t === "number" + ); + + // If all timestamps are stale (or entry is empty), mark for removal + if (timestamps.length === 0 || timestamps.every(t => t < staleThreshold)) { + accountsToRemove.push(account); + } + } + + for (const account of accountsToRemove) { + this.operationTimestamps.delete(account); + } + + if (accountsToRemove.length > 0) { + this.logger.debug(`Cleaned up ${accountsToRemove.length} stale operation timestamp entries`); + } + } + private getOperationWindow(account: string) { + // Run cleanup periodically to prevent memory leak + this.cleanupStaleEntries(); + if (!this.operationTimestamps.has(account)) { this.operationTimestamps.set(account, {}); } @@ -457,6 +501,14 @@ export class FreebitOperationsService { ); } + // Validate that at least one feature is specified + if (!hasVoiceFeatures && !hasNetworkTypeChange) { + throw new BadRequestException( + "No features specified for update. Please provide at least one of: " + + "voiceMailEnabled, callWaitingEnabled, internationalRoamingEnabled, or networkType." + ); + } + if (hasVoiceFeatures) { // Only voice features (PA05-06) await this.updateVoiceFeatures(account, voiceFeatures);