f266c386f3
* 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>
161 lines
5.0 KiB
TypeScript
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)
|
|
})
|