diff --git a/DECISIONS.md b/DECISIONS.md index 7ff58d59..4cc64959 100644 --- a/DECISIONS.md +++ b/DECISIONS.md @@ -50,3 +50,4 @@ One line per decision: `[YYYY-MM-DD] : `. Appended by agents and [2026-07-08] One reconciliation PR adopts 3 prod-orphaned migrations (20260707113729 enrichment + 20260708120000/130000 ledger-stats RPCs) plus their pg-tests/fixtures onto main, instead of waiting on #927+#935 to merge: prod ledger was 3 versions ahead of the repo, leaving the default Supabase branch MIGRATIONS_FAILED and blocking every preview branch from being created. SQL committed byte-identical under the exact apply-time versions -> no-op on prod (idempotent), clean on fresh replays, and a no-op on #927/#935's next rebase. Carries #935's DB layer only (migrations + pg-tests + fixtures), not its UI/lib/i18n. Root anti-pattern: all three applied to prod via MCP apply_migration without committing the file (CLAUDE.md "never leave the remote DB ahead of the repo"). [2026-07-08] Pinned @anthropic-ai/bedrock-sdk to exact 0.29.1 (dependabot #884 auto-bumped it to 0.32.0, which broke Bedrock streaming in prod: empty stream / "request ended without sending any chunks"). Guarded three ways against accidental re-bump: exact pin in package.json, dependabot ignore, and a pinned-dep check in scripts/checks/no-new-antipatterns.mjs (check:guards). Unpin only once 0.32.x streaming is verified against Bedrock. [2026-07-09] Issue #916 (disconnect orphans ledger accounts): release claims by demoting cash_accounts rows to manual (bank_connection_id = null), never deleting: transactions.cash_account_id and ledger history reference the rows, and upsertFromPsd2 promotes a manual holder in place on reconnect so the bank lands back on its original BAS slot. Orphans predating the fix self-heal via a revoked-status filter in the allocator + collision guard (not data repair). When a promote collides with a duplicate row for the same connection+uid (callback mirrored onto an overflow slot pre-fix), the duplicate is deleted only if it has zero linked transactions, otherwise demoted: preserves FK links while freeing the slot. Picker-save rejections now render inline in the picker instead of routing to the sync-progress modal, whose parent-unmount-on-close made every save outcome invisible. +[2026-07-09] #917 fix scoped to the current-year suggestion: "Sedan räkenskapsårets början" now resolves from the fiscal_periods row containing today, but the "Föregående räkenskapsårets start" custom option still derives from the recurring fiscal_year_start_month: the issue only covers the current-year date and a first-year company has no previous period row to resolve against. diff --git a/extensions/general/enable-banking/components/AccountPickerDialog.tsx b/extensions/general/enable-banking/components/AccountPickerDialog.tsx index 5652e6be..89824047 100644 --- a/extensions/general/enable-banking/components/AccountPickerDialog.tsx +++ b/extensions/general/enable-banking/components/AccountPickerDialog.tsx @@ -25,10 +25,13 @@ import { Loader2 } from 'lucide-react' import { createClient } from '@/lib/supabase/client' import { useCompany } from '@/contexts/CompanyContext' import { - getCurrentFiscalYearStart, getPreviousFiscalYearStart, daysBetween, } from '@/lib/company/fiscal-year' +import { + resolveBookedCoverage, + resolveFiscalYearStart, +} from '../lib/date-suggestions' import type { CompanySettings } from '@/types' import type { StoredAccount } from '../types' import { @@ -79,8 +82,8 @@ export function AccountPickerDialog({ }: AccountPickerDialogProps) { const { toast } = useToast() // Memoise so the client has a stable reference across re-renders. Without this, - // listing `supabase` in the SIE-fetch effect's deps would re-fire that query on - // every checkbox tick or parent re-render. + // listing `supabase` in the data-fetch effects' deps would re-fire those queries + // on every checkbox tick or parent re-render. const supabase = useMemo(() => createClient(), []) const { company } = useCompany() @@ -90,11 +93,12 @@ export function AccountPickerDialog({ // the dialog: a rejected save persisted nothing and started no sync, so the // user must see why and be able to correct the picks. const [saveError, setSaveError] = useState(null) - const [sieLastDate, setSieLastDate] = useState(null) + const [lastBookedDate, setLastBookedDate] = useState(null) const [chartAccounts, setChartAccounts] = useState([]) const [chartError, setChartError] = useState(false) const [ledgerByUid, setLedgerByUid] = useState>({}) const [companySettings, setCompanySettings] = useState | null>(null) + const [currentPeriodStart, setCurrentPeriodStart] = useState(null) const [settingsLoaded, setSettingsLoaded] = useState(false) const [lookbackMode, setLookbackMode] = useState('fiscal-year') @@ -143,43 +147,68 @@ export function AccountPickerDialog({ }, [open, accounts]) // Load fiscal_year_start_month + entity_type so "Sedan räkenskapsårets början" - // resolves to the right date for non-calendar fiscal years. + // resolves to the right date for non-calendar fiscal years, plus the actual + // fiscal_periods row containing today: the recurring setting cannot represent + // an extended or shortened first year, so the period row wins when it exists. useEffect(() => { if (!open || !company?.id) return let cancelled = false ;(async () => { - const { data } = await supabase - .from('company_settings') - .select('fiscal_year_start_month, entity_type') - .eq('company_id', company.id) - .maybeSingle() + const today = new Date().toISOString().split('T')[0] + const [settingsRes, periodRes] = await Promise.all([ + supabase + .from('company_settings') + .select('fiscal_year_start_month, entity_type') + .eq('company_id', company.id) + .maybeSingle(), + supabase + .from('fiscal_periods') + .select('period_start') + .eq('company_id', company.id) + .lte('period_start', today) + .gte('period_end', today) + .order('period_start', { ascending: false }) + .limit(1) + .maybeSingle(), + ]) if (cancelled) return - setCompanySettings((data as { fiscal_year_start_month?: number; entity_type?: CompanySettings['entity_type'] } | null) as Pick | null) + if (settingsRes.error || periodRes.error) { + // A failed fetch must not present the calendar-year fallback as the + // authoritative fiscal-year start (issue #917). Leave settingsLoaded + // false so the date stays masked; if the user proceeds anyway the + // request falls back to the recurring-setting derivation. + return + } + setCompanySettings((settingsRes.data as { fiscal_year_start_month?: number; entity_type?: CompanySettings['entity_type'] } | null) as Pick | null) + setCurrentPeriodStart((periodRes.data as { period_start?: string } | null)?.period_start || null) setSettingsLoaded(true) })() return () => { cancelled = true } }, [open, company?.id, supabase]) - // Fetch the latest SIE import end date so we can offer "day after last SIE entry" - // as a one-click escape from the default fiscal-year start. Only matters on the - // initial activation flow: selection edits don't re-run sync. + // Fetch the latest posted verifikat date so we can offer "day after the last + // booked entry" as a one-click escape from the default fiscal-year start. + // Deliberately NOT sie_imports.fiscal_year_end (issue #917): that is the + // fiscal period's end, which can lie months past the last actually booked + // transaction and would make the user skip everything unbooked in between. + // Only matters on the initial activation flow: selection edits don't re-run sync. useEffect(() => { if (!open || !isInitialSelection || !company?.id) { - setSieLastDate(null) + setLastBookedDate(null) return } let cancelled = false ;(async () => { const { data } = await supabase - .from('sie_imports') - .select('fiscal_year_end') + .from('journal_entries') + .select('entry_date') .eq('company_id', company.id) - .eq('status', 'completed') - .order('fiscal_year_end', { ascending: false }) + .eq('status', 'posted') + .order('entry_date', { ascending: false }) .limit(1) .maybeSingle() if (cancelled) return - setSieLastDate((data as { fiscal_year_end?: string } | null)?.fiscal_year_end || null) + setLastBookedDate((data as { entry_date?: string } | null)?.entry_date || null) })() return () => { cancelled = true } }, [open, isInitialSelection, company?.id, supabase]) @@ -384,16 +413,14 @@ export function AccountPickerDialog({ } } - const dayAfterSie = useMemo(() => { - if (!sieLastDate) return null - const d = new Date(sieLastDate) - d.setDate(d.getDate() + 1) - return d.toISOString().split('T')[0] - }, [sieLastDate]) + const bookedCoverage = useMemo( + () => resolveBookedCoverage(lastBookedDate), + [lastBookedDate], + ) const fiscalYearStart = useMemo( - () => getCurrentFiscalYearStart(companySettings), - [companySettings], + () => resolveFiscalYearStart(currentPeriodStart, companySettings), + [currentPeriodStart, companySettings], ) const previousFiscalYearStart = useMemo( @@ -468,13 +495,13 @@ export function AccountPickerDialog({

- {sieLastDate && dayAfterSie && ( + {bookedCoverage && (

- Senaste SIE-importen täcker till{' '} - {sieLastDate}. + Ditt senaste bokförda verifikat är daterat{' '} + {bookedCoverage.lastBookedDate}. Vi föreslår{' '} - {dayAfterSie}{' '} + {bookedCoverage.suggestedStartDate}{' '} som startdatum så inget överlappar din bokföring.