ec27228a8e
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>
146 lines
5.3 KiB
TypeScript
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)
|
|
})
|