Skip to content

Switch public holiday source from openholidaysapi.org to date.nager.at #548

Description

@tjorim

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.namename, holiday.localNamelocalName. 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 useOpenHolidaysuseHolidaysApi 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.

Metadata

Metadata

Assignees

Labels

backendFastAPI backend / Python APIenhancementNew feature or requestpriority-mediumMedium priority - important but not urgenttestingTesting improvements and new test coverage

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions