Files
accounted/scripts/backfill-tax-deadlines.ts
T
Mattsson a5e37d3510 Fix/build (#1041)
* fix(bookkeeping): harden correction account changes

* feat(tax): enhance tax deadline generation with new settings and filing methods

- Added new company settings: tax_turnover_over_40m, vat_has_eu_trade, vat_filing_method, periodisk_sammanstallning_enabled, and periodisk_sammanstallning_filing_method.
- Updated deadline generation logic to accommodate new settings affecting VAT and employer declaration deadlines.
- Implemented tests for new functionality, ensuring that completed obligations are preserved and not replaced by new pending rows.
- Introduced a cron job to backfill missing tax deadlines for companies with settings but no upcoming deadlines.
- Updated API routes for generating tax deadlines and handling cron jobs.
- Modified database schema to include new columns for tax filing profiles and constraints for filing methods.

* fix(invoices): record credit note reconciliation guard

* fix(tax): correct automatic deadline settings

* fix(tax): key AGI deadline to VAT taxable base and add storforetag payment deadline

The 26th filing day for the skattedeklaration (AGI and VAT together) hinges
on one statutory measure, a VAT taxable base above SEK 40 million (SFL 26
kap.), not a separate employer turnover. Drop employer_turnover_over_40m and
derive the AGI schedule from vat_registered plus vat_taxable_base_over_40m,
so a non-VAT-reporting employer is never shown the 26th when its binding
date is the 12th.

Also:
- add a skatteinbetalning deadline row (12th, 17 January) for storforetag,
  whose deducted tax and employer contributions are due before the 26th
  filing date
- normalize legally incoherent over-40m flag combinations to the earlier
  small-company schedule in a follow-up migration
- replace hardcoded 27 December dates with the banking-day adjustment
- extend the 40m help text to cover the SKV-decided early filing election
  and the payment-still-on-the-12th rule
- document the regeneration race repaired by the daily backfill cron

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(migrations): add AGI and VAT filing logic with employer column removal

* feat(settings): implement VAT registration logic and update related flags; enhance deadline handling

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 00:52:57 +02:00

161 lines
5.0 KiB
TypeScript

#!/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 {
DEADLINE_SETTINGS_SELECT,
generateTaxDeadlinesForUser,
toDeadlineSettings,
} 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 extends Partial<CompanySettingsForDeadlines> {
company_id: string
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(`${DEADLINE_SETTINGS_SELECT}, 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 = toDeadlineSettings(s)
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)
})