Skip to content

fix(export): use UTC timestamps in CSV export - #2483

Merged
i5hi merged 1 commit into
payjoin-upgradefrom
pj/06-csv-utc
Jul 24, 2026
Merged

fix(export): use UTC timestamps in CSV export#2483
i5hi merged 1 commit into
payjoin-upgradefrom
pj/06-csv-utc

Conversation

@ethicnology

Copy link
Copy Markdown
Member

Fixes local vs. UTC timestamp handling in the transaction CSV export.

Merge order: independent — any time after #2443. No dependency on PR1-PR5, PR6.
#2443 → PR7 (this)

Building a plain (local) DateTime from a UTC end date's wall-clock
fields shifted the inclusive-day boundary by the device's UTC offset, so
the same export included or excluded edge transactions depending on the
machine's timezone. Round up in UTC when the input is UTC.
@ethicnology ethicnology self-assigned this Jul 20, 2026
@claude

claude Bot commented Jul 21, 2026

Copy link
Copy Markdown

Claude finished @ethicnology's task in 2m 12s —— View job


Audit: fix(export): use UTC timestamps in CSV export

  • Read the PR diff against origin/payjoin-upgrade
  • Read the usecase, formatter, UI caller, and tests
  • Post audit findings

Verdict: correct, minimal, and safe to merge. The fix does exactly what the commit message says — it preserves the input's UTC-ness when rounding the end date up to the next day — and it fixes a real, timezone-dependent bug.

The bug is real

Before this change, export_transactions_csv_usecase.dart built the exclusive end boundary as DateTime(end.year, end.month, end.day + 1) — a local DateTime constructed from a possibly-UTC input's wall-clock fields. With end = DateTime.utc(2026, 1, 15):

  • On a UTC-5 device, the boundary became 2026-01-16 00:00 local = 05:00 UTC, wrongly including transactions from the early hours of Jan 16 UTC.
  • On a UTC+7 device, it became Jan 15 17:00 UTC, wrongly excluding same-day evening transactions.

Notably, the existing test filters by inclusive date range, end rounds to end of day (test line 162) was timezone-dependent before this fix: it passes end: DateTime.utc(2026, 1, 15) with an "inside" tx at 18:00 UTC and an "after" tx at 00:01 UTC, so it would fail on a dev machine at UTC+7 (drops "inside") or UTC-5 (keeps "after"). It only passed on UTC CI runners. The fix makes both the behavior and the test deterministic.

Things I verified

  • Production behavior is unchanged. The only real caller is export_transactions_screen.dart:30, where showDatePicker returns local DateTimes, so end.isUtc is false and the pre-existing local branch runs. That's the right semantics — the exported range matches the local calendar days the user picked.
  • Day overflow is safe. Dart normalizes end.day + 1 past month/year boundaries (Jan 31 → Feb 1) in both the DateTime and DateTime.utc constructors.
  • Mixed-zone comparisons are fine. timestamp.isBefore(exclusiveEnd) compares instants regardless of each side's isUtc flag, so a local start with a UTC exclusiveEnd (or UTC tx timestamps) still filters correctly.
  • The CSV output itself was already UTC_date() in csv_transaction_export_formatter.dart:258 does toUtc() + Z suffix, correctly. So the PR title is slightly broader than the change; the commit message ("preserve UTC when rounding the CSV export end date") describes it more precisely. Cosmetic only.

Optional follow-ups (non-blocking)

  1. Pin the exclusive boundary in a test. The "after" tx sits at 00:01; a tx at exactly 2026-01-16T00:00:00Z (excluded) and one at 2026-01-15T23:59:59Z (included) would pin the boundary precisely. Fix this →
  2. Pre-existing asymmetry, just noting: start's time-of-day is honored as-is while end's is truncated to the day. Both current callers (the date picker and the tests) pass midnight, so this has no effect today.

I could not run flutter analyze or the test suite on this runner (no Flutter SDK installed), but the change is small enough to verify by inspection, and it doesn't alter any code path exercised by the app's UI.
· Branch pj/06-csv-utc

@ethicnology

Copy link
Copy Markdown
Member Author

@bullock can you handle that:

Pin the exclusive boundary in a test. The "after" tx sits at 00:01; a tx at exactly 2026-01-16T00:00:00Z (excluded) and one at 2026-01-15T23:59:59Z (included) would pin the boundary precisely. Fix this →
Pre-existing asymmetry, just noting: start's time-of-day is honored as-is while end's is truncated to the day. Both current callers (the date picker and the tests) pass midnight, so this has no effect today.

@ethicnology

Copy link
Copy Markdown
Member Author

@bullock can you handle that:

Pin the exclusive boundary in a test. The "after" tx sits at 00:01; a tx at exactly 2026-01-16T00:00:00Z (excluded) and one at 2026-01-15T23:59:59Z (included) would pin the boundary precisely. Fix this →
Pre-existing asymmetry, just noting: start's time-of-day is honored as-is while end's is truncated to the day. Both current callers (the date picker and the tests) pass midnight, so this has no effect today.

@github-actions

Copy link
Copy Markdown

🐂 Done — Bullock opened #2489 targeting pj/06-csv-utc. Review it before merging.

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.

2 participants