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>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
db8983ba9e
commit
43925bc2d3
@@ -0,0 +1,102 @@
|
||||
/**
|
||||
* 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)
|
||||
})
|
||||
Reference in New Issue
Block a user