Skip to main content

19 — Withholding and the true-up: why the final cutoff is different

Read this first: this lesson is about timing, not arithmetic. Payroll asks two questions that look like one: how much does this employee owe, and how much of it does this payslip collect. The first has one home, the business-rules reference, which owns every table, rate, ceiling and peso figure here — nothing below restates them. The second is a design problem the code answers in comments, so you read javadoc and signatures only, never a body, the rule lesson 16 used. Carry away one idea: an obligation measured per month, paid per cutoff, must be reconciled by a cutoff that knows it is the last one.

Time: about 35 minutes. Assumes lesson 18.

The table has to match the schedule​

Lesson 18 left you with compliance stored as versioned data: cohorts of rows, resolved as-of the period being computed. Withholding tables are versioned that way and sliced one more way on top. The tax authority publishes a separate prescribed table per pay frequency, each calibrated to how many times a year that frequency pays; a table is not a rate you can rescale, it is a schedule of brackets built for one cadence.

So the engine picks the table by cadence. WithholdingTaxCalculator is a package-private class, constructed per run, whose constructor takes the bracket repository and the run's PayFrequency and resolves it once — through TaxPayPeriod.fromPayFrequency — into the table every later call reads. Its class javadoc opens by saying so:

* BIR compensation withholding engine. Selects the prescribed table matching the
* configured pay frequency (a semi-monthly payroll must use the semi-monthly
* table, not the monthly one), and computes the annual-table true-up applied on
* the year's final payroll run (RR 11-2018 year-end adjustment: the last
* payslip's withholding is set so the year's total equals the annual tax due —
* a negative result is a refund of over-withheld tax).

Two consequences follow from constructed per run. The frequency it is built with is the run's snapshotted frequency — the column lesson 16 made you predict about — not whatever the settings singleton says today, so regenerating an older run taxes it on the table its own periods were cut under. And because the calculator is a per-run object rather than a shared bean, one run cannot mix tables halfway through: the choice is made once, for every payslip in it.

The rest of the class javadoc hands the versioning question straight back to lesson 18, and states the failure mode:

* dates predating every table. A missing bracket inside a cohort is a data error
* and fails loudly — the coverage validator guarantees gapless tables.

"Fails loudly" is literal. When the lookup finds no bracket covering an income, it throws IllegalStateException naming the table it searched and the income it could not place — the one exception lesson 05 told you is deliberately not mapped to a friendly response. It surfaces as a server error, because a run computed against an incomplete table is not a user mistake to explain away; it is a run that must not finish. Lesson 18's coverage validator exists so that this throw stays unreachable in practice.

The frequency that was deleted​

PayFrequency has three members today. It used to have four, and the javadoc explains the subtraction rather than quietly moving on:

* doesn't run payroll at. BI_WEEKLY was removed in V93: BIR publishes no bi-weekly
* table, so it borrowed the semi-monthly one —

The sentence finishes by naming the consequence: a systematic per-period under-withholding. With no prescribed bi-weekly table, a bi-weekly payroll was taxed on brackets calibrated for a different cadence — every period, for every employee, in the same direction.

Predict: removing a frequency is a breaking change. Existing settings rows and existing runs referenced it, the enum is persisted as a string, and the UI offered it. Why delete it instead of keeping it and mapping it onto the nearest table — which is, after all, exactly what it was already doing?

Because "the nearest table" was never a mapping, it was a bug wearing a mapping's clothes. A borrowed table returns a number for every payslip — in range, plausible, and wrong on every single one, wrong in one direction, surfacing only as a lump at year-end when the annual adjustment finally reconciles it. Money errors that look plausible are the worst kind: they do not fail, they accumulate. Keeping the frequency would mean keeping a cadence the tax authority publishes no table for and calling the mismatch a rounding detail. V93 instead remaps stored values onto the frequency whose table they were already using, and its header spends more lines justifying the removal than performing it.

Notice what the enum keeps: requiresAnchor, true only for weekly. Periods that are not calendar-month aligned need an anchor date to define the cycle, and the javadoc names the harm of going without — the month's final cutoff has to be guessed from period length, "which can silently skip the statutory contribution true-up". That is the bridge to the rest of this lesson.

A monthly obligation, paid in slices​

The statutory contributions are monthly obligations. Pay is not: it arrives per cutoff, and how many cutoffs a month holds depends on the schedule and, for weekly payroll, on the calendar. So a monthly figure must be spread across cutoffs — and spreading is where money goes missing, because a remainder nobody owns is either lost or charged twice.

MotorPH's answer is to make the split deliberately approximate and the last cutoff exact. Every non-final cutoff of the month takes a standard share. The month's final cutoff does not take a share at all — it collects whatever is still outstanding against the true monthly amount. The javadoc on payslipsPerMonth, the helper that supplies the assumed cutoff count, says why that arrangement lets it be casual:

* Standard per-cutoff share of the monthly statutory contribution, used on every
* non-final cutoff of the month. The month's final cutoff instead collects whatever
* remains of the true monthly amount (see {@link #resolveContributionDeduction}),
* so this only needs to be a reasonable approximation, not exact — any drift from an
* assumed cutoff count self-corrects on the final cutoff.

Read that as a design rule, not a helper description. The assumed count is a pacing device — it decides what the employee sees taken each cutoff, so the deduction feels even — and it does not decide the total. Whatever the assumption got wrong, the final cutoff absorbs, because that cutoff is defined by what remains rather than by a share. The month reconciles to the correct statutory amount regardless of how many cutoffs actually occurred.

resolveContributionDeduction(monthlyAmount, settings, frequency, finalCutoffOfMonth, alreadyRecordedThisMonth) applies it, and the signature is the whole story: it is given the month's true amount, told whether this is the last cutoff, and told what the month has already been charged. Settings choose between taking the full amount every payslip, spreading it across cutoffs, or taking nothing until the last one — three schedules, whose home is lesson 20. The two that spread settle on the same cutoff.

Which cutoff is final is a property of the schedule, not of the period​

Everything above rests on one boolean. isFinalCutoffOfMonth(frequency, settings, periodStart, periodEnd) answers it, and the interesting case is weekly, where periods do not line up with calendar months. It could have measured: take this period's length, project one more like it, see whether that lands in the next month. Instead it consults the schedule — the anchor grid from lesson 17's PayPeriodPlanner. The javadoc says why that distinction matters:

* next scheduled period end falls in a different month. Deriving it from the
* schedule rather than from this period's own length matters — a single ragged
* period would otherwise mis-answer for itself AND for its neighbour, and a month
* that never sees a final cutoff never reconciles its statutory contributions.

Measurement uses the period as evidence about itself. But a ragged period — one created a few days short to cover a correction — is exactly the period whose own length least describes the schedule. And the error does not stay local: a short period projects a short successor into the wrong month, so it claims a finality belonging to its neighbour, and the neighbour, asked later, declines it — the month ends with no final cutoff at all and nothing ever reconciles it. The schedule, by contrast, knows where the next cutoff belongs whatever this one happened to measure.

Predict: an employee is on weekly pay and the calendar hands their month an extra cutoff beyond the usual count. Each cutoff has taken a standard share based on an assumed count that is now too low. How does the system know which cutoff settles the month, and what does that cutoff charge?

The last one, and it charges the shortfall. Only the cutoff whose next scheduled end lands in a new month answers yes, and it ignores the standard share entirely: it collects what is outstanding against the true monthly amount. The extra cutoff just means each earlier slice was a little small and the last a little larger — the monthly total is still correct, and nothing had to count cutoffs.

Two things fall out. It is why lesson 16's create-time schedule gate refuses off-grid weekly periods unless a human confirms them, and why PayPeriodPlanner is a pure static class with disproportionate test coverage — a date-walking bug there is not a display glitch, it is an under-collected month.

Two smaller mirrors of the same idea​

De minimis benefits. Certain allowances are non-taxable up to a ceiling that covers the calendar month, not the payslip. A payslip testing itself against that ceiling in isolation would grant it once per cutoff instead of once per month, so the calculation is month-aware:

* <p>The ceiling covers the calendar month, not the payslip, so this returns only
* the share of the excess that this payslip newly creates: the month's excess
* including this allowance, minus the excess the month already had.

Same principle, opposite direction — contributions spread a monthly obligation across cutoffs, the ceiling accumulates one — and neither lets the cutoff count change the monthly answer. The ceilings are in business-rules.md.

Custom deductions. An EmployeeDeduction carries a DeductionAmountBasis, and the enum exists so that a company-defined deduction can declare which of the two readings it wants:

* it is split across the month's cutoffs and trued up on the month's final cutoff, the
* same treatment the statutory contributions get. Under PER_MONTH the total collected
* in a calendar month stays correct however many cutoffs that month happens to have.

PER_PAYSLIP reads the figure as per cutoff and takes it in full on every applicable run; PER_MONTH mirrors the statutory treatment. The distinction earns its keep the day a company changes pay frequency: without it, a deduction entered as a monthly figure would silently multiply by the new cutoff count, over-charging every affected employee by something that still looks like a legitimate deduction on the payslip.

The year's last run trues up too​

The pattern repeats one level up. Within a month, the final cutoff reconciles the month; within a year, the year's final run reconciles the year — withholding on the last payslip is adjusted so the year's total equals the annual tax due, read from the annual schedule rather than the per-period one, and because it is a difference it can come out negative, refunding tax that was over-withheld. That is the year-end adjustment the class javadoc named at the top of this lesson, in the shape you have now seen three times: pace it approximately, settle it exactly, on a period the schedule names ahead.

Every figure this lesson deliberately did not print — brackets, contribution rates, ceilings, caps — lives in business-rules.md. The form these withheld amounts are reported on is documented at the 1601-C reference, which owns its entry map; do not learn the form from here.

Where this shows up in MotorPH​

Recap​

  • The withholding table is chosen by the run's pay frequency, once, at construction — a semi-monthly payroll uses the semi-monthly table, and the frequency comes from the run's snapshot, not from today's settings.
  • A cadence with no prescribed table is not supported, it is deleted. BI_WEEKLY borrowed the semi-monthly table and under-withheld every period; V93 removed it rather than keep a plausible wrong answer alive.
  • Monthly obligations are paced approximately and settled exactly. Non-final cutoffs take a standard share; the final cutoff collects what remains, however many cutoffs the month had.
  • Finality comes from the schedule, never from the period's own length. A ragged period mis-answers for itself and its neighbour, and a month with no final cutoff never reconciles.
  • The same shape repeats at every scale — de minimis ceilings across a month, PER_MONTH deductions, and the year-end adjustment on the year's last run, which may refund.

Next: 20 — Settings, reports, and the edges of the engine.