Files
accounted/scripts/backfill-tax-deadlines.ts
T
Jakob Wennberg f266c386f3 chore: repo-wide bloat sweep, remove dead code and fold duplicate helpers (#2150)
* chore: repo-wide bloat sweep, remove dead code and fold duplicate helpers

Remove 33 dead files, ~270 unreferenced exports/types, 13 dead i18n
namespaces and 4 unused dependencies; fold byte-identical helper copies
into one canonical home each (lib/utils chunk/sleep/utcDateStamp,
lib/dates/iso, lib/invariants/uuid, lib/xml/escape, lib/reports/sru/format,
lib/pdf/number-text, lib/browser/panel-request, lib/api/v1/body +
v1ValidationError rolled out to ~55 v1 routes, booking-template schemas).

No behaviour change: v1 bodies and status codes, MCP tool schemas, DB
writes and money math are untouched. Naive ore rounding was deliberately
not swapped for roundOre; see DECISIONS.md 2026-09-02 for the full list
of things left alone on purpose.

tsc, lint, 19588 unit tests and check:guards green; antipattern baseline
ratcheted (naive-ore-round 622 -> 620, hand-rolled-invariant 115 -> 113).

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

* test(transactions): import RawTransaction from @/types after the ingest re-export removal

CI's type ratchet (check:types, full tsconfig) caught the one test file
that still imported the type through lib/transactions/ingest.

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

---------

Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-02 11:51:16 +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
* the since-removed 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)
})