103 lines
3.2 KiB
TypeScript
103 lines
3.2 KiB
TypeScript
/**
|
|
* @module algo.coldThreshold
|
|
*
|
|
* Cold-Detection Algorithm
|
|
* ========================
|
|
*
|
|
* Determines whether an opportunity has stalled in a status long enough
|
|
* to be considered "cold". When an opportunity goes cold it is
|
|
* automatically moved to InternalReview, a system-generated activity is
|
|
* logged, and it is flagged for the internal review report.
|
|
*
|
|
* ## Thresholds (defaults)
|
|
*
|
|
* | Status | Stall Threshold |
|
|
* |-----------------|-----------------|
|
|
* | QuoteSent | 14 days |
|
|
* | ConfirmedQuote | 30 days |
|
|
*
|
|
* Only these two statuses are eligible for cold detection. All other
|
|
* statuses return `cold: false`.
|
|
*
|
|
* ## How "last activity date" is determined
|
|
*
|
|
* The algorithm uses `lastActivityDate` — the most recent of:
|
|
* - the latest activity's `dateStart`
|
|
* - the opportunity's `cwLastUpdated`
|
|
*
|
|
* The caller is responsible for resolving this value before calling
|
|
* `checkColdStatus`.
|
|
*/
|
|
|
|
import type { OpportunityController } from "../../controllers/OpportunityController";
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Config
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/** Stall thresholds in milliseconds, keyed by CW status ID. */
|
|
export const COLD_THRESHOLDS: Record<number, { days: number; ms: number }> = {
|
|
/** QuoteSent — CW status ID 43, "03. Quote Sent" */
|
|
43: { days: 14, ms: 14 * 24 * 60 * 60 * 1000 },
|
|
|
|
/** ConfirmedQuote — CW status ID 57, "04. Confirmed Quote" */
|
|
57: { days: 30, ms: 30 * 24 * 60 * 60 * 1000 },
|
|
};
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Types
|
|
// ---------------------------------------------------------------------------
|
|
|
|
export interface ColdCheckInput {
|
|
/** Current CW status ID of the opportunity. */
|
|
statusCwId: number | null;
|
|
|
|
/**
|
|
* The most recent meaningful date to measure staleness from.
|
|
* Typically the latest of the last activity dateStart or cwLastUpdated.
|
|
*/
|
|
lastActivityDate: Date | null;
|
|
|
|
/** Override for "now" — useful for testing. Defaults to `new Date()`. */
|
|
now?: Date;
|
|
}
|
|
|
|
export interface ColdCheckResult {
|
|
/** Whether the opportunity is considered cold. */
|
|
cold: boolean;
|
|
|
|
/**
|
|
* Which threshold triggered the cold flag.
|
|
* `null` when `cold` is `false`.
|
|
*/
|
|
triggeredBy: {
|
|
statusCwId: number;
|
|
statusName: string;
|
|
thresholdDays: number;
|
|
staleDays: number;
|
|
} | null;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Helpers
|
|
// ---------------------------------------------------------------------------
|
|
|
|
const STATUS_NAMES: Record<number, string> = {
|
|
43: "QuoteSent",
|
|
57: "ConfirmedQuote",
|
|
};
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Core
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/**
|
|
* Evaluate whether an opportunity has exceeded its cold-stall threshold.
|
|
*
|
|
* @returns A `ColdCheckResult` indicating cold status and trigger metadata.
|
|
*/
|
|
export function checkColdStatus(_input: ColdCheckInput): ColdCheckResult {
|
|
// Bypassed — always returns not-cold until cold-stall feature is ready
|
|
return { cold: false, triggeredBy: null };
|
|
}
|