Files
accounted/extensions/general/enable-banking/lib/__tests__/date-suggestions.test.ts
T
MattssonandClaude Fable 5 a47ba9fede fix(enable-banking): stop renewal history floods (gap-fill default + backfill reconciliation) (#1590)
* chore(ci): guard bedrock-sdk against automated version bumps

The 2026-07 prod outage (empty Bedrock streams breaking invoice OCR and
the assistant) came from an unreviewed @anthropic-ai/bedrock-sdk 0.32.0
bump. The package is exact-pinned to 0.29.1, but nothing stopped an
automated PR from proposing the bump again. Add a dependabot config in
security-updates-only posture (open-pull-requests-limit: 0) with an
ignore for bedrock-sdk >=0.30.0 so neither scheduled nor security
updates can reintroduce it silently.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(enable-banking): stop renewal history floods (gap-fill default + backfill reconciliation)

Renewing a bank connection walks the same pending_selection -> active
flow as a first connect, and a fresh consent often makes the bank
release history the first connect never delivered. Two gaps turned that
into a flood of falsely 'unhandled' rows over already-bookkept periods
(11 companies, ~600 rows in prod):

- The picker defaulted every renewal to the fiscal-year lookback. It now
  probes the connection's newest imported transaction and defaults a
  renewal to 'continue where the last fetch stopped' (7-day overlap,
  absorbed by external_id dedup), with an .attn warning when a longer
  lookback re-requests already-fetched periods.
- The inline initial backfill ran without the SIE-overlap guard that the
  manual /sync route and the cron both apply. It now suppresses
  auto-categorization on overlap and runs the same unattended-threshold
  reconciliation sweep, scoped per ledger account via
  resolveCashAccountScope instead of the pooled unscoped form
  (#1290/#1298), and surfaces the linked count as auto_matched in the
  sync summary.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(enable-banking): renewal guard survives mid-open refetch; harden sweep scope and window

Skeptic pass findings on the renewal-flood guard:

- REFUTED: the settings panel's visibility refetch (near-certain in a
  BankID reconnect) hands the open picker a fresh accounts identity; the
  pre-existing reset effect then wiped the gap-fill state while the
  probe effect never re-ran, silently stranding the renewal back on the
  fiscal-year default with no warning. The probe now keys its state by
  connectionId and shares the reset's triggers via an accounts dep, so
  wipe and re-probe always pair up.
- The sweep skips accounts whose cash_accounts row did not resolve
  (found: false) instead of degrading to the pooled currency-only form
  (#1290 write shape), which could otherwise follow a same-request
  mirror-upsert failure.
- The sweep window opens at the oldest booking date the bank actually
  returned: over-returning ASPSPs ingest rows outside the requested
  window, which the sweep would otherwise never examine.
- resolveGapFillStart clamps to the backend's 365-day lookback floor so
  the radio never promises a start date the backfill cannot honor.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(enable-banking): address PR #1590 review findings

- zizmor: add a 7-day cooldown to the dependabot npm entry.
- CodeRabbit: clear the event bus in the accounts-route beforeEach (repo
  test convention); surface probe query failures in AccountPickerDialog
  so a failed probe cannot read as a first connect and silently restore
  the fiscal-year default; build the sweep's ledger-account list with a
  string filter instead of a nullish fallback so the pooled scope path
  is structurally unreachable.
- Swedish compliance review: document at the sweep site that linking
  writes bank-feed metadata only, never journal tables, with the
  opening-balance link trigger and unlinkReconciliation reversibility
  spelled out.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 16:50:24 +02:00

126 lines
4.9 KiB
TypeScript

import { describe, it, expect } from 'vitest'
import {
resolveBookedCoverage,
resolveFiscalYearStart,
resolveGapFillStart,
} from '../date-suggestions'
describe('resolveBookedCoverage', () => {
it('suggests the day after the last posted verifikat date', () => {
expect(resolveBookedCoverage('2026-05-14')).toEqual({
lastBookedDate: '2026-05-14',
suggestedStartDate: '2026-05-15',
})
})
it('rolls over month and year boundaries', () => {
// Pin "today" past every case so the clamp does not kick in.
const today = new Date('2030-01-01T12:00:00Z')
expect(resolveBookedCoverage('2026-01-31', today)?.suggestedStartDate).toBe('2026-02-01')
expect(resolveBookedCoverage('2026-12-31', today)?.suggestedStartDate).toBe('2027-01-01')
// Leap year: 2028-02-28 is not the last day of February.
expect(resolveBookedCoverage('2028-02-28', today)?.suggestedStartDate).toBe('2028-02-29')
})
it('clamps to today when the last posted verifikat is dated today (backend rejects non-past dates)', () => {
// Day after 2026-07-09 would be 2026-07-10 (tomorrow), which the PATCH
// handler rejects with 400; the suggestion must stay clickable.
const today = new Date('2026-07-09T12:00:00Z')
expect(resolveBookedCoverage('2026-07-09', today)).toEqual({
lastBookedDate: '2026-07-09',
suggestedStartDate: '2026-07-09',
})
})
it('clamps to today when the last posted verifikat is dated in the future', () => {
const today = new Date('2026-07-09T12:00:00Z')
expect(resolveBookedCoverage('2026-08-15', today)).toEqual({
lastBookedDate: '2026-08-15',
suggestedStartDate: '2026-07-09',
})
})
it('returns null when the company has no posted entries (issue #917: never fall back to fiscal_year_end)', () => {
expect(resolveBookedCoverage(null)).toBeNull()
expect(resolveBookedCoverage(undefined)).toBeNull()
expect(resolveBookedCoverage('')).toBeNull()
})
})
describe('resolveGapFillStart', () => {
it('suggests one week of overlap before the newest imported transaction', () => {
const today = new Date('2026-08-13T12:00:00Z')
expect(resolveGapFillStart('2026-08-06', today)).toEqual({
latestImportedDate: '2026-08-06',
suggestedStartDate: '2026-07-30',
})
})
it('rolls over month and year boundaries', () => {
// Todays within the 365-day floor of each case so only the 7-day overlap acts.
expect(resolveGapFillStart('2026-01-03', new Date('2026-01-15T12:00:00Z'))?.suggestedStartDate).toBe('2025-12-27')
expect(resolveGapFillStart('2026-03-04', new Date('2026-03-10T12:00:00Z'))?.suggestedStartDate).toBe('2026-02-25')
})
it('clamps to today when bank data claims a future date (backend rejects non-past dates)', () => {
const today = new Date('2026-08-13T12:00:00Z')
expect(resolveGapFillStart('2026-09-20', today)).toEqual({
latestImportedDate: '2026-09-20',
suggestedStartDate: '2026-08-13',
})
})
it('clamps a stale renewal to the backend 365-day lookback floor so the shown date matches the actual backfill', () => {
const today = new Date('2026-08-13T12:00:00Z')
// Newest import 2025-01-10; minus 7d = 2025-01-03, older than the 365-day
// floor (2025-08-13), which the backend would silently clamp to anyway.
expect(resolveGapFillStart('2025-01-10', today)).toEqual({
latestImportedDate: '2025-01-10',
suggestedStartDate: '2025-08-13',
})
})
it('returns null when the connection has never imported anything (first connect has no gap)', () => {
expect(resolveGapFillStart(null)).toBeNull()
expect(resolveGapFillStart(undefined)).toBeNull()
expect(resolveGapFillStart('')).toBeNull()
})
it('returns null for an unparsable date', () => {
expect(resolveGapFillStart('not-a-date')).toBeNull()
})
})
describe('resolveFiscalYearStart', () => {
const calendarYearSettings = {
fiscal_year_start_month: 1,
entity_type: 'aktiebolag' as const,
}
it('prefers the actual fiscal period row over the recurring start month (issue #917: extended first year)', () => {
// Company with an extended first fiscal year 2025-10-01 to 2026-12-31 that
// later runs calendar years: the recurring setting would wrongly resolve
// to 2026-01-01.
expect(
resolveFiscalYearStart('2025-10-01', calendarYearSettings, new Date('2026-07-09')),
).toBe('2025-10-01')
})
it('falls back to the recurring fiscal_year_start_month when no period row exists', () => {
expect(
resolveFiscalYearStart(null, calendarYearSettings, new Date('2026-07-09')),
).toBe('2026-01-01')
expect(
resolveFiscalYearStart(
undefined,
{ fiscal_year_start_month: 7, entity_type: 'aktiebolag' },
new Date('2026-05-01'),
),
).toBe('2025-07-01')
})
it('falls back to calendar year when settings are missing too', () => {
expect(resolveFiscalYearStart(null, null, new Date('2026-07-09'))).toBe('2026-01-01')
})
})