import { getDb } from "./db"; import { isLinkedTransactionFkError } from "./balance.service"; import { TransactionLinkedToBalanceError } from "./transactionService"; import type { ImportedFile, ImportedFileWithSource } from "../shared/types"; export async function getFilesBySourceId( sourceId: number ): Promise { const db = await getDb(); return db.select( "SELECT * FROM imported_files WHERE source_id = $1 ORDER BY import_date DESC", [sourceId] ); } export async function existsByHash( fileHash: string ): Promise { const db = await getDb(); const rows = await db.select( "SELECT * FROM imported_files WHERE file_hash = $1", [fileHash] ); return rows.length > 0 ? rows[0] : null; } export async function createImportedFile(file: { source_id: number; filename: string; file_hash: string; row_count: number; status: string; notes?: string; }): Promise { const db = await getDb(); // Check if file already exists by filename (e.g. re-import of same file) const existing = await db.select( "SELECT id FROM imported_files WHERE source_id = $1 AND filename = $2", [file.source_id, file.filename] ); if (existing.length > 0) { await db.execute( `UPDATE imported_files SET file_hash = $1, row_count = $2, status = $3, notes = $4, import_date = CURRENT_TIMESTAMP WHERE id = $5`, [file.file_hash, file.row_count, file.status, file.notes || null, existing[0].id] ); return existing[0].id; } const result = await db.execute( `INSERT INTO imported_files (source_id, filename, file_hash, row_count, status, notes) VALUES ($1, $2, $3, $4, $5, $6)`, [ file.source_id, file.filename, file.file_hash, file.row_count, file.status, file.notes || null, ] ); return result.lastInsertId as number; } export async function updateFileStatus( id: number, status: string, rowCount?: number, notes?: string ): Promise { const db = await getDb(); await db.execute( `UPDATE imported_files SET status = $1, row_count = COALESCE($2, row_count), notes = COALESCE($3, notes) WHERE id = $4`, [status, rowCount ?? null, notes ?? null, id] ); } export async function getAllImportedFiles(): Promise { const db = await getDb(); return db.select( `SELECT f.*, s.name AS source_name FROM imported_files f JOIN import_sources s ON s.id = f.source_id ORDER BY f.import_date DESC` ); } export async function deleteImportWithTransactions( fileId: number ): Promise { const db = await getDb(); // Look up the source_id before deleting const files = await db.select( "SELECT source_id FROM imported_files WHERE id = $1", [fileId] ); const sourceId = files.length > 0 ? files[0].source_id : null; // Pre-flight: if any transaction in this file is linked to a balance // account via `balance_account_transfers`, the FK RESTRICT will fire on // the bulk DELETE. Surface a typed error BEFORE touching the row so the // UI can prompt the user to unlink first (Issue #142). const linked = await db.select< Array<{ transaction_id: number; account_id: number; account_name: string; direction: "in" | "out" }> >( `SELECT bat.transaction_id AS transaction_id, bat.account_id AS account_id, a.name AS account_name, bat.direction AS direction FROM balance_account_transfers bat JOIN transactions t ON t.id = bat.transaction_id JOIN balance_accounts a ON a.id = bat.account_id WHERE t.file_id = $1`, [fileId] ); if (linked.length > 0) { throw new TransactionLinkedToBalanceError(null, linked); } let result; try { result = await db.execute( "DELETE FROM transactions WHERE file_id = $1", [fileId] ); } catch (err) { if (isLinkedTransactionFkError(err)) { throw new TransactionLinkedToBalanceError(null, []); } throw err; } await db.execute("DELETE FROM imported_files WHERE id = $1", [fileId]); // Clean up orphaned source if no files remain if (sourceId) { const remaining = await db.select>( "SELECT COUNT(*) AS cnt FROM imported_files WHERE source_id = $1", [sourceId] ); if (remaining[0]?.cnt === 0) { await db.execute("DELETE FROM import_sources WHERE id = $1", [sourceId]); } } return result.rowsAffected; } export async function deleteAllImportsWithTransactions(): Promise { const db = await getDb(); // Same pre-flight as the per-file path: if ANY transaction is linked to // a balance account, the bulk wipe would explode on FK RESTRICT — surface // a typed error so the UI can prompt the user to unlink first. const linked = await db.select< Array<{ transaction_id: number; account_id: number; account_name: string; direction: "in" | "out" }> >( `SELECT bat.transaction_id AS transaction_id, bat.account_id AS account_id, a.name AS account_name, bat.direction AS direction FROM balance_account_transfers bat JOIN balance_accounts a ON a.id = bat.account_id LIMIT 50` ); if (linked.length > 0) { throw new TransactionLinkedToBalanceError(null, linked); } let result; try { result = await db.execute("DELETE FROM transactions"); } catch (err) { if (isLinkedTransactionFkError(err)) { throw new TransactionLinkedToBalanceError(null, []); } throw err; } await db.execute("DELETE FROM imported_files"); await db.execute("DELETE FROM import_sources"); return result.rowsAffected; }