diff --git a/src/lib/server/services/imports.test.ts b/src/lib/server/services/imports.test.ts index 020440f..d9945f8 100644 --- a/src/lib/server/services/imports.test.ts +++ b/src/lib/server/services/imports.test.ts @@ -3,6 +3,7 @@ import { openDatabase } from '../db.ts'; import { commitImport, createDraftImport, + deleteDraftImport, findPotentialDuplicates, previewImport, saveDecisions, @@ -463,6 +464,40 @@ Deno.test('undo then re-import with the same mapping revives rather than duplica db.close(); }); +Deno.test('cancelling a draft deletes it and its archived bytes', () => { + const db = testDb(); + const id = createDraftImport(db, 'chk', 'scratch.csv', 'Date,Description,Amount\n2026-06-01,Shell,-40.00\n'); + deleteDraftImport(db, id); + + const row = db.prepare('SELECT id FROM imports WHERE id = ?').get(id); + if (row) throw new Error('the draft row and its payload should be gone'); + db.close(); +}); + +Deno.test('cancel refuses a committed import so history is never hard-deleted', () => { + const db = testDb(); + const id = draft(db, 'Date,Description,Amount\n2026-06-01,Shell,-40.00\n'); + commitImport(db, id); + + let threw = false; + try { + deleteDraftImport(db, id); + } catch { + threw = true; + } + if (!threw) throw new Error('a committed import must not be hard-deletable'); + // It survives, and its rows with it — undo is the only removal path. + const still = db.prepare('SELECT status FROM imports WHERE id = ?').get(id) as { status: string }; + if (still.status !== 'committed') throw new Error('the committed import must be untouched'); + if ( + (db.prepare("SELECT COUNT(*) AS n FROM transactions WHERE import_id = ?").get(id) as { n: number }) + .n !== 1 + ) { + throw new Error('the committed import kept its transaction'); + } + db.close(); +}); + Deno.test('only a committed import can be undone', () => { const db = testDb(); const id = draft(db, 'Date,Description,Amount\n2026-06-01,Shell,-40.00\n'); diff --git a/src/lib/server/services/imports.ts b/src/lib/server/services/imports.ts index d5f9c54..458681c 100644 --- a/src/lib/server/services/imports.ts +++ b/src/lib/server/services/imports.ts @@ -390,3 +390,20 @@ export function undoImport(db: DatabaseSync, importId: number): number { throw err; } } + +/** + * Discard an unfinished import, archived bytes and all. A draft was never + * committed and owns no transactions, so unlike undo (which preserves a committed + * import as history) there is nothing to keep — this is the "cancel and delete the + * file" the user asked for. Restricted to drafts precisely so it can never become + * a path to hard-deleting a committed import's record; that stays soft, via undo. + */ +export function deleteDraftImport(db: DatabaseSync, importId: number): void { + const record = getImport(db, importId); + if (!record) throw new Error(`Unknown import: ${importId}`); + if (record.status !== 'draft') { + throw new Error(`Only an unfinished import can be cancelled; ${importId} is ${record.status}.`); + } + // A draft has no transactions referencing it, so the row deletes cleanly. + db.prepare('DELETE FROM imports WHERE id = ? AND status = ?').run(importId, 'draft'); +} diff --git a/src/routes/(app)/settings/import/+page.server.ts b/src/routes/(app)/settings/import/+page.server.ts index 1a04d11..ac580f8 100644 --- a/src/routes/(app)/settings/import/+page.server.ts +++ b/src/routes/(app)/settings/import/+page.server.ts @@ -1,9 +1,14 @@ import { fail, redirect } from '@sveltejs/kit'; import { getDb } from '$lib/server/db'; import { listAccounts } from '$lib/server/services/accounts'; -import { createDraftImport, listImports, undoImport } from '$lib/server/services/imports'; +import { + createDraftImport, + deleteDraftImport, + listImports, + saveMapping, + undoImport +} from '$lib/server/services/imports'; import { detectMapping, parseCsv } from '$lib/server/services/csv-import'; -import { saveMapping } from '$lib/server/services/imports'; import type { Actions, PageServerLoad } from './$types'; /** Guards against a stray upload of something enormous or binary. */ @@ -74,5 +79,16 @@ export const actions: Actions = { } catch (err) { return fail(400, { message: err instanceof Error ? err.message : 'Undo failed.' }); } + }, + + cancel: async ({ request }) => { + const form = await request.formData(); + const importId = Number(form.get('importId')); + try { + deleteDraftImport(getDb(), importId); + return { message: 'Discarded the unfinished import and its file.' }; + } catch (err) { + return fail(400, { message: err instanceof Error ? err.message : 'Could not cancel.' }); + } } }; diff --git a/src/routes/(app)/settings/import/+page.svelte b/src/routes/(app)/settings/import/+page.svelte index f86ea2f..e34f794 100644 --- a/src/routes/(app)/settings/import/+page.svelte +++ b/src/routes/(app)/settings/import/+page.svelte @@ -66,6 +66,20 @@