Files
accounted/scripts/debug-bl-unbalanced.ts
T
Mattsson 43925bc2d3 fix(import): SIE bulk-delete on service client + provider/reporting/b… (#724)
* fix(import): SIE bulk-delete on service client + provider/reporting/banking fixes

Rebuilt branch onto main as a single commit.

- import: run SIE bulk-delete RPCs on the service client to escape the 8s
  statement_timeout; undo_sie_import now takes an explicit actor (p_user_id)
  so its owner/admin gate works when auth.uid() is NULL on the service
  client (migration 20260624120000) + pg-real regression test
- providers: distinguish missing Fortnox license from expired connection;
  provider_consent_tokens PK regression test
- reports: include unmapped BAS expense groups in the income statement
- enable-banking: reconnect closed/expired bank sessions in place
- bookkeeping: surface linked invoices as underlag on the verifikat view
- scripts: track BL cleanup/diagnostic tooling; data files (*.csv) are
  git-ignored and consentId is now a required arg with no silent default

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(import): add Cache-Control header to journal entry references response

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 23:40:26 +02:00

103 lines
4.0 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Temporary diagnostic: list every unbalanced voucher in the BL SIE export
* with its lines, write a CSV report, and print summary statistics.
* Run: npx tsx --env-file=.env --env-file=.env.local scripts/debug-bl-unbalanced.ts
*/
import { writeFileSync } from 'node:fs'
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 } from '@/lib/import/sie-parser'
const consentId = process.argv[2]
if (!consentId) {
console.error('Usage: npx tsx --env-file=.env --env-file=.env.local scripts/debug-bl-unbalanced.ts <consentId>')
console.error('Refusing to run without an explicit consentId — this script reads a live BL company and writes a report.')
process.exit(1)
}
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}`
}
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!,
)
const { files } = await fetchProviderSieFiles('bjornlunden', token.access_token, userKey)
for (const f of files) {
const parsed = parseSIEFile(f.rawContent)
const bad = parsed.vouchers
.map((v) => ({
voucher: `${v.series}${v.number}`,
date: isoLocal(v.date),
description: v.description,
lineCount: v.lines.length,
diff: Math.round(v.lines.reduce((s, l) => s + l.amount, 0) * 100) / 100,
lines: v.lines.map((l) => `${l.account}:${l.amount}`).join(' '),
}))
.filter((v) => Math.abs(v.diff) > 0.01 || v.lineCount === 0)
const total = parsed.vouchers.length
const empty = bad.filter((v) => v.lineCount === 0).length
const single = bad.filter((v) => v.lineCount === 1).length
const multi = bad.filter((v) => v.lineCount > 1).length
console.log(`fiscal year ${f.fiscalYear}: ${total} vouchers total, ${bad.length} broken`)
console.log(` empty (0 lines): ${empty}`)
console.log(` one-sided (1 line): ${single}`)
console.log(` multi-line unbalanced: ${multi}`)
// Distribution of diffs to spot recurring patterns (e.g. 1.50 bank fees)
const byDiff = new Map<string, number>()
for (const v of bad.filter((b) => b.lineCount > 0)) {
const key = Math.abs(v.diff).toFixed(2)
byDiff.set(key, (byDiff.get(key) ?? 0) + 1)
}
const topDiffs = [...byDiff.entries()].sort((a, b) => b[1] - a[1]).slice(0, 10)
console.log(' most common |diff| amounts:')
for (const [amount, count] of topDiffs) console.log(` ${amount} kr × ${count}`)
// Per-series breakdown
const bySeries = new Map<string, number>()
for (const v of bad) {
const series = v.voucher.replace(/\d+$/, '')
bySeries.set(series, (bySeries.get(series) ?? 0) + 1)
}
console.log(' broken per series:', [...bySeries.entries()].map(([s, n]) => `${s}=${n}`).join(' '))
const csvEscape = (s: string) => `"${s.replace(/"/g, '""')}"`
const csv = [
'voucher;date;diff;line_count;description;lines',
...bad.map((v) =>
[v.voucher, v.date, v.diff.toFixed(2), v.lineCount, csvEscape(v.description), csvEscape(v.lines)].join(';'),
),
].join('\n')
const outPath = `scripts/bl-unbalanced-${f.fiscalYear}.csv`
writeFileSync(outPath, '' + csv, 'utf8')
console.log(` full list written to ${outPath}`)
}
}
main().catch((e) => {
console.error(e)
process.exit(1)
})