Payroll And Wages
Published
A wage engine is arithmetic wrapped in a handful of flags, and each flag decides what somebody is paid. This pass protected a file that configures one, renamed its property names a group at a time, and read the payslip off the far side. The sharpest result is not the largest number: it is the arm where the payslip still itemised twelve overtime hours and priced them as though they were not overtime at all.
The payslip, and the four things deciding it
The sample is an ordinary weekly payroll configuration. Overtime begins after forty hours. Overtime hours are paid at one and a half times the base rate. A double-time band opens after sixty. A sanity cap refuses any timecard claiming more than eighty hours in a week, on the grounds that nobody worked it and somebody mistyped it. Two deductions are configured, one pre-tax and one post-tax, and a rounding interval is pinned to the value the engine already uses so it can serve as a control.
The week that matters is fifty-two hours at thirty dollars an hour. Unprotected, the engine pays forty hours straight and twelve hours at one and a half, for a gross of 1,740 dollars. The pre-tax deduction reduces taxable income to 1,620, withholding comes to 356.40, and the net is 1,223.60. Every figure in this article is one the protected sample printed.
Four separate options contributed to that number. They were written at different times for different reasons, and only one of them is named in a way that suggests it controls money.
Two options, the same loss, and only one of them admits it
Renaming overtimeThresholdHours alone removes the concept of overtime. The configuration line reads (none - no hour is ever overtime), all fifty-two hours are paid straight, and the gross falls from 1,740 to 1,560 dollars. The payslip shows ot=0h. An employee who worked twelve hours of overtime and can see that their payslip records none of it raises a ticket the same week.
Renaming overtimeMultiplier alone produces the identical gross of 1,560 dollars by a completely different route. The threshold still fires. The engine still identifies twelve overtime hours. It still prints them: ot=12h@1x. The premium rate has simply reverted to the library default of one, so those twelve hours are itemised as overtime and paid as though they were not. The payslip is internally consistent, the overtime line is present and correct, and the total is exactly what it would have been if no overtime had been recognised at all.
That is the quiet one, and the difference matters more than the money. An employee checking whether they were paid for their overtime looks for the overtime line. It is there. It says twelve hours. Establishing that those twelve hours were priced at the wrong rate requires recomputing the gross by hand from the base rate, which is not what anyone does when the line item they were looking for is present.
Which half wins is decided by evaluation order, not by severity
Renaming both names in one pattern produced a gross of 1,560 dollars again, with ot=0h@1x. On the fifty-two hour week all three arms cost exactly the same. The sixty-eight hour crunch week separates them: unprotected it pays 2,580 dollars, the threshold arm pays 2,040, the multiplier arm pays 2,280, and the combined arm pays 2,040 -- identical to the threshold arm alone.
The threshold is consulted first. Once it is gone there are no overtime hours for a multiplier to price, so the second name stops mattering. This is the same shape a previous pass in this series measured on a data-retention engine, where a surviving dry-run flag masked the loss of a purge scope. The general statement is that when two options on one object both fail, the outcome is decided by which one the library reads first, and not by which one sounds more important.
The reading consequence is the useful part. A combined arm that looks no worse than one of its halves is not evidence that the other half is harmless. It usually means the other half was never reached.
The cap, and the timecard nobody worked
Renaming capWeeklyHours is the largest single number in the file. The cap reads (none - any number of hours is payable), and the four-hundred-hour timecard that the unprotected engine refuses with refused-implausible-timecard is instead paid in full: forty hours straight, twenty at time and a half, three hundred and forty at double time, for a gross of 22,500 dollars and a net of 17,416.40. A week has one hundred and sixty-eight hours in it.
Renaming hoursWorked, a plain field on the timecard rather than an option, loses the guard and the value together. The cap comparison becomes undefined > 80, which is false, so the implausible card is no longer refused; and every downstream figure is NaN. The payslip reads GROSS=NaN NET=NaN. That is loud, and loud is recoverable.
Renaming rate is quieter in mechanism and equally loud in effect: the cap still fires correctly on the mistyped card because the hours survived, but every genuine payslip computes to NaN. Worth stating plainly, because this series is mostly a catalogue of failures that do not announce themselves: several arms in this file do announce themselves.
The deduction flag, and the exempt flag
Renaming pretax -- a boolean on a deduction row, about as unremarkable a field as exists in this file -- moves the 401(k) contribution from the pre-tax side of the calculation to the post-tax side. Taxable income rises from 1,620 to 1,740, withholding rises with it, and the net falls from 1,223.60 to 1,083.20 on the sample week. The gross is unchanged, the deduction still appears at the correct amount, and the only figure that moved is the one nobody recomputes.
Renaming exempt works in the opposite direction: the salaried employee who is not entitled to a premium receives one. Fifty-five hours at fifty dollars goes from 2,750 to 3,125 dollars gross. Overpaying an exempt employee is a smaller problem than underpaying a non-exempt one, but it is still a payroll figure that no longer matches the policy that was configured.
It is worth noticing that neither of these fields looks like a control. One is a tax classification and one is an employment classification. In this measurement both moved real money, and the pre-tax one moved it in the direction the employee absorbs.
What protection alone did, and what to do about it
Nothing. The file was protected on five profiles -- the ES5 default, the modern target, both emit-gate configurations and the string-encoding profile -- and produced byte-identical output on all five. Every payslip matched to the cent. Renaming identifiers and encoding strings does not disturb a wage engine.
Every result above required member renaming, which is off by default and needs a regular expression naming the members it may rewrite. Keep that pattern anchored to names your own code owns on both sides. A payroll configuration is the opposite of that: your file writes the keys and an installed engine reads them, so every key on that object is a contract, and so is every field on the timecard rows you hand it.
Then add the assertion that no rename can satisfy by accident. Take one timecard with a known answer -- fifty-two hours at thirty dollars, gross 1,740 -- and assert the exact gross, the exact overtime hours, and the exact net. Every result in this article would have failed that assertion on the first build, including the one where the payslip still said twelve overtime hours.
Frequently asked questions
Does obfuscating a payroll bundle change what anyone is paid?
Not by itself. The sample was protected on five profiles including the ES5 default, the modern target and string encoding, and produced identical payslips on all five. Every result in this article required member renaming, which is off unless you enable it and supply a pattern.
Why was renaming the overtime multiplier worse than renaming the threshold?
Both cost the same money on the sample week, but the threshold arm removes the overtime line from the payslip while the multiplier arm keeps it. The payslip still reads twelve overtime hours; they are simply priced at the base rate. The evidence an employee would check is present and looks correct.
What happened when the weekly hours cap was renamed?
A timecard claiming four hundred hours in a week, which the engine normally refuses as implausible, was paid in full: 22,500 dollars gross and 17,416.40 net. The cap is the only thing in the file that distinguishes a typo from a paycheque.
Can renaming a field make a payslip compute to NaN?
Yes. Renaming the hours or the rate on a timecard leaves the arithmetic reading undefined, and the result propagates: the sample printed GROSS=NaN NET=NaN. That failure is loud, which makes it one of the safer outcomes in this file.
What did renaming the pre-tax flag on a deduction do?
It moved a 401(k) contribution to the post-tax side of the calculation. Taxable income rose, withholding rose, and the net pay fell from 1,223.60 to 1,083.20. The gross was unchanged and the deduction still appeared at its correct amount.
Which names should a renaming pattern never touch in payroll code?
Any key on the options object the wage engine reads, and any field on the timecard rows themselves, including ones that look like classifications rather than controls. Both are read by installed code that is not rebuilt with your bundle.
What single test would have caught all of these?
One timecard with a known answer, asserted exactly: the gross, the overtime hours and the net. Asserting only that a number was produced, or that it was greater than zero, passes in every arm above.
Related reading