fix: generate tax deadlines for the installed base + correct 2893 label carryover (#1029)
* fix(bookkeeping): refresh correction line description on account change When editing an ändringsverifikation, CorrectionEntryDialog pre-filled each line's description from the original entry but never re-derived it when the user changed the account, so a description carried over from the old account (e.g. 2393 "Lån från närstående personer, långfristig del") stayed stale on the newly chosen account (e.g. 2893, the kortfristig account). The regular JournalEntryForm already auto-fills on account change; this mirrors it. The refresh is guarded: it only overwrites the description when it is empty or still equals the previously selected account's name, so a memo the user typed themselves is preserved. Logic is extracted into a pure, unit-tested helper. Note: the wrong text on an already-posted correction cannot be repaired (line descriptions of posted verifikat are immutable per BFL / migration 017); this prevents recurrence on future corrections. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(deadlines): generate tax deadlines for the installed base Automatic tax deadlines only regenerated when a tax-relevant settings field changed value (didTaxFieldsChange). Companies fill those fields once at onboarding, so a later save changed nothing and generated nothing; the annual cron was the only unconditional trigger. As a result only ~5 of ~776 real companies had any system deadlines, and the /deadlines empty state told users to "check the tax settings" that were already complete. - Settings save now also regenerates when the company has zero system deadlines yet (safe first-time backfill; cannot reset is_completed/status). Decision extracted into shouldRegenerateTaxDeadlines() with tests. - The empty-state banner gets a "Generera nu" action wired to the existing /api/tax-deadlines/generate route (previously it had no caller). New sv/en strings. - generateNewYearDeadlines (annual cron) paginates company_settings via fetchAllRows: a plain .select() silently caps at 1000 rows, leaving companies beyond the cap without next-year deadlines. - scripts/backfill-tax-deadlines.ts: one-off that reruns the real generator for non-sandbox companies with zero system deadlines. Known gap (follow-up): moms_period='yearly' has no deadline config, so annual VAT filers get no momsdeklaration deadline yet. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(deadlines): address review feedback + fix settings-route test - settings/route.ts: fail safe when the system-deadline count query errors. A null count on error was treated as 0, which would trigger a delete+regenerate and reset is_completed/status on a transient failure; now a count error keeps the self-heal off (CodeRabbit, Major). - Update app/api/settings/__tests__/route.test.ts (added on main via the withRouteContext refactor) for the extra deadline-count query and the new shouldRegenerateTaxDeadlines export; add self-heal / no-regen cases. - Soften the "no deadlines created" copy: zero generated rows can also mean no applicable obligations (or the moms_yearly gap), not just incomplete settings (CodeRabbit, Minor). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
8534ff1006
commit
5ac560ce41
@@ -0,0 +1,169 @@
|
||||
#!/usr/bin/env npx tsx
|
||||
/**
|
||||
* Backfill automatic tax deadlines for the installed base.
|
||||
*
|
||||
* Root cause (bug): tax deadlines only ever regenerated when a tax-relevant
|
||||
* settings field CHANGED value (app/api/settings/route.ts, gated on
|
||||
* didTaxFieldsChange). Companies fill these fields once at onboarding, so a
|
||||
* later save changed nothing and generated nothing. The annual cron
|
||||
* (generateNewYearDeadlines) is the only unconditional trigger and runs Jan 2,
|
||||
* so the installed base sat empty. Result before this backfill: only ~5 of ~776
|
||||
* real companies had system-generated deadlines.
|
||||
*
|
||||
* This script runs the REAL generator (generateTaxDeadlinesForUser) so the
|
||||
* backfilled rows are byte-identical to what the app produces. It targets ONLY
|
||||
* non-sandbox companies that currently have ZERO source='system' deadlines, so
|
||||
* it can never reset is_completed/status on a company that already has progress.
|
||||
*
|
||||
* Known gap (intentionally NOT covered here): moms_period='yearly' has no
|
||||
* deadline config yet, so ~295 annual VAT filers will not get a momsdeklaration
|
||||
* deadline until moms_yearly ships. Every other applicable deadline is created.
|
||||
*
|
||||
* Usage:
|
||||
* npx tsx scripts/backfill-tax-deadlines.ts --dry-run # report only (default-safe)
|
||||
* npx tsx scripts/backfill-tax-deadlines.ts --apply # write to the database
|
||||
*/
|
||||
|
||||
import { config } from 'dotenv'
|
||||
config({ path: '.env.local' })
|
||||
import { createClient } from '@supabase/supabase-js'
|
||||
import { generateTaxDeadlinesForUser } from '../lib/tax/deadline-generator'
|
||||
import type { CompanySettingsForDeadlines } from '../lib/tax/deadline-config'
|
||||
|
||||
const APPLY = process.argv.includes('--apply')
|
||||
const DRY_RUN = !APPLY
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL
|
||||
const serviceRoleKey = process.env.SUPABASE_SERVICE_ROLE_KEY
|
||||
|
||||
if (!supabaseUrl || !serviceRoleKey) {
|
||||
console.error('Missing NEXT_PUBLIC_SUPABASE_URL or SUPABASE_SERVICE_ROLE_KEY in .env.local')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const supabase = createClient(supabaseUrl, serviceRoleKey)
|
||||
|
||||
const PAGE = 1000
|
||||
|
||||
interface SettingsRow {
|
||||
company_id: string
|
||||
entity_type: CompanySettingsForDeadlines['entity_type']
|
||||
moms_period: CompanySettingsForDeadlines['moms_period']
|
||||
f_skatt: boolean
|
||||
vat_registered: boolean
|
||||
pays_salaries: boolean | null
|
||||
fiscal_year_start_month: number
|
||||
is_sandbox: boolean | null
|
||||
}
|
||||
|
||||
async function fetchAllSettings(): Promise<SettingsRow[]> {
|
||||
const rows: SettingsRow[] = []
|
||||
let from = 0
|
||||
for (;;) {
|
||||
const { data, error } = await supabase
|
||||
.from('company_settings')
|
||||
.select('company_id, entity_type, moms_period, f_skatt, vat_registered, pays_salaries, fiscal_year_start_month, is_sandbox')
|
||||
.order('company_id', { ascending: true })
|
||||
.range(from, from + PAGE - 1)
|
||||
if (error) {
|
||||
console.error('Failed to fetch company_settings', error)
|
||||
process.exit(1)
|
||||
}
|
||||
if (!data || data.length === 0) break
|
||||
rows.push(...(data as SettingsRow[]))
|
||||
if (data.length < PAGE) break
|
||||
from += PAGE
|
||||
}
|
||||
return rows
|
||||
}
|
||||
|
||||
async function fetchCompaniesWithSystemDeadlines(): Promise<Set<string>> {
|
||||
const ids = new Set<string>()
|
||||
let from = 0
|
||||
for (;;) {
|
||||
const { data, error } = await supabase
|
||||
.from('deadlines')
|
||||
.select('company_id')
|
||||
.eq('source', 'system')
|
||||
.order('company_id', { ascending: true })
|
||||
.range(from, from + PAGE - 1)
|
||||
if (error) {
|
||||
console.error('Failed to fetch companies with system deadlines', error)
|
||||
process.exit(1)
|
||||
}
|
||||
if (!data || data.length === 0) break
|
||||
for (const row of data as Array<{ company_id: string }>) ids.add(row.company_id)
|
||||
if (data.length < PAGE) break
|
||||
from += PAGE
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const all = await fetchAllSettings()
|
||||
const realCompanies = all.filter((s) => !s.is_sandbox)
|
||||
const withDeadlines = await fetchCompaniesWithSystemDeadlines()
|
||||
|
||||
let scanned = 0
|
||||
let alreadyHad = 0
|
||||
let missingEntityType = 0
|
||||
let generatedCompanies = 0
|
||||
let generatedRows = 0
|
||||
const errors: Array<{ company_id: string; error: string }> = []
|
||||
|
||||
for (const s of realCompanies) {
|
||||
scanned++
|
||||
|
||||
if (!s.entity_type) {
|
||||
// Generator gates every config on entity_type; nothing to create.
|
||||
missingEntityType++
|
||||
continue
|
||||
}
|
||||
|
||||
if (withDeadlines.has(s.company_id)) {
|
||||
alreadyHad++
|
||||
continue
|
||||
}
|
||||
|
||||
const settings: CompanySettingsForDeadlines = {
|
||||
entity_type: s.entity_type,
|
||||
moms_period: s.moms_period,
|
||||
f_skatt: s.f_skatt,
|
||||
vat_registered: s.vat_registered,
|
||||
pays_salaries: s.pays_salaries ?? false,
|
||||
fiscal_year_start_month: s.fiscal_year_start_month,
|
||||
}
|
||||
|
||||
if (DRY_RUN) {
|
||||
// In dry-run we cannot cheaply know the row count without inserting, so we
|
||||
// just report the company as a target.
|
||||
generatedCompanies++
|
||||
continue
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await generateTaxDeadlinesForUser(supabase, s.company_id, settings)
|
||||
if (result.created > 0) {
|
||||
generatedCompanies++
|
||||
generatedRows += result.created
|
||||
}
|
||||
} catch (err) {
|
||||
errors.push({ company_id: s.company_id, error: err instanceof Error ? err.message : String(err) })
|
||||
}
|
||||
}
|
||||
|
||||
console.log(JSON.stringify({
|
||||
mode: DRY_RUN ? 'dry-run' : 'apply',
|
||||
real_companies_scanned: scanned,
|
||||
already_had_system_deadlines: alreadyHad,
|
||||
skipped_missing_entity_type: missingEntityType,
|
||||
companies_generated: generatedCompanies,
|
||||
rows_generated: DRY_RUN ? null : generatedRows,
|
||||
errors,
|
||||
}, null, 2))
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(err)
|
||||
process.exit(1)
|
||||
})
|
||||
Reference in New Issue
Block a user