import { Injectable } from "@nestjs/common"; import { CacheService } from "@bff/infra/cache/cache.service.js"; import { CacheStrategyBase } from "@bff/infra/cache/cache-strategy.base.js"; import type { CacheBucketMetrics } from "@bff/infra/cache/cache.types.js"; import type { SupportCaseList, CaseMessageList } from "@customer-portal/domain/support"; /** * Cache TTL configuration for support cases * * Unlike orders (which use CDC events), support cases use TTL-based caching * because Salesforce Case changes don't trigger platform events in our setup. */ const CACHE_TTL = { /** Case list TTL: 2 minutes - customers checking status periodically */ CASE_LIST: 120, /** Case messages TTL: 1 minute - fresher for active conversations */ CASE_MESSAGES: 60, } as const; interface SupportCacheMetrics { caseList: CacheBucketMetrics; messages: CacheBucketMetrics; invalidations: number; } /** * Support cases cache service * * Uses TTL-based caching (not CDC) because: * - Cases don't trigger platform events in our Salesforce setup * - Customers can refresh to see updates (no real-time requirement) * - Short TTLs (1-2 min) ensure reasonable freshness * * Features: * - Request coalescing: Prevents duplicate API calls on cache miss * - Write-through invalidation: Cache is cleared after customer adds comment * - Metrics tracking: Monitors hits, misses, and invalidations */ @Injectable() export class SupportCacheService extends CacheStrategyBase<"caseList" | "messages"> { constructor(cache: CacheService) { super(cache, { caseList: { ttlSeconds: CACHE_TTL.CASE_LIST }, messages: { ttlSeconds: CACHE_TTL.CASE_MESSAGES }, }); } /** * Get cached case list for an account */ async getCaseList( sfAccountId: string, fetcher: () => Promise ): Promise { const key = this.buildCaseListKey(sfAccountId); return this.getOrSet("caseList", key, fetcher); } /** * Get cached messages for a case */ async getCaseMessages( caseId: string, fetcher: () => Promise ): Promise { const key = this.buildMessagesKey(caseId); return this.getOrSet("messages", key, fetcher); } /** * Invalidate case list cache for an account * Called after customer creates a new case */ async invalidateCaseList(sfAccountId: string): Promise { const key = this.buildCaseListKey(sfAccountId); await this.invalidate(key); } /** * Invalidate messages cache for a case * Called after customer adds a comment */ async invalidateCaseMessages(caseId: string): Promise { const key = this.buildMessagesKey(caseId); await this.invalidate(key); } /** * Invalidate all caches for an account's cases * Called after any write operation to ensure fresh data */ async invalidateAllForAccount(sfAccountId: string, caseId?: string): Promise { await this.invalidateCaseList(sfAccountId); if (caseId) { await this.invalidateCaseMessages(caseId); } } /** * Get cache metrics for monitoring */ getMetrics(): SupportCacheMetrics { return { caseList: this.getBucketMetrics("caseList"), messages: this.getBucketMetrics("messages"), invalidations: this.getInvalidationCount(), }; } private buildCaseListKey(sfAccountId: string): string { return `support:cases:${sfAccountId}`; } private buildMessagesKey(caseId: string): string { return `support:messages:${caseId}`; } }