Mcp/template data feedback (#617)

* fix(booking-templates): scope template list to the active company

GET /api/settings/booking-templates relied solely on the btl_select RLS
policy, which is membership-wide (user_company_ids) and returns templates
from every company the user belongs to. A user who owns multiple companies
saw all their templates merged regardless of which company was active.

Narrow the list in the API layer (mirroring counterparty-templates) to
system + the active company + the active company's team. RLS stays the
security backstop; this fixes the cross-company merge within a single
user's own view (it was never a cross-tenant data leak).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(import): show proper message for duplicate bank file upload

The bank file import page mis-parsed the structured error envelope
({ error: { code, message, details } }), so a BANK_FILE_DUPLICATE
(409) fell through to the generic "Kunde inte läsa filen" fallback.
The upload step also hardcoded that same string as the error heading,
so duplicates were doubly misreported as parse failures.

- Parse the structured envelope by error.code; surface error.message
  for all codes instead of rendering the error object.
- Add a dedicated BANK_FILE_DUPLICATE message using the importedAt /
  importedCount details the route already returns.
- Add an optional errorTitle prop to BankFileUploadStep (defaults to
  the previous text) and pass "Filen är redan importerad" for dupes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(tests): add comprehensive tests for recordateEntry, inbox-linking, and external-id handling

- Implemented unit tests for recordateEntry in the bookkeeping module to validate various scenarios including date changes, non-posted entries, and fiscal period restrictions.
- Created tests for inbox-linking status in pending operations to ensure correct handling of invoice inbox items and supplier invoices, addressing historical bugs related to status updates.
- Added tests for external-id utilities to ensure consistent handling of monetary amounts and deduplication keys across different transaction sources.
- Introduced new functions in external-id.ts for stable external ID generation and normalization of imported descriptions, enhancing transaction deduplication reliability.

feat(migrations): add new database migrations for transaction handling

- Created migration to exclude storno and correction vouchers from unmatched GL lines, ensuring accurate reconciliation.
- Added a migration to preserve original bank transaction descriptions in a new immutable column, allowing for user edits while maintaining audit trails and deduplication integrity.

* feat(migrations): add function to exclude storno/correction vouchers from unmatched GL lines

* feat(transactions): enhance transaction handling with improved description normalization and preloaded original entries

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Mattsson
2026-06-01 14:45:49 +02:00
committed by GitHub
co-authored by Claude Opus 4.8
parent 2c59c3633f
commit c6c86cded4
54 changed files with 3673 additions and 158 deletions
@@ -12,6 +12,8 @@ function makeTx(overrides: Partial<Transaction> = {}): Transaction {
external_id: 'eb_sek_1',
date: '2026-06-12',
description: 'Överföring till EUR-konto',
original_description: 'Överföring till EUR-konto',
title_edited_at: null,
amount: -1000,
currency: 'SEK',
amount_sek: -1000,
+96 -3
View File
@@ -16,6 +16,9 @@ export const CURRENCY_REVALUATION_ALREADY_EXISTS = 'CURRENCY_REVALUATION_ALREADY
export const INVALID_MAPPING_RESULT = 'INVALID_MAPPING_RESULT' as const
export const BOOKKEEPING_DATABASE_ERROR = 'BOOKKEEPING_DATABASE_ERROR' as const
export const MEANINGLESS_CORRECTION = 'MEANINGLESS_CORRECTION' as const
export const NO_OPEN_PERIOD_FOR_DATE = 'NO_OPEN_PERIOD_FOR_DATE' as const
export const TARGET_PERIOD_CLOSED = 'TARGET_PERIOD_CLOSED' as const
export const TARGET_PERIOD_LOCKED = 'TARGET_PERIOD_LOCKED' as const
// ============================================================================
// AccountsNotInChartError — kept for back-compat (many existing call sites)
@@ -139,7 +142,10 @@ export class CurrencyRevaluationAlreadyExistsError extends Error {
}
}
export type MeaninglessCorrectionReason = 'net_zero_per_account' | 'identical_to_original'
export type MeaninglessCorrectionReason =
| 'net_zero_per_account'
| 'identical_to_original'
| 'no_date_change'
export class MeaninglessCorrectionError extends Error {
readonly code = MEANINGLESS_CORRECTION
@@ -147,12 +153,57 @@ export class MeaninglessCorrectionError extends Error {
super(
reason === 'net_zero_per_account'
? 'Correction lines net to zero on every account — no economic event represented (BFL 5 kap. 5 §).'
: 'Correction lines are identical to the original entry — nothing to correct.'
: reason === 'no_date_change'
? 'New date equals the current date — nothing to move.'
: 'Correction lines are identical to the original entry — nothing to correct.'
)
this.name = 'MeaninglessCorrectionError'
}
}
/**
* Raised when a verifikation is moved (recordate) to a date that no fiscal
* period covers. We do not auto-create periods on a correction.
*/
export class NoOpenPeriodForDateError extends Error {
readonly code = NO_OPEN_PERIOD_FOR_DATE
constructor(public readonly date: string) {
super(`No fiscal period covers ${date}`)
this.name = 'NoOpenPeriodForDateError'
}
}
/**
* Raised when the target date of a recordate falls in a closed fiscal year
* (bokslut). A closed year cannot be reopened — the correction must be booked
* in the current open period instead.
*/
export class TargetPeriodClosedError extends Error {
readonly code = TARGET_PERIOD_CLOSED
constructor(public readonly date: string) {
super(`The fiscal period covering ${date} is closed`)
this.name = 'TargetPeriodClosedError'
}
}
/**
* Raised when the target date of a recordate falls in a locked period or is
* covered by the company-wide bookkeeping lock date. Carries the lock date so
* the UI can offer an unlock affordance.
*/
export class TargetPeriodLockedError extends Error {
readonly code = TARGET_PERIOD_LOCKED
constructor(
public readonly date: string,
public readonly lockDate: string | null
) {
super(
`The fiscal period covering ${date} is locked${lockDate ? ` (lock date ${lockDate})` : ''}`
)
this.name = 'TargetPeriodLockedError'
}
}
export class InvalidMappingResultError extends Error {
readonly code = INVALID_MAPPING_RESULT
constructor(
@@ -222,7 +273,10 @@ export function isBookkeepingError(err: unknown): boolean {
err instanceof CurrencyRevaluationAlreadyExistsError ||
err instanceof InvalidMappingResultError ||
err instanceof BookkeepingDatabaseError ||
err instanceof MeaninglessCorrectionError
err instanceof MeaninglessCorrectionError ||
err instanceof NoOpenPeriodForDateError ||
err instanceof TargetPeriodClosedError ||
err instanceof TargetPeriodLockedError
)
}
@@ -385,6 +439,45 @@ export function bookkeepingErrorResponse(err: unknown): NextResponse | null {
)
}
if (err instanceof NoOpenPeriodForDateError) {
return NextResponse.json(
{
error: {
code: err.code,
message: err.message,
details: { date: err.date },
},
},
{ status: 400 }
)
}
if (err instanceof TargetPeriodClosedError) {
return NextResponse.json(
{
error: {
code: err.code,
message: err.message,
details: { date: err.date },
},
},
{ status: 409 }
)
}
if (err instanceof TargetPeriodLockedError) {
return NextResponse.json(
{
error: {
code: err.code,
message: err.message,
details: { date: err.date, lockDate: err.lockDate },
},
},
{ status: 409 }
)
}
if (err instanceof BookkeepingDatabaseError) {
return NextResponse.json(
{