Overtime Calculation Engines

Overtime calculation engines are the deterministic core inside the Payroll Calculation Engines & Validation Rules framework that turns raw timekeeping punches into legally defensible premium pay, and they fail in expensive, silent ways when treated as simple arithmetic. The hard problem is not multiplying hours by 1.5; it is deciding which hours qualify, at which multiplier, under which jurisdiction’s threshold, computed against a regular rate that has to absorb non-discretionary bonuses and shift differentials before any premium is applied. Get the threshold precedence wrong and California daily overtime gets swallowed by a weekly accumulator; store the rate as binary float and a $0.005 rounding drift compounds into a wage-and-hour claim. This page covers how to normalize timecards at the boundary, resolve the controlling threshold rule by jurisdiction and effective date, compute the regular rate under 29 CFR § 778.208, and emit a Decimal-precise gross that the downstream withholding kernel can trust.

Deterministic overtime engine data flow UTC-aware timecard punches enter boundary, schema, and normalization, which anchors timestamps in UTC, requires an ISO-3166-2 worksite jurisdiction, computes DST-safe duration, and applies Decimal-safe rounding. Records missing shift_end, with negative duration, or with an unresolvable jurisdiction branch to a quarantine/review queue rather than being defaulted. Clean blocks enter the threshold resolver, which applies three ordered stages: an effective-date filter on a half-open window (effective less than or equal to work_date, less than expiration), a jurisdiction filter resolving Municipal over State over Federal, and threshold precedence that evaluates daily overtime before the weekly cap. The resolved rule set feeds the regular rate under 29 CFR 778.208 (base plus shift differential plus apportioned non-discretionary bonus across all hours) and Decimal premium tiers at 1.0x, 1.5x, and 2.0x, producing a quantized Decimal gross that is the taxable-wage input to the gross-to-net withholding kernel. When no rule is active for a jurisdiction the resolver routes to a federal weekly-only fallback (40-hour, 1.5x, fallback_triggered True), serializes the record to a dead-letter queue, and trips a circuit breaker when fallback volume exceeds 0.5 percent of the run. Timecard punches UTC clock feed · raw, timezone-naive Boundary, schema & normalization UTC anchor · ISO-3166-2 jurisdiction DST-safe duration · Decimal · rounding Threshold resolver 1 · Effective-date filter — effective ≤ work_date < exp 2 · Jurisdiction — Municipal ▸ State ▸ Federal 3 · Precedence — daily overtime before weekly cap returns one rule set, or routes to fallback resolved rule set Regular rate & premium tiers § 778.208 blended rate (base + diff + bonus) tiers 1.0× · 1.5× · 2.0× — Decimal Decimal gross → withholding kernel quantized cents · taxable-wage input reject Quarantine / review missing shift_end · negative duration unresolvable jurisdiction (no default) no rule Federal weekly-only fallback 40-hr · 1.5× · fallback_triggered = True Dead-letter queue serialize + alert ops Circuit breaker trips > 0.5%

An overtime engine is not a labor-policy authority. It never invents thresholds, never overrides statutory precedence, and never papers over a missing jurisdiction code with a convenient default. It resolves exactly one authoritative rule set per employee per workweek, applies it deterministically, and either produces a reproducible result or fails loudly into a fallback path that a compliance officer can review.

Data Normalization & Boundary Enforcement

Timecard data arrives noisy: timezone-naive punches, DST boundary crossings, rounding artifacts from clock hardware, and shift differentials encoded inconsistently across sites. None of that can reach the multiplier stage. Normalization runs at the edge, enforcing the same Data Boundary Definitions the rest of the platform uses, so the calculation layer only ever sees clean, attributable, UTC-anchored time blocks.

The field-level constraints that matter most:

  • UTC anchoring with explicit offset. Every shift_start and shift_end is stored as a timezone-aware UTC timestamp. A naive datetime is a hard rejection, not a coerced guess, because the worked-hours count for a graveyard shift depends entirely on whether the spring-forward hour is counted once or duplicated.
  • Jurisdiction anchor. Every record carries a resolved ISO-3166-2 jurisdiction_code (the employee’s worksite, not the employer’s headquarters). An unscoped record cannot select a daily-overtime rule and is quarantined rather than defaulted to the federal baseline.
  • Decimal monetary fields. base_hourly_rate, shift_differential, and non_discretionary_bonus use Decimal precision end to end. The regular rate is a divisor in the premium formula, so a single binary-float field propagates rounding error into every premium hour it touches.
  • Rounding policy applied to duration only. Jurisdiction-specific punch rounding (nearest quarter-hour, nearest tenth) is applied to the raw worked duration before threshold evaluation, and both the pre-round and post-round values are logged so an auditor can reconstruct the decision.

Quarantine conditions specific to overtime:

  • Missing shift_end. An open punch cannot be assigned hours; route it to review rather than assuming a 24-hour shift or zero.
  • Negative or zero duration. shift_end <= shift_start after timezone resolution is a clock or feed artifact; reject it before it produces negative premium pay.
  • Unresolvable jurisdiction. A blank or unknown jurisdiction_code is held out, because guessing the wrong state silently drops daily overtime in California or Colorado.
  • Implausible duration. A single shift exceeding a configurable ceiling (commonly 16–24 hours) is flagged as a likely missed clock-out, not silently paid at double time.

Like the idempotent ingestion used upstream, normalization must be repeatable: re-running the same punch file produces byte-identical normalized blocks, which is what makes a payroll retry safe.

Jurisdictional Resolution & Effective Dating

The federal floor under the FLSA is 1.5× the regular rate for hours over 40 in a workweek for non-exempt employees (29 CFR § 778.107). State and local rules layer on top: California adds daily overtime past 8 hours and double time past 12, Colorado adds daily overtime past 12, and several jurisdictions add seventh-consecutive-day premiums. Resolution therefore follows a strict Municipal > State > Federal override hierarchy, applied after a temporal-validity filter so the engine first narrows to rules in force on the workweek’s dates and only then selects the most specific jurisdiction. This is the same override discipline the FLSA Threshold Mapping gate uses to resolve exempt versus non-exempt status before gross-to-net begins; exempt employees never reach this engine at all.

Effective-date windows are half-open — effective_date <= work_date < expiration_date — so a rule’s last active day is unambiguous and the minimum-wage or threshold change that lands mid-pay-period applies to the correct shifts. Overlapping windows for the same jurisdiction_code are a load-time error, never a runtime tie-break: two threshold rule sets valid on the same day make resolution non-deterministic, so the loader rejects the pair before activation. Retroactive runs resolve against the rule version active at the original work date, not the run date, which keeps an amended paycheck consistent with the rules that governed the period.

Threshold precedence within the resolved rule set is the part naive engines get wrong. Daily thresholds evaluate before weekly accumulation, and hours already paid as daily overtime are not double-counted toward the 40-hour weekly cap. The ordering is: regular hours, then daily overtime, then daily double time, then weekly overtime on any straight-time hours still below the daily threshold across the week. The full seventh-day and 12-hour double-time interaction is worked end to end in Calculating Double Overtime for California.

Daily precedence before the weekly cap A California 10-hour shift is partitioned at the 8-hour daily threshold into 8 hours of regular time paid at 1.0x and 2 hours of daily overtime paid at 1.5x. When the weekly accumulator applies the 40-hour cap, only the 8 straight-time hours are counted toward it; the 2 daily-overtime hours have already been paid at the daily premium and are excluded, so no worked hour is counted in both the daily and weekly tiers. California 10-hour shift — daily precedence before the weekly cap Daily evaluation 8-h daily threshold 8 h regular @ 1.0× 2 h daily OT @ 1.5× 0 h 10 h Weekly accumulator (40-h cap) + 8 straight-time hrs counted toward 40-h cap 2 daily-OT hrs excluded — never double-counted

The regular rate itself is computed per the weighted-average method in 29 CFR § 778.208 — non-discretionary bonuses and shift differentials are folded into the rate across all hours worked in the period, not bolted onto the premium hours alone:

Rreg=(Htotal×rbase)+Ddiff+BnondiscHtotalR_{\text{reg}} = \frac{(H_{\text{total}} \times r_{\text{base}}) + D_{\text{diff}} + B_{\text{nondisc}}}{H_{\text{total}}}

where HtotalH_{\text{total}} is total hours worked, rbaser_{\text{base}} the base hourly rate, DdiffD_{\text{diff}} total shift-differential pay, and BnondiscB_{\text{nondisc}} non-discretionary bonus apportioned to the period. The premium owed on overtime hours is then Hot×Rreg×(m1)H_{\text{ot}} \times R_{\text{reg}} \times (m - 1) when straight time has already been paid at RregR_{\text{reg}}, or Hot×Rreg×mH_{\text{ot}} \times R_{\text{reg}} \times m when computing the full premium line, where mm is the jurisdictional multiplier (1.5 or 2.0).

Production Implementation Pattern

The engine below is runnable and PEP 8-clean. Rules are frozen dataclasses validated at construction; the resolver applies the temporal-then-jurisdiction filter; all arithmetic is Decimal; every calculation emits a structured key=value log line; and an unmapped jurisdiction routes to a federal weekly-only fallback with an explicit flag rather than crashing the run.

from dataclasses import dataclass, field
from datetime import date, datetime, timezone
from decimal import Decimal, ROUND_HALF_UP
from typing import Dict, List, Optional
import hashlib
import logging

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s level=%(levelname)s %(message)s",
)
logger = logging.getLogger("overtime_engine")

ZERO = Decimal("0")
CENTS = Decimal("0.01")
RATE_Q = Decimal("0.0001")


@dataclass(frozen=True)
class OvertimeRule:
    jurisdiction_code: str            # ISO-3166-2 worksite code or "FEDERAL"
    effective_date: date
    expiration_date: Optional[date]
    daily_ot_threshold: Decimal       # hours/day before 1.5x; high value disables
    daily_dt_threshold: Decimal       # hours/day before 2.0x; high value disables
    weekly_ot_threshold: Decimal      # hours/week before 1.5x
    ot_multiplier: Decimal = Decimal("1.5")
    dt_multiplier: Decimal = Decimal("2.0")
    rounding_increment: Decimal = Decimal("0.25")

    def __post_init__(self) -> None:
        if self.daily_dt_threshold < self.daily_ot_threshold:
            raise ValueError(
                f"jurisdiction={self.jurisdiction_code} "
                "daily double-time threshold below overtime threshold"
            )
        if self.rounding_increment <= ZERO:
            raise ValueError(
                f"jurisdiction={self.jurisdiction_code} rounding_increment must be positive"
            )


@dataclass(frozen=True)
class Shift:
    employee_id: str
    jurisdiction_code: str
    start_utc: datetime
    end_utc: datetime
    base_hourly_rate: Decimal
    shift_differential: Decimal = Decimal("0.00")
    non_discretionary_bonus: Decimal = Decimal("0.00")

    def __post_init__(self) -> None:
        for label, ts in (("start_utc", self.start_utc), ("end_utc", self.end_utc)):
            if ts.tzinfo is None or ts.utcoffset() != timezone.utc.utcoffset(None):
                raise ValueError(f"emp={self.employee_id} {label} must be UTC-aware")
        if self.end_utc <= self.start_utc:
            raise ValueError(f"emp={self.employee_id} end_utc must follow start_utc")


@dataclass
class OvertimeResult:
    employee_id: str
    pay_period_id: str
    regular_hours: Decimal
    ot_hours: Decimal
    dt_hours: Decimal
    regular_rate: Decimal
    gross_pay: Decimal
    audit_hash: str
    fallback_triggered: bool = False
    quarantined: bool = False


FALLBACK_RULE = OvertimeRule(
    jurisdiction_code="FEDERAL",
    effective_date=date(1938, 10, 24),
    expiration_date=None,
    daily_ot_threshold=Decimal("9999"),   # disable daily OT
    daily_dt_threshold=Decimal("9999"),   # disable daily DT
    weekly_ot_threshold=Decimal("40"),
)


class OvertimeEngine:
    def __init__(self, rules: List[OvertimeRule]) -> None:
        self._index: Dict[str, List[OvertimeRule]] = {}
        for rule in rules:
            self._index.setdefault(rule.jurisdiction_code, []).append(rule)

    def _resolve_rule(self, code: str, work_date: date) -> tuple[OvertimeRule, bool]:
        candidates = [
            r for r in self._index.get(code, [])
            if r.effective_date <= work_date
            and (r.expiration_date is None or work_date < r.expiration_date)
        ]
        if not candidates:
            logger.warning(
                "fallback_routing jurisdiction=%s work_date=%s reason=no_active_rule",
                code, work_date,
            )
            return FALLBACK_RULE, True
        if len({(r.effective_date, r.expiration_date) for r in candidates}) != len(candidates):
            raise ValueError(f"overlapping rule windows jurisdiction={code}")
        return candidates[0], False

    def _worked_hours(self, shift: Shift, increment: Decimal) -> Decimal:
        seconds = Decimal(str((shift.end_utc - shift.start_utc).total_seconds()))
        raw = seconds / Decimal("3600")
        rounded = (raw / increment).quantize(Decimal("1"), ROUND_HALF_UP) * increment
        logger.info(
            "rounding emp=%s raw_hours=%s rounded_hours=%s increment=%s",
            shift.employee_id, raw, rounded, increment,
        )
        return rounded

    def _regular_rate(self, shift: Shift, hours: Decimal) -> Decimal:
        if hours == ZERO:
            return shift.base_hourly_rate
        straight = hours * shift.base_hourly_rate
        total = straight + shift.shift_differential + shift.non_discretionary_bonus
        return (total / hours).quantize(RATE_Q, ROUND_HALF_UP)

    def calculate(
        self, shift: Shift, pay_period_id: str, work_date: date
    ) -> OvertimeResult:
        rule, fallback = self._resolve_rule(shift.jurisdiction_code, work_date)
        hours = self._worked_hours(shift, rule.rounding_increment)

        # Daily precedence: regular -> 1.5x -> 2.0x, evaluated before weekly cap.
        dt_hours = max(ZERO, hours - rule.daily_dt_threshold)
        ot_hours = max(ZERO, min(hours, rule.daily_dt_threshold) - rule.daily_ot_threshold)
        regular_hours = hours - ot_hours - dt_hours

        rate = self._regular_rate(shift, hours)
        gross = (
            regular_hours * rate
            + ot_hours * rate * rule.ot_multiplier
            + dt_hours * rate * rule.dt_multiplier
        ).quantize(CENTS, ROUND_HALF_UP)

        payload = f"{shift.employee_id}|{pay_period_id}|{hours}|{rate}|{gross}"
        audit_hash = hashlib.sha256(payload.encode()).hexdigest()[:16]

        logger.info(
            "overtime_calculated emp=%s period=%s reg_hours=%s ot_hours=%s "
            "dt_hours=%s rate=%s gross=%s fallback=%s hash=%s",
            shift.employee_id, pay_period_id, regular_hours, ot_hours,
            dt_hours, rate, gross, fallback, audit_hash,
        )
        return OvertimeResult(
            employee_id=shift.employee_id,
            pay_period_id=pay_period_id,
            regular_hours=regular_hours,
            ot_hours=ot_hours,
            dt_hours=dt_hours,
            regular_rate=rate,
            gross_pay=gross,
            audit_hash=audit_hash,
            fallback_triggered=fallback,
        )

The engine runs before the gross-to-net kernel: its Decimal gross is the taxable-wage input that Tax Bracket Validation withholds against and that Deduction Mapping Rules apply pre- and post-tax elections to. Weekly accumulation is performed by summing per-shift OvertimeResult records across the workweek and applying the weekly threshold to straight-time hours that fell below the daily threshold, so daily premium hours are never double-counted toward the 40-hour cap.

Compliance Verification & Fallback Routing

Promote an overtime rule set to production only after this sequence passes in CI. Each step is a deterministic gate, not a manual spot-check.

  1. Threshold boundary test. Feed shifts at 7.99, 8.00, and 8.01 hours (and 11.99 / 12.00 / 12.01 for double time) and assert the premium tier flips exactly at the threshold, not one increment early or late.
  2. Daily-before-weekly precedence test. Construct a workweek where daily overtime and the 40-hour weekly cap both apply, and assert hours paid at the daily premium are excluded from the weekly accumulator so no hour is counted twice.
  3. Regular-rate reconciliation. Assert the § 778.208 weighted average folds non-discretionary bonuses and shift differentials across all hours worked, and that regular_rate >= base_hourly_rate for any record carrying a differential or bonus.
  4. Effective-date drift test. Replay the prior three pay periods against historical rule snapshots and confirm resolution returns the version active on each original work date, not the current one.
  5. Decimal precision check. Confirm every monetary field round-trips as Decimal and that no float enters the rate or gross path; a lint rule bans float in the engine modules.
  6. Rounding-audit check. Verify the pre-round and post-round duration logs match the jurisdiction’s policy; any deviation beyond one rounding increment quarantines the record.
  7. Fallback activation test. Submit an unmapped jurisdiction_code and assert the record resolves to the federal weekly-only baseline with fallback_triggered=True and lands in the review queue.

Production deployments need an explicit fallback chain, built on the same Fallback Routing Strategies used across the platform, so a data gap never halts a payroll run:

  1. Primary resolution — exact jurisdiction_code plus temporal match selects the controlling daily/weekly rule set.
  2. Federal fallback — when no state or local rule is active, route to the FLSA weekly-only baseline (40-hour, 1.5×) with daily premiums disabled and flag the record.
  3. Dead-letter queue — any shift that fails normalization (missing shift_end, negative duration, unresolvable jurisdiction) is serialized with full context to a DLQ and alerts payroll operations rather than being paid on a guess.
  4. Circuit breaker — if fallback or DLQ volume exceeds 0.5% of shifts in a run, the breaker trips and holds the batch for review, because that scale of resolution failure signals a corrupted jurisdiction table, not isolated bad rows.

Failure Modes & Gotchas

  • Float-stored regular rate. Storing base_hourly_rate or the computed rate as float lets binary rounding move a true $32.5000 to $32.49999…, and because the rate multiplies every premium hour, the error compounds into a wage-and-hour exposure across the workforce. Fix: Decimal end to end, a type assertion at the engine boundary, and a lint rule banning float.
  • Weekly accumulator eating daily overtime. Adding all hours into a single weekly bucket and only then applying the 40-hour cap silently drops California daily overtime, because hours already over 8/day get absorbed into straight time under 40/week. Fix: evaluate daily thresholds first and exclude daily-premium hours from the weekly accumulator.
  • Missing jurisdiction code. A blank or wrong jurisdiction_code falls through to the federal baseline and skips state daily premiums entirely. Fix: require a resolved ISO-3166-2 worksite code at ingestion and quarantine records that cannot be scoped, rather than defaulting to FEDERAL.
  • DST shift miscount. Computing duration from local wall-clock times double-counts or omits the spring-forward/fall-back hour, mispaying any overnight shift on the changeover weekend. Fix: anchor punches in UTC and compute duration in UTC, resolving to local time only for threshold-day attribution.
  • Bonus applied only to premium hours. Folding a non-discretionary bonus into the multiplier line instead of the regular rate underpays the § 778.208 weighted average. Fix: amortize the bonus across all hours worked in the period before computing the premium.

Frequently Asked Questions

Should daily or weekly overtime be evaluated first?

Daily, always — and the hours paid at a daily premium must be removed from the weekly accumulator. In California a 10-hour shift produces 2 daily-overtime hours; those 2 hours are paid at 1.5× and do not also count toward the 40-hour weekly threshold. Bucketing everything into a weekly total first and applying 40/week last is the most common way engines silently under-pay daily-overtime states. The daily-before-weekly precedence test asserts no hour is counted in both tiers.

Do non-discretionary bonuses change the overtime rate?

Yes. Under 29 CFR § 778.208 the regular rate is a weighted average that folds non-discretionary bonuses and shift differentials across all hours worked in the period, so the overtime premium is computed on the higher blended rate, not the base hourly wage alone. A production bonus, attendance bonus, or contractual shift differential all raise the regular rate; a truly discretionary, unannounced bonus does not. The regular-rate reconciliation gate asserts regular_rate >= base_hourly_rate whenever a differential or bonus is present.

How does the engine handle a shift that crosses a daylight-saving boundary?

Duration is computed from UTC-anchored timestamps, so the worked-hours count is correct regardless of the local clock change — an overnight shift that spans spring-forward yields one fewer wall-clock hour but the right number of actual worked hours. Local time is used only to attribute the shift to the correct threshold day. Naive datetimes are rejected at the boundary precisely so a graveyard shift on a changeover weekend cannot be silently mispaid.

What happens when a worksite's jurisdiction code is not in the rule table?

The resolver routes to the federal weekly-only baseline (40-hour, 1.5×, daily premiums disabled), sets fallback_triggered=True, and flags the record for compliance review rather than crashing the run. That is deliberately conservative: it guarantees the FLSA floor is met but does not assume any state daily premium that might actually apply. If fallback volume exceeds 0.5% of the run, the circuit breaker trips, because that usually means a stale jurisdiction table rather than a handful of edge cases.

Why round duration before threshold evaluation instead of after?

Because the threshold is a step function: whether a shift produces overtime depends on the rounded duration, so rounding has to happen first and be logged. Rounding the premium dollars after the fact instead would let a 7.97-hour punch that rounds to 8.00 either gain or lose an overtime increment depending on order of operations. The engine applies the jurisdiction’s rounding increment to the raw duration, logs both pre- and post-round values, and the rounding-audit check quarantines any record whose logged values do not match policy.

External Compliance References