Skip to content

Promote: staging -> develop - #922

Open
github-actions[bot] wants to merge 1 commit into
developfrom
staging
Open

Promote: staging -> develop#922
github-actions[bot] wants to merge 1 commit into
developfrom
staging

Conversation

@github-actions

Copy link
Copy Markdown
Contributor

Automatic Staging PR

This PR was automatically created after changes were pushed to staging.

Commits: 1 new commit(s)

Checklist

  • Review all changes
  • Verify CI passes
  • Approve and merge to promote into develop

…tory filter (#907)

EN:
The transaction-history filter compared the end bound against the picked
day at local midnight, so every transaction timestamped later on that
day was dropped (#779). The bound is now the start of the following
local day, compared exclusively, which makes the selected end day fully
inclusive. Three regression tests cover the same-day-afternoon case, the
day after the bound, and a UTC-flagged timestamp falling on the local
end day.

DE:
Der Transaktions-Filter verglich die obere Grenze gegen den gewählten
Tag um lokale Mitternacht, wodurch jede später an diesem Tag datierte
Transaktion herausfiel (#779). Die Grenze ist jetzt der Beginn des
Folgetags in lokaler Zeit, exklusiv verglichen, womit der gewählte
Endtag vollständig enthalten ist. Drei Regressionstests decken den
Nachmittagsfall, den Tag nach der Grenze und einen UTC-markierten
Zeitstempel am lokalen Endtag ab.

<details>
<summary>Details</summary>

## Root cause

`DatePickerField` → `DatePicker.pickDate` returns the selected day as
`DateTime(y, m, d)`, i.e. **local midnight**.
`TransactionHistoryFilterCubit._applyFilter` treated that value as the
inclusive upper bound:

```dart
final beforeEnd = endDate == null || !transactionDate.isAfter(endDate);
```

For an end date of `2026-04-01` that bound is `2026-04-01 00:00:00.000`
local, so a transaction at `2026-04-01 14:00` satisfies `isAfter(bound)`
and is excluded. Only transactions at exactly 00:00:00.000 on the
selected end day survived — an off-by-one day on the upper edge of every
range the user picks.

The start bound is not affected: local midnight is already the correct
*start* of the picked day, so a picked start date behaves as intended.

## Fix

```dart
final endBound = endDate == null ? null : _startOfNextLocalDay(endDate);
...
final beforeEnd = endBound == null || transactionDate.isBefore(endBound);
```

with

```dart
DateTime _startOfNextLocalDay(DateTime date) {
  final local = date.toLocal();
  return DateTime(local.year, local.month, local.day + 1);
}
```

Two deliberate details:

- **Exclusive next-day bound rather than `23:59:59.999`.** `DateTime`
carries microsecond resolution, so an end-of-day sentinel would still
leave a sub-millisecond hole. `isBefore(startOfNextDay)` has no gap.
- **`DateTime(y, m, d + 1)` rather than `.add(const Duration(days:
1))`.** `Duration` adds 24 absolute hours; on a DST-transition day that
lands at 23:00 or 01:00 local instead of the next local midnight. The
constructor normalises overflowing day values and resolves the
wall-clock instant correctly, including across month and year ends.

## The zone concern raised in the issue

Checked and **not a defect**: `DateTime.isBefore` / `isAfter` compare
`microsecondsSinceEpoch`, i.e. absolute instants, so a UTC-flagged
`timestamp` (from `DateTime.parse` on an API `...Z` string) is compared
correctly against a local bound — no cross-boundary drift. The trap the
team has hit before is `DateTime.==` / equality, which additionally
compares the `isUtc` flag; this comparison path never uses it.

What *does* matter for zone consistency is which calendar day the bound
denotes. `TransactionHistoryRow` renders
`transaction.timestamp.toLocal()`, so the user reads local days; the
bound is therefore normalised via `toLocal()` before the day components
are taken, so the filtered range matches the days shown in the list. The
added `toLocal()` is a no-op for the values the picker produces and
guards the bound against a caller passing a UTC-flagged `DateTime`.

## Blast radius

`state.filtered` also feeds `TransactionHistoryDownloadButton`, so the
exported statement inherits the same correction — a statement generated
with an end date of "today" previously omitted everything from today.

## Tests

Added to
`test/screens/transaction_history/cubits/transaction_history_filter_cubit_test.dart`:

1. `changeFilter keeps transactions timestamped later on the selected
end day (issue #779 regression)` — 00:00, 14:00 and 23:59:59 on the end
day; expects all three. Fails on the old code with 1 of 3.
2. `changeFilter still excludes the day after the selected end date` —
guards against over-correcting the bound by a day.
3. `changeFilter includes a UTC-flagged timestamp falling on the local
end day` — pins the instant-vs-flag semantics described above.

One existing test name (`changeFilter includes the boundaries (isBefore
/ isAfter, not isAtSameMoment)`) was shortened to `changeFilter includes
the boundary days`, since it no longer describes the implementation it
names. Its assertions are unchanged, and all four pre-existing filter
tests keep passing under the new bound.

**Not executed locally:** no Flutter toolchain was available in the
environment this change was prepared in, so `flutter test` and `flutter
analyze` have not been run against it — CI on this PR is the first
execution. The diff was reviewed by hand against the existing test
file's structure and the repo's `page_width: 100` formatter setting.

</details>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant