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({
- 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.