fix tests
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* computeCacheTTL
|
||||
*
|
||||
* Computes the Redis TTL (in milliseconds) for an opportunity record based on
|
||||
* its activity dates and closed state.
|
||||
*
|
||||
* Rules (highest priority first):
|
||||
* 1a. Closed > 30 days ago → null (do not cache)
|
||||
* 1b. Closed ≤ 30 days ago → TTL_LOW_ACTIVITY (15 min)
|
||||
* 2. High activity (≤ 5 days) → TTL_HIGH_ACTIVITY (30 s)
|
||||
* 3. Moderate activity (6–14 days) → TTL_MODERATE_ACTIVITY (60 s)
|
||||
* 4. Everything else → TTL_LOW_ACTIVITY (15 min)
|
||||
*/
|
||||
|
||||
export const TTL_HIGH_ACTIVITY = 30_000; // 30 seconds
|
||||
export const TTL_MODERATE_ACTIVITY = 60_000; // 60 seconds
|
||||
export const TTL_LOW_ACTIVITY = 900_000; // 15 minutes
|
||||
|
||||
export interface ComputeCacheTTLInput {
|
||||
closedFlag: boolean;
|
||||
closedDate: Date | null;
|
||||
expectedCloseDate: Date | null;
|
||||
lastUpdated: Date | null;
|
||||
/** Override "now" for deterministic tests. Defaults to `new Date()`. */
|
||||
now?: Date;
|
||||
}
|
||||
|
||||
const DAY_MS = 24 * 60 * 60 * 1000;
|
||||
|
||||
/** Returns the absolute age in milliseconds between `date` and `now`. */
|
||||
function ageMs(date: Date, now: Date): number {
|
||||
return Math.abs(now.getTime() - date.getTime());
|
||||
}
|
||||
|
||||
export function computeCacheTTL(input: ComputeCacheTTLInput): number | null {
|
||||
const now = input.now ?? new Date();
|
||||
|
||||
// Rule 1 — closed opportunities
|
||||
if (input.closedFlag) {
|
||||
if (!input.closedDate) {
|
||||
// Unknown close date → treat as stale
|
||||
return null;
|
||||
}
|
||||
const daysClosed = ageMs(input.closedDate, now) / DAY_MS;
|
||||
if (daysClosed > 30) {
|
||||
return null;
|
||||
}
|
||||
return TTL_LOW_ACTIVITY;
|
||||
}
|
||||
|
||||
// Helper: minimum absolute distance (in ms) of a date from now
|
||||
const dates = [input.lastUpdated, input.expectedCloseDate].filter(
|
||||
(d): d is Date => d !== null
|
||||
);
|
||||
|
||||
if (dates.length === 0) {
|
||||
return TTL_LOW_ACTIVITY;
|
||||
}
|
||||
|
||||
const minDeltaMs = Math.min(...dates.map((d) => ageMs(d, now)));
|
||||
const minDeltaDays = minDeltaMs / DAY_MS;
|
||||
|
||||
if (minDeltaDays <= 5) {
|
||||
return TTL_HIGH_ACTIVITY;
|
||||
}
|
||||
if (minDeltaDays <= 14) {
|
||||
return TTL_MODERATE_ACTIVITY;
|
||||
}
|
||||
return TTL_LOW_ACTIVITY;
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* computeProductsCacheTTL
|
||||
*
|
||||
* Computes the Redis TTL (in milliseconds) for an opportunity's products
|
||||
* cache entry.
|
||||
*
|
||||
* Rules (highest priority first):
|
||||
* 1. Won / Lost status → null (do not cache — products are final)
|
||||
* 2. Opp not cacheable (closed > 30 days) → null
|
||||
* 3. Updated within 3 days → PRODUCTS_TTL_HOT (45 s)
|
||||
* 4. Everything else → PRODUCTS_TTL_LAZY (20 min)
|
||||
*/
|
||||
|
||||
import { computeCacheTTL } from "./computeCacheTTL";
|
||||
|
||||
export const PRODUCTS_TTL_HOT = 45_000; // 45 seconds
|
||||
export const PRODUCTS_TTL_LAZY = 1_200_000; // 20 minutes
|
||||
|
||||
/** CW Opportunity Status IDs that indicate a final won/lost state. */
|
||||
export const WON_LOST_STATUS_IDS = new Set<number>([
|
||||
29, // Won
|
||||
53, // Lost
|
||||
59, // Canceled
|
||||
]);
|
||||
|
||||
export interface ComputeProductsCacheTTLInput {
|
||||
statusCwId: number | null;
|
||||
closedFlag: boolean;
|
||||
closedDate: Date | null;
|
||||
expectedCloseDate: Date | null;
|
||||
lastUpdated: Date | null;
|
||||
/** Override "now" for deterministic tests. Defaults to `new Date()`. */
|
||||
now?: Date;
|
||||
}
|
||||
|
||||
const DAY_MS = 24 * 60 * 60 * 1000;
|
||||
|
||||
export function computeProductsCacheTTL(
|
||||
input: ComputeProductsCacheTTLInput
|
||||
): number | null {
|
||||
const now = input.now ?? new Date();
|
||||
|
||||
// Rule 1 — Won/Lost status means products are final
|
||||
if (input.statusCwId !== null && WON_LOST_STATUS_IDS.has(input.statusCwId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Rule 2 — If the parent opportunity wouldn't be cached, skip products too
|
||||
const oppTTL = computeCacheTTL({
|
||||
closedFlag: input.closedFlag,
|
||||
closedDate: input.closedDate,
|
||||
expectedCloseDate: input.expectedCloseDate,
|
||||
lastUpdated: input.lastUpdated,
|
||||
now,
|
||||
});
|
||||
if (oppTTL === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Rule 3 — Recently updated → hot cache
|
||||
if (input.lastUpdated !== null) {
|
||||
const ageDays =
|
||||
Math.abs(now.getTime() - input.lastUpdated.getTime()) / DAY_MS;
|
||||
if (ageDays <= 3) {
|
||||
return PRODUCTS_TTL_HOT;
|
||||
}
|
||||
}
|
||||
|
||||
// Rule 4 — Everything else → lazy cache
|
||||
return PRODUCTS_TTL_LAZY;
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* computeSubResourceCacheTTL
|
||||
*
|
||||
* Computes the Redis TTL (in milliseconds) for opportunity sub-resources
|
||||
* (notes, contacts, activities, etc.) that share the parent opportunity's
|
||||
* activity window.
|
||||
*
|
||||
* Rules are identical to the main opportunity TTL but use different
|
||||
* exported constant names so callers can distinguish them.
|
||||
*/
|
||||
|
||||
export const SUB_TTL_HIGH_ACTIVITY = 60_000; // 60 seconds
|
||||
export const SUB_TTL_MODERATE_ACTIVITY = 120_000; // 2 minutes
|
||||
export const SUB_TTL_LOW_ACTIVITY = 300_000; // 5 minutes
|
||||
|
||||
export interface ComputeSubResourceCacheTTLInput {
|
||||
closedFlag: boolean;
|
||||
closedDate: Date | null;
|
||||
expectedCloseDate: Date | null;
|
||||
lastUpdated: Date | null;
|
||||
now?: Date;
|
||||
}
|
||||
|
||||
const DAY_MS = 24 * 60 * 60 * 1000;
|
||||
|
||||
function ageMs(date: Date, now: Date): number {
|
||||
return Math.abs(now.getTime() - date.getTime());
|
||||
}
|
||||
|
||||
export function computeSubResourceCacheTTL(
|
||||
input: ComputeSubResourceCacheTTLInput
|
||||
): number | null {
|
||||
const now = input.now ?? new Date();
|
||||
|
||||
// Rule 1a — closed > 30 days → no cache
|
||||
if (input.closedFlag) {
|
||||
if (!input.closedDate) return null;
|
||||
const daysClosed = ageMs(input.closedDate, now) / DAY_MS;
|
||||
if (daysClosed > 30) return null;
|
||||
return SUB_TTL_LOW_ACTIVITY;
|
||||
}
|
||||
|
||||
const dates = [input.lastUpdated, input.expectedCloseDate].filter(
|
||||
(d): d is Date => d !== null
|
||||
);
|
||||
|
||||
if (dates.length === 0) {
|
||||
return SUB_TTL_LOW_ACTIVITY;
|
||||
}
|
||||
|
||||
const minDeltaDays =
|
||||
Math.min(...dates.map((d) => ageMs(d, now))) / DAY_MS;
|
||||
|
||||
if (minDeltaDays <= 5) return SUB_TTL_HIGH_ACTIVITY;
|
||||
if (minDeltaDays <= 14) return SUB_TTL_MODERATE_ACTIVITY;
|
||||
return SUB_TTL_LOW_ACTIVITY;
|
||||
}
|
||||
Reference in New Issue
Block a user