Bug/gh issues fiz (#1103)

* refactor: optimize page loading and data fetching

* fix: resolve recurring production runtime errors

* feat: add MCP company and customer updates

* fix: handle year-end tax adjustments

* feat: harden annual report compliance

* fix: expand invoice logo and font support

* fix: sanitize API route error responses

* fix: sanitize user-facing error messages

* feat: persist onboarding and tax assessment notices

* fix: reduce cloud backup audit churn

* feat: refine invoice editor layout

* fix: show saved tax adjustments in INK2

* fix: complete annual report API mappings

* docs: record operational safeguards and decisions

* fix: harden annual report review findings

* fix: adjust column span for description based on VAT registration

* New css class name
This commit is contained in:
Mattsson
2026-07-21 23:00:15 +02:00
committed by GitHub
parent 702512437a
commit e11f70b347
467 changed files with 17569 additions and 2221 deletions
@@ -17,6 +17,7 @@ import { registerEndpoint, dataEnvelope } from '@/lib/api/v1/registry'
import { withApiV1 } from '@/lib/api/v1/with-api-v1'
import { v1ErrorResponse, v1ErrorResponseFromCode } from '@/lib/api/v1/errors'
import { eventBus } from '@/lib/events'
import { getErrorMessage as getUserErrorMessage } from '@/lib/errors/get-error-message'
const DocumentDownloadResponse = z.object({
id: z.string().uuid(),
@@ -122,7 +123,7 @@ export const GET = withApiV1<{ params: Promise<{ companyId: string; id: string }
ctx.log.error('createSignedUrl failed', signErr as Error, { documentId })
return v1ErrorResponseFromCode('DOC_DOWNLOAD_FAILED', ctx.log, {
requestId: ctx.requestId,
details: { reason: signErr?.message ?? 'unknown' },
details: { reason: getUserErrorMessage(signErr) ?? 'unknown' },
})
}
@@ -41,6 +41,7 @@ import {
ALLOWED_DOCUMENT_TYPES,
} from '@/lib/core/documents/document-service'
import type { DocumentUploadSource } from '@/types'
import { getErrorMessage } from '@/lib/errors/get-error-message'
const DocumentUploaded = z.object({
id: z.string().uuid(),
@@ -297,7 +298,7 @@ export const POST = withApiV1<{ params: Promise<{ companyId: string }> }>(
opLog.warn('document upload rejected by content validation', { reason: message })
return v1ErrorResponseFromCode('DOC_UPLOAD_INVALID_CONTENT', opLog, {
requestId: ctx.requestId,
details: { reason: message },
details: { reason: getErrorMessage(err) },
})
}
// Full error is logged above; the raw message can leak storage-layer
@@ -23,6 +23,7 @@ import { withApiV1 } from '@/lib/api/v1/with-api-v1'
import { v1ErrorResponseFromCode } from '@/lib/api/v1/errors'
import { OpeningBalancesFieldsSchema } from '@/lib/api/schemas'
import { getOpeningBalances, setOpeningBalancesBulk } from '@/lib/salary/opening-balances'
import { getErrorMessage as getUserErrorMessage } from '@/lib/errors/get-error-message'
const OpeningBalancesResponse = z.object({
employee_opening_balances_id: z.string().uuid().nullable(),
@@ -200,7 +201,7 @@ export const PUT = withApiV1<{ params: Promise<{ companyId: string; id: string }
const itemError = result.itemErrors?.[0]
return v1ErrorResponseFromCode(itemError?.code ?? result.code, ctx.log, {
requestId: ctx.requestId,
details: itemError ? { message: itemError.message } : result.details,
details: itemError ? { message: getUserErrorMessage(itemError) } : result.details,
})
}
@@ -15,6 +15,7 @@ import { registerEndpoint, dataEnvelope } from '@/lib/api/v1/registry'
import { withApiV1 } from '@/lib/api/v1/with-api-v1'
import { v1ErrorResponseFromCode } from '@/lib/api/v1/errors'
import { closePeriod } from '@/lib/core/bookkeeping/period-service'
import { getErrorMessage } from '@/lib/errors/get-error-message'
const PeriodClosedResponse = z.object({
id: z.string().uuid(),
@@ -128,7 +129,7 @@ export const POST = withApiV1<{ params: Promise<{ companyId: string; id: string
})
}
return v1ErrorResponseFromCode('INTERNAL_ERROR', ctx.log, {
requestId: ctx.requestId, details: { reason: msg },
requestId: ctx.requestId, details: { reason: getErrorMessage(err, { locale: 'en' }) },
})
}
},
@@ -18,6 +18,7 @@ import { v1ErrorResponseFromCode } from '@/lib/api/v1/errors'
import { ownsFiscalPeriod } from '@/lib/api/v1/owns-fiscal-period'
import { startOperation, completeOperation, failOperation } from '@/lib/api/v1/operations'
import { executeCurrencyRevaluation } from '@/lib/bookkeeping/currency-revaluation'
import { getErrorMessage } from '@/lib/errors/get-error-message'
const Body = z
.object({ as_of_date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional() })
@@ -177,7 +178,7 @@ export const POST = withApiV1<{ params: Promise<{ companyId: string; id: string
id: operationId,
error: {
code: msg.includes('already exists') ? 'CURRENCY_REVALUATION_ALREADY_EXISTS' : 'CURRENCY_REVALUATION_FAILED',
message: msg,
message: getErrorMessage(err, { locale: 'en' }),
},
},
ctx.log,
@@ -12,6 +12,7 @@ import { registerEndpoint, dataEnvelope } from '@/lib/api/v1/registry'
import { withApiV1 } from '@/lib/api/v1/with-api-v1'
import { v1ErrorResponseFromCode } from '@/lib/api/v1/errors'
import { lockPeriod } from '@/lib/core/bookkeeping/period-service'
import { getErrorMessage } from '@/lib/errors/get-error-message'
const PeriodLockedResponse = z.object({
id: z.string().uuid(),
@@ -83,7 +84,7 @@ export const POST = withApiV1<{ params: Promise<{ companyId: string; id: string
if (msg.includes('already closed') || msg.includes('already locked')) {
return v1ErrorResponseFromCode('CONFLICT', ctx.log, {
requestId: ctx.requestId,
details: { reason: msg },
details: { reason: getErrorMessage(err, { locale: 'en' }) },
})
}
// lockPeriod's uncategorised-transactions error message is in Swedish
@@ -94,13 +95,13 @@ export const POST = withApiV1<{ params: Promise<{ companyId: string; id: string
if (msg.includes('saknar bokföring') || msg.toLowerCase().includes('uncategorised')) {
return v1ErrorResponseFromCode('PERIOD_HAS_UNBOOKED_TRANSACTIONS', ctx.log, {
requestId: ctx.requestId,
details: { reason: msg },
details: { reason: getErrorMessage(err, { locale: 'en' }) },
})
}
ctx.log.error('fiscal-periods.lock unexpected error', err as Error, { fiscalPeriodId: idParse.data })
return v1ErrorResponseFromCode('INTERNAL_ERROR', ctx.log, {
requestId: ctx.requestId,
details: { reason: msg },
details: { reason: getErrorMessage(err, { locale: 'en' }) },
})
}
},
@@ -17,6 +17,7 @@ import { withApiV1 } from '@/lib/api/v1/with-api-v1'
import { v1ErrorResponseFromCode } from '@/lib/api/v1/errors'
import { ownsFiscalPeriod } from '@/lib/api/v1/owns-fiscal-period'
import { generateOpeningBalances } from '@/lib/core/bookkeeping/year-end-service'
import { getErrorMessage } from '@/lib/errors/get-error-message'
const Body = z.object({ next_period_id: z.string().uuid() }).strict()
@@ -153,7 +154,8 @@ export const POST = withApiV1<{ params: Promise<{ companyId: string; id: string
})
}
return v1ErrorResponseFromCode('BOOKKEEPING_DATABASE_ERROR', ctx.log, {
requestId: ctx.requestId, details: { reason: msg, step: 'opening_balances' },
requestId: ctx.requestId,
details: { reason: getErrorMessage(err, { locale: 'en' }), step: 'opening_balances' },
})
}
},
@@ -21,6 +21,7 @@ import { v1ErrorResponseFromCode } from '@/lib/api/v1/errors'
import { ownsFiscalPeriod } from '@/lib/api/v1/owns-fiscal-period'
import { startOperation, completeOperation, failOperation } from '@/lib/api/v1/operations'
import { executeYearEndClosing } from '@/lib/core/bookkeeping/year-end-service'
import { getErrorMessage } from '@/lib/errors/get-error-message'
const YearEndAcceptedResponse = z.object({
operation_id: z.string().uuid(),
@@ -133,13 +134,12 @@ export const POST = withApiV1<{ params: Promise<{ companyId: string; id: string
)
return accepted(operationId, 'fiscal_periods.year_end', { requestId: ctx.requestId })
} catch (err) {
const msg = err instanceof Error ? err.message : 'unknown'
ctx.log.error('fiscal-periods.year-end failed', err as Error, { fiscalPeriodId, operationId })
await failOperation(
ctx.supabase,
{
id: operationId,
error: { code: 'YEAR_END_FAILED', message: msg },
error: { code: 'YEAR_END_FAILED', message: getErrorMessage(err, { locale: 'en' }) },
},
ctx.log,
)
@@ -37,6 +37,7 @@ import {
} from '@/lib/import/bank-file/parser'
import { ingestTransactions, type RawTransaction } from '@/lib/transactions/ingest'
import type { BankFileFormatId } from '@/lib/import/bank-file/types'
import { getErrorMessage as getUserErrorMessage } from '@/lib/errors/get-error-message'
const BankImportAccepted = z.object({
operation_id: z.string().uuid(),
@@ -292,14 +293,14 @@ export const POST = withApiV1<{ params: Promise<{ companyId: string }> }>(
id: op.id,
error: {
code: 'BANK_IMPORT_FAILED',
message: err instanceof Error ? err.message : 'Unknown failure during bank import.',
message: err instanceof Error ? getUserErrorMessage(err) : 'Unknown failure during bank import.',
},
},
ctx.log,
)
return v1ErrorResponseFromCode('BANK_IMPORT_FAILED', ctx.log, {
requestId: ctx.requestId,
details: { operation_id: op.id, reason: err instanceof Error ? err.message : 'unknown' },
details: { operation_id: op.id, reason: err instanceof Error ? getUserErrorMessage(err) : 'unknown' },
})
}
@@ -45,6 +45,7 @@ import {
import { suggestMappings } from '@/lib/import/account-mapper'
import { BAS_REFERENCE } from '@/lib/bookkeeping/bas-data'
import type { SIEAccountMappingRecord } from '@/lib/import/types'
import { getErrorMessage as getUserErrorMessage } from '@/lib/errors/get-error-message'
const SieImportAccepted = z.object({
operation_id: z.string().uuid(),
@@ -141,7 +142,7 @@ export const POST = withApiV1<{ params: Promise<{ companyId: string }> }>(
requestId: ctx.requestId,
details: {
field: 'options',
message: `options must be a valid JSON string: ${err instanceof Error ? err.message : 'parse error'}`,
message: `options must be a valid JSON string: ${err instanceof Error ? getUserErrorMessage(err) : 'parse error'}`,
},
})
}
@@ -205,7 +206,7 @@ export const POST = withApiV1<{ params: Promise<{ companyId: string }> }>(
ctx.log.error('SIE parse failed', err as Error)
return v1ErrorResponseFromCode('SIE_PARSE_FAILED', ctx.log, {
requestId: ctx.requestId,
details: { reason: err instanceof Error ? err.message : 'unknown' },
details: { reason: err instanceof Error ? getUserErrorMessage(err) : 'unknown' },
})
}
@@ -309,14 +310,14 @@ export const POST = withApiV1<{ params: Promise<{ companyId: string }> }>(
id: op.id,
error: {
code: 'SIE_IMPORT_FAILED',
message: err instanceof Error ? err.message : 'Unknown failure during SIE import.',
message: err instanceof Error ? getUserErrorMessage(err) : 'Unknown failure during SIE import.',
},
},
ctx.log,
)
return v1ErrorResponseFromCode('SIE_IMPORT_FAILED', ctx.log, {
requestId: ctx.requestId,
details: { operation_id: op.id, reason: err instanceof Error ? err.message : 'unknown' },
details: { operation_id: op.id, reason: err instanceof Error ? getUserErrorMessage(err) : 'unknown' },
})
}
@@ -38,7 +38,7 @@ import {
createInvoicePaymentJournalEntry,
} from '@/lib/bookkeeping/invoice-entries'
import { createJournalEntry, findFiscalPeriod } from '@/lib/bookkeeping/engine'
import { AccountsNotInChartError, isBookkeepingError } from '@/lib/bookkeeping/errors'
import { AccountsNotInChartError } from '@/lib/bookkeeping/errors'
import { getErrorMessage } from '@/lib/errors/get-error-message'
import { eventBus } from '@/lib/events'
import { findDuplicatePaymentCandidatesForInvoice } from '@/lib/invoices/duplicate-payment-candidates'
@@ -451,14 +451,9 @@ export const POST = withApiV1<{ params: Promise<{ companyId: string; id: string
invoiceId,
companyId: ctx.companyId,
})
const message = isBookkeepingError(err)
? getErrorMessage(err, { context: 'invoice' })
: err instanceof Error
? err.message
: 'Unknown error'
return v1ErrorResponseFromCode('INVOICE_PAID_BOOK_FAILED', ctx.log, {
requestId: ctx.requestId,
details: { reason: message },
details: { reason: getErrorMessage(err, { context: 'invoice' }) },
})
}
}
@@ -25,6 +25,7 @@ import { CreateJournalEntrySchema } from '@/lib/api/schemas'
import { createDraftEntry } from '@/lib/bookkeeping/engine'
import { isBookkeepingError } from '@/lib/bookkeeping/errors'
import type { Logger } from '@/lib/logger'
import { getErrorMessage as getUserErrorMessage } from '@/lib/errors/get-error-message'
const BulkRequest = z.object({
journal_entries: z.array(CreateJournalEntrySchema).min(1).max(50),
@@ -140,7 +141,7 @@ async function createOne(
request_index: index,
error: {
code: e.code ?? 'BOOKKEEPING_DATABASE_ERROR',
message: e.message ?? 'Engine error',
message: getUserErrorMessage(e) ?? 'Engine error',
details: e.details,
},
}
@@ -37,6 +37,7 @@ import { createSalaryRunEntries } from '@/lib/salary/salary-entries'
import { syncVacationLedgerForEmployees } from '@/lib/salary/vacation-ledger'
import { isBookkeepingError } from '@/lib/bookkeeping/errors'
import { eventBus } from '@/lib/events'
import { getErrorMessage as getUserErrorMessage } from '@/lib/errors/get-error-message'
const SalaryRunBooked = z.object({
id: z.string().uuid(),
@@ -272,7 +273,7 @@ export const POST = withApiV1<{ params: Promise<{ companyId: string; id: string
})
return v1ErrorResponseFromCode('SALARY_RUN_BOOK_FAILED', ctx.log, {
requestId: ctx.requestId,
details: { reason: err instanceof Error ? err.message : 'unknown' },
details: { reason: err instanceof Error ? getUserErrorMessage(err) : 'unknown' },
})
}
@@ -30,6 +30,7 @@ import { reverseEntry, createJournalEntry, findFiscalPeriod } from '@/lib/bookke
import { isBookkeepingError } from '@/lib/bookkeeping/errors'
import { eventBus } from '@/lib/events'
import type { SupplierInvoice, SupplierInvoiceItem } from '@/types'
import { getErrorMessage as getUserErrorMessage } from '@/lib/errors/get-error-message'
const SI_PAID_RESPONSE_COLUMNS =
'id, supplier_id, arrival_number, supplier_invoice_number, status, currency, total, paid_amount, remaining_amount, paid_at, payment_journal_entry_id'
@@ -409,7 +410,7 @@ export const POST = withApiV1<{ params: Promise<{ companyId: string; id: string
})
return v1ErrorResponseFromCode('SI_PAID_FAILED', ctx.log, {
requestId: ctx.requestId,
details: { reason: err instanceof Error ? err.message : 'unknown' },
details: { reason: err instanceof Error ? getUserErrorMessage(err) : 'unknown' },
})
}
@@ -32,7 +32,7 @@ import {
import { resolveSettlementAccount } from '@/lib/bookkeeping/settlement-account'
import { findUnresolvableAccounts } from '@/lib/bookkeeping/account-validation'
import { reverseEntry, createJournalEntry, findFiscalPeriod } from '@/lib/bookkeeping/engine'
import { AccountsNotInChartError, isBookkeepingError } from '@/lib/bookkeeping/errors'
import { AccountsNotInChartError } from '@/lib/bookkeeping/errors'
import { getErrorMessage } from '@/lib/errors/get-error-message'
import { logMatchEvent } from '@/lib/invoices/match-log'
import { planInvoicePayment } from '@/lib/invoices/apply-invoice-payment'
@@ -453,14 +453,9 @@ export const POST = withApiV1<{ params: Promise<{ companyId: string; id: string
return v1ErrorResponse(err, txLog, { requestId: ctx.requestId })
}
txLog.error('match-invoice: payment JE creation failed: aborting before state mutation', err as Error)
const message = isBookkeepingError(err)
? getErrorMessage(err, { context: 'invoice' })
: err instanceof Error
? err.message
: 'Unknown error'
return v1ErrorResponseFromCode('INVOICE_PAID_BOOK_FAILED', txLog, {
requestId: ctx.requestId,
details: { reason: message },
details: { reason: getErrorMessage(err, { context: 'invoice' }) },
})
}
@@ -18,7 +18,7 @@ import {
} from '@/lib/bookkeeping/supplier-invoice-entries'
import { resolveSettlementAccount } from '@/lib/bookkeeping/settlement-account'
import { reverseEntry, createJournalEntry, findFiscalPeriod } from '@/lib/bookkeeping/engine'
import { AccountsNotInChartError, isBookkeepingError } from '@/lib/bookkeeping/errors'
import { AccountsNotInChartError } from '@/lib/bookkeeping/errors'
import { findUnresolvableAccounts } from '@/lib/bookkeeping/account-validation'
import { getErrorMessage } from '@/lib/errors/get-error-message'
import { logMatchEvent } from '@/lib/invoices/match-log'
@@ -365,14 +365,9 @@ export const POST = withApiV1<{ params: Promise<{ companyId: string; id: string
if (err instanceof AccountsNotInChartError) {
return v1ErrorResponse(err, txLog, { requestId: ctx.requestId })
}
const message = isBookkeepingError(err)
? getErrorMessage(err, { context: 'supplier_invoice' })
: err instanceof Error
? err.message
: 'Unknown error'
return v1ErrorResponseFromCode('MATCH_SI_RECORD_PAYMENT_FAILED', txLog, {
requestId: ctx.requestId,
details: { reason: message },
details: { reason: getErrorMessage(err, { context: 'supplier_invoice' }) },
})
}
@@ -346,7 +346,7 @@ async function categorizeOne(
ok: false,
request_index: index,
transaction_id: transactionId,
error: { code: 'INTERNAL_ERROR', message: updateErr.message },
error: { code: 'INTERNAL_ERROR', message: getErrorMessage(updateErr) },
}
}
if ((!updated || updated.length === 0) && journalEntryId) {