I built a small page that writes appeal letters for UK private parking tickets. You answer a few questions, it produces a letter.
I tested it the way I test most forms. Fill in good data, check the output looks right. Every combination passed: three parts of the UK, keeper or driver, five grounds. No script errors.
Then I did the thing I should have done first. I picked a ground and typed in data that did not support it.
The ground
One of the five grounds is about a deadline. In England and Wales, if a private parking company posts you a ticket and nothing was left on the windscreen, the notice has to be delivered to the registered keeper within 14 days. It's in Schedule 4 of the Protection of Freedoms Act 2012, paragraph 9. People ask about it on forums all the time. Here is one thread where the notice took more than 30 days: https://www.ftla.uk/private-parking-tickets/late-notice-to-keeper-30-days-worth-appealing/
So the form asks for the date of parking and the date the notice arrived.
The bug
I entered a parking date and a received date 7 days apart. Under the field, the helper text did the right thing:
You received it 7 days after the parking date. This ground needs more than 14 days, so it may not apply.
And the letter, a few centimetres to the right, said this:
Your notice is dated 6 October 2026 and I received it on 6 October 2026, which is outside that period.
That sentence is false, and the page wrote it. The person using it is stressed, skims the preview, prints, signs.
The cause was boring. The helper text and the letter were built by two different functions. One compared the dates. The other just filled a template.
The fix
I did not make the letter clever. A letter that silently rewrites its own argument is hard to trust. The page now puts a warning above the letter, next to the count of unfilled fields:
const pd = $('parkDate').value, rd = $('receivedDate').value;
if (radio('ground') === 'keeper' && $('jurisdiction').value === 'EW'
&& pd && rd && daysBetween(pd, rd) <= 14) {
add('', '<strong>Check your dates.</strong> This letter says the notice ' +
'arrived more than 14 days after parking, but the dates you entered are ' +
'within 14 days. Correct the dates or choose a different ground.');
}
The second one
While I was in there I found the letter's date came from this:
new Date().toISOString().slice(0, 10)
toISOString() is UTC. The UK is an hour ahead of UTC in summer. Between midnight and 1 am the letter would be dated yesterday. Not a disaster, but it is a dated legal letter, and the fix is three lines:
const localIso = d =>
`${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
What I took from it
If a form generates text that a person will sign, test each option with inputs that contradict it. "Does it render" is not the test. "Can it say something untrue" is.
The page is here if you want to poke at it: https://mianazeemdaula.github.io/parking-appeal-letter/
Source: https://github.com/mianazeemdaula/parking-appeal-letter
I'm a developer, not a lawyer, and no solicitor has reviewed the wording. It was tested in headless Chrome only, not on real phones, Safari or Firefox.
Top comments (2)
The "two functions, one for the helper text and one for the letter" cause is the most common version of this bug, and the warning banner treats the symptom. The sturdier fix is one computed object (e.g.
{daysGap, groundApplies}) that both the helper text and the letter template read, so they cannot disagree.Two things to test in the date check itself. If
daysBetweendivides millisecond differences by 86,400,000 and floors, a span that crosses a clock change is off by an hour: 14 calendar days across the spring change is 335 hours, which floors to 13 days, and the same span across the autumn change is 337 hours, which floors to 14. The UK clocks go back on 25 October 2026, so a parking date of 20 October and a received date of 3 November would sit right on your 14-day boundary. Parse as UTC dates or useMath.round, and add 13/14/15-day cases around that weekend.Separately, on the law (I'm not a lawyer either): my understanding is that the Schedule 4 test is when the notice was given, and that a posted notice is presumed delivered on the second working day after posting. If so, the date the keeper received it is evidence but not quite the test, and the postmark or the date printed on the notice might be worth asking for. Worth checking against paragraph 9 itself before the letter says "outside that period".
Thanks, this was useful. You were right on all three, and I changed the page today.
The helper text, the warning and the letter now read one computed object (the gap, the presumed delivery date and a verdict), so they can't disagree any more.
Dates are now counted on calendar days in UTC. The old version used Math.round, so it happened to survive the clock change, but I added your 13/14/15-day cases around 25 October, and the spring change, to the tests anyway.
The law point was the bigger catch. Paragraph 9(6) presumes a posted notice was given on the second working day after posting, so the page now works that out from the date printed on the notice. If only the received date is past 14 days, the letter no longer states that as fact: it flags the ground as weak and asks the operator for proof of the posting date.
One gap left: it skips weekends but not bank holidays. That can only make the page more cautious, not less, but I'll add them.