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:
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user