2ad8731dc9
* feat: import system improvements, INK2 fix, and Swedish text corrections - SIE parser: Windows-1252 and CP437 encoding detection and decoding - Bank file parser: add Nordea Business (Företag) CSV format - Bank file parser: improve format detection for SEB, Länsförsäkringar, generic CSV - INK2 engine: calculate årets resultat (7222) from income statement for open fiscal years - Dashboard: parallel Supabase queries, simplified dashboard page - Fix Swedish characters (å, ä, ö) in BAS data descriptions, validation messages, AI consent disclosures - Import wizard UI improvements across all steps - Migration: add 'bas_range' match type to sie_account_mappings constraint - Extensive new tests for SIE parser encoding and bank file parser Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: arcim migration wizard UX fixes, Sentry setup, and extension scaffolding Arcim migration wizard improvements: - Progress bar now excludes non-interactive steps (migrating/result) - Fix OAuth text to match target="_blank" behavior (new tab, not redirect) - Display month names instead of "Månad X" in preview - Fix Swedish typo "förifylla" in no-company-info message - Replace native checkboxes with shadcn Switch in options step - Add ConfirmationDialog before starting migration - Show progress percentage during migration - Add "Nästa steg" guidance and navigation links in result step - Add "Försök igen" button in error state (returns to options) - Add Bokio company ID help text (GUID from URL) - Add Fortnox integration add-on hint on connection failure Also includes: SIE import system improvements, INK2 fixes, Swedish text corrections, Sentry error tracking setup, and arcim-migration extension scaffolding. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: address PR review feedback - Fix OAuth error recovery blank page (restore provider from URL params) - Pass real userId to MigrationWizard instead of empty string - Remove ~50 debug console.log statements from sie-import.ts - Fix comment referencing account 3740 → 3741 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
131 lines
3.7 KiB
TypeScript
131 lines
3.7 KiB
TypeScript
#!/usr/bin/env npx tsx
|
|
/**
|
|
* Backfill SIE file archival for existing imports.
|
|
*
|
|
* Accepts a directory of SIE files, computes SHA-256 hashes, matches against
|
|
* sie_imports.file_hash, uploads to Supabase Storage, and populates
|
|
* file_storage_path.
|
|
*
|
|
* Usage: npx tsx scripts/backfill-sie-files.ts <directory-of-sie-files>
|
|
*/
|
|
|
|
import { config } from 'dotenv'
|
|
config({ path: '.env.local' })
|
|
import { createClient } from '@supabase/supabase-js'
|
|
import { readdir, readFile } from 'node:fs/promises'
|
|
import { join } from 'node:path'
|
|
import { createHash } from 'node:crypto'
|
|
|
|
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL
|
|
const serviceRoleKey = process.env.SUPABASE_SERVICE_ROLE_KEY
|
|
|
|
if (!supabaseUrl || !serviceRoleKey) {
|
|
console.error('Missing NEXT_PUBLIC_SUPABASE_URL or SUPABASE_SERVICE_ROLE_KEY in .env')
|
|
process.exit(1)
|
|
}
|
|
|
|
const supabase = createClient(supabaseUrl, serviceRoleKey)
|
|
|
|
async function calculateFileHash(content: string): Promise<string> {
|
|
const hash = createHash('sha256')
|
|
hash.update(content)
|
|
return hash.digest('hex')
|
|
}
|
|
|
|
async function main() {
|
|
const dir = process.argv[2]
|
|
if (!dir) {
|
|
console.error('Usage: npx tsx scripts/backfill-sie-files.ts <directory-of-sie-files>')
|
|
process.exit(1)
|
|
}
|
|
|
|
// 1. Read all SIE files from directory
|
|
const files = await readdir(dir)
|
|
const sieFiles = files.filter(f => f.toLowerCase().endsWith('.se') || f.toLowerCase().endsWith('.si'))
|
|
|
|
if (sieFiles.length === 0) {
|
|
console.error(`No .se/.si files found in ${dir}`)
|
|
process.exit(1)
|
|
}
|
|
|
|
console.log(`Found ${sieFiles.length} SIE files in ${dir}`)
|
|
|
|
// 2. Get all existing imports without file_storage_path
|
|
const { data: imports, error: importError } = await supabase
|
|
.from('sie_imports')
|
|
.select('id, user_id, file_hash, filename, file_storage_path')
|
|
.is('file_storage_path', null)
|
|
|
|
if (importError) {
|
|
console.error('Failed to fetch imports:', importError.message)
|
|
process.exit(1)
|
|
}
|
|
|
|
if (!imports || imports.length === 0) {
|
|
console.log('No imports need backfilling.')
|
|
return
|
|
}
|
|
|
|
console.log(`Found ${imports.length} imports without archived files`)
|
|
|
|
// Build hash→import mapping
|
|
const hashToImport = new Map<string, typeof imports[number]>()
|
|
for (const imp of imports) {
|
|
if (imp.file_hash) {
|
|
hashToImport.set(imp.file_hash, imp)
|
|
}
|
|
}
|
|
|
|
// 3. Match files by hash and upload
|
|
let matched = 0
|
|
let uploaded = 0
|
|
|
|
for (const filename of sieFiles) {
|
|
const filePath = join(dir, filename)
|
|
const content = await readFile(filePath, 'utf-8')
|
|
const hash = await calculateFileHash(content)
|
|
|
|
const imp = hashToImport.get(hash)
|
|
if (!imp) {
|
|
console.log(` ${filename} — no matching import (hash: ${hash.substring(0, 12)}...)`)
|
|
continue
|
|
}
|
|
|
|
matched++
|
|
console.log(` ${filename} → import ${imp.id} (${imp.filename})`)
|
|
|
|
// Upload to storage
|
|
const storagePath = `${imp.user_id}/${imp.id}.se`
|
|
const fileBlob = new Blob([content], { type: 'text/plain; charset=cp437' })
|
|
const { error: uploadError } = await supabase.storage
|
|
.from('sie-files')
|
|
.upload(storagePath, fileBlob, { upsert: false })
|
|
|
|
if (uploadError) {
|
|
console.error(` Upload failed: ${uploadError.message}`)
|
|
continue
|
|
}
|
|
|
|
// Update import record
|
|
const { error: updateError } = await supabase
|
|
.from('sie_imports')
|
|
.update({ file_storage_path: storagePath })
|
|
.eq('id', imp.id)
|
|
|
|
if (updateError) {
|
|
console.error(` DB update failed: ${updateError.message}`)
|
|
continue
|
|
}
|
|
|
|
uploaded++
|
|
console.log(` Archived to ${storagePath}`)
|
|
}
|
|
|
|
console.log(`\nDone: ${matched} matched, ${uploaded} uploaded, ${sieFiles.length - matched} unmatched`)
|
|
}
|
|
|
|
main().catch(err => {
|
|
console.error(err)
|
|
process.exit(1)
|
|
})
|