Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/dmaman86/shiftly/llms.txt

Use this file to discover all available pages before exploring further.

Calculators are Shiftly’s innermost computation layer. Every salary figure that appears in a payslip breakdown — overtime bracket hours, night bonuses, Shabbat premiums, per-diem points, and meal allowance amounts — originates in a calculator. Each calculator receives a typed input, applies a single, well-scoped rule, and returns a typed output. There is no shared state, no I/O, and no dependency on Redux or React — calculators are plain TypeScript classes that can be instantiated and tested in complete isolation from the rest of the application.

The Calculator Interface

export interface Calculator<Input, Output> {
  calculate(params: Input): Output;
}
Several calculators also implement Reducer<State> so they can both produce and accumulate their own output type, saving the cost of a separate reducer class for simple structures.
All calculator instances are created once by buildCalculators() and shared across the entire domain pipeline. They carry no mutable state, so sharing a single instance is safe.

Calculators Overview

RegularByShiftCalculator

Applies per-shift bracket logic: 100% → 125% → 150% from lowest to highest, using per-shift hour position.

RegularByDayCalculator

Applies per-day bracket logic using total accumulated day hours: 100% up to standard, then 125% for the next 2h, then 150%.

ExtraCalculator

Sums labeled segment hours for evening (20%) and night (50%) bonus windows from a shift’s LabeledSegmentRange[].

SpecialCalculator

Sums labeled segment hours for Shabbat/holiday windows: 150% daytime and 200% night-time.

FixedSegmentFactory

Creates Segment values at 100% for sick days, vacation days, and extra-Shabbat hour bookkeeping.

LargeMealAllowanceCalculator

Awards 1 large meal allowance point for shifts ≥ 10 hours that span both morning and night, or pure night duty.

SmallMealAllowanceCalculator

Awards 1 small meal allowance point for shifts that include a qualifying night portion but do not qualify for large.

DefaultPerDiemDayCalculator

Determines per-diem tier (A/B/C) from total field-duty hours in a day and calculates the monetary amount.

DefaultPerDiemMonthCalculator

Accumulates and subtracts PerDiemInfo across days to build the monthly per-diem total.

Core Breakdown Types

All calculators produce or consume one of these three breakdown structures:
export interface RegularBreakdown {
  hours100: Segment; // standard pay (×1.00)
  hours125: Segment; // first overtime bracket (×1.25)
  hours150: Segment; // extended overtime (×1.50)
}

export interface ExtraBreakdown {
  hours20: Segment; // evening bonus: 14:00–22:00 window (×0.20 additive)
  hours50: Segment; // night bonus: 22:00–06:00 window (×0.50 additive)
}

export interface SpecialBreakdown {
  shabbat150: Segment; // Shabbat/holiday daytime (×1.50)
  shabbat200: Segment; // Shabbat/holiday nighttime (×2.00)
}

export interface Segment {
  percent: number; // the rate multiplier for this segment
  hours: number;   // total hours in this segment
}

Regular Calculators

Both regular calculators extend BaseRegularCalculator, which also implements Reducer<RegularBreakdown> so the same class handles accumulation without a separate reducer. The input type is shared:
export type RegularInput = {
  totalHours: number;
  standardHours: number; // the configured standard work-day length (e.g. 8h or 8.6h)
  meta: WorkDayMeta;
};

RegularByShiftCalculator

Used in the shift-level builder. Applies the bracket overflow logic starting from the shift’s own hour count:
class RegularByShiftCalculator
  extends BaseRegularCalculator
  implements Calculator<RegularInput, RegularBreakdown>
  • If meta.typeDay === SpecialFull and this is not a cross-day continuation, all hours go to 150% (Shabbat shifts have no regular bracket).
  • Otherwise, subtracts overflow into each bracket from the top down: anything beyond standardHours + 2 → 150%; the next 2h → 125%; the remainder → 100%.

RegularByDayCalculator

Used in the day-level builder. Applies bracket logic to the total accumulated hours across all shifts in the day:
class RegularByDayCalculator
  extends BaseRegularCalculator
  implements Calculator<RegularInput, RegularBreakdown>
  • Starts by capping at standardHours for 100%; the excess goes to 125% up to 2 hours; anything further goes to 150%.
  • This produces the correct bracket split when multiple short shifts combine over a day, which the per-shift version would over-count.

RegularConfig

export interface RegularConfig {
  midTierThreshold: number; // default: 2 (hours in the 125% bracket)
  percentages: {
    hours100: number; // default: 1.00
    hours125: number; // default: 1.25
    hours150: number; // default: 1.50
  };
}

RegularFactory

RegularFactory is the static entry point for constructing the three regular-related instances used in the pipeline:
export class RegularFactory {
  static byShift(): RegularCalculator;       // → RegularByShiftCalculator
  static byDay(): RegularCalculator;         // → RegularByDayCalculator
  static monthReducer(): Reducer<RegularBreakdown>; // → RegularByMonthAccumulator
}

ExtraCalculator

ExtraCalculator computes the evening and night bonus hours from the labeled segment list produced by ShiftSegmentBuilder. It implements both Calculator and Reducer so it can calculate a shift’s extra hours and accumulate them across shifts within a day.
class ExtraCalculator
  implements
    Calculator<LabeledSegmentRange[], ExtraBreakdown>,
    Reducer<ExtraBreakdown>

Logic

calculate(labeledSegments: LabeledSegmentRange[]): ExtraBreakdown
Filters labeledSegments by key:
KeyWindowRate
"hours20"14:00 – 22:0020% additive bonus
"hours50"22:00 – 06:0050% additive bonus
Duration of each matching segment is computed as (point.end - point.start) / 60.

SpecialCalculator

SpecialCalculator computes Shabbat and holiday pay from the same labeled segment list, using segment keys "shabbat150" and "shabbat200".
class SpecialCalculator
  implements
    Calculator<LabeledSegmentRange[], SpecialBreakdown>,
    Reducer<SpecialBreakdown>

Logic

calculate(labeledSegments: LabeledSegmentRange[]): SpecialBreakdown
KeyWindow on a full special dayRate
"shabbat150"06:00 – 22:00150%
"shabbat200"00:00 – 06:00 and 22:00 – 30:00200%
On a SpecialPartialStart day (Friday, eve-of-holiday), the 150% window begins at 17:00 or 18:00 depending on DST.

FixedSegmentFactory

FixedSegmentFactory creates Segment values with a fixed 100% rate. Three separate instances are created — one each for sick, vacation, and extra-Shabbat tracking — so they remain independently identifiable in the WorkDayMap.
class FixedSegmentFactory {
  create(hours: number): Segment
  // Returns: { percent: 1, hours: hours }
}
Usage context:
  • sick — filled with standardHours on a WorkDayStatus.sick day.
  • vacation — filled with standardHours on a WorkDayStatus.vacation day.
  • extraShabbat — filled with total Shabbat hours on a normal worked day where a Shabbat/holiday shift was worked, to enable separate payslip line tracking.

Meal Allowance Calculators

Meal allowance eligibility is determined by the structure of the working day: whether it has a meaningful morning portion, a meaningful night portion (≥ 4 night hours), and whether it is a field-duty day.

Input Type

export type MealAllowanceCalcParams = {
  day: MealAllowanceDayInfo;
  rate: number; // monetary rate per point, from TimelineMealAllowanceRateResolver
};

export type MealAllowanceDayInfo = {
  totalHours: number;
  hasMorning: boolean;
  hasNight: boolean;      // true if night hours >= 4
  isFieldDutyDay: boolean;
};

LargeMealAllowanceCalculator

class LargeMealAllowanceCalculator implements Calculator<MealAllowanceCalcParams, MealAllowanceEntry>
Awards 1 large meal point ({ points: 1, amount: rate }) when:
  • totalHours >= 10, and
  • The day has both morning and night portions, or is a pure night shift, or is a non-field-duty day shift.
Returns { points: 0, amount: 0 } for field-duty day shifts under 10 hours, as per-diem replaces meal allowance in that context.

SmallMealAllowanceCalculator

class SmallMealAllowanceCalculator implements Calculator<MealAllowanceCalcParams, MealAllowanceEntry>
Awards 1 small meal point when day.hasNight === true and the large calculator returned 0 points.

Output Type

export interface MealAllowanceEntry {
  points: number; // 0 or 1
  amount: number; // points × rate (ILS)
}

export interface MealAllowance {
  small: MealAllowanceEntry;
  large: MealAllowanceEntry;
}

Per-Diem Calculators

Per-diem tracks days worked in the field (when isFieldDutyShift: true is set on a shift). The applicable monetary rate is resolved per-year-month from TimelinePerDiemRateResolver.

DefaultPerDiemDayCalculator

class DefaultPerDiemDayCalculator implements PerDiemDayCalculator

// PerDiemDayCalculator = Calculator<
//   { shifts: PerDiemShiftInfo[]; rate: number },
//   DailyPerDiemInfo
// >
Determines tier from total field-duty hours in the day:
Total field-duty hoursTierPoints
< 4h0
≥ 4hA1
≥ 8hB2
≥ 12hC3
export interface PerDiemInfo {
  tier: "A" | "B" | "C" | null;
  points: number;
  amount: number; // points × rate
}

export interface DailyPerDiemInfo {
  isFieldDutyDay: boolean;
  diemInfo: PerDiemInfo;
}

DefaultPerDiemMonthCalculator

class DefaultPerDiemMonthCalculator implements PerDiemMonthReducer
// PerDiemMonthReducer = Reducer<PerDiemInfo>
Accumulates PerDiemInfo across days for the monthly total. The monthly aggregate does not carry a tier — only points and amount are summed. Subtraction clamps each field at 0 to prevent negative totals.

Pipeline Entry Point

All calculator instances are assembled once by the buildCalculators() pipeline function and returned as a typed Calculators object:
export interface Calculators {
  regular: {
    byShift: RegularCalculator;
    byDay: RegularCalculator;
    accumulator: Reducer<RegularBreakdown>;
  };
  extra: ExtraCalculator;
  special: SpecialCalculator;
  fixedSegments: {
    sick: FixedSegmentFactory;
    vacation: FixedSegmentFactory;
    extraShabbat: FixedSegmentFactory;
  };
  mealAllowance: {
    large: LargeMealAllowanceCalculator;
    small: SmallMealAllowanceCalculator;
  };
  perDiem: {
    day: DefaultPerDiemDayCalculator;
    month: DefaultPerDiemMonthCalculator;
  };
}
See Composition Pipelines for how buildCalculators() is called and how these instances are distributed across the shift, day, and month layers.

Build docs developers (and LLMs) love