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.
Beyond hourly pay, Israeli employment regulations entitle field workers to a daily per-diem allowance and employees to meal allowances based on shift characteristics. Shiftly computes both independently, using historical rate timelines to ensure the correct rate is applied regardless of which year or month is being calculated.
Per-diem and meal allowance amounts only appear in the output when a baseRate > 0 is configured. If no base rate is set, both per-diem and meal allowance amounts will be zero even if points are accumulated.
Per-Diem
Type Definitions
// Single-day per-diem result
interface PerDiemInfo {
tier: "A" | "B" | "C" | null; // null when total field-duty hours < 4
points: number; // 1 (Tier A), 2 (Tier B), or 3 (Tier C)
amount: number; // points × rate for the given year/month
}
// Per-day wrapper, also tracks whether any shift was a field-duty shift
interface DailyPerDiemInfo {
isFieldDutyDay: boolean;
diemInfo: PerDiemInfo;
}
Tier Thresholds
Per-diem is only awarded when the worker has at least one shift marked as isDuty: true. The total field-duty hours on the day determine the tier:
| Tier | Field-duty hours | Points |
|---|
| None | < 4 h | 0 |
| A | 4 h – 7 h 59 m | 1 |
| B | 8 h – 11 h 59 m | 2 |
| C | ≥ 12 h | 3 |
DefaultPerDiemDayCalculator
DefaultPerDiemDayCalculator computes the per-diem result for a single day given a list of per-shift duty records and the applicable rate:
class DefaultPerDiemDayCalculator implements PerDiemDayCalculator {
private getTier(totalHours: number): {
tier: "A" | "B" | "C" | null;
points: number;
} {
if (totalHours >= 12) return { tier: "C", points: 3 };
if (totalHours >= 8) return { tier: "B", points: 2 };
if (totalHours >= 4) return { tier: "A", points: 1 };
return { tier: null, points: 0 };
}
calculate(params: { shifts: PerDiemShiftInfo[]; rate: number }) {
const isFieldDutyDay = params.shifts.some((shift) => shift.isFieldDutyShift);
// Only field-duty hours count toward the tier threshold
const totalHours = params.shifts
.filter((shift) => shift.isFieldDutyShift)
.reduce((sum, shift) => sum + shift.hours, 0);
if (!isFieldDutyDay) {
return {
isFieldDutyDay: false,
diemInfo: { tier: null, points: 0, amount: 0 },
};
}
const { tier, points } = this.getTier(totalHours);
return {
isFieldDutyDay: true,
diemInfo: {
tier,
points,
amount: params.rate * points,
},
};
}
}
Check for field duty
If none of the day’s shifts are marked as field-duty (isDuty: true), the calculator immediately returns an empty result with isFieldDutyDay: false.
Sum field-duty hours
Only the hours from shifts where isFieldDutyShift is true are summed. Non-duty shifts on the same day do not contribute to the tier threshold.
Resolve tier and points
The getTier() helper maps the total duty hours to the matching tier and point value.
Calculate amount
amount = rate × points. The rate is supplied by TimelinePerDiemRateResolver for the selected year/month.
DefaultPerDiemMonthCalculator
At the month level, DefaultPerDiemMonthCalculator accumulates all daily PerDiemInfo records into a single monthly total. Note that the monthly PerDiemInfo has tier: null — the tier concept is only meaningful per day:
class DefaultPerDiemMonthCalculator implements PerDiemMonthReducer {
createEmpty(): PerDiemInfo {
return { tier: null, points: 0, amount: 0 };
}
accumulate(base: PerDiemInfo, add: PerDiemInfo): PerDiemInfo {
return {
tier: null, // no per-month tier concept
points: base.points + add.points,
amount: base.amount + add.amount,
};
}
subtract(base: PerDiemInfo, sub: PerDiemInfo): PerDiemInfo {
return {
tier: null,
points: Math.max(base.points - sub.points, 0),
amount: Math.max(base.amount - sub.amount, 0),
};
}
}
TimelinePerDiemRateResolver
The per-diem rate changes over time as government regulations are updated. TimelinePerDiemRateResolver stores a timeline of historical rates and selects the most recent entry that is on or before the requested year/month:
class TimelinePerDiemRateResolver
implements Resolver<{ year: number; month: number }, number>
{
private readonly timeline = [
{ year: 2000, month: 1, rateA: 33.9 }, // rate effective from Jan 2000
{ year: 2024, month: 9, rateA: 36.3 }, // rate effective from Sep 2024
];
resolve(params: { year: number; month: number }): number {
const { year, month } = params;
const applicable = this.timeline
.filter(
(entry) =>
entry.year < year || (entry.year === year && entry.month <= month),
)
.sort((a, b) =>
a.year !== b.year ? b.year - a.year : b.month - a.month,
);
return applicable[0]?.rateA ?? 0;
}
}
| Effective from | Tier A rate per point |
|---|
| January 2000 | ₪33.90 |
| September 2024 | ₪36.30 |
Meal Allowance
Type Definitions
type MealAllowanceKind = "SMALL" | "LARGE";
interface MealAllowanceRates {
small: number; // ILS rate per small-allowance point
large: number; // ILS rate per large-allowance point
}
interface MealAllowanceEntry {
points: number; // 0 or 1
amount: number; // points × rate
}
interface MealAllowance {
small: MealAllowanceEntry; // night-shift meal allowance
large: MealAllowanceEntry; // long-shift or non-field-duty day-shift allowance
}
Meal allowance inputs are bundled in a MealAllowanceDayInfo object:
type MealAllowanceDayInfo = {
totalHours: number; // total hours worked in the day
hasMorning: boolean; // shift includes hours in the morning (day-shift indicator)
hasNight: boolean; // shift includes overnight / late-night hours
isFieldDutyDay: boolean;
};
type MealAllowanceCalcParams = {
day: MealAllowanceDayInfo;
rate: number; // resolved from TimelineMealAllowanceRateResolver
};
SmallMealAllowanceCalculator
The small meal allowance is awarded on any shift that includes night hours:
class SmallMealAllowanceCalculator
implements Calculator<MealAllowanceCalcParams, MealAllowanceEntry>
{
calculate(params: MealAllowanceCalcParams): MealAllowanceEntry {
const { day, rate } = params;
if (day.hasNight) return { points: 1, amount: rate };
return { points: 0, amount: 0 };
}
}
| Condition | Points | Amount |
|---|
hasNight === true | 1 | rate |
hasNight === false | 0 | 0 |
LargeMealAllowanceCalculator
The large meal allowance has more complex eligibility rules — it requires at least 10 hours worked and its award depends on the shift’s time-of-day profile and whether it is a field-duty day:
class LargeMealAllowanceCalculator
implements Calculator<MealAllowanceCalcParams, MealAllowanceEntry>
{
calculate(params: MealAllowanceCalcParams): MealAllowanceEntry {
const { day, rate } = params;
const { totalHours, hasMorning, hasNight, isFieldDutyDay } = day;
// Minimum 10 hours required
if (totalHours < 10) return { points: 0, amount: 0 };
// Split shifts (morning + night) always qualify
if (hasMorning && hasNight) {
return { points: 1, amount: rate };
}
const isDayShift = hasMorning && !hasNight;
// Night-only or evening shifts qualify
if (!isDayShift) return { points: 1, amount: rate };
// Day shifts qualify only when NOT a field-duty day
if (isDayShift && !isFieldDutyDay) return { points: 1, amount: rate };
// Day shift + field duty = no large allowance
return { points: 0, amount: 0 };
}
}
Eligibility Matrix
Rationale
totalHours | Shift profile | isFieldDutyDay | Large allowance |
|---|
| < 10 | any | any | ❌ Not awarded |
| ≥ 10 | Morning and night | any | ✅ Awarded |
| ≥ 10 | Night only (no morning) | any | ✅ Awarded |
| ≥ 10 | Morning only (day shift) | false | ✅ Awarded |
| ≥ 10 | Morning only (day shift) | true | ❌ Not awarded |
The rule reflects the principle that a field-duty worker who works a standard day shift already receives a per-diem allowance to cover meals. The large meal allowance is therefore only awarded to day-shift field workers if per-diem does not apply (i.e., isFieldDutyDay is false).
TimelineMealAllowanceRateResolver
Like the per-diem resolver, meal allowance rates are tracked historically and selected by year/month:
class TimelineMealAllowanceRateResolver
implements Resolver<{ year: number; month: number }, MealAllowanceRates>
{
private readonly timeline = [
{ year: 2000, month: 1, rates: { small: 13.5, large: 19.7 } },
{ year: 2024, month: 9, rates: { small: 14.5, large: 21.1 } },
];
resolve(params: { year: number; month: number }): MealAllowanceRates {
const { year, month } = params;
const applicable = this.timeline
.filter(
(entry) =>
entry.year < year || (entry.year === year && entry.month <= month),
)
.sort((a, b) =>
a.year !== b.year ? b.year - a.year : b.month - a.month,
);
return applicable[0]?.rates ?? { small: 0, large: 0 };
}
}
| Effective from | Small allowance rate | Large allowance rate |
|---|
| January 2000 | ₪13.50 | ₪19.70 |
| September 2024 | ₪14.50 | ₪21.10 |
When calculating payroll for a past period, make sure to select the correct year and month in the Configuration panel. Both TimelinePerDiemRateResolver and TimelineMealAllowanceRateResolver look up rates as of the selected period — choosing the wrong month could apply a newer (or older) rate to historical shifts.