Files
accounted/app/api/import/sie/execute/route.ts
T
Mattsson e11f70b347 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
2026-07-21 23:00:15 +02:00

135 lines
4.8 KiB
TypeScript

import { NextResponse } from 'next/server'
import { parseSIEFile, detectEncoding, decodeBuffer } from '@/lib/import/sie-parser'
import { suggestMappings } from '@/lib/import/account-mapper'
import { executeSIEImport, checkDuplicateImport } from '@/lib/import/sie-import'
import { BAS_REFERENCE } from '@/lib/bookkeeping/bas-data'
import { withRouteContext } from '@/lib/api/with-route-context'
import { errorResponseFromCode } from '@/lib/errors/get-structured-error'
import type { AccountMapping, SIEAccountMappingRecord } from '@/lib/import/types'
import { getErrorMessage as getUserErrorMessage } from '@/lib/errors/get-error-message'
// SIE imports with many vouchers need extended execution time
export const maxDuration = 300
/** POST /api/import/sie/execute: execute the SIE import. */
export const POST = withRouteContext(
'sie_import.execute',
async (request, ctx) => {
const { user, supabase, companyId, log, requestId } = ctx
const formData = await request.formData()
const file = formData.get('file') as File | null
const mappingsJson = formData.get('mappings') as string | null
const optionsJson = formData.get('options') as string | null
if (!file) {
return errorResponseFromCode('SIE_PARSE_NO_FILE', log, { requestId })
}
const opLog = log.child({ filename: file.name, sizeBytes: file.size })
try {
// The voucherSeries option is a fallback for vouchers that arrive without
// a series (SIE4I subsystem files); the import engine preserves each
// #VER's source series per voucher.
const parsedOptions = optionsJson ? JSON.parse(optionsJson) : null
const { data: companySettings } = await supabase
.from('company_settings')
.select('default_voucher_series')
.eq('company_id', companyId)
.maybeSingle()
const companyDefaultSeries = companySettings?.default_voucher_series || 'B'
const options = parsedOptions ?? {
createFiscalPeriod: true,
importOpeningBalances: true,
importTransactions: true,
voucherSeries: companyDefaultSeries,
updateAccountNames: true,
}
const arrayBuffer = await file.arrayBuffer()
const encoding = detectEncoding(arrayBuffer)
const content = decodeBuffer(arrayBuffer, encoding)
const parsed = parseSIEFile(content)
const duplicate = await checkDuplicateImport(supabase, companyId!, content)
if (duplicate) {
return errorResponseFromCode('SIE_DUPLICATE_FILE', opLog, {
requestId,
details: { importId: duplicate.id, importedAt: duplicate.imported_at },
})
}
let mappings: AccountMapping[]
if (mappingsJson) {
mappings = JSON.parse(mappingsJson)
} else {
const { data: storedMappings } = await supabase
.from('sie_account_mappings')
.select('*')
.eq('company_id', companyId)
mappings = suggestMappings(
parsed.accounts,
BAS_REFERENCE,
(storedMappings as SIEAccountMappingRecord[]) || undefined,
)
}
const unmapped = mappings.filter((m) => !m.targetAccount)
if (unmapped.length > 0) {
return errorResponseFromCode('SIE_IMPORT_UNMAPPED_ACCOUNTS', opLog, {
requestId,
details: {
unmappedCount: unmapped.length,
unmappedAccounts: unmapped.slice(0, 5).map((m) => ({
account: m.sourceAccount,
name: m.sourceName,
})),
},
})
}
// Account creation (and #KONTO renames) happen inside executeSIEImport
// via syncMappedAccounts: the pre-create block that used to live here
// was a duplicate of that logic.
const result = await executeSIEImport(
supabase,
companyId!,
user.id,
parsed,
mappings,
{
filename: file.name,
fileContent: content,
createFiscalPeriod: options.createFiscalPeriod,
importOpeningBalances: options.importOpeningBalances,
importTransactions: options.importTransactions,
voucherSeries: options.voucherSeries || companyDefaultSeries,
updateAccountNames: options.updateAccountNames ?? true,
markImportedNoDocRequired: options.markImportedNoDocRequired ?? false,
},
)
if (!result.success) {
return errorResponseFromCode('SIE_IMPORT_FAILED', opLog, {
requestId,
details: { result },
})
}
return NextResponse.json({ success: true, result })
} catch (err) {
opLog.error('sie execute unexpected error', err as Error)
return errorResponseFromCode('SIE_IMPORT_UNEXPECTED', opLog, {
requestId,
details: { reason: err instanceof Error ? getUserErrorMessage(err) : 'unknown' },
})
}
},
{ requireWrite: true },
)