* 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
108 lines
3.0 KiB
TypeScript
108 lines
3.0 KiB
TypeScript
export interface ArelleValidationIssue {
|
|
code: string
|
|
severity: 'error' | 'warning' | 'info'
|
|
message: string
|
|
}
|
|
|
|
export interface ArelleValidationResult {
|
|
status: 'passed' | 'warnings' | 'failed' | 'unavailable'
|
|
validator_version: string | null
|
|
issues: ArelleValidationIssue[]
|
|
}
|
|
|
|
interface ArelleValidatorResponse {
|
|
ok?: boolean
|
|
validator_version?: string
|
|
issues?: Array<{
|
|
code?: string
|
|
severity?: string
|
|
message?: string
|
|
}>
|
|
}
|
|
|
|
/**
|
|
* Validate exact iXBRL bytes through the configured Arelle service. The
|
|
* service boundary keeps the Vercel application free from a Python runtime
|
|
* while still making taxonomy validation mandatory for connected filing.
|
|
*/
|
|
export async function validateIxbrlWithArelle(
|
|
xhtml: string,
|
|
options: {
|
|
url?: string
|
|
token?: string
|
|
timeoutMs?: number
|
|
} = {},
|
|
): Promise<ArelleValidationResult> {
|
|
const url = options.url ?? process.env.BOLAGSVERKET_ARELLE_VALIDATOR_URL
|
|
if (!url) {
|
|
return {
|
|
status: 'unavailable',
|
|
validator_version: null,
|
|
issues: [
|
|
{
|
|
code: 'ARELLE-NOT-CONFIGURED',
|
|
severity: 'error',
|
|
message: 'Arelle validator service is not configured.',
|
|
},
|
|
],
|
|
}
|
|
}
|
|
|
|
try {
|
|
const token = options.token ?? process.env.BOLAGSVERKET_ARELLE_VALIDATOR_TOKEN
|
|
const response = await fetch(url, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
|
},
|
|
body: JSON.stringify({
|
|
filename: 'arsredovisning.xhtml',
|
|
content_base64: Buffer.from(xhtml, 'utf8').toString('base64'),
|
|
}),
|
|
signal: AbortSignal.timeout(options.timeoutMs ?? 60_000),
|
|
})
|
|
if (!response.ok) {
|
|
return {
|
|
status: 'unavailable',
|
|
validator_version: null,
|
|
issues: [
|
|
{
|
|
code: `ARELLE-HTTP-${response.status}`,
|
|
severity: 'error',
|
|
message: `Arelle validator returned HTTP ${response.status}.`,
|
|
},
|
|
],
|
|
}
|
|
}
|
|
|
|
const body = (await response.json()) as ArelleValidatorResponse
|
|
const issues: ArelleValidationIssue[] = (body.issues ?? []).map((item) => ({
|
|
code: item.code?.trim() || 'ARELLE',
|
|
severity:
|
|
item.severity === 'warning' || item.severity === 'info'
|
|
? item.severity
|
|
: 'error',
|
|
message: item.message?.trim() || 'Arelle reported a validation issue.',
|
|
}))
|
|
const hasErrors = body.ok === false || issues.some((item) => item.severity === 'error')
|
|
return {
|
|
status: hasErrors ? 'failed' : issues.length > 0 ? 'warnings' : 'passed',
|
|
validator_version: body.validator_version ?? null,
|
|
issues,
|
|
}
|
|
} catch (error) {
|
|
return {
|
|
status: 'unavailable',
|
|
validator_version: null,
|
|
issues: [
|
|
{
|
|
code: 'ARELLE-UNAVAILABLE',
|
|
severity: 'error',
|
|
message: error instanceof Error ? error.message : 'Arelle validator is unavailable.',
|
|
},
|
|
],
|
|
}
|
|
}
|
|
}
|