import { type CalculationRule, type SpanCategory, deriveDaySpans } from "./_timeCalculation"; import type { TimeClassificationStrategy } from "./timeClassificationStrategy"; import type { Schema, WorkRule } from "./types"; function toCalculationRule(workRule: WorkRule): CalculationRule { return { clockInRounding: { unitMinutes: workRule.clockInRoundingUnitMinutes, direction: workRule.clockInRoundingDirection, }, clockOutRounding: { unitMinutes: workRule.clockOutRoundingUnitMinutes, direction: workRule.clockOutRoundingDirection, }, breakRounding: { unitMinutes: workRule.breakRoundingUnitMinutes, direction: workRule.breakRoundingDirection, }, breakDeductionMinutes: workRule.breakDeductionMinutes, dailyOvertimeThresholdMinutes: workRule.dailyOvertimeThresholdMinutes, nightWindowStart: workRule.nightWindowStart, nightWindowEnd: workRule.nightWindowEnd, }; } /** * The exact TimeEntryCode `category` keys the bundled JP strategy emits as each span's * `codeCategory`. calculateTimeBlocks/recalculateRange resolve a span to a TimeEntryCode by matching * this key against `TimeEntryCode.category` (and `WorkRule.premiumRatePercent[].category`), so a * consumer on the default strategy MUST seed those catalogs with exactly these strings — otherwise * resolution returns nothing and derivation fails at runtime. Exposed as the single source of truth * (previously only the private `spanCodeCategory` switch knew them) so callers reference these rather * than hardcoding string literals. */ export const jpTimeEntryCodeCategories = { /** REGULAR worked time. */ WORK: "WORK", /** Daily statutory-excess overtime (beyond the legal limit). */ OVERTIME_BEYOND_STATUTORY: "OVERTIME_BEYOND_STATUTORY", /** Late-night window, stacked as an independent premium axis. */ NIGHT: "NIGHT", /** Statutory holiday. */ HOLIDAY_STATUTORY: "HOLIDAY_STATUTORY", /** Prescribed (company) holiday. */ HOLIDAY_PRESCRIBED: "HOLIDAY_PRESCRIBED", } as const; // JP mapping from the internal span category to the TimeEntryCode category key bound for premium // determination: REGULAR→WORK, OVERTIME→OVERTIME_BEYOND_STATUTORY (daily statutory-excess), // NIGHT→NIGHT (late-night, stacked), HOLIDAY→statutory/prescribed by the holiday kind. function spanCodeCategory( category: SpanCategory, holidayKind: "STATUTORY" | "PRESCRIBED" | null, ): string { switch (category) { case "REGULAR": return jpTimeEntryCodeCategories.WORK; case "OVERTIME": return jpTimeEntryCodeCategories.OVERTIME_BEYOND_STATUTORY; case "NIGHT": return jpTimeEntryCodeCategories.NIGHT; case "HOLIDAY": return holidayKind === "PRESCRIBED" ? jpTimeEntryCodeCategories.HOLIDAY_PRESCRIBED : jpTimeEntryCodeCategories.HOLIDAY_STATUTORY; } } /** * Japan (Labor Standards Act) time-classification strategy — the bundled default. It decomposes * the workday into REGULAR / OVERTIME (daily threshold) / NIGHT (late-night window, stacked as an * independent premium axis) / HOLIDAY (statutory vs prescribed) spans from the WorkRule's * parameters, and maps each to its TimeEntryCode category key. It is a pure per-workday * calculation and does not use the transaction. */ export const jpTimeClassificationStrategy: TimeClassificationStrategy = { classify(_db, { workDate, reportedBlocks, workRule, holidayKind }) { const spans = deriveDaySpans({ workDate, reportedBlocks, rule: toCalculationRule(workRule), holidayKind, }); return spans.map((span) => ({ category: span.category, codeCategory: spanCodeCategory(span.category, holidayKind), startAt: span.startAt, endAt: span.endAt, minutes: span.minutes, calculationTagKeys: span.calculationTagKeys, sourceReportedBlockIds: span.sourceReportedBlockIds, })); }, };