apache / apache/fineract-backoffice-ui

Date fields default to the browser's timezone while Fineract validates in the tenant's, so users outside the tenant zone are refused for dates they never chose

Open
#358 0 comments 0 reactions 0 assignees View on GitHub
angular bug effort: M impact: branch-ops priority: high typescript
Dominant language
TypeScript
Stars
15
Forks
60
Avg merge
10h 15m
Merged PRs (30d)
108

Description

## Business value

An officer opens a dialog, accepts the date the application already filled in, clicks Confirm — and the platform refuses it. The date they never touched is wrong, and nothing on screen says so.

For roughly five and a half hours of every day, a branch working in a timezone ahead of the tenant's sees actions fail for no reason it can act on. The refusals are the confusing kind — *"Submitted on date cannot be after the activation date"* — naming a date the user did not enter and cannot see. The workaround is to open the picker and choose yesterday, which no one will discover, and which is indistinguishable from data entry error when it shows up later in an audit.

It also silently damages the ledger's meaning. Where the platform *accepts* the browser's date, the record is stamped with a day the institution was not working — a client activated "tomorrow", a transaction dated a day out. That is worse than a refusal, because nothing fails.

This is not hypothetical. It reproduces on demand, and it is currently the cause of an intermittent CI failure (see below).

## What is wrong

Fineract stamps and validates dates using the **tenant's** timezone — `m_tenants.timezone_id`, which its seed data sets to `Asia/Kolkata`. The application fills date fields from the **browser's** clock:

```ts
// e.g. src/app/features/centers/center-action-dialog.component.ts
date = new Date().toISOString();

// src/app/core/utils/date-formatter.ts — local components, deliberately
const day = String(d.getDate()).padStart(2, '0');
const month = MONTHS[d.getMonth()];
const year = d.getFullYear();
```

Between **18:30 and 24:00 UTC** a UTC browser and an `Asia/Kolkata` tenant are a day apart, so the two disagree about what "today" is.

## Reproduction

With the e2e stack up and the wall clock between 18:30 and 24:00 UTC:

```bash
TZ=UTC npx playwright test e2e/center-servicing.spec.ts --project=backend -g "activated"
```

The centre is created (Fineract stamps `submittedOnDate` as the *tenant's* today), the UI offers the *browser's* today as the activation date, and the platform answers:

```json
{
"httpStatusCode": "400",
"errors": [{
"userMessageGlobalisationCode": "error.msg.group.submittedOnDate.after.activation.date",
"defaultUserMessage": "Submitted on date cannot be after the activation date",
"parameterName": "submittedOnDate",
"args": [{ "value": "2026-08-16" }]
}]
}
```

The same run passes from a machine already in `Asia/Kolkata`, and passes at any hour outside that window — which is exactly what makes it hard to see.

## Scope of the problem

This is not specific to centres. Every dialog that seeds a date from `new Date()` is affected — activation, closure, transaction, disbursal, repayment and the rest. Centres are simply where it surfaced, because activation is validated against a date the platform stamped moments earlier.

## What to build

Default date fields from the **platform's** notion of today rather than the browser's.

The application already displays a business date in the header, so the platform's date is available to the client — that is the value these pickers should open on. `GET /v1/businessdate` returns the configured business date when the tenant has one; note it answers `[]` when none is configured, so a fallback is still needed and it should be the tenant's current date, not the browser's.

Suggested shape:

- A single service that answers "what is today, according to the platform", with the browser's clock as a last resort.
- Date dialogs seed from it instead of `new Date()`.
- `formatDateToFineract` keeps formatting local components — the fix belongs at the source of the value, not in the formatter, so that a date a user genuinely picked is still sent as picked.

### Two things to get right

1. **Do not simply switch the formatter to UTC.** That trades one wrong answer for another: a user who explicitly picks a date would then have it shifted a day. The defect is in what the field is *seeded* with.
2. **A refused date must say which date was refused.** Today the dialog closes and the record silently stays in its previous state; the platform's message names a date the user never saw. Surfacing the platform's error is part of this fix.

## Testing

- **Unit:** a dialog seeds its date from the platform date service, not `new Date()`.
- **Unit:** with the platform a day ahead of the browser, the submitted payload carries the platform's date.
- **Backend e2e:** the `TZ=UTC` reproduction above should pass on its own merits, without the harness pinning the browser timezone.

## Relationship to the CI harness

`playwright.config.ts` pins the browser timezone to the tenant's so the suite stops depending on the hour it runs at. **That makes the harness deterministic; it does not fix this.** When this issue is resolved, that pin should be removed — and its removal is a good confirmation that the fix is real.

## Getting started

- `src/app/core/utils/date-formatter.ts` — `formatDateToFineract`, and the `FINERACT_DATE_FORMAT`/`FINERACT_LOCALE` constants
- `src/app/features/centers/center-action-dialog.component.ts` — a small, clear example of the seeding pattern
- The business-date indicator in the application header, for where the platform date is already read
- `npm test`, `npm run lint`, `npm run build`

Contributor guide

Open the contributing guide

Research direction

Start with src/app/features/centers/center-action-dialog.component.ts and the business-date indicator in the header, then inspect date fields seeded from new Date() and the existing date formatter. Run the TZ=UTC Playwright reproduction and the relevant unit tests. Done means dialogs use the platform date with a fallback, explicitly selected dates remain unchanged, refused dates are surfaced, and the playwright.config.ts timezone pin can be removed.

Written by the indexing model from the issue text.

Assessment

Tech stack
angular, playwright, typescript
Domain
frontend, testing
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
68/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.