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.

Composition pipelines are Shiftly’s dependency-wiring layer. Each pipeline function constructs a group of related domain objects, resolves their dependencies from already-assembled groups, and returns a typed record of instances. No component ever instantiates its own dependencies — they are always injected from outside. This means every dependency edge in the domain layer is explicit, testable in isolation, and replaceable without touching the components that consume it.

Why Pipelines Exist

In a system where calculators, resolvers, and builders all depend on each other in specific ways, ad-hoc instantiation leads to hidden coupling. Pipelines solve this by making the dependency graph visible and centralized:
  • Swapping a calculator implementation requires changing exactly one pipeline function.
  • Integration tests can construct a pipeline directly and exercise the whole chain without a browser or Redux store.
  • The domain has no ambient globals or module-level singletons — everything flows through buildPayMapPipeline().

Pipeline Execution Order

The six pipeline functions must be called in dependency order. composition.ts orchestrates this sequence:
buildCoreServices()    buildResolvers()    buildCalculators()
         |                   |                   |
         +-------------------+-------------------+
         |                   |                   |
   buildShiftLayer()   buildDayLayer()    buildMonthLayer()
         |                   |                   |
         +-------------------+-------------------+
                             |
                      PayMapPipeline

buildCoreServices()CoreServices

The first pipeline creates the two foundational services that the rest of the domain depends on.
export const buildCoreServices = (): CoreServices => {
  const dateService = new DateService();
  const shiftService = new ShiftService(dateService);
  return { dateService, shiftService };
};

export interface CoreServices {
  dateService: DateService;
  shiftService: ShiftService;
}
DateService provides date arithmetic: minutes-from-midnight calculations, day difference, date formatting, DST-aware special-day start time, and month boundary helpers. All date operations in the domain flow through this service. ShiftService wraps DateService with shift-specific helpers: total shift duration in hours, cross-midnight detection, and shift validity checks.

buildResolvers()Resolvers

The resolver pipeline creates all context and rate-lookup instances. It takes no arguments — resolvers have no dependencies on services or calculators.
export const buildResolvers = (): Resolvers => {
  const holidayResolver = new HolidayResolverService();
  const workDayInfoResolver = new WorkDayInfoResolver();
  const monthResolver = new DefaultMonthResolver();
  const perDiemRateResolver = new TimelinePerDiemRateResolver();
  const mealAllowanceRateResolver = new TimelineMealAllowanceRateResolver();
  return {
    holidayResolver,
    workDayInfoResolver,
    monthResolver,
    perDiemRateResolver,
    mealAllowanceRateResolver,
  };
};

export interface Resolvers {
  holidayResolver: HolidayResolverService;
  workDayInfoResolver: WorkDayInfoResolver;
  monthResolver: DefaultMonthResolver;
  perDiemRateResolver: TimelinePerDiemRateResolver;
  mealAllowanceRateResolver: TimelineMealAllowanceRateResolver;
}
See Domain Resolvers for the behavior of each resolver.

buildCalculators()Calculators

The calculator pipeline creates all computation instances. Like resolvers, it takes no arguments — calculators are pure functions with no external dependencies.
export const buildCalculators = (): Calculators => {
  const regularByShift = RegularFactory.byShift();
  const regularByDay = RegularFactory.byDay();
  const regularAccumulator = RegularFactory.monthReducer();

  const extraCalculator = new ExtraCalculator();
  const specialCalculator = new SpecialCalculator();

  const sickCalculator = new FixedSegmentFactory();
  const vacationCalculator = new FixedSegmentFactory();
  const extraShabbatCalculator = new FixedSegmentFactory();

  const largeMealAllowanceCalculator = new LargeMealAllowanceCalculator();
  const smallMealAllowanceCalculator = new SmallMealAllowanceCalculator();

  const perDiemDayCalculator = new DefaultPerDiemDayCalculator();
  const perDiemMonthCalculator = new DefaultPerDiemMonthCalculator();

  return { regular, extra, special, fixedSegments, mealAllowance, perDiem };
};

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 Domain Calculators for the behavior of each calculator.

buildShiftLayer({ shiftService, calculators })ShiftLayer

The shift layer wires the segment resolver, segment builder, and shift map builder into a single shiftMapBuilder that produces a ShiftPayMap from any given shift.
export const buildShiftLayer = ({
  shiftService,
  calculators,
}: BuildShiftLayerParams): ShiftLayer => {
  const segmentResolver = new ShiftSegmentResolver();
  const shiftSegmentBuilder = new ShiftSegmentBuilder(segmentResolver, shiftService);

  const shiftsCalculators: PayCalculationBundle = {
    regular: calculators.regular.byShift,
    extra: calculators.extra,
    special: calculators.special,
  };

  const shiftMapBuilder = new DefaultShiftMapBuilder(
    shiftSegmentBuilder,
    shiftsCalculators,
    shiftService,
  );

  return { shiftMapBuilder };
};

export interface BuildShiftLayerParams {
  shiftService: ShiftService;
  calculators: Calculators;
}

export interface ShiftLayer {
  shiftMapBuilder: DefaultShiftMapBuilder;
}
The shift layer uses calculators.regular.byShift — the per-shift regular bracket calculator — rather than byDay. The per-day calculator is reserved for buildDayLayer.

buildDayLayer({ dateService, calculators, resolvers })DayLayer

The day layer assembles two builders:
  1. dayPayMapBuilder — aggregates ShiftPayMap[] into a single WorkDayMap, applying per-diem and meal allowance.
  2. workDaysForMonthBuilder — iterates a month’s calendar days and classifies each using holiday event data.
export const buildDayLayer = ({
  dateService,
  calculators,
  resolvers,
}: BuildDayLayerParams): DayLayer => {
  const payCalculatorBundle: PayCalculationBundle = {
    regular: calculators.regular.byDay,
    extra: calculators.extra,
    special: calculators.special,
  };

  const fixedSegmentBundle: FixedSegmentBundle = { ... };

  const perDiemBundle: PerDiemBundle = {
    calculator: calculators.perDiem.day,
    rateResolver: resolvers.perDiemRateResolver,
  };

  const mealAllowanceResolver = new MealAllowanceResolver(
    calculators.mealAllowance.large,
    calculators.mealAllowance.small,
  );

  const mealAllowanceBundle: MealAllowanceBundle = {
    resolver: mealAllowanceResolver,
    rateResolver: resolvers.mealAllowanceRateResolver,
  };

  const dayPayMapBuilder = new DefaultDayPayMapBuilder(
    payCalculatorBundle,
    fixedSegmentBundle,
    perDiemBundle,
    mealAllowanceBundle,
  );

  const workDaysForMonthBuilder = new DefaultWorkDaysForMonthBuilder(
    resolvers.holidayResolver,
    resolvers.workDayInfoResolver,
    dateService,
  );

  return { dayPayMapBuilder, workDaysForMonthBuilder };
};

export interface BuildDayLayerParams {
  dateService: DateService;
  calculators: Calculators;
  resolvers: Resolvers;
}

export interface DayLayer {
  dayPayMapBuilder: DefaultDayPayMapBuilder;
  workDaysForMonthBuilder: DefaultWorkDaysForMonthBuilder;
}
The MealAllowanceResolver is instantiated inside buildDayLayer rather than in buildResolvers because it directly depends on calculators.mealAllowance.* — it straddles the resolver/calculator boundary.

buildMonthLayer({ calculators })MonthLayer

The month layer assembles the MonthPayMapReducer — the root reducer that accumulates and subtracts WorkDayMap contributions into the running MonthPayMap.
export const buildMonthLayer = ({ calculators }: BuildMonthLayerParams): MonthLayer => {
  const workPay: WorkDayReducerBundle = {
    regular: calculators.regular.accumulator,
    extra: calculators.extra,
    special: calculators.special,
  };

  const fixedSegmentBundle: FixedSegmentBundle = { ... };

  const workPayMonthReducer = new WorkDayMonthReducer(workPay);
  const fixedMonthReducer = new FixedSegmentMonthReducer(fixedSegmentBundle);
  const allowancesMonthReducer = new MealAllowanceMonthReducer();

  const monthPayMapCalculator = new MonthPayMapReducer(
    workPayMonthReducer,
    fixedMonthReducer,
    allowancesMonthReducer,
    calculators.perDiem.month,
  );

  return { monthPayMapCalculator };
};

export interface BuildMonthLayerParams {
  calculators: Calculators;
}

export interface MonthLayer {
  monthPayMapCalculator: MonthPayMapReducer;
}
See Domain Reducers for how MonthPayMapReducer manages incremental state.

The PayMapPipeline Type

PayMapPipeline is the complete, assembled domain interface consumed by Redux and the application’s feature components.
export interface PayMapPipeline {
  payMap: {
    shiftMapBuilder: DefaultShiftMapBuilder;
    dayPayMapBuilder: DefaultDayPayMapBuilder;
    monthPayMapCalculator: MonthPayMapReducer;
    workDaysForMonthBuilder: DefaultWorkDaysForMonthBuilder;
  };
  resolvers: Resolvers;
  services: CoreServices;
}
All four builders and reducers are accessible under payMap. The resolvers subtree is exposed separately because feature components need direct access to monthResolver and workDayInfoResolver outside the pay calculation flow.

composition.ts — The Entry Point

composition.ts exports buildPayMapPipeline(), the single function that wires all six pipelines together and returns the PayMapPipeline:
export const buildPayMapPipeline = (): PayMapPipeline => {
  const services = buildCoreServices();
  const resolvers = buildResolvers();
  const calculators = buildCalculators();

  const shiftLayer = buildShiftLayer({
    shiftService: services.shiftService,
    calculators,
  });

  const dayLayer = buildDayLayer({
    dateService: services.dateService,
    calculators,
    resolvers,
  });

  const monthLayer = buildMonthLayer({ calculators });

  return {
    payMap: {
      shiftMapBuilder: shiftLayer.shiftMapBuilder,
      dayPayMapBuilder: dayLayer.dayPayMapBuilder,
      monthPayMapCalculator: monthLayer.monthPayMapCalculator,
      workDaysForMonthBuilder: dayLayer.workDaysForMonthBuilder,
    },
    resolvers: {
      perDiemRateResolver: resolvers.perDiemRateResolver,
      holidayResolver: resolvers.holidayResolver,
      workDayInfoResolver: resolvers.workDayInfoResolver,
      monthResolver: resolvers.monthResolver,
      mealAllowanceRateResolver: resolvers.mealAllowanceRateResolver,
    },
    services: {
      dateService: services.dateService,
      shiftService: services.shiftService,
    },
  };
};
This function is called once at application startup (typically in a domain.instance.ts module) and the resulting PayMapPipeline is passed into the Redux store configuration. All subsequent pay calculations throughout the application’s lifetime use the same pre-wired instances.
Because buildPayMapPipeline() is a plain function with no ambient dependencies, it can be called identically in unit tests, integration tests, and the production application. There is no mocking infrastructure needed to exercise the full domain pipeline in tests.

Build docs developers (and LLMs) love