feat(statements): flag billing periods that overlap another statement
ci / lint-test (push) Successful in 46s

An account cannot be billed twice for the same day, so an overlap means those
transactions are in the ledger twice. ANZ statements 107 and 143 overlap by 118
days and put roughly $42,000 of duplicate rows in; nothing anywhere said so.

Two details decide whether this catches the real case:

  - Account numbers compare with non-digits stripped. The duplicate got in
    because the existing key compared raw text and ANZ wrote the same account
    as 408556264 on one statement and 4085-56264 on the other.
  - The range is half-open. These statements are issued back-to-back with one
    period ending the day the next starts, so inclusive bounds flagged 5 pairs
    of which 3 were consecutive and fine. Half-open leaves exactly the 2 real
    ones.

NULL bounds are excluded rather than handed to daterange, where NULL means
unbounded and an undated statement would overlap all of history.

Detection only. It does not refuse the import or touch the duplicate rows --
cleaning those is separate, and must supersede rather than delete because every
child of transactions is ON DELETE CASCADE and the curation sits on the
duplicate side.

Both subtleties have a test, and both fail if you undo them.
This commit is contained in:
2026-07-28 11:46:45 +10:00
parent 7a1acc32a9
commit d5589b2980
3 changed files with 134 additions and 2 deletions
+15
View File
@@ -206,6 +206,21 @@ export default function StatementsPage() {
</td>
<td className="px-4 py-3 text-zinc-400 whitespace-nowrap">
{formatPeriod(s.billing_start_date, s.billing_end_date)}
{/* An account cannot be billed twice for the same day, so
an overlap means these transactions are in the ledger
twice. Red rather than amber: the balance warning above
means a statement doesn't add up, this means the data is
double-counted everywhere it is summed. */}
{s.overlaps?.length > 0 && (
<div
className="text-[10px] text-red-400 mt-0.5"
title={`Billing period overlaps statement ${s.overlaps
.map((o) => `#${o.id} by ${o.days} day${o.days === 1 ? "" : "s"}`)
.join(", ")}. The overlapping transactions are likely imported twice.`}
>
overlaps #{s.overlaps.map((o) => o.id).join(", #")}
</div>
)}
</td>
<td className="px-4 py-3 text-zinc-400 whitespace-nowrap">
{formatDate(s.payment_due_date ?? s.billing_end_date)}