Weighted-Average Regular Rate with Nondiscretionary Bonuses: Retroactive Overtime Recomputation

When a quarterly production bonus, an attendance bonus, or a safety bonus is paid out after the workweeks it covers have already closed and been disbursed, an engine that treats the bonus as a flat, standalone payment underpays every overtime hour in that period, because the bonus itself raises the regular rate those hours should have been paid against. This pattern sits inside Overtime Calculation Engines, the parent topic within the Payroll Calculation Engines & Validation Rules framework, and it isolates the bonus-allocation and retroactive-recompute stage as its own deterministic job so a closed pay period can be reopened, corrected, and re-disbursed without touching unrelated payroll runs.

Problem Framing

Under 29 CFR § 778.208, nondiscretionary bonuses — production bonuses, attendance bonuses, safety bonuses, and any bonus promised or announced in advance to induce performance — must be included in the regular rate of pay used to compute overtime. A bonus is discretionary, and therefore excluded, only when both the fact and the amount of payment remain in the employer’s sole discretion until at or near the end of the period; a posted production-incentive schedule or an attendance policy with a fixed payout amount is not discretionary no matter what the earnings statement calls it.

The complication that breaks naive implementations is temporal, not arithmetic. A single-workweek bonus is easy: fold it into that week’s regular rate before multiplying overtime hours, exactly as the parent engine’s production pattern already does. This scenario is different — the bonus is earned across several workweeks (a monthly attendance bonus, a quarterly production bonus) but paid once, after those weeks have already been processed and the overtime on them already disbursed at the pre-bonus rate. 29 CFR § 778.209 requires the bonus to be allocated back across the workweeks in which it was earned, each affected week’s regular rate recomputed with its share of the bonus folded in, and additional overtime compensation paid on every overtime hour in those weeks — retroactively, after the fact. This is distinct from blended-rate overtime, which reconciles multiple hourly rates worked within a single, still-open workweek; see Blended-Rate Overtime for Multiple Pay Rates for that case. Here there is only one rate per week, but the bonus reaches backward across many already-closed weeks.

One exception matters at the boundary: under 29 CFR § 778.210, a bonus paid as a fixed percentage of an employee’s total straight-time and overtime earnings for the period requires no recompute at all, because it already increases the overtime pay component proportionally by construction. An engine that runs the full retroactive-recompute pipeline against a percentage-of-total-earnings bonus overpays; the allocation stage must classify the bonus type before doing any work.

Bonus allocation and retroactive overtime recompute across four workweeks A 600 dollar nondiscretionary production bonus earned across four workweeks is allocated equally, 150 dollars per week, per 29 CFR 778.209. Each week's regular rate is recomputed by folding its 150 dollar share into that week's straight-time earnings and dividing by hours worked, raising the rate in every week. Weeks 1 and 3 have zero overtime hours and owe no additional premium. Week 2, with 6 overtime hours, and week 4, with 4 overtime hours, each generate an additional half-time overtime premium of 9.78 and 6.82 dollars respectively, computed as the allocated bonus per hour times 0.5 times the overtime hours in that week. The two amounts converge into a single 16.60 dollar retroactive adjustment owed across the bonus period. Nondiscretionary bonus $600.00 earned across 4 workweeks · 29 CFR § 778.208 Equal allocation → $150.00 / workweek 29 CFR § 778.209 fallback method Week 1 (Apr 04) 40 hrs · 0 OT hrs rate $18.00 → $21.75 no OT hrs — no premium due Week 2 (Apr 11) 46 hrs · 6 OT hrs rate $18.00 → $21.2609 + $9.78 half-time OT Week 3 (Apr 18) 40 hrs · 0 OT hrs rate $18.00 → $21.75 no OT hrs — no premium due Week 4 (Apr 25) 44 hrs · 4 OT hrs rate $18.00 → $21.4091 + $6.82 half-time OT Retro adjustment: $16.60 owed across the bonus period
Only the two workweeks that contain overtime hours generate an additional half-time premium once the bonus is folded into each week's regular rate.

Prerequisites & Data Requirements

The retroactive recompute stage needs both the bonus record and the closed workweek records it covers. Every monetary and hour field is Decimal; the recompute touches weeks that have already been paid, so a float-induced cent drift here reopens a paycheck that was previously correct.

Field Type Precondition
bonus_id str Stable identifier for the audit trail linking bonus to affected weeks.
bonus_amount Decimal Total nondiscretionary bonus paid; never a float.
is_discretionary bool True bonuses are rejected before allocation per 29 CFR § 778.211.
is_percentage_of_total_earnings bool True routes to the § 778.210 exception — no recompute performed.
covered_weeks list[WorkWeek] Every already-closed workweek the bonus period spans, in chronological order.
week.hours_worked Decimal Total hours worked that week, including overtime hours.
week.overtime_hours Decimal Hours already paid at 1.5× before the bonus existed.
week.base_hourly_rate Decimal The straight-time rate used for the original disbursement.

Two preconditions sit outside the function signature:

  1. The covered-weeks list must match the bonus’s actual earning period exactly, not the pay period in which the bonus happens to be disbursed. A quarterly production bonus paid in the first week of the next quarter still covers the thirteen prior weeks — not the week it lands in.
  2. Discretionary status is decided before allocation runs, using the same fixed-in-advance test Overtime Calculation Engines applies to shift differentials: if the amount or the fact of payment was ever contingent on the employer’s unannounced discretion, the bonus is excluded and the pipeline stops.

Step-by-Step Implementation

Step 1 — Model the bonus and the affected weeks. Freeze both as dataclasses so the recompute is reproducible and the audit trail carries typed fields end to end.

from dataclasses import dataclass
from decimal import Decimal, ROUND_HALF_UP
from typing import List, Tuple
import logging

logger = logging.getLogger("payroll.overtime.bonus_retro")

RATE_Q = Decimal("0.0001")
CENTS = Decimal("0.01")
HALF = Decimal("0.5")


@dataclass(frozen=True)
class WorkWeek:
    week_ending: str          # ISO date, e.g. "2026-04-04"
    hours_worked: Decimal
    overtime_hours: Decimal   # already paid at 1.5x before this bonus existed
    base_hourly_rate: Decimal


@dataclass(frozen=True)
class NondiscretionaryBonus:
    bonus_id: str
    amount: Decimal
    is_discretionary: bool
    is_percentage_of_total_earnings: bool
    covered_weeks: List[WorkWeek]


@dataclass
class RetroAdjustment:
    week_ending: str
    allocated_bonus: Decimal
    prior_regular_rate: Decimal
    recomputed_regular_rate: Decimal
    additional_ot_owed: Decimal

Step 2 — Allocate the bonus across the covered weeks. Reject discretionary bonuses and the § 778.210 percentage-of-total-earnings case before doing any allocation work; only a true nondiscretionary bonus tied to a fixed dollar amount reaches equal allocation. Expected output for $600.00 across 4 weeks: Decimal("150.0000") per week.

def allocate_bonus_across_weeks(bonus: NondiscretionaryBonus) -> Decimal:
    """Allocate a nondiscretionary bonus equally across its covered weeks."""
    if bonus.is_discretionary:
        raise ValueError(
            f"bonus={bonus.bonus_id} is discretionary — excluded per 29 CFR § 778.211"
        )
    if bonus.is_percentage_of_total_earnings:
        raise ValueError(
            f"bonus={bonus.bonus_id} is a percentage-of-earnings bonus — "
            "29 CFR § 778.210 requires no recompute"
        )
    n_weeks = len(bonus.covered_weeks)
    if n_weeks == 0:
        raise ValueError(f"bonus={bonus.bonus_id} has no covered weeks")

    per_week = (bonus.amount / Decimal(n_weeks)).quantize(RATE_Q, ROUND_HALF_UP)
    logger.info(
        "bonus_allocate id=%s amount=%s weeks=%s per_week=%s",
        bonus.bonus_id, bonus.amount, n_weeks, per_week,
    )
    return per_week

Step 3 — Recompute one week’s regular rate and incremental overtime. Fold the allocated share into that week’s straight-time earnings, divide by hours worked, and pay half-time — not full time-and-a-half — on the overtime hours already in that week. Expected output for week 2 (46 hours, 6 OT hours, $18.00 base, $150.00 allocated): recomputed_regular_rate = Decimal("21.2609"), additional_ot_owed = Decimal("9.78").

def recompute_week(week: WorkWeek, allocated_bonus: Decimal) -> RetroAdjustment:
    """Recompute one closed workweek's regular rate with its allocated bonus share."""
    if week.hours_worked <= Decimal("0"):
        raise ValueError(f"week={week.week_ending} has non-positive hours_worked")

    straight_time_pay = week.hours_worked * week.base_hourly_rate
    prior_rate = (straight_time_pay / week.hours_worked).quantize(RATE_Q, ROUND_HALF_UP)

    recomputed_pay = straight_time_pay + allocated_bonus
    recomputed_rate = (recomputed_pay / week.hours_worked).quantize(RATE_Q, ROUND_HALF_UP)

    bonus_per_hour = (allocated_bonus / week.hours_worked).quantize(RATE_Q, ROUND_HALF_UP)
    additional_ot = (bonus_per_hour * HALF * week.overtime_hours).quantize(CENTS, ROUND_HALF_UP)

    logger.info(
        "week_recompute week=%s alloc=%s prior_rate=%s new_rate=%s ot_hours=%s additional_ot=%s",
        week.week_ending, allocated_bonus, prior_rate, recomputed_rate,
        week.overtime_hours, additional_ot,
    )
    return RetroAdjustment(
        week.week_ending, allocated_bonus, prior_rate, recomputed_rate, additional_ot
    )

The additional overtime owed on each affected week follows directly from the bonus-adjusted regular rate. For workweek ww with allocated bonus share bwb_w and hours worked HwH_w:

Radj,w=(Hw×rbase)+bwHwR_{\text{adj},w} = \frac{(H_w \times r_{\text{base}}) + b_w}{H_w}

Because the employee already received full straight-time pay for every hour, including the overtime hours, the retroactive amount owed is only the additional half-time premium on the overtime hours already worked that week:

ΔOTw=bwHw×0.5×Hot,w\Delta OT_w = \frac{b_w}{H_w} \times 0.5 \times H_{\text{ot},w}

Step 4 — Sum the retro adjustments for the whole bonus period. Round only at the per-week cent boundary; sum the already-rounded weekly amounts so the total reconciles line-for-line with the audit log.

def compute_retro_overtime(
    bonus: NondiscretionaryBonus,
) -> Tuple[Decimal, List[RetroAdjustment]]:
    """Compute total retroactive overtime owed across a bonus's covered weeks."""
    per_week = allocate_bonus_across_weeks(bonus)
    adjustments = [recompute_week(w, per_week) for w in bonus.covered_weeks]

    total = sum(
        (a.additional_ot_owed for a in adjustments), Decimal("0")
    ).quantize(CENTS, ROUND_HALF_UP)

    logger.info(
        "bonus_retro_total id=%s weeks=%s total_additional_ot=%s",
        bonus.bonus_id, len(adjustments), total,
    )
    return total, adjustments

Step 5 — Run the pipeline against a real quarterly production bonus. A $600.00 production bonus earned across four workweeks, disbursed after two of those weeks already carried overtime hours:

bonus = NondiscretionaryBonus(
    bonus_id="BON-2026Q2-0091",
    amount=Decimal("600.00"),
    is_discretionary=False,
    is_percentage_of_total_earnings=False,
    covered_weeks=[
        WorkWeek("2026-04-04", Decimal("40"), Decimal("0"), Decimal("18.00")),
        WorkWeek("2026-04-11", Decimal("46"), Decimal("6"), Decimal("18.00")),
        WorkWeek("2026-04-18", Decimal("40"), Decimal("0"), Decimal("18.00")),
        WorkWeek("2026-04-25", Decimal("44"), Decimal("4"), Decimal("18.00")),
    ],
)
total, adjustments = compute_retro_overtime(bonus)
# total == Decimal("16.60")
# adjustments[0].additional_ot_owed == Decimal("0.00")   # week 1, no OT hours
# adjustments[1].additional_ot_owed == Decimal("9.78")   # week 2, 6 OT hours
# adjustments[2].additional_ot_owed == Decimal("0.00")   # week 3, no OT hours
# adjustments[3].additional_ot_owed == Decimal("6.82")   # week 4, 4 OT hours

Verification

Confirm correctness against the specific properties of the bonus-retro recompute, not just the summed total:

  1. Bonus raises the regular rate. For any week with allocated_bonus > 0, assert recomputed_regular_rate > prior_regular_rate. In the example, week 2’s rate moves from Decimal("18.0000") to Decimal("21.2609").
  2. Retro overtime owed only on overtime weeks. Assert additional_ot_owed == Decimal("0.00") for every week where overtime_hours == Decimal("0"), even though the bonus still raised that week’s regular rate. Weeks 1 and 3 in the example receive no additional payment; the bonus itself, already disbursed, is their only compensation.
  3. Discretionary bonus excluded before allocation. Call allocate_bonus_across_weeks with is_discretionary=True and assert a ValueError — the pipeline must reject the bonus at the boundary rather than fold a true discretionary payment into the regular rate.
  4. Decimal determinism. Run compute_retro_overtime against the same NondiscretionaryBonus three times and assert byte-identical total and per-week additional_ot_owed values. Any drift means a float leaked into the allocation or rate path.
  5. Percentage-of-earnings exception. Call with is_percentage_of_total_earnings=True and assert a ValueError rather than a recomputed total — that bonus type is handled entirely outside this pipeline per § 778.210.

Failure Modes

  • Bonus excluded from the regular rate entirely. Root cause: the bonus is disbursed through a separate “extra pay” earning code that never reaches the overtime engine, so the regular rate used for the original weeks — and any later audit — never reflects it. Remediation: route every nondiscretionary bonus through the same classification gate the overtime engine uses for shift differentials, and fail the payroll run if a bonus record has no covered_weeks to allocate against.
  • No retroactive recompute performed. Root cause: the bonus is treated as a forward-looking, single-transaction payment, so it is added to gross pay in the week it is disbursed but the overtime hours in the weeks it was actually earned are never revisited. Remediation: trigger compute_retro_overtime for the bonus’s entire covered period as part of bonus disbursement, and require a retro_adjustment_id on every affected week’s record before the bonus run closes.
  • Allocating the bonus to the wrong weeks. Root cause: the allocation uses the disbursement pay period, or the calendar month the bonus check was cut, instead of the weeks the bonus was actually earned — a quarterly bonus paid in week 14 gets allocated to week 14 alone instead of weeks 1 through 13. Remediation: require covered_weeks to be supplied explicitly from the bonus-earning schedule, never inferred from the disbursement date, and reject a bonus record with an empty or single-week coverage list when the bonus policy defines a longer earning period.

Frequently Asked Questions

Why is only half-time owed retroactively, not the full time-and-a-half?

Because the employee already received straight-time pay for every hour worked that week, including the overtime hours, at the original rate — and the original 1.5× overtime payment already covered the “half” premium on the base rate. Once the bonus is folded in, the regular rate rises, so only the additional half-time premium attributable to that rate increase is still owed on the overtime hours. Paying the full 1.5× again on the bonus-adjusted rate would double-pay the straight-time portion.

How should the bonus be allocated when workweeks have unequal hours?

Equal per-week allocation, as used here, is the fallback method 29 CFR § 778.209 permits when the actual amount attributable to each week cannot be determined. When the bonus is tied to measurable weekly output or earnings — a per-unit production bonus, for example — the employer may instead allocate in proportion to hours worked or earnings in each week, which better reflects where the bonus was actually earned. Either method must be applied consistently and documented, since the two can produce different per-week regular rates.

Does this recompute apply to discretionary bonuses like a holiday gift or a spot bonus?

No. A bonus is discretionary, and excluded from the regular rate under 29 CFR § 778.211, only when both the fact and the amount of payment remain in the employer’s sole discretion until at or near the end of the period. A holiday gift of a fixed amount announced in advance, or a spot bonus paid under a standing policy, is nondiscretionary despite the informal label and must go through this same allocation and recompute pipeline.

What if the bonus cannot be attributed to specific workweeks at all?

If the bonus period genuinely cannot be tied to identifiable workweeks, 29 CFR § 778.209 permits allocating it equally to each week of the employment period during which it was earned, which is the equal-per-week method implemented above. What is never acceptable is skipping allocation entirely and treating the bonus as if it were paid outside the regular-rate calculation — the recompute obligation exists regardless of how coarse the allocation method has to be.