I run a small free site of UAE pay and labour calculators. I'm not a lawyer, and most of the code was written with Claude Code (Anthropic's coding agent) while I directed it. This post was also drafted with its help and checked by me. What I want to share is the process we settled on, because "AI writes a gratuity calculator" goes wrong in very specific ways.
The problem
UAE end of service gratuity looks simple. Every blog says "21 days per year for five years, then 30". But the actual rule is in Article 51 of Federal Decree-Law No. 33 of 2021, and it has details that matter:
- you need at least one year of continuous service,
- it uses the last basic wage only, no allowances,
- parts of a year count in proportion, once the first year is done,
- days of unpaid absence don't count as service,
- the total is capped at two years' wage.
An LLM will happily give you the blog version. So the first rule we set was:
1. No rule goes in unless it was read in the official text
The law text is saved locally, and every rule in the code cites its article. The module header looks like this:
/**
* Rules implemented (foreign full-time workers):
* - Art. 51(2): eligible after at least 1 year of continuous service.
* - Art. 51(2)(a): 21 days of basic wage per year for the first 5 years.
* - Art. 51(2)(b): 30 days of basic wage per year beyond 5 years.
* - Art. 51(3): parts of a year count in proportion, once 1 year is completed.
* - Art. 51(4): days of absence without pay are not counted as service.
* - Art. 51(6): total capped at 2 years' wage (modelled as 24 months of basic).
*/
When the AI suggested a rule, the question was always "which article?" If there was no answer, it didn't go in.
2. Separate the law from the conventions
Some things every calculator does are not in the law at all. The law never says "daily wage = monthly basic / 30". It's the usual conversion, but it's a convention. So the code has a second, separate list:
/**
* Conventions, NOT spelled out in the text:
* - Daily wage = monthly basic / 30 (the usual conversion for monthly pay).
* - When only a number of days is given, a year = 365 days.
*/
The pages say the same thing to readers. That one change made reviews much easier: if someone disputes a number, you know straight away whether you're arguing about the law or about a convention.
3. Expected values are worked by hand, never copied from the code
This is the one I'd push hardest. If you let the AI write the function and then the test, it will often run the function and paste the output into expect(). That tests nothing. Every test here has its arithmetic in the name:
it('8 years, basic 12000: (105 + 90) days x 400 = 78000', () => {
const r = calculateGratuity({ basicSalary: 12000, serviceDays: 8 * 365 });
expect(r.gratuityDays).toBe(195);
expect(r.gratuity).toBe(78000);
});
it('caps at 24 months of basic wage', () => {
// 30 years, basic 3000: uncapped would be far above 72000
const r = calculateGratuity({ basicSalary: 3000, serviceDays: 30 * 365 });
expect(r.gratuity).toBe(72000);
});
You can check each one with a pen. Across the site's calculators there are 150 of these now.
4. Dates are where it actually breaks
The first version turned dates into days and divided by 365. Someone who worked 1 January 2017 to 31 December 2024 has done exactly 8 years, but that span has two leap days, so 2,922 / 365 = 8.005 years. Small, but it's wrong, and in other calculators a fraction past a threshold can change the result.
The fix counts whole calendar years first, then the fraction of the year after the last anniversary:
export function yearsBetween(start: Date, end: Date): number {
const utc = (y: number, m: number, d: number) => Date.UTC(y, m, d);
const endEx = utc(end.getFullYear(), end.getMonth(), end.getDate() + 1);
const sy = start.getFullYear(), sm = start.getMonth(), sd = start.getDate();
let years = end.getFullYear() - sy;
while (years > 0 && utc(sy + years, sm, sd) > endEx) years--;
const last = utc(sy + years, sm, sd);
const next = utc(sy + years + 1, sm, sd);
return years + Math.max(0, endEx - last) / (next - last);
}
And the test pins the leap-year case:
expect(yearsBetween(new Date(2017, 0, 1), new Date(2024, 11, 31))).toBe(8);
5. Write down what you don't model
Part-time workers (pro rata by contracted hours) and the deductions an employer may make are not modelled. That's written in the module header and on the page. A calculator that silently ignores a case is worse than one that says "not covered".
What I'd tell anyone building legal or tax logic with an AI
- Make it cite the source for every rule, and check the citation yourself.
- Keep law and convention in separate lists.
- Work test values by hand. Never let the code grade itself.
- Test dates across leap years and month ends.
- Say what you don't cover.
The plain-English version of the rule, with worked examples, is here if you want to check the numbers yourself: how UAE gratuity is calculated.
If you've built anything similar (tax, payroll, benefits), I'd like to hear how you handle rule changes. Laws get amended and the code has to follow.
Disclosure: the code was built with Claude Code and this article was drafted with AI help, then checked against the code and the law text by me.
Top comments (0)