diff --git a/DECISIONS.md b/DECISIONS.md index 067b9b22..c5887e40 100644 --- a/DECISIONS.md +++ b/DECISIONS.md @@ -213,3 +213,5 @@ One line per decision: `[YYYY-MM-DD] : `. Appended by agents and [2026-07-17] System-deadline delete = soft dismiss (dismissed_at) rather than a mute endpoint or hard delete: hard deletes were silently resurrected by the nightly backfill cron; dismissed rows satisfy the generator/backfill like completed rows. [2026-07-17] AGI deadline gate = employer_registered (nullable, pays_salaries fallback) with migration backfill from salary_runs: a registered employer owes monthly AGI incl. nil months (SFL 26 kap. 3 §); companies running payroll in-app are treated as employers (SFL 7 kap. 1 § obliges registration), erring toward a dismissible reminder over a missed statutory filing. Seasonal employers get only the December-period row. [2026-07-17] AGI XML generation no longer completes the arbetsgivardeklaration deadline: SFL 26 kap. deems the duty met only when the declaration reaches Skatteverket; kvittens reconcile remains the confirming path. +[2026-07-17] Removed 'bokslut' deadline type (replaced by statutory 'arsstamma', ABL 7:10, 6 months): the 3-month milestone had no legal basis and its broken-FY date math was off by one month (May-start FY got 31 Aug; Nov-start rolled "Feb 31" into March). Completed bokslut rows kept for history; type removed from the union like the earlier 'moms'/'inkomstdeklaration' retirements. +[2026-07-17] EU-trade/PS settings stay opt-in flags; a ledger-derived signal (postings on 3108/3308/3107, 15 months) only renders a suggestion callout in tax settings. Auto-flipping registration flags from ledger data would assert a Skatteverket registration we cannot know. diff --git a/app/api/extensions/skatteverket/vat/kvittenser/cron/__tests__/route.test.ts b/app/api/extensions/skatteverket/vat/kvittenser/cron/__tests__/route.test.ts index e9102435..6cbbefea 100644 --- a/app/api/extensions/skatteverket/vat/kvittenser/cron/__tests__/route.test.ts +++ b/app/api/extensions/skatteverket/vat/kvittenser/cron/__tests__/route.test.ts @@ -217,6 +217,25 @@ describe('VAT kvittenser cron', () => { ) }) + it('yearly picker params complete the moms_yearly deadline with the fiscal-year label', async () => { + mockCreateClient.mockReturnValueOnce( + stubHappyTables({ ...LOCKED_STATE, periodType: 'yearly', period: 12 }), + ) + mockSkvRequest.mockResolvedValueOnce({ + ok: true, + status: 200, + json: async () => ({ kvittensnummer: 'KV-999' }), + } as any) + + await GET(makeRequest()) + + // Calendar FY (company_settings unstubbed → default start month 1): + // the moms_yearly tax_period is the plain year label. + expect(mockCompleteTaxDeadline).toHaveBeenCalledWith( + expect.anything(), 'comp-1', ['moms_yearly'], '2026', 'confirmed', + ) + }) + it('legacy state without picker params still flips status but skips the deadline', async () => { const { periodType: _pt, year: _y, period: _p, ...legacyState } = LOCKED_STATE mockCreateClient.mockReturnValueOnce(stubHappyTables(legacyState)) diff --git a/app/api/extensions/skatteverket/vat/kvittenser/cron/route.ts b/app/api/extensions/skatteverket/vat/kvittenser/cron/route.ts index 804d7525..eb88a076 100644 --- a/app/api/extensions/skatteverket/vat/kvittenser/cron/route.ts +++ b/app/api/extensions/skatteverket/vat/kvittenser/cron/route.ts @@ -192,20 +192,30 @@ export async function GET(request: Request) { // carries the picker params (written by the one-click chain; states // persisted by the older step-by-step routes lack them). if (state.periodType && state.year && state.period) { - const taxPeriod = - state.periodType === 'monthly' - ? `${state.year}-${String(state.period).padStart(2, '0')}` - : state.periodType === 'quarterly' - ? `${state.year}-Q${state.period}` - : null + let taxPeriod: string | null = null + let deadlineTypes: ('moms_monthly' | 'moms_quarterly' | 'moms_yearly')[] = [ + 'moms_monthly', + 'moms_quarterly', + ] + if (state.periodType === 'monthly') { + taxPeriod = `${state.year}-${String(state.period).padStart(2, '0')}` + } else if (state.periodType === 'quarterly') { + taxPeriod = `${state.year}-Q${state.period}` + } else if (state.periodType === 'yearly') { + // moms_yearly rows carry the generator's fiscal-year label: + // `YYYY` for calendar FYs, `YYYY-1/YYYY` for broken ones. + const { data: fySettings } = await supabase + .from('company_settings') + .select('fiscal_year_start_month') + .eq('company_id', companyId) + .maybeSingle() + const startMonth = fySettings?.fiscal_year_start_month ?? 1 + const yearNum = Number(state.year) + taxPeriod = startMonth === 1 ? `${yearNum}` : `${yearNum - 1}/${yearNum}` + deadlineTypes = ['moms_yearly'] + } if (taxPeriod) { - await completeTaxDeadline( - supabase, - companyId, - ['moms_monthly', 'moms_quarterly'], - taxPeriod, - 'confirmed' - ) + await completeTaxDeadline(supabase, companyId, deadlineTypes, taxPeriod, 'confirmed') } } diff --git a/app/api/settings/eu-trade-signal/__tests__/route.test.ts b/app/api/settings/eu-trade-signal/__tests__/route.test.ts new file mode 100644 index 00000000..de2eebcc --- /dev/null +++ b/app/api/settings/eu-trade-signal/__tests__/route.test.ts @@ -0,0 +1,61 @@ +/** + * Tests for /api/settings/eu-trade-signal — ledger-derived EU sales signal. + */ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { NextResponse } from 'next/server' +import { createMockRequest, parseJsonResponse } from '@/tests/helpers' + +const requireAuthMock = vi.fn() +vi.mock('@/lib/auth/require-auth', () => ({ + requireAuth: (...args: unknown[]) => requireAuthMock(...args), +})) + +vi.mock('@/lib/company/context', () => ({ + getActiveCompanyId: vi.fn().mockResolvedValue('company-1'), + requireCompanyId: vi.fn().mockResolvedValue('company-1'), +})) + +const fetchEntryLinesMock = vi.fn() +vi.mock('@/lib/bookkeeping/entry-lines', () => ({ + fetchEntryLines: (...args: unknown[]) => fetchEntryLinesMock(...args), +})) + +import { GET } from '../route' + +beforeEach(() => { + vi.clearAllMocks() +}) + +describe('GET /api/settings/eu-trade-signal', () => { + it('returns 401 when not authenticated', async () => { + requireAuthMock.mockResolvedValue({ + user: null, + supabase: {}, + error: NextResponse.json({ error: 'Unauthorized' }, { status: 401 }), + }) + const res = await GET(createMockRequest('/api/settings/eu-trade-signal'), {}) + expect(res.status).toBe(401) + }) + + it('reports EU sales when the ledger has postings on the PS accounts', async () => { + requireAuthMock.mockResolvedValue({ user: { id: 'user-1' }, supabase: {}, error: null }) + fetchEntryLinesMock.mockResolvedValue([{ account_number: '3308' }]) + + const { status, body } = await parseJsonResponse<{ data: { has_eu_sales: boolean } }>( + await GET(createMockRequest('/api/settings/eu-trade-signal'), {}) + ) + expect(status).toBe(200) + expect(body.data.has_eu_sales).toBe(true) + }) + + it('reports no EU sales for an empty result', async () => { + requireAuthMock.mockResolvedValue({ user: { id: 'user-1' }, supabase: {}, error: null }) + fetchEntryLinesMock.mockResolvedValue([]) + + const { status, body } = await parseJsonResponse<{ data: { has_eu_sales: boolean } }>( + await GET(createMockRequest('/api/settings/eu-trade-signal'), {}) + ) + expect(status).toBe(200) + expect(body.data.has_eu_sales).toBe(false) + }) +}) diff --git a/app/api/settings/eu-trade-signal/route.ts b/app/api/settings/eu-trade-signal/route.ts new file mode 100644 index 00000000..e40183db --- /dev/null +++ b/app/api/settings/eu-trade-signal/route.ts @@ -0,0 +1,43 @@ +import { NextResponse } from 'next/server' +import { withRouteContext } from '@/lib/api/with-route-context' +import { fetchEntryLines, type EntryLinesQuery } from '@/lib/bookkeeping/entry-lines' + +/** + * Accounts whose postings indicate EU B2B sales: 3108 goods, 3308 services, + * 3107 triangulation. Same set the periodisk sammanställning report keys on. + */ +const EU_SALES_ACCOUNTS = ['3108', '3308', '3107'] + +/** How far back to look for EU sales postings. */ +const LOOKBACK_MONTHS = 15 + +/** + * GET /api/settings/eu-trade-signal + * + * Derived suggestion signal for the tax settings page: companies with EU + * sales in the ledger have a statutory periodisk sammanställning obligation + * (SFL 35 kap.), but the driving settings flags are opt-in and easily left + * off. The settings UI uses this to prompt the user to confirm EU trade and + * PS registration; it never flips the flags itself. + */ +export const GET = withRouteContext( + 'settings.eu_trade_signal', + async (_request, { supabase, companyId }) => { + const since = new Date() + since.setMonth(since.getMonth() - LOOKBACK_MONTHS) + const sinceStr = since.toISOString().split('T')[0] + + const lines = await fetchEntryLines<{ account_number: string }>({ + supabase, + lineColumns: 'account_number', + filterEntries: (q: EntryLinesQuery) => + q + .eq('company_id', companyId) + .in('status', ['posted', 'reversed']) + .gte('entry_date', sinceStr), + filterLines: (q: EntryLinesQuery) => q.in('account_number', EU_SALES_ACCOUNTS), + }) + + return NextResponse.json({ data: { has_eu_sales: lines.length > 0 } }) + }, +) diff --git a/components/settings/TaxSettingsForm.tsx b/components/settings/TaxSettingsForm.tsx index 9c778c66..a23af95f 100644 --- a/components/settings/TaxSettingsForm.tsx +++ b/components/settings/TaxSettingsForm.tsx @@ -10,9 +10,11 @@ import type { CompanySettings } from '@/types' interface TaxSettingsFormProps { settings: CompanySettings + /** Ledger-derived signal: EU sales postings exist (3108/3308/3107). */ + euSalesDetected?: boolean } -export function TaxSettingsForm({ settings }: TaxSettingsFormProps) { +export function TaxSettingsForm({ settings, euSalesDetected = false }: TaxSettingsFormProps) { const t = useTranslations('settings_tax_form') const [vatRegistered, setVatRegistered] = useState(settings.vat_registered ?? false) const [fSkatt, setFSkatt] = useState(settings.f_skatt ?? true) @@ -96,6 +98,15 @@ export function TaxSettingsForm({ settings }: TaxSettingsFormProps) { + {euSalesDetected && vatRegistered && (!hasEuTrade || !psEnabled) && ( +
+

{t('eu_trade_suggestion_title')}

+

+ {t('eu_trade_suggestion_help')} +

+
+ )} + {vatRegistered && (
diff --git a/components/settings/sections/TaxSettingsContent.tsx b/components/settings/sections/TaxSettingsContent.tsx index 06d9d715..9466ec0d 100644 --- a/components/settings/sections/TaxSettingsContent.tsx +++ b/components/settings/sections/TaxSettingsContent.tsx @@ -1,6 +1,6 @@ 'use client' -import { useEffect } from 'react' +import { useEffect, useState } from 'react' import { useTranslations } from 'next-intl' import { useSearchParams, useRouter } from 'next/navigation' import { TaxSettingsForm } from '@/components/settings/TaxSettingsForm' @@ -22,6 +22,25 @@ export function TaxSettingsContent() { const hasSkatteverketExtension = ENABLED_EXTENSION_IDS.has('skatteverket') + // Derived EU-sales signal: postings on 3108/3308/3107 imply a periodisk + // sammanställning obligation the opt-in flags may not reflect. Suggestion + // only; the user confirms via the ordinary checkboxes. + const [euSalesDetected, setEuSalesDetected] = useState(false) + useEffect(() => { + let cancelled = false + fetch('/api/settings/eu-trade-signal') + .then((res) => (res.ok ? res.json() : null)) + .then((json) => { + if (!cancelled && json?.data?.has_eu_sales) setEuSalesDetected(true) + }) + .catch(() => { + // Best-effort signal: a failed fetch just hides the suggestion. + }) + return () => { + cancelled = true + } + }, []) + // Skatteverket OAuth callback: the connect flow returns to /settings/tax with // a status query param (returnTo set in SkatteverketConnectPanel). useEffect(() => { @@ -108,7 +127,7 @@ export function TaxSettingsContent() { {showSkatteverket && } - +
) diff --git a/extensions/general/skatteverket/index.ts b/extensions/general/skatteverket/index.ts index f473bbe0..1a691481 100644 --- a/extensions/general/skatteverket/index.ts +++ b/extensions/general/skatteverket/index.ts @@ -2278,8 +2278,8 @@ function parseQueryParams( /** * Build the deadline generator's tax_period string (`YYYY-MM` monthly, - * `YYYY-QN` quarterly) from the picker params. Annual VAT has no system - * deadline type, so yearly returns null and the deadline hook is a no-op. + * `YYYY-QN` quarterly) from the picker params. Yearly periods use the + * fiscal-year label and need company settings; see yearlyVatTaxPeriod. */ function vatTaxPeriod(periodType: VatPeriodType, year: number, period: number): string | null { if (periodType === 'monthly') return `${year}-${String(period).padStart(2, '0')}` @@ -2287,11 +2287,28 @@ function vatTaxPeriod(periodType: VatPeriodType, year: number, period: number): return null } +/** + * The moms_yearly row's tax_period is the generator's fiscal-year label: + * `YYYY` for calendar fiscal years and `YYYY-1/YYYY` for broken ones (year + * = the FY-end year). Derived from company settings because the picker only + * carries the year. + */ +async function yearlyVatTaxPeriod(ctx: ExtensionContext, year: number): Promise { + const { data } = await ctx.supabase + .from('company_settings') + .select('fiscal_year_start_month') + .eq('company_id', ctx.companyId) + .maybeSingle() + const startMonth = data?.fiscal_year_start_month ?? 1 + return startMonth === 1 ? `${year}` : `${year - 1}/${year}` +} + /** * Complete the moms deadline for the period identified by the request's - * optional periodType/year/period query params. Both moms deadline types are - * passed: company settings decide which one exists, the other is a no-op. - * Best-effort by design (completeTaxDeadline never throws). + * optional periodType/year/period query params. Both monthly and quarterly + * types are passed for sub-annual periods: company settings decide which one + * exists, the other is a no-op. Best-effort by design (completeTaxDeadline + * never throws). */ async function completeVatDeadlineFromRequest( request: Request, @@ -2305,12 +2322,15 @@ async function completeVatDeadlineFromRequest( if (!periodType || !Number.isFinite(year) || !Number.isFinite(period) || !year || !period) { return } - const taxPeriod = vatTaxPeriod(periodType, year, period) + const taxPeriod = + periodType === 'yearly' + ? await yearlyVatTaxPeriod(ctx, year) + : vatTaxPeriod(periodType, year, period) if (!taxPeriod) return await completeTaxDeadline( ctx.supabase, ctx.companyId, - ['moms_monthly', 'moms_quarterly'], + periodType === 'yearly' ? ['moms_yearly'] : ['moms_monthly', 'moms_quarterly'], taxPeriod, newStatus ) diff --git a/lib/api/__tests__/schemas.test.ts b/lib/api/__tests__/schemas.test.ts index 1097a7ae..2dd3a6b9 100644 --- a/lib/api/__tests__/schemas.test.ts +++ b/lib/api/__tests__/schemas.test.ts @@ -248,11 +248,13 @@ describe('Enum schemas', () => { const types = [ 'moms_monthly', 'moms_quarterly', 'moms_yearly', 'f_skatt', 'arbetsgivardeklaration', 'inkomstdeklaration_ef', 'inkomstdeklaration_ab', - 'arsredovisning', 'periodisk_sammanstallning', 'bokslut', + 'arsredovisning', 'arsstamma', 'periodisk_sammanstallning', ] for (const t of types) { expect(TaxDeadlineTypeSchema.safeParse(t).success).toBe(true) } + // Retired: replaced by the statutory arsstamma deadline (ABL 7:10). + expect(TaxDeadlineTypeSchema.safeParse('bokslut').success).toBe(false) }) it('NormalBalanceSchema and MappingRuleTypeSchema', () => { diff --git a/lib/api/schemas.ts b/lib/api/schemas.ts index 9e2d4087..83f6efe0 100644 --- a/lib/api/schemas.ts +++ b/lib/api/schemas.ts @@ -260,8 +260,8 @@ export const TaxDeadlineTypeSchema = z.enum([ 'inkomstdeklaration_ef', 'inkomstdeklaration_ab', 'arsredovisning', + 'arsstamma', 'periodisk_sammanstallning', - 'bokslut', ]) export const DeadlineSourceSchema = z.enum(['system', 'user']) diff --git a/lib/calendar/ics-generator.ts b/lib/calendar/ics-generator.ts index 0a8d7333..2448a82c 100644 --- a/lib/calendar/ics-generator.ts +++ b/lib/calendar/ics-generator.ts @@ -182,8 +182,8 @@ function getSwedishTaxTypeLabel(type: string): string { inkomstdeklaration_ef: 'Inkomstdeklaration EF', inkomstdeklaration_ab: 'Inkomstdeklaration AB', arsredovisning: 'Årsredovisning', + arsstamma: 'Årsstämma', periodisk_sammanstallning: 'Periodisk sammanställning', - bokslut: 'Bokslut', } return labels[type] || type } diff --git a/lib/tax/__tests__/deadline-config.test.ts b/lib/tax/__tests__/deadline-config.test.ts index 151a105a..cc0df6b8 100644 --- a/lib/tax/__tests__/deadline-config.test.ts +++ b/lib/tax/__tests__/deadline-config.test.ts @@ -296,6 +296,38 @@ describe('inkomstdeklaration_ab: digital filing deadlines', () => { }) }) +describe('arsstamma: 6 months after FY end (ABL 7:10)', () => { + const config = getConfig('arsstamma') + + it('applies only to aktiebolag', () => { + expect(config.condition(makeSettings())).toBe(true) + expect(config.condition(makeSettings({ entity_type: 'enskild_firma' }))).toBe(false) + }) + + it('FY end Dec (calendar year) → Jun 30 next year', () => { + const dates = config.generateDates(2026, makeSettings({ fiscal_year_start_month: 1 })) + expect(dates.length).toBe(1) + expect(dates[0]).toMatchObject({ day: 30, month: 5, year: 2026, period: '2025' }) + }) + + it('FY end Apr → Oct 31 same year, broken-FY period label', () => { + const dates = config.generateDates(2026, makeSettings({ fiscal_year_start_month: 5 })) + expect(dates.length).toBe(1) + expect(dates[0]).toMatchObject({ day: 31, month: 9, year: 2026, period: '2025/2026' }) + }) + + it('uses the last day of the deadline month (handles Feb)', () => { + // FY end Aug 2026 → +6 months = Feb 2027 + const dates = config.generateDates(2027, makeSettings({ fiscal_year_start_month: 9 })) + expect(dates.length).toBe(1) + expect(dates[0]).toMatchObject({ day: 28, month: 1, year: 2027 }) + }) + + it('no bokslut deadline type exists anymore', () => { + expect(TAX_DEADLINE_CONFIGS.find((c) => (c.type as string) === 'bokslut')).toBeUndefined() + }) +}) + describe('arsredovisning: 7 months after FY end (ÅRL 8:3)', () => { const config = getConfig('arsredovisning') diff --git a/lib/tax/deadline-config.ts b/lib/tax/deadline-config.ts index e1206ecf..5017b57d 100644 --- a/lib/tax/deadline-config.ts +++ b/lib/tax/deadline-config.ts @@ -441,28 +441,42 @@ export const TAX_DEADLINE_CONFIGS: TaxDeadlineConfig[] = [ }, }, - // Bokslut (AB) - 31 mars for calendar year + // Årsstämma (AB): within 6 months of FY end per ABL 7 kap. 10 §. Replaces + // the former non-statutory 'bokslut' milestone (3 months had no legal + // basis). The stämma gates the årsredovisning chain: the AR is presented + // and adopted there, and the Bolagsverket filing (arsredovisning row, + // 7 months) requires the adopted AR. { - type: 'bokslut', - titleTemplate: 'Bokslut räkenskapsår {periodLabel}', - description: 'Bokslut för aktiebolag', + type: 'arsstamma', + titleTemplate: 'Årsstämma räkenskapsår {periodLabel}', + description: 'Årsstämma för aktiebolag (senast sex månader efter räkenskapsårets utgång)', condition: (s) => s.entity_type === 'aktiebolag', priority: 'important', linkedReportType: null, generateDates: (year, settings) => { - // For calendar year, December 31 is fiscal year end, deadline March 31 - if (settings.fiscal_year_start_month === 1) { - return [ - { day: 31, month: 2, year, period: `${year - 1}`, periodLabel: `${year - 1}` }, // March - ] + // FY end month (1-indexed) + const fyEndMonth = settings.fiscal_year_start_month === 1 ? 12 : settings.fiscal_year_start_month - 1 + + // Last day of (FY end month + 6). Swedish fiscal years always end on + // the last day of a calendar month (BFL 3 kap.), so this equals the + // statutory six-month limit. + const results: DeadlineInstance[] = [] + for (const endYr of [year - 1, year]) { + const dlMonth0 = ((fyEndMonth - 1) + 6) % 12 + const dlYear = (fyEndMonth - 1) + 6 >= 12 ? endYr + 1 : endYr + if (dlYear === year) { + const lastDay = new Date(dlYear, dlMonth0 + 1, 0).getDate() + const periodLabel = fyEndMonth === 12 ? `${endYr}` : `${endYr - 1}/${endYr}` + results.push({ + day: lastDay, + month: dlMonth0, + year: dlYear, + period: periodLabel, + periodLabel, + }) + } } - // For non-calendar fiscal years, 3 months after year end - const fiscalYearEnd = settings.fiscal_year_start_month - 1 - const deadlineMonth = (fiscalYearEnd + 3) % 12 - const deadlineYear = deadlineMonth < fiscalYearEnd ? year + 1 : year - return [ - { day: 31, month: deadlineMonth, year: deadlineYear, period: `${year - 1}/${year}`, periodLabel: `${year - 1}/${year}` }, - ] + return results }, }, ] diff --git a/messages/en.json b/messages/en.json index 2b35fa54..e64f0cad 100644 --- a/messages/en.json +++ b/messages/en.json @@ -1780,6 +1780,8 @@ "pays_salaries_help": "Controls the salary module in the menu. Paying out salary requires employer registration with Skatteverket.", "employer_registered_label": "Registered as employer", "employer_registered_help": "Registered employers must file an employer declaration (AGI) every month, including months without salary payments (nil declaration). Controls AGI deadlines.", + "eu_trade_suggestion_title": "Your bookkeeping contains EU sales", + "eu_trade_suggestion_help": "There are booked EU sales (accounts 3108, 3308 or 3107) in the last 15 months, but EU trade or the EU sales list is not enabled below. The EU sales list (periodisk sammanställning) must be filed monthly or quarterly when selling to businesses in other EU countries; the late fee is 1,250 kr per report.", "employer_seasonal_label": "Seasonal employer (säsongsregistrerad)", "employer_seasonal_help": "Files the employer declaration only for months with salary payments, plus December if no salary was paid during the year.", "preliminary_tax_heading": "Preliminary tax", diff --git a/messages/sv.json b/messages/sv.json index e58d073a..7ad7eb2d 100644 --- a/messages/sv.json +++ b/messages/sv.json @@ -1780,6 +1780,8 @@ "pays_salaries_help": "Styr lönemodulen i menyn. Att betala ut lön kräver arbetsgivarregistrering hos Skatteverket.", "employer_registered_label": "Registrerad som arbetsgivare", "employer_registered_help": "Registrerade arbetsgivare ska lämna arbetsgivardeklaration varje månad, även månader utan löneutbetalning (nolldeklaration). Styr AGI-deadlines.", + "eu_trade_suggestion_title": "Bokföringen innehåller EU-försäljning", + "eu_trade_suggestion_help": "Det finns bokförda EU-försäljningar (konto 3108, 3308 eller 3107) de senaste 15 månaderna, men EU-handel eller periodisk sammanställning är inte aktiverad nedan. Periodisk sammanställning ska lämnas varje månad eller kvartal vid försäljning till företag i andra EU-länder; förseningsavgiften är 1 250 kr per rapport.", "employer_seasonal_label": "Säsongsregistrerad arbetsgivare", "employer_seasonal_help": "Lämnar arbetsgivardeklaration bara för månader med löneutbetalning, samt för december om ingen lön betalats under året.", "preliminary_tax_heading": "Preliminärskatt", diff --git a/supabase/migrations/20260717152000_replace_bokslut_with_arsstamma.sql b/supabase/migrations/20260717152000_replace_bokslut_with_arsstamma.sql new file mode 100644 index 00000000..f074db6c --- /dev/null +++ b/supabase/migrations/20260717152000_replace_bokslut_with_arsstamma.sql @@ -0,0 +1,15 @@ +-- Remove the non-statutory 'bokslut' deadline (issue #1028). +-- +-- The "3 months after FY end" bokslut deadline had no legal basis (the +-- statutory anchors for AB are the arsstamma within 6 months, ABL 7 kap. +-- 10 §, and the Bolagsverket filing within 7 months, ARL 8 kap.), and its +-- non-calendar-FY date math was off by one month (a May-start FY produced +-- 31 Aug instead of 31 Jul; a Nov-start FY produced "Feb 31" which rolled +-- into March). The generator now produces an 'arsstamma' deadline instead; +-- the daily backfill cron creates those rows on its next run. +-- +-- Completed rows are kept for history; pending rows are template noise. +DELETE FROM public.deadlines +WHERE source = 'system' + AND tax_deadline_type = 'bokslut' + AND is_completed = false; diff --git a/types/index.ts b/types/index.ts index 00877271..562f146d 100644 --- a/types/index.ts +++ b/types/index.ts @@ -2174,8 +2174,8 @@ export type TaxDeadlineType = | 'inkomstdeklaration_ef' | 'inkomstdeklaration_ab' | 'arsredovisning' + | 'arsstamma' | 'periodisk_sammanstallning' - | 'bokslut' // Deadline status workflow export type DeadlineStatus = @@ -2352,8 +2352,8 @@ export const TAX_DEADLINE_TYPE_LABELS: Record = { inkomstdeklaration_ef: 'Inkomstdeklaration EF', inkomstdeklaration_ab: 'Inkomstdeklaration AB', arsredovisning: 'Årsredovisning', - periodisk_sammanstallning: 'Periodisk sammanställning', - bokslut: 'Bokslut' + arsstamma: 'Årsstämma', + periodisk_sammanstallning: 'Periodisk sammanställning' } // ============================================================