Skip to content

Timezone configuration

HOPE stores datetime values in UTC. Users and Business Areas store IANA timezone identifiers, which are used to display those values in the appropriate local timezone. The stored datetime values and REST API datetime responses remain in UTC.

Examples of valid IANA identifiers include UTC, Africa/Nairobi, America/New_York, and Europe/London. Identifiers are validated by django-timezone-field against the installed timezone database. Consumers should use the API choice list and prefer stable region/city identifiers; a numeric offset such as +02:00 is not a substitute for a timezone because it does not contain daylight-saving rules.

Business Area timezone

Every persisted Business Area has a non-empty timezone, which is visible and editable in Django admin. When a new Business Area is saved without an explicit timezone, HOPE initializes it from office_country.iso_code2. The first IANA timezone in the country mapping is used, with UTC as the fallback when the country is missing or unmapped. An explicitly selected timezone takes precedence.

The custom Create Business Office form follows the same rule. The new Business Office copies its parent's office_country and derives its timezone from that country when no explicit timezone is selected. Changing an existing Business Area's office_country does not overwrite its current timezone.

Existing Business Areas are initialized during migration using the same first timezone mapped to office_country.iso_code2, with UTC used when the country is missing or unmapped. This is a provisional migration value: administrators must review migrated Business Areas, especially in multi-timezone countries, and explicitly select the correct operational value. Country-derived values for newly created Business Areas in multi-timezone countries require the same review.

User timezone

User.timezone is an optional personal preference:

  • An IANA identifier means that the User explicitly selected that timezone.
  • UTC means that the User explicitly selected UTC.
  • null means that the User has no personal preference and inherits from the current Business Area.

Administrators can edit the field in the User's Personal info section in Django admin. An authenticated User can also update only their own preference through the scoped REST endpoint described below.

A User can work in several Business Areas. HOPE therefore never selects a fallback Business Area from the User's role assignments. The current Business Area must be supplied explicitly when resolving the effective timezone.

Effective timezone

The effective timezone is the timezone that applies to a particular human-facing context. It is resolved as follows:

Context Resolution order
Business Area-scoped User interface User preference, current Business Area, UTC
Notification Recipient User, related Business Area, UTC
User-requested single-Business-Area output Requesting User, current Business Area, UTC
Unattended Business-Area-specific human output Business Area, UTC
Global or multi-Business-Area output UTC
Celery trigger UTC
Grievance digest and reminder delivery window Recipient User, related Business Area, UTC
Other scheduled-task windows UTC unless explicitly documented otherwise

For example, if a User has no personal preference and the current Business Area uses Africa/Nairobi, both timezone: null and effective_timezone: "Africa/Nairobi" are returned for that scoped profile. If the User then selects Europe/London, that personal preference becomes effective in every Business Area context.

REST API

Reading timezone values

The standalone Business Area endpoints expose the stored Business Area timezone as timezone: string.

The scoped User profile exposes the stored preference as timezone: string | null and also exposes effective_timezone: string, resolved against the Business Area in the URL. User lists, program-user responses, shared nested User representations, and Business Areas nested in the profile do not expose timezone fields. The frontend reads the effective value once from the scoped profile and shares it through its authenticated context:

GET /api/rest/business-areas/{business_area_slug}/users/profile/

Example profile fields:

{
  "timezone": null,
  "effective_timezone": "Africa/Nairobi",
  "business_areas": [
    {
      "name": "Kenya",
      "slug": "kenya"
    }
  ]
}

The nested Business Area identifies the source of an inherited preference for display purposes; effective_timezone contains the value that clients use for datetime conversion.

Business Area REST resources are read-only. Administrators change their timezone through Django admin.

Updating the User preference

An authenticated User updates their own preference with:

PATCH /api/rest/business-areas/{business_area_slug}/users/profile-timezone/
Content-Type: application/json

To select a timezone:

{
  "timezone": "Europe/Warsaw"
}

To clear the preference and inherit from the current Business Area:

{
  "timezone": null
}

The response contains both the stored preference and the newly resolved effective timezone:

{
  "timezone": "Europe/Warsaw",
  "effective_timezone": "Europe/Warsaw"
}

The endpoint updates only timezone; no other User field is writable through it. An invalid or unknown identifier returns HTTP 400 with a field-level validation error. Saving the preference invalidates the User's cached profile after the database transaction commits.

Listing accepted timezone identifiers

Clients can load the validated, alphabetically sorted choice list from:

GET /api/rest/choices/timezones/

The response uses the standard HOPE choice shape:

[
  {
    "name": "Europe/London",
    "value": "Europe/London"
  },
  {
    "name": "Europe/Warsaw",
    "value": "Europe/Warsaw"
  }
]

This static list is independent of Business Area scope and is built once when the backend process starts. Clients should load it only when it is needed instead of including it in every profile request.

REST datetime values

REST datetime fields remain UTC. They are serialized with a trailing Z, which identifies UTC:

{
  "created_at": "2026-08-24T10:30:00Z"
}

The API does not replace this value with a User-local or Business-Area-local timestamp. A client displays it by:

  1. Parsing the REST value as a UTC instant.
  2. Selecting effective_timezone for the current scoped context.
  3. Converting the instant with an IANA-aware library such as the browser Intl.DateTimeFormat API.
  4. Showing the IANA identifier with the rendered value when the timezone context matters.

For example, 2026-08-24T10:30:00Z displayed in Europe/Warsaw is 24 August 2026 12:30 PM (Europe/Warsaw) while daylight-saving time is active. The source instant remains unchanged.

True date-only values such as 2026-08-24 have no time or timezone and must not be converted. Converting a date-only value as though it were midnight UTC can incorrectly move it to the previous or next calendar day.

Some date-only values remain backed by legacy datetime database columns. Their human-facing and REST contracts are still date-only: HOPE converts the stored value to its UTC calendar date and returns YYYY-MM-DD. Clients must not localize these fields:

  • Payment delivery_date.
  • Payment Plan start_date and end_date.
  • Document issuance_date and expiry_date.
  • Sanction-list individual listed_on.

New Payment delivery dates are stored as midnight UTC. The other legacy columns retain their existing storage and write behavior; the explicit date-only conversion occurs at their approved presentation boundaries.

Human-facing notifications and outputs

Human-facing timezone-aware values use a stable IANA identifier rather than only an ambiguous abbreviation. The backend human-readable format is:

24 August 2026 12:30 PM (Europe/Warsaw)

Payment Plan and Periodic Data Update email notifications resolve each recipient's effective timezone. Recipients with the same effective timezone are grouped so that one correctly localized message can be rendered for the group. The creation and action timestamps in those emails are converted to that timezone and include its IANA identifier.

Scheduled grievance notifications

The grievance daily digest and overdue/sensitive reminders target the local hour configured by the GRIEVANCE_NOTIFICATION_HOUR Constance setting. Its default is 6, meaning 06:00. Enter an integer from 0 through 23 in 24-hour format. The Constance administration form rejects values outside that range before saving the configuration. Both notification mechanisms use the same setting.

Their Celery beat entries run once per UTC hour and determine which recipient timezone has reached its configured delivery window. This avoids creating a separate beat schedule for every timezone. With the default setting, timezones with a non-whole-hour offset are processed during the first hourly run after 06:00, for example at 06:30 or 06:45 local time.

The dispatcher catches up the most recent missed local delivery window after a scheduler outage. Daily digest jobs use a Business Area, effective timezone, and local digest date as their delivery key. The digest contains changes from the previous local calendar day; its local midnight boundaries are converted separately to UTC so 23-hour and 25-hour daylight-saving transition days are handled correctly. After the send loop finishes, successful recipient IDs are stored before a partial failure is raised, so a retry does not resend messages to those recipients.

Sensitive reminders remain eligible after one elapsed day, and other overdue reminders after 30 elapsed days. Once eligible, they are delivered in the assignee's next configured local delivery window. The actual send time and last_notification_sent remain UTC datetimes. An unassigned ticket has no recipient timezone and is not marked as notified.

For a human-facing report or document concerning one Business Area, use the requesting User's preference, then the Business Area timezone, then UTC. Global or multi-Business-Area outputs remain UTC and must state that timezone explicitly.

The User XLSX export contains a TIMEZONE column with the stored personal preference. A blank value means that the User inherits the relevant Business Area timezone; the export does not replace that blank value with one arbitrary effective timezone.

UTC boundaries

The timezone settings affect human-facing presentation. They do not change these UTC contracts:

  • REST and OpenAPI datetime values.
  • Datetime storage and audit/event storage.
  • Celery triggers and scheduled-task windows, except for the documented scheduled grievance notification windows.
  • FSP and payment-gateway interchange files.
  • Partner-facing machine imports and exports.
  • Round-trip files containing timestamps that are parsed again on import.
  • Global processing and multi-Business-Area output.

Changing a Business Area timezone does not automatically change business-rule deadlines, date filters, or other task day boundaries. Code that intentionally implements a local calendar rule must receive the Business Area explicitly and calculate its local date from an aware datetime value.

Timezone conversion requires aware datetime values. Callers must attach the correct UTC or source offset before conversion; naive datetime values are rejected instead of being assigned an assumed timezone.

Daylight-saving time

Timezone conversion must use the complete IANA identifier, not a fixed UTC offset. IANA rules account for seasonal offset changes and daylight-saving transitions. Europe/Warsaw, for example, cannot be represented correctly for a whole year by a fixed UTC+1 or UTC+2 preference.

HOPE's shared timezone helpers resolve effective timezones, convert aware datetime values, format human-readable timestamps, and calculate a scoped local date. They do not globally activate a User or Business Area timezone for a request because doing so could change the UTC REST serialization contract.

Operational checklist

When creating or maintaining a Business Area:

  1. Select the office_country; select an explicit timezone when the derived country default is not appropriate.
  2. Save the Business Area and verify that the timezone column is populated.
  3. Update the timezone manually if the Business Area's operational location changes.

When diagnosing a User's displayed timezone:

  1. Check the stored User.timezone value.
  2. Check the current Business Area's stored timezone.
  3. Check effective_timezone in the scoped profile response.
  4. Confirm that the REST datetime was parsed as UTC before display conversion.
  5. Confirm that a date-only field was not passed through datetime conversion.