* 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>
121 lines
5.2 KiB
TypeScript
121 lines
5.2 KiB
TypeScript
import type { CompanySettings } from '@/types'
|
|
import { getCurrentFiscalYearStart } from '@/lib/company/fiscal-year'
|
|
|
|
export interface BookedCoverage {
|
|
/** Entry date of the company's latest posted verifikat. */
|
|
lastBookedDate: string
|
|
/**
|
|
* Day after lastBookedDate (the earliest sync start that cannot overlap
|
|
* booked entries), clamped to today (UTC) so the backend accepts it.
|
|
*/
|
|
suggestedStartDate: string
|
|
}
|
|
|
|
/**
|
|
* Turn the latest posted verifikat date into a "start syncing from" suggestion.
|
|
*
|
|
* Issue #917: this used to be derived from sie_imports.fiscal_year_end, which
|
|
* is the fiscal PERIOD end, not how far the bookkeeping actually reaches. For
|
|
* a company whose SIE covered an extended first year (2025-10-01 to 2026-12-31)
|
|
* but whose entries stopped in May, the old value suggested a start date past
|
|
* every unbooked transaction. Returns null when there is nothing booked: no
|
|
* suggestion beats a misleading one.
|
|
*/
|
|
export function resolveBookedCoverage(
|
|
lastPostedEntryDate: string | null | undefined,
|
|
today: Date = new Date(),
|
|
): BookedCoverage | null {
|
|
if (!lastPostedEntryDate) return null
|
|
// Pin the math to UTC so the day-after arithmetic is timezone-independent.
|
|
const d = new Date(lastPostedEntryDate + 'T00:00:00Z')
|
|
d.setUTCDate(d.getUTCDate() + 1)
|
|
const dayAfter = d.toISOString().split('T')[0]
|
|
// The backend PATCH handler (index.ts) rejects initial_lookback_from_date
|
|
// unless Date.now() is past the date's UTC midnight, so the newest date it
|
|
// accepts is the current UTC date. A company whose latest verifikat is
|
|
// dated today (plausible at initial bank activation) would otherwise get
|
|
// tomorrow suggested here and a 400 when saving. Clamp to today; ISO date
|
|
// strings compare correctly as plain strings.
|
|
const todayUtc = today.toISOString().split('T')[0]
|
|
return {
|
|
lastBookedDate: lastPostedEntryDate,
|
|
suggestedStartDate: dayAfter <= todayUtc ? dayAfter : todayUtc,
|
|
}
|
|
}
|
|
|
|
export interface GapFillSuggestion {
|
|
/** Date of the newest transaction this connection has already imported. */
|
|
latestImportedDate: string
|
|
/**
|
|
* Suggested sync start: latestImportedDate minus GAP_FILL_OVERLAP_DAYS,
|
|
* clamped to today (UTC) so the backend accepts it.
|
|
*/
|
|
suggestedStartDate: string
|
|
}
|
|
|
|
/**
|
|
* Overlap requested before the newest already-imported row. The external_id
|
|
* dedup makes re-imported rows no-ops, so the overlap costs nothing, and it
|
|
* catches transactions the bank booked late around the boundary.
|
|
*/
|
|
export const GAP_FILL_OVERLAP_DAYS = 7
|
|
|
|
/**
|
|
* Turn the newest transaction a connection has already imported into a
|
|
* "continue where the last fetch stopped" suggestion for RENEWALS.
|
|
*
|
|
* A reconnect 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. Re-requesting a long lookback then floods the inbox
|
|
* with rows over already-bookkept periods (the 2026-08 renewal flood), so a
|
|
* renewal should default to fetching only the gap since the last import.
|
|
* Returns null when the connection has never imported anything: a first
|
|
* connect has no gap to fill.
|
|
*/
|
|
export function resolveGapFillStart(
|
|
latestImportedDate: string | null | undefined,
|
|
today: Date = new Date(),
|
|
): GapFillSuggestion | null {
|
|
if (!latestImportedDate) return null
|
|
const d = new Date(latestImportedDate + 'T00:00:00Z')
|
|
if (!Number.isFinite(d.getTime())) return null
|
|
d.setUTCDate(d.getUTCDate() - GAP_FILL_OVERLAP_DAYS)
|
|
let start = d.toISOString().split('T')[0]
|
|
// The backend clamps every lookback to 365 days. A renewal staler than that
|
|
// must show the date the backfill will actually start from, not promise a
|
|
// gap it cannot fill (the >90-day helper already points at SIE/file import
|
|
// for older history).
|
|
const floor = new Date(today.getTime())
|
|
floor.setUTCDate(floor.getUTCDate() - 365)
|
|
const floorUtc = floor.toISOString().split('T')[0]
|
|
if (start < floorUtc) start = floorUtc
|
|
// The backend PATCH handler rejects initial_lookback_from_date unless it is
|
|
// strictly in the past; today (UTC) is the newest value it accepts. A
|
|
// latestImportedDate in the future can only come from bad bank data: clamp
|
|
// rather than propagate it.
|
|
const todayUtc = today.toISOString().split('T')[0]
|
|
return {
|
|
latestImportedDate,
|
|
suggestedStartDate: start <= todayUtc ? start : todayUtc,
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Resolve the start of the current fiscal year, preferring the actual
|
|
* fiscal_periods row that contains today over the recurring
|
|
* fiscal_year_start_month setting.
|
|
*
|
|
* Issue #917: the recurring setting cannot represent an extended or shortened
|
|
* first fiscal year (e.g. 2025-10-01 to 2026-12-31 for a company that later
|
|
* runs calendar years), so deriving from it alone returned 2026-01-01 where
|
|
* the real start was 2025-10-01. The period row is authoritative when it
|
|
* exists; the setting remains the fallback for companies without period rows.
|
|
*/
|
|
export function resolveFiscalYearStart(
|
|
currentPeriodStart: string | null | undefined,
|
|
settings: Pick<CompanySettings, 'fiscal_year_start_month' | 'entity_type'> | null | undefined,
|
|
today: Date = new Date(),
|
|
): string {
|
|
return currentPeriodStart || getCurrentFiscalYearStart(settings, today)
|
|
}
|