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.

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:
TierField-duty hoursPoints
None< 4 h0
A4 h – 7 h 59 m1
B8 h – 11 h 59 m2
C≥ 12 h3

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,
      },
    };
  }
}
1

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.
2

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.
3

Resolve tier and points

The getTier() helper maps the total duty hours to the matching tier and point value.
4

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 fromTier 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 };
  }
}
ConditionPointsAmount
hasNight === true1rate
hasNight === false00

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 };
  }
}
totalHoursShift profileisFieldDutyDayLarge allowance
< 10anyany❌ Not awarded
≥ 10Morning and nightany✅ Awarded
≥ 10Night only (no morning)any✅ Awarded
≥ 10Morning only (day shift)false✅ Awarded
≥ 10Morning only (day shift)true❌ Not awarded

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 fromSmall allowance rateLarge 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.

Build docs developers (and LLMs) love