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.
Israeli labor law divides every worked day into three pay brackets: a base-rate period at 100%, a first-overtime tier at 125%, and a second-overtime tier at 150%. Shiftly implements this through a pair of calculators — one that works on a per-shift basis and one that works on per-day totals — both derived from a shared base class that holds the bracket configuration.
Type Definitions
The output of every regular-hours calculation is a RegularBreakdown, which holds one Segment per bracket:
interface Segment {
percent: number; // pay-rate multiplier (1, 1.25, or 1.5)
hours: number; // hours accumulated in this bracket
}
interface RegularBreakdown {
hours100: Segment; // base rate — 100%
hours125: Segment; // first overtime tier — 125%
hours150: Segment; // second overtime tier — 150%
}
The input to both calculators is a RegularInput, which bundles the total hours with the day’s standard working hours and metadata:
type RegularInput = {
totalHours: number; // total hours to allocate across brackets
standardHours: number; // daily standard (e.g. 8 or 8.5 hours)
meta: WorkDayMeta; // date, WorkDayType, crossDayContinuation flag
};
The RegularConfig
Both calculators are constructed with a RegularConfig that governs the bracket thresholds and multipliers. All values have defaults matching Israeli law:
interface RegularConfig {
midTierThreshold: number; // extra hours before 150% kicks in (default: 2)
percentages: {
hours100: number; // default: 1 (100%)
hours125: number; // default: 1.25 (125%)
hours150: number; // default: 1.5 (150%)
};
}
With the defaults, the bracket allocation works as follows for a standard 8-hour day:
| Hours worked | Bracket |
|---|
| 0 – 8 | 100% (hours100) |
| 8 – 10 | 125% (hours125) — up to midTierThreshold (2 h) beyond standardHours |
| > 10 | 150% (hours150) |
midTierThreshold defaults to 2 hours. If a collective agreement sets a different threshold (e.g., 1 hour), you can supply a custom RegularConfig when constructing either calculator.
BaseRegularCalculator
Both concrete calculators extend BaseRegularCalculator, which provides shared helpers:
abstract class BaseRegularCalculator implements Reducer<RegularBreakdown> {
protected readonly config: RegularConfig;
constructor(config?: Partial<RegularConfig>) {
this.config = {
midTierThreshold: config?.midTierThreshold ?? 2,
percentages: {
hours100: config?.percentages?.hours100 ?? 1,
hours125: config?.percentages?.hours125 ?? 1.25,
hours150: config?.percentages?.hours150 ?? 1.5,
},
};
}
// Returns an empty breakdown with zeroed hours and the configured percents.
createEmpty(): RegularBreakdown { ... }
// Used for full special days: all hours go directly to 150%.
handleSpecial(totalHours: number): RegularBreakdown { ... }
// Adds two RegularBreakdowns together (used when accumulating shifts).
accumulate(base: RegularBreakdown, add: RegularBreakdown): RegularBreakdown { ... }
// Subtracts, flooring at zero (used when removing a previously counted shift).
subtract(base: RegularBreakdown, sub: RegularBreakdown): RegularBreakdown { ... }
}
The handleSpecial() Method
When the day’s WorkDayType is SpecialFull (Shabbat or a public holiday) and crossDayContinuation is false, the calculator bypasses the normal bracket logic entirely and routes all hours to the 150% bucket:
handleSpecial(totalHours: number): RegularBreakdown {
return {
hours100: { percent: this.config.percentages.hours100, hours: 0 },
hours125: { percent: this.config.percentages.hours125, hours: 0 },
hours150: {
percent: this.config.percentages.hours150,
hours: totalHours, // ← everything goes to 150%
},
};
}
This is correct because on a full special day, even the “base” portion of work is compensated at 150% (the 150% and 200% Shabbat segments are tracked separately in SpecialBreakdown).
RegularByShiftCalculator
RegularByShiftCalculator is used when a day has a single shift or when bracket allocation must be evaluated independently for each shift entry before day-level re-aggregation.
class RegularByShiftCalculator
extends BaseRegularCalculator
implements Calculator<RegularInput, RegularBreakdown>
{
calculate(params: RegularInput): RegularBreakdown {
const { totalHours, standardHours, meta } = params;
// Full special day: skip bracket logic, all hours → 150%
if (meta.typeDay === WorkDayType.SpecialFull && !meta.crossDayContinuation)
return this.handleSpecial(totalHours);
let remaining = totalHours;
// Peel off the 150% overflow first (hours beyond standardHours + midTierThreshold)
const overflow150 = Math.max(
remaining - (standardHours + this.config.midTierThreshold),
0,
);
remaining -= overflow150;
// Then the 125% overflow (hours between standardHours and standardHours + midTierThreshold)
const overflow125 = Math.max(remaining - standardHours, 0);
remaining -= overflow125;
// Everything that is left fits within standardHours → 100%
const overflow100 = Math.max(remaining, 0);
return {
hours100: { percent: this.config.percentages.hours100, hours: overflow100 },
hours125: { percent: this.config.percentages.hours125, hours: overflow125 },
hours150: { percent: this.config.percentages.hours150, hours: overflow150 },
};
}
}
Example — a 10-hour shift on a standard 8-hour day:
| Step | Value |
|---|
totalHours | 10 |
standardHours | 8 |
midTierThreshold | 2 |
overflow150 = max(10 − (8+2), 0) | 0 h |
overflow125 = max(10 − 0 − 8, 0) | 2 h |
overflow100 = 10 − 0 − 2 | 8 h |
RegularByDayCalculator
RegularByDayCalculator is used at the day level, where totalHours is the sum of all shifts on that calendar day. Because overtime thresholds reset every calendar day, this calculator always gets the day’s full accumulated hours:
class RegularByDayCalculator
extends BaseRegularCalculator
implements Calculator<RegularInput, RegularBreakdown>
{
calculate(params: RegularInput): RegularBreakdown {
const { totalHours, standardHours, meta } = params;
if (meta.typeDay === WorkDayType.SpecialFull && !meta.crossDayContinuation)
return this.handleSpecial(totalHours);
// Hours up to standardHours → 100%
const adj100 = Math.min(totalHours, standardHours);
const overflow100 = totalHours - adj100;
// Next midTierThreshold hours → 125%
const adj125 = Math.min(overflow100, this.config.midTierThreshold);
const overflow125 = overflow100 - adj125;
// Remaining → 150%
return {
hours100: { percent: this.config.percentages.hours100, hours: adj100 },
hours125: { percent: this.config.percentages.hours125, hours: adj125 },
hours150: { percent: this.config.percentages.hours150, hours: overflow125 },
};
}
}
Why Two Calculators?
RegularByShift
RegularByDay
Used when computing a preliminary breakdown per individual shift, for display purposes in the shift-level view. Each shift is treated as if it were the only shift of the day.
Used when computing the authoritative bracket allocation for a calendar day. This is the value written into WorkDayMap and ultimately into MonthPayMap. It considers the sum of all shifts so the midTierThreshold is only crossed once per day — not once per shift.
Daily Reset Behavior
Overtime brackets are reset every calendar day. A worker who does 6 hours on Monday and 6 hours on Tuesday earns 100% for all 12 hours (assuming an 8-hour standard). They do not accumulate 12 total hours and enter the 125% bracket. Each day’s RegularByDayCalculator.calculate() call receives only that day’s totalHours, so the reset is implicit.
Shifts that cross midnight are split at the 00:00 boundary by the pipeline. The hours before midnight are attributed to the first calendar day, and the hours after midnight are attributed to the next. This ensures each day’s bracket threshold is applied independently.