Problem
openholidaysapi.org returns more entries under "public" holidays than are actually public holidays — many are school or authorities-only. The frontend/payday logic currently relies on this data to determine business days, so the noise causes incorrect payday calculations.
Relates to #546.
Solution
Switch /holidays/public to date.nager.at, which tags every holiday with a types array (Public, Bank, School, Authorities, Optional, Observance). Filter to types containing "Public" only.
School holidays stay on openholidaysapi.org — Nager.Date does not provide them.
API comparison
openholidaysapi.org GET /PublicHolidays?countryIsoCode=NL&validFrom=...&validTo=...&languageIsoCode=EN
{
"id": "...",
"startDate": "2026-01-01",
"endDate": "2026-01-01",
"type": "Public",
"name": [{ "language": "EN", "text": "New Year's Day" }, { "language": "NL", "text": "Nieuwjaar" }],
"nationwide": true
}
date.nager.at GET /api/v3/PublicHolidays/2026/NL
{
"date": "2026-01-01",
"localName": "Nieuwjaar",
"name": "New Year's Day",
"countryCode": "NL",
"global": true,
"counties": null,
"types": ["Public"]
}
Key differences: path params instead of query params, single date instead of startDate/endDate range, flat name/localName strings instead of a [{language, text}] array, no language request param (always returns English + native).
Backend changes (backend/app/routers/holidays.py)
- Replace openholidaysapi.org call in
get_public_holidays with https://date.nager.at/api/v3/PublicHolidays/{year}/{country}.
- Filter response to entries where
"Public" in holiday["types"].
- Remove
language query parameter from /holidays/public — Nager always returns name (English) and localName (native).
- Update
_make_cache_key / _build_upstream_params accordingly — no language dimension for public holidays.
- School holidays (
get_school_holidays) unchanged.
Frontend changes
usePublicHolidays.ts: Replace the PublicHoliday interface (openholidays shape) with the Nager shape:
interface PublicHoliday {
date: string; // "2026-01-01"
name: string; // English
localName: string; // Native language
global: boolean;
counties: string[] | null;
types: string[];
}
- Remove
PublicHolidayName interface and getPublicHolidayName helper — no longer needed.
- Simplify
toHolidayMap: holiday.name → name, holiday.localName → localName. No date-range expansion (Nager always has a single date).
- Remove
NATIVE_LANGUAGE constant — Nager's localName is always the native language.
- Remove
language param forwarding to the backend for the public endpoint (the param can stay on usePublicHolidays for the toHolidayMap name field if needed, but it no longer goes to the API).
- Rename
useOpenHolidays → useHolidaysApi or keep and note it now only applies to school holidays.
Note for #546
The cached_holidays table design should account for the fact that public holidays (Nager) and school holidays (openholidaysapi.org) have different response shapes. Store them as opaque JSON blobs keyed by (holiday_type, country, year, subdivision, language) — the normalization happens at query time in the router.
Problem
openholidaysapi.org returns more entries under "public" holidays than are actually public holidays — many are school or authorities-only. The frontend/payday logic currently relies on this data to determine business days, so the noise causes incorrect payday calculations.
Relates to #546.
Solution
Switch
/holidays/publicto date.nager.at, which tags every holiday with atypesarray (Public,Bank,School,Authorities,Optional,Observance). Filter totypescontaining"Public"only.School holidays stay on openholidaysapi.org — Nager.Date does not provide them.
API comparison
openholidaysapi.org
GET /PublicHolidays?countryIsoCode=NL&validFrom=...&validTo=...&languageIsoCode=EN{ "id": "...", "startDate": "2026-01-01", "endDate": "2026-01-01", "type": "Public", "name": [{ "language": "EN", "text": "New Year's Day" }, { "language": "NL", "text": "Nieuwjaar" }], "nationwide": true }date.nager.at
GET /api/v3/PublicHolidays/2026/NL{ "date": "2026-01-01", "localName": "Nieuwjaar", "name": "New Year's Day", "countryCode": "NL", "global": true, "counties": null, "types": ["Public"] }Key differences: path params instead of query params, single
dateinstead ofstartDate/endDaterange, flatname/localNamestrings instead of a[{language, text}]array, nolanguagerequest param (always returns English + native).Backend changes (
backend/app/routers/holidays.py)get_public_holidayswithhttps://date.nager.at/api/v3/PublicHolidays/{year}/{country}."Public" in holiday["types"].languagequery parameter from/holidays/public— Nager always returnsname(English) andlocalName(native)._make_cache_key/_build_upstream_paramsaccordingly — nolanguagedimension for public holidays.get_school_holidays) unchanged.Frontend changes
usePublicHolidays.ts: Replace thePublicHolidayinterface (openholidays shape) with the Nager shape:PublicHolidayNameinterface andgetPublicHolidayNamehelper — no longer needed.toHolidayMap:holiday.name→name,holiday.localName→localName. No date-range expansion (Nager always has a singledate).NATIVE_LANGUAGEconstant — Nager'slocalNameis always the native language.languageparam forwarding to the backend for the public endpoint (the param can stay onusePublicHolidaysfor thetoHolidayMapnamefield if needed, but it no longer goes to the API).useOpenHolidays→useHolidaysApior keep and note it now only applies to school holidays.Note for #546
The
cached_holidaystable design should account for the fact that public holidays (Nager) and school holidays (openholidaysapi.org) have different response shapes. Store them as opaque JSON blobs keyed by(holiday_type, country, year, subdivision, language)— the normalization happens at query time in the router.