Interface: PricingMatrixSchedule
Defined in: shared/pricing/types/matrix.ts:76
One column of the matrix: a PRICING_SCHEDULE Conditions row.
Properties
conditionId
conditionId: string;Defined in: shared/pricing/types/matrix.ts:77
config
config:
| {
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;
}
| null;Defined in: shared/pricing/types/matrix.ts:96
The schedule's full config, so a column header can render a summary of the
rules it encodes ("9 AM - 3 PM, Mon–Fri", "At most 2 hours") rather than
relying on someone having typed an accurate description. The design's
header subtitles are derived from this, not from prose.
null when the stored config doesn't satisfy PricingScheduleConfig — a
legacy row, a direct SQL write, or (most likely) a taproot upgrade that
tightened the schema. The column is still returned WITH a null config rather
than dropped, deliberately: dropping it would hide a schedule that pricing
evaluation still applies, leaving an invisible column that prices real orders
and that no admin can see, fix, or delete. A visible column with no summary
is the honest degradation.
Type-only import, so this adds nothing to a browser bundle.
Union Members
Type Literal
{
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;
}condition_type
condition_type: "PRICING_SCHEDULE";date_range?
optional date_range?: {
end: string;
exclude?: boolean;
start: string;
};date_range.end
end: string;date_range.exclude?
optional exclude?: boolean;date_range.start
start: string;day_of_week?
optional day_of_week?: {
days: number[];
exclude?: boolean;
};day_of_week.days
days: number[];day_of_week.exclude?
optional exclude?: boolean;duration?
optional duration?: {
max?: number;
max_exclusive?: boolean;
min?: number;
min_exclusive?: boolean;
unit?: "days" | "minutes" | "hours";
};duration.max?
optional max?: number;duration.max_exclusive?
optional max_exclusive?: boolean;Make 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.
duration.min?
optional min?: number;duration.min_exclusive?
optional min_exclusive?: boolean;Make 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).
duration.unit?
optional unit?: "days" | "minutes" | "hours";end_field
end_field: string = FieldPath;holidays?
optional 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")[];
};holidays.exclude?
optional exclude?: boolean;holidays.include_observed?
optional include_observed?: boolean;holidays.names
names: (
| "NEW_YEARS_DAY"
| "MLK_DAY"
| "PRESIDENTS_DAY"
| "MEMORIAL_DAY"
| "JUNETEENTH"
| "INDEPENDENCE_DAY"
| "LABOR_DAY"
| "COLUMBUS_DAY"
| "VETERANS_DAY"
| "THANKSGIVING"
| "CHRISTMAS_DAY")[];offered_until?
optional offered_until?: string;The 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'spricingSchedulehandler does not read it. A line is not "outside the schedule" because the column retired; the column simply stops being live, whichloadRulesenforces 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. -
schedulesOverlapdoes 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 keepsMultipleBasePriceRulesErrorunreachable. Changing a column's price is a new VERSION of that column (updateSchedule/setCellPriceappend 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 —loadRulesfilters onis_current, so an edit takes effect on save. Tracked separately.
start_field
start_field: string = FieldPath;time_range?
optional time_range?: {
end_min: number;
exclude?: boolean;
start_min: number;
};time_range.end_min
end_min: number;time_range.exclude?
optional exclude?: boolean;time_range.start_min
start_min: number;zone?
optional zone?: string;null
description
description: string | null;Defined in: shared/pricing/types/matrix.ts:79
name
name: string;Defined in: shared/pricing/types/matrix.ts:78