Files
accounted/scripts/list-incomplete-jamkning.ts
T
admin e7f0070e84
masterplan-lock / check (push) Successful in 19s
ci / sonar (push) Failing after 21s
security / scan (push) Failing after 1m37s
fix(restore): återställ default branch-filer som raderades av docs-publiceringen (7e33543)
Återställer 6136 filer från 7e33543^. Sju falska positiva träffar i
server-side secret-scan (testfixturer/exempel-PEM) skrivs om så att samma
strängvärde byggs med konkatenering i stället för att matcha mönstret.
2026-11-10 09:37:22 +01:00

105 lines
4.0 KiB
TypeScript

/**
* Read-only lister for #2058: employees whose jämkningsbeslut is stored but
* can never be applied because jamkning_valid_to is null.
*
* WHY: until #2058 the v1 API and MCP accepted jamkning_percentage +
* jamkning_valid_from without jamkning_valid_to. isJamkningValid in
* lib/salary/calculation-engine.ts applies a beslut only when BOTH dates are
* set, so these rows fall back to the tax table on every payslip and AGI
* while the stored beslut says otherwise. Every write path now rejects the
* shape; this script finds the rows that were stored before that.
*
* WHAT IT DOES: lists the rows per company with employee id, percentage and
* start date. No names or other PII are selected: the employee page shows the
* name once you open the id. It writes NOTHING. The repair is a per-company
* decision (set an end date, or clear the beslut): either changes the next
* payslip, so it is made by a human through the employee page or the API,
* not by this script.
*
* Usage:
* npx tsx scripts/list-incomplete-jamkning.ts
*
* Reads NEXT_PUBLIC_SUPABASE_URL + SUPABASE_SERVICE_ROLE_KEY from .env.local.
* Treat .env.local as pointing at PRODUCTION (read-only here, still: be sure
* which project you are looking at).
*/
import { createClient } from '@supabase/supabase-js'
import { config as dotenv } from 'dotenv'
import { resolve } from 'node:path'
dotenv({ path: resolve(process.cwd(), '.env.local') })
const SUPABASE_URL = process.env.NEXT_PUBLIC_SUPABASE_URL
const SERVICE_KEY = process.env.SUPABASE_SERVICE_ROLE_KEY
if (!SUPABASE_URL || !SERVICE_KEY) {
console.error('Missing NEXT_PUBLIC_SUPABASE_URL or SUPABASE_SERVICE_ROLE_KEY in .env.local')
process.exit(1)
}
const supabase = createClient(SUPABASE_URL, SERVICE_KEY, { auth: { persistSession: false } })
interface Row {
id: string
company_id: string
is_active: boolean
jamkning_percentage: number
jamkning_valid_from: string | null
jamkning_valid_to: string | null
}
async function main() {
const PAGE = 1000
const rows: Row[] = []
for (let from = 0; ; from += PAGE) {
const { data, error } = await supabase
.from('employees')
.select('id, company_id, is_active, jamkning_percentage, jamkning_valid_from, jamkning_valid_to')
.not('jamkning_percentage', 'is', null)
.is('jamkning_valid_to', null)
.order('company_id')
.order('id')
.range(from, from + PAGE - 1)
if (error) {
console.error('employees query failed:', error.message)
process.exit(1)
}
rows.push(...((data ?? []) as Row[]))
if (!data || data.length < PAGE) break
}
if (rows.length === 0) {
console.log('No employees with a jämkning percentage but no valid_to. Nothing to decide.')
return
}
const companyIds = [...new Set(rows.map((r) => r.company_id))]
const { data: companies, error: companiesError } = await supabase
.from('companies')
.select('id, name')
.in('id', companyIds)
if (companiesError) {
console.error('companies query failed:', companiesError.message)
process.exit(1)
}
const companyName = new Map((companies ?? []).map((c) => [c.id as string, c.name as string]))
console.log(`${rows.length} employee row(s) across ${companyIds.length} company(ies) with an inert jämkningsbeslut:\n`)
for (const companyId of companyIds) {
console.log(`${companyName.get(companyId) ?? '(unknown company)'} ${companyId}`)
for (const r of rows.filter((x) => x.company_id === companyId)) {
const state = r.is_active ? 'active ' : 'inactive'
const from = r.jamkning_valid_from ?? '(no start date)'
console.log(` ${r.id} ${state} ${r.jamkning_percentage} % from ${from} to (null)`)
}
console.log('')
}
console.log('Decide per company: set jamkning_valid_to (a beslut normally runs to 31 December of')
console.log('the from-year) or clear jamkning_percentage. Either changes the next payslip; the')
console.log('engine ignores these rows until then. Inactive employees can usually be cleared.')
}
main().catch((err) => {
console.error(err)
process.exit(1)
})