# Authoring a time-classification strategy

Time-tracking computes `CalculatedTimeBlock`s from reported time by delegating **how spans are
categorized and how premiums stack** to a pluggable `TimeClassificationStrategy`. The bundled
default is the Japan (Labor Standards Act) strategy; a consuming app injects its own strategy to
support another jurisdiction (US FLSA, AU Fair Work, …). This guide shows how to write one.

## Where the strategy plugs in

```ts
import {
  defineTimeTrackingModule,
  type TimeClassificationStrategy,
} from "@tailor-platform/erp-kit/module";

const usFlsaStrategy: TimeClassificationStrategy = {
  /* … */
};

const timeTracking = defineTimeTrackingModule({
  workforce: { /* … */ },
  userManagement: { /* … */ },
  approval: { /* … */ },
  // Omit `timeCalculation` to use the bundled JP strategy.
  timeCalculation: { strategy: usFlsaStrategy },
});
```

## What the core does vs. what the strategy does

The core, per Assignment + workday, resolves:

- the effective `WorkRule` (via the workforce context / `WorkRuleAssignment`),
- the `holidayKind` for that date (scoped to the rule's `holidayCalendarId`, or `null`),
- the current (non-superseded) `ReportedTimeBlock`s.

It then calls `strategy.classify(...)` and, for each returned span, **binds a `TimeEntryCode` by
the span's `codeCategory`** (honoring `WorkRule.premiumRatePercent` pins) and **stores the span's
`category`** on the `CalculatedTimeBlock`. The core owns no week/period concept, no rounding
policy beyond what the strategy applies, and no premium taxonomy — those all live in the strategy.

## The contract

```ts
interface ClassifyInput {
  assignmentId: string;
  workDate: Date;
  reportedBlocks: { id: string; blockType: "WORK" | "BREAK" | "STEP_OUT"; startAt: Date; endAt: Date }[];
  workRule: WorkRule;            // the resolved rule row (rounding, thresholds, night window, …)
  holidayKind: "STATUTORY" | "PRESCRIBED" | null;
}

interface ClassifiedSpan {
  category: string;              // stored on CalculatedTimeBlock.category (your vocabulary)
  codeCategory: string;         // used to bind a TimeEntryCode / premium pin (your vocabulary)
  startAt: Date;
  endAt: Date;
  minutes: number;
  calculationTagKeys: string[]; // audit trail of which rule fragments fired
  sourceReportedBlockIds: string[];
}

interface TimeClassificationStrategy {
  classify(db, input: ClassifyInput, ctx): ClassifiedSpan[] | Promise<ClassifiedSpan[]>;
}
```

- **`category` vs `codeCategory`** — both are open string keys your strategy defines. `category`
  is the coarse span type persisted for reporting/rollup; `codeCategory` is what the core matches
  against `TimeEntryCode.category` (and `WorkRule.premiumRatePercent[].category`) to pick the
  bound code. They may be identical if your jurisdiction needs only one axis.
- **Category vocabulary is yours.** Create `TimeEntryCode`s whose `category` matches the
  `codeCategory` values your strategy emits, and pin premiums on the `WorkRule` by those keys.

## Period context (e.g. US weekly overtime)

The core classifies **one workday at a time**. When a jurisdiction needs a wider window — US FLSA
overtime is weekly (> 40h/week), not daily — the strategy computes it itself using the passed
transaction. It is not a core concern.

```ts
const usFlsaStrategy: TimeClassificationStrategy = {
  async classify(db, { assignmentId, workDate, reportedBlocks, workRule }, ctx) {
    // Week-to-date worked minutes BEFORE this day, queried by the strategy itself.
    const weekStart = startOfFlsaWeek(workDate);
    const priorMinutes = await sumWorkedMinutes(db, assignmentId, weekStart, workDate);
    // …decompose reportedBlocks, then split minutes into REGULAR vs OVERTIME_WEEKLY once the
    // running weekly total crosses 40h; emit ClassifiedSpan[] with your category keys…
  },
};
```

Because re-derivation is per-day, design the weekly split to be **deterministic and idempotent**
for a given (assignment, week) — recomputing a mid-week day must yield the same result.

## Reference: the bundled JP strategy

`jpTimeClassificationStrategy` (exported from the module) is the worked example: daily overtime
threshold, a stacked late-night span as an independent premium axis, and statutory vs
prescribed holiday classification driven by the `WorkRule` parameters and `holidayKind`. Read it
as a template; a pure per-day strategy does not touch `db`.

## Skeleton

```ts
import type { TimeClassificationStrategy } from "@tailor-platform/erp-kit/module";

export const myJurisdictionStrategy: TimeClassificationStrategy = {
  classify(_db, { workDate, reportedBlocks, workRule, holidayKind }, _ctx) {
    const spans: ClassifiedSpan[] = [];
    // 1. Turn reportedBlocks (WORK/BREAK/STEP_OUT) into worked intervals, applying the
    //    WorkRule's rounding + break rules as your jurisdiction requires.
    // 2. Classify each interval into your category keys (regular / overtime / premium / holiday …),
    //    stacking overlapping premium axes as additional spans if needed.
    // 3. Push a ClassifiedSpan per resulting span, setting category + codeCategory to your keys.
    return spans;
  },
};
```
