Kaizen
Browse modulesRulesrules/sharedFunctions

Function: schedulesOverlap()

function schedulesOverlap(a: {
  condition_type: "PRICING_SCHEDULE";
  date_range?: {
     end: string;
     exclude?: boolean;
     start: string;
  };
  day_of_week?: {
     days: number[];
     exclude?: boolean;
  };
  duration?: {
     max?: number;
     max_exclusive?: boolean;
     min?: number;
     min_exclusive?: boolean;
     unit?: "days" | "minutes" | "hours";
  };
  end_field: string;
  holidays?: {
     exclude?: boolean;
     include_observed?: boolean;
     names: (
        | "NEW_YEARS_DAY"
        | "MLK_DAY"
        | "PRESIDENTS_DAY"
        | "MEMORIAL_DAY"
        | "JUNETEENTH"
        | "INDEPENDENCE_DAY"
        | "LABOR_DAY"
        | "COLUMBUS_DAY"
        | "VETERANS_DAY"
        | "THANKSGIVING"
       | "CHRISTMAS_DAY")[];
  };
  offered_until?: string;
  start_field: string;
  time_range?: {
     end_min: number;
     exclude?: boolean;
     start_min: number;
  };
  zone?: string;
}, b: {
  condition_type: "PRICING_SCHEDULE";
  date_range?: {
     end: string;
     exclude?: boolean;
     start: string;
  };
  day_of_week?: {
     days: number[];
     exclude?: boolean;
  };
  duration?: {
     max?: number;
     max_exclusive?: boolean;
     min?: number;
     min_exclusive?: boolean;
     unit?: "days" | "minutes" | "hours";
  };
  end_field: string;
  holidays?: {
     exclude?: boolean;
     include_observed?: boolean;
     names: (
        | "NEW_YEARS_DAY"
        | "MLK_DAY"
        | "PRESIDENTS_DAY"
        | "MEMORIAL_DAY"
        | "JUNETEENTH"
        | "INDEPENDENCE_DAY"
        | "LABOR_DAY"
        | "COLUMBUS_DAY"
        | "VETERANS_DAY"
        | "THANKSGIVING"
       | "CHRISTMAS_DAY")[];
  };
  offered_until?: string;
  start_field: string;
  time_range?: {
     end_min: number;
     exclude?: boolean;
     start_min: number;
  };
  zone?: string;
}): boolean;

Defined in: shared/rules/schedule-overlap.ts:166

Two PRICING_SCHEDULE configs conflict iff all three axes simultaneously overlap — a real conflicting moment requires a shared calendar date AND a shared minute-of-day AND a shared duration. PricingMatrixService. addSchedule/updateSchedule call this against every existing active schedule for the org+domain after fetching them from the DB; this function itself is pure (no DB access) and takes only the two configs.

Parameters

ParameterTypeDescription
a{ condition_type: "PRICING_SCHEDULE"; date_range?: { end: string; exclude?: boolean; start: string; }; day_of_week?: { days: number[]; exclude?: boolean; }; duration?: { max?: number; max_exclusive?: boolean; min?: number; min_exclusive?: boolean; unit?: "days" | "minutes" | "hours"; }; end_field: string; holidays?: { exclude?: boolean; include_observed?: boolean; names: ( | "NEW_YEARS_DAY" | "MLK_DAY" | "PRESIDENTS_DAY" | "MEMORIAL_DAY" | "JUNETEENTH" | "INDEPENDENCE_DAY" | "LABOR_DAY" | "COLUMBUS_DAY" | "VETERANS_DAY" | "THANKSGIVING" | "CHRISTMAS_DAY")[]; }; offered_until?: string; start_field: string; time_range?: { end_min: number; exclude?: boolean; start_min: number; }; zone?: string; }-
a.condition_type"PRICING_SCHEDULE"-
a.date_range?{ end: string; exclude?: boolean; start: string; }-
a.date_range.endstring-
a.date_range.exclude?boolean-
a.date_range.startstring-
a.day_of_week?{ days: number[]; exclude?: boolean; }-
a.day_of_week.daysnumber[]-
a.day_of_week.exclude?boolean-
a.duration?{ max?: number; max_exclusive?: boolean; min?: number; min_exclusive?: boolean; unit?: "days" | "minutes" | "hours"; }-
a.duration.max?number-
a.duration.max_exclusive?booleanMake the max bound EXCLUSIVE — "Less than 2 hours" rather than "At most 2 hours". Same absent-means-inclusive default as min_exclusive. Exclusivity is what lets two schedules TILE a boundary exactly: "Less than 2 hours" and "At least 2 hours" partition every duration with no gap and no overlap, which is the pairing the Facility designs use. With only inclusive bounds one of the two has to double-count 2 hours or leave it unpriced.
a.duration.min?number-
a.duration.min_exclusive?booleanMake the min bound EXCLUSIVE — "More than 2 hours" rather than "At least 2 hours" (PLA-285). Absent/false = inclusive, so every config written before this field keeps its exact meaning and no migration is needed. Stored as an explicit flag rather than inferred, because the operator cannot be recovered from the bounds alone. durationOperatorOf used to derive it from which bounds were present (min only → "at least"), so "More than 2 hours" and "At least 2 hours" were the same stored config — the label and the value could not round-trip, and reopening the dialog silently changed the rule (2 hours starts qualifying where it shouldn't).
a.duration.unit?"days" | "minutes" | "hours"-
a.end_fieldstring-
a.holidays?{ exclude?: boolean; include_observed?: boolean; names: ( | "NEW_YEARS_DAY" | "MLK_DAY" | "PRESIDENTS_DAY" | "MEMORIAL_DAY" | "JUNETEENTH" | "INDEPENDENCE_DAY" | "LABOR_DAY" | "COLUMBUS_DAY" | "VETERANS_DAY" | "THANKSGIVING" | "CHRISTMAS_DAY")[]; }-
a.holidays.exclude?boolean-
a.holidays.include_observed?boolean-
a.holidays.names( | "NEW_YEARS_DAY" | "MLK_DAY" | "PRESIDENTS_DAY" | "MEMORIAL_DAY" | "JUNETEENTH" | "INDEPENDENCE_DAY" | "LABOR_DAY" | "COLUMBUS_DAY" | "VETERANS_DAY" | "THANKSGIVING" | "CHRISTMAS_DAY")[]-
a.offered_until?stringThe last calendar day this column may be SOLD — the design's "Ending · On [mm/dd/yyyy]" (PLA-286). Absent = "Never", i.e. open-ended. ## It is a sale window, NOT a reservation window — and the name says so This is the rate-management pair every mature pricing system carries: a sale window ("may I still sell this?") alongside a service window ("what does this reservation date cost?"). Airlines call them sale dates and travel dates; hotels, booking window and stay dates. PricingScheduleDateRangeConfig | date_range is already the service window. This is the sale window, and it is enforced as WALL CLOCK: the column's cell rules carry it as rule_expiration_date, which loadRules checks against DateTime.now(). So a booking made in August for an October reservation still gets this price if it was purchased before the column retired — which is the point. It is named offered_until rather than ends_on deliberately. "Ends" reads equally as "stops applying to reservations after", which is the other window and a materially different price for every advance booking. The identifier is where that ambiguity gets closed, because a comment can be skipped. ## Inclusive, and a calendar day rather than an instant "Offered until 2026-09-01" means the column sells through the whole of Sep 1 in this config's zone (UTC when absent, as with every other dimension here). Storing the DAY and deriving the instant server-side keeps the end-of-day arithmetic in one place — scheduleExpiresAt in server/pricing/schedule-validation.ts. A caller that stored its own instant would hit the off-by-one-day bug this shape prevents: loadRules compares instants with no end-of-day widening, so midnight kills the column before the day it is labelled with ever trades. ## Not part of the predicate, and not part of overlap Like zone / start_field / end_field, this is column METADATA, not a schedule dimension. Two consequences, both deliberate: - condition-handlers.ts's pricingSchedule handler does not read it. A line is not "outside the schedule" because the column retired; the column simply stops being live, which loadRules enforces from the cell rules' rule_expiration_date. Reading it here as well would double-enforce it and turn a retired column into a non-matching one — a different, wrong price. - schedulesOverlap does not read it, so a retiring column still conflicts with a same-shaped one. That is the invariant working, not a gap: two coexisting same-shape columns is exactly the ambiguity that keeps MultipleBasePriceRulesError unreachable. Changing a column's price is a new VERSION of that column (updateSchedule / setCellPrice append one), never a second column beside it. This field is for genuinely retiring a column, which needs no overlap change. Staging a change for a future date ("weekends cost more from Sep 2") is a different feature whose primitive is rule_effective_date, not a second column — loadRules filters on is_current, so an edit takes effect on save. Tracked separately.
a.start_fieldstring-
a.time_range?{ end_min: number; exclude?: boolean; start_min: number; }-
a.time_range.end_minnumber-
a.time_range.exclude?boolean-
a.time_range.start_minnumber-
a.zone?string-
b{ condition_type: "PRICING_SCHEDULE"; date_range?: { end: string; exclude?: boolean; start: string; }; day_of_week?: { days: number[]; exclude?: boolean; }; duration?: { max?: number; max_exclusive?: boolean; min?: number; min_exclusive?: boolean; unit?: "days" | "minutes" | "hours"; }; end_field: string; holidays?: { exclude?: boolean; include_observed?: boolean; names: ( | "NEW_YEARS_DAY" | "MLK_DAY" | "PRESIDENTS_DAY" | "MEMORIAL_DAY" | "JUNETEENTH" | "INDEPENDENCE_DAY" | "LABOR_DAY" | "COLUMBUS_DAY" | "VETERANS_DAY" | "THANKSGIVING" | "CHRISTMAS_DAY")[]; }; offered_until?: string; start_field: string; time_range?: { end_min: number; exclude?: boolean; start_min: number; }; zone?: string; }-
b.condition_type"PRICING_SCHEDULE"-
b.date_range?{ end: string; exclude?: boolean; start: string; }-
b.date_range.endstring-
b.date_range.exclude?boolean-
b.date_range.startstring-
b.day_of_week?{ days: number[]; exclude?: boolean; }-
b.day_of_week.daysnumber[]-
b.day_of_week.exclude?boolean-
b.duration?{ max?: number; max_exclusive?: boolean; min?: number; min_exclusive?: boolean; unit?: "days" | "minutes" | "hours"; }-
b.duration.max?number-
b.duration.max_exclusive?booleanMake the max bound EXCLUSIVE — "Less than 2 hours" rather than "At most 2 hours". Same absent-means-inclusive default as min_exclusive. Exclusivity is what lets two schedules TILE a boundary exactly: "Less than 2 hours" and "At least 2 hours" partition every duration with no gap and no overlap, which is the pairing the Facility designs use. With only inclusive bounds one of the two has to double-count 2 hours or leave it unpriced.
b.duration.min?number-
b.duration.min_exclusive?booleanMake the min bound EXCLUSIVE — "More than 2 hours" rather than "At least 2 hours" (PLA-285). Absent/false = inclusive, so every config written before this field keeps its exact meaning and no migration is needed. Stored as an explicit flag rather than inferred, because the operator cannot be recovered from the bounds alone. durationOperatorOf used to derive it from which bounds were present (min only → "at least"), so "More than 2 hours" and "At least 2 hours" were the same stored config — the label and the value could not round-trip, and reopening the dialog silently changed the rule (2 hours starts qualifying where it shouldn't).
b.duration.unit?"days" | "minutes" | "hours"-
b.end_fieldstring-
b.holidays?{ exclude?: boolean; include_observed?: boolean; names: ( | "NEW_YEARS_DAY" | "MLK_DAY" | "PRESIDENTS_DAY" | "MEMORIAL_DAY" | "JUNETEENTH" | "INDEPENDENCE_DAY" | "LABOR_DAY" | "COLUMBUS_DAY" | "VETERANS_DAY" | "THANKSGIVING" | "CHRISTMAS_DAY")[]; }-
b.holidays.exclude?boolean-
b.holidays.include_observed?boolean-
b.holidays.names( | "NEW_YEARS_DAY" | "MLK_DAY" | "PRESIDENTS_DAY" | "MEMORIAL_DAY" | "JUNETEENTH" | "INDEPENDENCE_DAY" | "LABOR_DAY" | "COLUMBUS_DAY" | "VETERANS_DAY" | "THANKSGIVING" | "CHRISTMAS_DAY")[]-
b.offered_until?stringThe last calendar day this column may be SOLD — the design's "Ending · On [mm/dd/yyyy]" (PLA-286). Absent = "Never", i.e. open-ended. ## It is a sale window, NOT a reservation window — and the name says so This is the rate-management pair every mature pricing system carries: a sale window ("may I still sell this?") alongside a service window ("what does this reservation date cost?"). Airlines call them sale dates and travel dates; hotels, booking window and stay dates. PricingScheduleDateRangeConfig | date_range is already the service window. This is the sale window, and it is enforced as WALL CLOCK: the column's cell rules carry it as rule_expiration_date, which loadRules checks against DateTime.now(). So a booking made in August for an October reservation still gets this price if it was purchased before the column retired — which is the point. It is named offered_until rather than ends_on deliberately. "Ends" reads equally as "stops applying to reservations after", which is the other window and a materially different price for every advance booking. The identifier is where that ambiguity gets closed, because a comment can be skipped. ## Inclusive, and a calendar day rather than an instant "Offered until 2026-09-01" means the column sells through the whole of Sep 1 in this config's zone (UTC when absent, as with every other dimension here). Storing the DAY and deriving the instant server-side keeps the end-of-day arithmetic in one place — scheduleExpiresAt in server/pricing/schedule-validation.ts. A caller that stored its own instant would hit the off-by-one-day bug this shape prevents: loadRules compares instants with no end-of-day widening, so midnight kills the column before the day it is labelled with ever trades. ## Not part of the predicate, and not part of overlap Like zone / start_field / end_field, this is column METADATA, not a schedule dimension. Two consequences, both deliberate: - condition-handlers.ts's pricingSchedule handler does not read it. A line is not "outside the schedule" because the column retired; the column simply stops being live, which loadRules enforces from the cell rules' rule_expiration_date. Reading it here as well would double-enforce it and turn a retired column into a non-matching one — a different, wrong price. - schedulesOverlap does not read it, so a retiring column still conflicts with a same-shaped one. That is the invariant working, not a gap: two coexisting same-shape columns is exactly the ambiguity that keeps MultipleBasePriceRulesError unreachable. Changing a column's price is a new VERSION of that column (updateSchedule / setCellPrice append one), never a second column beside it. This field is for genuinely retiring a column, which needs no overlap change. Staging a change for a future date ("weekends cost more from Sep 2") is a different feature whose primitive is rule_effective_date, not a second column — loadRules filters on is_current, so an edit takes effect on save. Tracked separately.
b.start_fieldstring-
b.time_range?{ end_min: number; exclude?: boolean; start_min: number; }-
b.time_range.end_minnumber-
b.time_range.exclude?boolean-
b.time_range.start_minnumber-
b.zone?string-

Returns

boolean

On this page