Files
accounted/scripts/bl-delete-broken.ts
T
Jakob Wennberg ec27228a8e style: remove em/en dashes repo-wide, add CLAUDE.md rule against them (#890)
Em dashes (—) and en dashes (–) had spread across comments, docs, tests,
and a few UI strings, reading as AI-generated boilerplate rather than
house style. Replaced each with punctuation matching its context: colon
for explanatory clauses, comma for asides, plain hyphen for numeric/legal
ranges (e.g. "21-23§"), "to"/"till" for date ranges, parentheses for
paired-dash asides. messages/en.json and messages/sv.json were fixed by
hand together to keep sv/en in sync.

Left untouched where the dash is the functional subject rather than
decorative punctuation: date-range-parser.ts's separator regex,
charset-repair.ts's CP1252 byte-mapping table (and its test), the SIE
encoding mojibake docs, generic-csv.ts's minus-sign normalizer, the
agent system-prompt files that already instruct against em dashes, and
a golden iXBRL test fixture compared byte-for-byte.

Also fixes two bugs surfaced along the way: an off-by-one in
ApiKeysPanel's scope-label split (a leftover from an earlier partial
pass), and a charset-repair test that had lost the literal en-dash it
exists to verify.

Regenerated the agent atom seed migration (skills:generate) since 27
SKILL.md files changed. Added a CLAUDE.md rule against em/en dashes,
with an explicit carve-out for the functional-dash cases above.

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-04 15:58:06 +02:00

146 lines
5.3 KiB
TypeScript

/**
* One-off cleanup: delete every broken voucher (empty or unbalanced) in the
* BL test company behind the given consent, via BL's
* DELETE /journal/entry/{journalId}/{journalEntryId}/{journalEntryDate}.
*
* Probes the first voucher and aborts if BL refuses the delete, then runs the
* full list at ~8 req/s and re-validates the SIE export at the end.
*
* Run: npx tsx --env-file=.env --env-file=.env.local scripts/bl-delete-broken.ts <consentId>
*/
import { createClient } from '@supabase/supabase-js'
import { fetchBjornLundenToken } from '@/lib/providers/bjornlunden/oauth'
import { fetchProviderSieFiles } from '@/extensions/general/arcim-migration/lib/sie-fetcher'
import { parseSIEFile, validateSIEFile } from '@/lib/import/sie-parser'
const BL_BASE_URL = 'https://apigateway.blinfo.se/bla-api/v1/sp'
const DELAY_MS = 125 // ~8 req/s, under BL's 10 req/s limit
const consentId = process.argv[2]
if (!consentId) {
console.error('Usage: npx tsx --env-file=.env --env-file=.env.local scripts/bl-delete-broken.ts <consentId>')
console.error('Refusing to run without an explicit consentId: this script deletes vouchers from a live BL company.')
process.exit(1)
}
interface BrokenVoucher {
series: string
number: number
date: string // yyyy-MM-dd as written in the SIE file
lineCount: number
diff: number
}
function isoLocal(d: Date): string {
const y = d.getFullYear()
const m = String(d.getMonth() + 1).padStart(2, '0')
const day = String(d.getDate()).padStart(2, '0')
return `${y}-${m}-${day}`
}
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms))
async function deleteVoucher(
accessToken: string,
userKey: string,
v: BrokenVoucher,
): Promise<{ ok: boolean; status: number; body: string }> {
const url = `${BL_BASE_URL}/journal/entry/${encodeURIComponent(v.series)}/${v.number}/${v.date}`
const response = await fetch(url, {
method: 'DELETE',
headers: {
Authorization: `Bearer ${accessToken}`,
'User-Key': userKey,
},
signal: AbortSignal.timeout(30_000),
})
const body = response.ok ? '' : await response.text().catch(() => '')
return { ok: response.ok, status: response.status, body }
}
async function main() {
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY!,
)
const { data: tokens, error } = await supabase
.from('provider_consent_tokens')
.select('provider_company_id')
.eq('consent_id', consentId)
.limit(1)
if (error || !tokens?.length) throw new Error(`no tokens for consent: ${error?.message ?? 'not found'}`)
const userKey = tokens[0]!.provider_company_id as string
const token = await fetchBjornLundenToken(
process.env.BJORN_LUNDEN_CLIENT_ID!,
process.env.BJORN_LUNDEN_CLIENT_SECRET!,
)
console.log('Fetching fresh SIE export to compute the broken-voucher list...')
const { files } = await fetchProviderSieFiles('bjornlunden', token.access_token, userKey)
const broken: BrokenVoucher[] = []
for (const f of files) {
const parsed = parseSIEFile(f.rawContent)
for (const v of parsed.vouchers) {
const diff = Math.round(v.lines.reduce((s, l) => s + l.amount, 0) * 100) / 100
if (Math.abs(diff) > 0.01 || v.lines.length === 0) {
broken.push({
series: v.series,
number: v.number,
date: isoLocal(v.date),
lineCount: v.lines.length,
diff,
})
}
}
}
console.log(`${broken.length} broken vouchers to delete`)
if (broken.length === 0) {
console.log('Nothing to do.')
return
}
// Probe with the first voucher: abort if BL refuses deletes
const probe = broken[0]!
console.log(`Probe delete: ${probe.series}${probe.number} (${probe.date})...`)
const probeResult = await deleteVoucher(token.access_token, userKey, probe)
if (!probeResult.ok) {
console.error(`ABORT: BL refused the probe delete (HTTP ${probeResult.status}): ${probeResult.body}`)
process.exit(1)
}
console.log('Probe OK: deleting the rest...')
const failures: { voucher: string; status: number; body: string }[] = []
let done = 1
for (const v of broken.slice(1)) {
await sleep(DELAY_MS)
const result = await deleteVoucher(token.access_token, userKey, v)
if (!result.ok) {
failures.push({ voucher: `${v.series}${v.number} (${v.date})`, status: result.status, body: result.body.slice(0, 200) })
}
done++
if (done % 100 === 0) console.log(` ${done}/${broken.length} (${failures.length} failed)`)
}
console.log(`\nDone: ${done - failures.length}/${broken.length} deleted, ${failures.length} failed`)
if (failures.length > 0) {
console.log('Failures (first 20):')
for (const f of failures.slice(0, 20)) console.log(` ${f.voucher}: HTTP ${f.status} ${f.body}`)
}
console.log('\nRe-fetching SIE export to validate...')
const after = await fetchProviderSieFiles('bjornlunden', token.access_token, userKey)
for (const f of after.files) {
const parsed = parseSIEFile(f.rawContent)
const validation = validateSIEFile(parsed)
console.log(`fiscal year ${f.fiscalYear}: ${parsed.vouchers.length} vouchers, valid=${validation.valid}`)
if (validation.errors.length) console.log('remaining errors:\n- ' + validation.errors.slice(0, 5).join('\n- '))
}
}
main().catch((e) => {
console.error(e)
process.exit(1)
})