fix: BankID fallback, bank import fixes, account deletion (#216)
* fix: BankID graceful fallback, bank import fixes, and account deletion - BankID: surface service_unavailable state in login/register with password fallback messaging; structured error codes in tic extension; poll failure counter in BankIdAuth avoids infinite retry when TIC API is down. - Transactions: allow deleting unbooked bank-synced and imported transactions (only posted entries remain protected); detect reconnect duplicates by also checking unbooked bank-synced rows in content-based dedup. - Enable Banking: key external_id by account iban/uid instead of connection id so reconnects don't create duplicates. - Bank import: detect SEB privatbanken CSV variant (Bokföringsdatum / Valutadatum headers) via regex. - Banking settings: replace full-screen sync loader with toast notifications. - delete_user_account: raise statement_timeout, pre-clear NO ACTION FK references, and disable audit/immutability triggers during CASCADE. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: address Greptile review — ingest dedup scoping and migration EXCEPTION handler - ingest: split buildExistingTransactionMap into two maps. Booked rows (any source) remain consumed by any incoming raw transaction, but unbooked enable_banking slots are only consumed when the incoming raw transaction is also enable_banking. This preserves reconnect dedup while preventing false positives where a pending bank-synced row silently blocks a legitimately separate CSV row with the same date/amount. - delete_user_account: add EXCEPTION WHEN OTHERS handler that re-enables every legally required enforcement trigger before re-raising. Postgres transactional DDL already rolls back on abort, but the explicit guard makes the intent unambiguous and covers sub-transaction edge cases so enforcement triggers are never left disabled. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
aa405b9a74
commit
bca00cc5bd
@@ -60,28 +60,30 @@ describe('DELETE /api/transactions/[id]', () => {
|
||||
expect(body.error).toContain('booked')
|
||||
})
|
||||
|
||||
it('returns 409 when transaction is bank-synced', async () => {
|
||||
it('allows deleting unbooked bank-synced transactions', async () => {
|
||||
const tx = makeTransaction({ bank_connection_id: 'bc-1', journal_entry_id: null, import_source: null })
|
||||
enqueue({ data: tx, error: null })
|
||||
enqueue({ data: tx, error: null }) // fetch
|
||||
enqueue({ data: null, error: null }) // delete
|
||||
|
||||
const request = new Request('http://localhost/api/transactions/tx-1', { method: 'DELETE' })
|
||||
const response = await DELETE(request, createMockRouteParams({ id: 'tx-1' }))
|
||||
const { status, body } = await parseJsonResponse<{ error: string }>(response)
|
||||
const { status, body } = await parseJsonResponse(response)
|
||||
|
||||
expect(status).toBe(409)
|
||||
expect(body.error).toContain('bank-synced')
|
||||
expect(status).toBe(200)
|
||||
expect(body).toEqual({ success: true })
|
||||
})
|
||||
|
||||
it('returns 409 when transaction was imported', async () => {
|
||||
it('allows deleting unbooked imported transactions', async () => {
|
||||
const tx = makeTransaction({ import_source: 'csv_nordea', journal_entry_id: null, bank_connection_id: null })
|
||||
enqueue({ data: tx, error: null })
|
||||
enqueue({ data: tx, error: null }) // fetch
|
||||
enqueue({ data: null, error: null }) // delete
|
||||
|
||||
const request = new Request('http://localhost/api/transactions/tx-1', { method: 'DELETE' })
|
||||
const response = await DELETE(request, createMockRouteParams({ id: 'tx-1' }))
|
||||
const { status, body } = await parseJsonResponse<{ error: string }>(response)
|
||||
const { status, body } = await parseJsonResponse(response)
|
||||
|
||||
expect(status).toBe(409)
|
||||
expect(body.error).toContain('imported')
|
||||
expect(status).toBe(200)
|
||||
expect(body).toEqual({ success: true })
|
||||
})
|
||||
|
||||
it('deletes a manually added unbooked transaction', async () => {
|
||||
|
||||
@@ -19,7 +19,7 @@ export async function DELETE(
|
||||
// Fetch the transaction with ownership check
|
||||
const { data: transaction, error: fetchError } = await supabase
|
||||
.from('transactions')
|
||||
.select('id, journal_entry_id, bank_connection_id, import_source')
|
||||
.select('id, journal_entry_id')
|
||||
.eq('id', id)
|
||||
.eq('company_id', companyId)
|
||||
.single()
|
||||
@@ -28,25 +28,13 @@ export async function DELETE(
|
||||
return NextResponse.json({ error: 'Transaction not found' }, { status: 404 })
|
||||
}
|
||||
|
||||
// Guard: only manually added, unbooked transactions can be deleted
|
||||
// Guard: only unbooked transactions can be deleted
|
||||
if (transaction.journal_entry_id) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Cannot delete a booked transaction. Use reversal (storno) instead.' },
|
||||
{ status: 409 }
|
||||
)
|
||||
}
|
||||
if (transaction.bank_connection_id) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Cannot delete a bank-synced transaction' },
|
||||
{ status: 409 }
|
||||
)
|
||||
}
|
||||
if (transaction.import_source) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Cannot delete an imported transaction' },
|
||||
{ status: 409 }
|
||||
)
|
||||
}
|
||||
|
||||
const { error: deleteError } = await supabase
|
||||
.from('transactions')
|
||||
|
||||
Reference in New Issue
Block a user