fix(import): treat a voucher-less SIE file as a no-op, not a failed migration (#1445)
* fix(import): treat a voucher-less SIE file as a no-op, not a failed migration A Fortnox migration aborted with the generic "Något gick fel. Försök igen." when the current fiscal year had nothing booked yet: Fortnox exports an empty SIE file for such a year, the finalizer's 0-entry safety net flipped it to 'failed', and the wizard stopped before the customer/supplier/invoice phase ever ran. Three layered fixes: - finalizeImportRecord only downgrades a 0-entry run to 'failed' when the file actually contained vouchers (parsed count via the documentation object). A file with no vouchers completes as a no-op with an explanatory warning; the mapping-fix retry loop the downgrade exists for (Lookma case) is unchanged. - The migration wizard no longer routes messages that are already user-facing Swedish (server envelopes, ImportResult.errors) through getErrorMessage's Swedish-pattern heuristic, which swallowed unrecognized sentences into the generic fallback. - The heuristic itself learns the import-error family (verifikation/importera) so other surfaces rethrowing engine messages keep the real reason too. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(import): cross-check raw #VER before accepting a 0-voucher file as empty The parsed voucher count alone cannot prove a fiscal year was empty: a field-separator or encoding mismatch can swallow every #VER block with only a warning-severity parse issue, and executeSIEImport does not fail on those. Only a raw content check proves the file never declared any vouchers. Addresses the truncation/corruption finding from the Swedish compliance review on #1445. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: record the raw #VER safeguard in the empty-SIE-file decision entry Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
Jakob Wennberg
parent
707d597b2e
commit
63d520719a
@@ -70,6 +70,34 @@ function apiErrorMessage(data: unknown, fallback: string): string {
|
||||
return fallback
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks an error whose message is already user-facing Swedish (server
|
||||
* envelopes, ImportResult.errors). The catch blocks must show these
|
||||
* verbatim: routing them through getErrorMessage would test them against
|
||||
* its Swedish-pattern heuristic and swallow any miss into the generic
|
||||
* "Något gick fel. Försök igen.", hiding the real reason the migration
|
||||
* stopped.
|
||||
*/
|
||||
class UserFacingError extends Error {}
|
||||
|
||||
/**
|
||||
* Build the throwable for a failed API response: an extracted server
|
||||
* message passes through to the UI verbatim, while the technical fallback
|
||||
* (e.g. "HTTP 500") stays a plain Error so getErrorMessage maps it to a
|
||||
* friendly message.
|
||||
*/
|
||||
function apiError(data: unknown, fallback: string): Error {
|
||||
const extracted = apiErrorMessage(data, '')
|
||||
return extracted ? new UserFacingError(extracted) : new Error(fallback)
|
||||
}
|
||||
|
||||
/** Resolve the message a catch block should display. */
|
||||
function displayError(err: unknown, nonErrorFallback?: string): string {
|
||||
if (err instanceof UserFacingError) return err.message
|
||||
if (!(err instanceof Error) && nonErrorFallback) return nonErrorFallback
|
||||
return getUserErrorMessage(err)
|
||||
}
|
||||
|
||||
/** Pull the structured error `code` from an envelope, if present. */
|
||||
function apiErrorCode(data: unknown): string | null {
|
||||
const err = (data as { error?: unknown } | null)?.error
|
||||
@@ -2094,11 +2122,11 @@ export default function ArcimMigrationWorkspace(_props: WorkspaceComponentProps)
|
||||
const validationErrors = data?.error === 'validation' ? data.validation?.errors : undefined
|
||||
if (Array.isArray(validationErrors)) {
|
||||
setErrorDetails(validationErrors.filter((e): e is string => typeof e === 'string'))
|
||||
throw new Error(
|
||||
throw new UserFacingError(
|
||||
'Bokföringsdatan hos leverantören klarade inte valideringen. Felen nedan måste rättas i källsystemet innan importen kan fortsätta.'
|
||||
)
|
||||
}
|
||||
throw new Error(apiErrorMessage(data, `HTTP ${res.status}`))
|
||||
throw apiError(data, `HTTP ${res.status}`)
|
||||
}
|
||||
|
||||
const data = await res.json()
|
||||
@@ -2114,7 +2142,7 @@ export default function ArcimMigrationWorkspace(_props: WorkspaceComponentProps)
|
||||
setStep('options')
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? getUserErrorMessage(err) : 'Kunde inte hämta SIE-data')
|
||||
setError(displayError(err, 'Kunde inte hämta SIE-data'))
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
@@ -2196,7 +2224,7 @@ export default function ArcimMigrationWorkspace(_props: WorkspaceComponentProps)
|
||||
|
||||
if (!res.ok) {
|
||||
const data = await res.json().catch(() => ({}))
|
||||
throw new Error(apiErrorMessage(data, `SIE import HTTP ${res.status}`))
|
||||
throw apiError(data, `SIE import HTTP ${res.status}`)
|
||||
}
|
||||
|
||||
const result = await res.json() as ImportResult
|
||||
@@ -2207,7 +2235,7 @@ export default function ArcimMigrationWorkspace(_props: WorkspaceComponentProps)
|
||||
// to /migrate would hit its SIE-guard, whose "SIE måste importeras
|
||||
// först" message masks the real error.
|
||||
if (!result.success) {
|
||||
throw new Error(result.errors.length > 0
|
||||
throw new UserFacingError(result.errors.length > 0
|
||||
? result.errors.join('\n')
|
||||
: 'SIE-importen misslyckades utan felmeddelande.')
|
||||
}
|
||||
@@ -2240,7 +2268,7 @@ export default function ArcimMigrationWorkspace(_props: WorkspaceComponentProps)
|
||||
|
||||
if (!res.ok) {
|
||||
const data = await res.json().catch(() => ({}))
|
||||
throw new Error(apiErrorMessage(data, `HTTP ${res.status}`))
|
||||
throw apiError(data, `HTTP ${res.status}`)
|
||||
}
|
||||
|
||||
const data = await res.json()
|
||||
@@ -2264,8 +2292,7 @@ export default function ArcimMigrationWorkspace(_props: WorkspaceComponentProps)
|
||||
description: 'Din bokföringsdata har importerats.',
|
||||
})
|
||||
} catch (err) {
|
||||
const msg = getUserErrorMessage(err)
|
||||
setError(msg)
|
||||
setError(displayError(err))
|
||||
setStep('result')
|
||||
}
|
||||
}, [consentId, migrationOptions, sieData, toast])
|
||||
|
||||
Reference in New Issue
Block a user