From e66e9f61e1a70c96ac00302e21da7089c12bfd68 Mon Sep 17 00:00:00 2001 From: Alex Jank Date: Tue, 10 Feb 2026 17:05:34 +0100 Subject: [PATCH 1/2] fix(xml): preserve CII element order when merging profile schemas defu's internal `Object.assign({}, defaults)` clones the last source argument first, giving its key order priority. In `mergeSchemas`, the extending profile's schema (e.g. EN16931) is the last argument to `defu()`, so its incomplete key set determines the element order. Keys that only exist in the base profiles (e.g. identifier, note, tradeDelivery from BASIC) get appended at the end instead of appearing in the correct CII XML element order. Add a `reorderSchema` step after the merge that walks the schema tree and reorders keys to match the base (extended) profiles' key order, which defines the correct CII element sequence. Keys only present in the extending profile are appended at the end. Co-Authored-By: Claude Opus 4.6 --- .../src/formatter/xml/formatter.ts | 42 ++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/packages/node-zugferd/src/formatter/xml/formatter.ts b/packages/node-zugferd/src/formatter/xml/formatter.ts index 0d0edd1..bdce8ba 100644 --- a/packages/node-zugferd/src/formatter/xml/formatter.ts +++ b/packages/node-zugferd/src/formatter/xml/formatter.ts @@ -86,6 +86,40 @@ const updateDefaultValues = (base: Schema, override: Schema): Schema => { return result; }; +/** + * Recursively reorder the keys of `schema` to match the key order of + * `reference`, appending any keys that only exist in `schema` at the end. + * + * defu's internal `Object.assign({}, defaults)` gives the *last* source's key + * order priority, which can break CII XML element ordering when a higher-level + * profile (e.g. EN16931) defines only a subset of keys. The base profiles + * (MINIMUM → BASIC_WL → BASIC) carry the correct element order, so we use + * their merged schema as the reference. + */ +const reorderSchema = (schema: Schema, reference: Schema): Schema => { + const result: Schema = {}; + + for (const key of Object.keys(reference)) { + if (key in schema) { + const field = schema[key]; + const refField = reference[key]; + if (field.shape && refField.shape) { + result[key] = { ...field, shape: reorderSchema(field.shape, refField.shape) }; + } else { + result[key] = field; + } + } + } + + for (const key of Object.keys(schema)) { + if (!(key in result)) { + result[key] = schema[key]; + } + } + + return result; +}; + export const mergeSchemas = (profile: Profile): Schema => { if (!profile.extends) { return profile.mask @@ -103,7 +137,13 @@ export const mergeSchemas = (profile: Profile): Schema => { profile.schema, ); - return profile.mask ? applyMask(mergedSchema, profile.mask) : mergedSchema; + // Reorder keys to match the base profiles' element order. + // defu gives the last argument's key order priority, so the extending + // profile's (incomplete) key set can push base-only keys to the end, + // producing invalid CII XML element ordering. + const reorderedSchema = reorderSchema(mergedSchema, mergedExtensions); + + return profile.mask ? applyMask(reorderedSchema, profile.mask) : reorderedSchema; }; export type ParseSchemaOptions = { From cd29e2427a01d780e082204a5dd109182a047faf Mon Sep 17 00:00:00 2001 From: Alex Jank Date: Tue, 10 Feb 2026 17:14:35 +0100 Subject: [PATCH 2/2] style: fix biome formatting in xml formatter Co-Authored-By: Claude Opus 4.6 --- packages/node-zugferd/src/formatter/xml/formatter.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/node-zugferd/src/formatter/xml/formatter.ts b/packages/node-zugferd/src/formatter/xml/formatter.ts index bdce8ba..a1d06ee 100644 --- a/packages/node-zugferd/src/formatter/xml/formatter.ts +++ b/packages/node-zugferd/src/formatter/xml/formatter.ts @@ -104,7 +104,10 @@ const reorderSchema = (schema: Schema, reference: Schema): Schema => { const field = schema[key]; const refField = reference[key]; if (field.shape && refField.shape) { - result[key] = { ...field, shape: reorderSchema(field.shape, refField.shape) }; + result[key] = { + ...field, + shape: reorderSchema(field.shape, refField.shape), + }; } else { result[key] = field; } @@ -143,7 +146,9 @@ export const mergeSchemas = (profile: Profile): Schema => { // producing invalid CII XML element ordering. const reorderedSchema = reorderSchema(mergedSchema, mergedExtensions); - return profile.mask ? applyMask(reorderedSchema, profile.mask) : reorderedSchema; + return profile.mask + ? applyMask(reorderedSchema, profile.mask) + : reorderedSchema; }; export type ParseSchemaOptions = {