import { ID } from '../../commonStateTypes/common'; import { ApprovalRule, Criteria } from './approvalRuleState'; /** * Approval Rules 3.0 — frontend overlap detection. * * Per product spec, two rules overlap iff: * 1. Their vendor criteria are exactly equal (same operator and * the same set of vendor IDs, or both absent), AND * 2. Their department criteria are exactly equal (same shape), AND * 3. Their amount intervals intersect. * * The intuition: we only flag overlap when the non-amount criteria * are identical, because the typical mistake is "I tried to create a * second rule for the same vendor / department slice at a different * amount threshold and forgot the existing one". Subset / superset * overlaps (e.g. one rule has a department condition and the other * does not) are intentionally not flagged in this iteration. * * Amount intervals are: * - greater_than(N) → [N, +∞) * - less_than(N) → (-∞, N] * - range(min, max) → [min, max] * - amount absent → (-∞, +∞) * * Two intervals overlap iff max(lo) <= min(hi). * * Exclusions from the comparison pool: * - The rule being edited (caller passes `excludeRuleId`). * - Fallback rules — they are a separate concept (match-all-others) * and not subject to the per-criteria overlap check. * * Callers should filter `existingRules` to the relevant entityType * (BillPay vs Reimbursement) before passing — the function does not * cross-check entityType. * * Pure function: no side effects, deterministic. Returns the list of * overlapping rules. Empty list means no overlap. */ /** * Structural shape of a rule the algorithm needs. The detector only * reads `criteria`, `isFallback`, and `approvalRuleId`; it never * touches `steps`, so callers may pass any 'ApprovalRule' subtype * (e.g. `ApprovalRuleWithUser`) and we keep the richer type in the * output. */ export type RuleForOverlapCheck = Pick; export interface RuleOverlap { conflictingRule: TRule; /** * 'duplicate' — every criterion matches exactly, including the * amount interval (same comparator + min + max, not just an * intersecting range). 'partial' — vendor/department match exactly * (required for any overlap at all) but the amount intervals only * intersect. * * Callers typically treat 'duplicate' as a blocking error (the * candidate rule is unreachable — the existing rule already * matches every bill/reimbursement it would) and 'partial' as a * dismissible warning. */ overlapKind: 'duplicate' | 'partial'; } export interface DetectRuleOverlapInput { candidateCriteria: Criteria[]; existingRules: TRule[]; /** When editing an existing rule, exclude it from the comparison. */ excludeRuleId?: ID; } export declare function detectRuleOverlap({ candidateCriteria, existingRules, excludeRuleId, }: DetectRuleOverlapInput): RuleOverlap[];