@fin.cx/einvoice 10.0.0 → 10.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,7 +3,7 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@fin.cx/einvoice',
6
- version: '10.0.0',
6
+ version: '10.0.1',
7
7
  description: 'A TypeScript module for creating, manipulating, and embedding XML data within PDF files specifically tailored for electronic invoice (einvoice) packages.'
8
8
  };
9
9
  //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMDBfY29tbWl0aW5mb19kYXRhLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vdHMvMDBfY29tbWl0aW5mb19kYXRhLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBOztHQUVHO0FBQ0gsTUFBTSxDQUFDLE1BQU0sVUFBVSxHQUFHO0lBQ3hCLElBQUksRUFBRSxrQkFBa0I7SUFDeEIsT0FBTyxFQUFFLFFBQVE7SUFDakIsV0FBVyxFQUFFLDJKQUEySjtDQUN6SyxDQUFBIn0=
@@ -49,7 +49,15 @@ export declare abstract class BaseDecoder {
49
49
  */
50
50
  protected parseCIIDate(dateStr: string, format?: string): number;
51
51
  /**
52
- * Parses a required CII date without inventing a fallback value.
52
+ * Parses a required CII date without inventing a fallback value: the
53
+ * calendar day it states, as the UTC midnight of that day. Format 102
54
+ * (`YYYYMMDD`) is the one EN 16931 asks for; a date stated with a time of
55
+ * day in format 203 (`YYYYMMDDhhmm`) or 204 (`YYYYMMDDhhmmss`), or without a
56
+ * format code as an ISO 8601 date or date and time (`parseIsoDateTimeDay`),
57
+ * is read as the day it states, whatever the time of day and the zone.
58
+ * Format 610 (`YYYYMM`) is read as the 1st of the month. Any other value is
59
+ * refused with an `EInvoiceParsingError`: no day is guessed, and none is read
60
+ * in the zone of the machine.
53
61
  */
54
62
  protected parseRequiredCIIDate(dateStr: string, format?: string): number;
55
63
  /**
@@ -1,6 +1,6 @@
1
1
  import { ValidationLevel } from '../../interfaces/common.js';
2
2
  import { EInvoiceParsingError } from '../../errors.js';
3
- import { parseXsdDateDay } from '../utils/date.value.js';
3
+ import { parseCiiDateDay, parseIsoDateTimeDay, parseXsdDateDay } from '../utils/date.value.js';
4
4
  import { statedValueProblemMessage } from '../utils/stated.values.js';
5
5
  /**
6
6
  * Base decoder class that defines common decoding functionality
@@ -82,7 +82,15 @@ export class BaseDecoder {
82
82
  return isNaN(parsed) ? Date.now() : parsed;
83
83
  }
84
84
  /**
85
- * Parses a required CII date without inventing a fallback value.
85
+ * Parses a required CII date without inventing a fallback value: the
86
+ * calendar day it states, as the UTC midnight of that day. Format 102
87
+ * (`YYYYMMDD`) is the one EN 16931 asks for; a date stated with a time of
88
+ * day in format 203 (`YYYYMMDDhhmm`) or 204 (`YYYYMMDDhhmmss`), or without a
89
+ * format code as an ISO 8601 date or date and time (`parseIsoDateTimeDay`),
90
+ * is read as the day it states, whatever the time of day and the zone.
91
+ * Format 610 (`YYYYMM`) is read as the 1st of the month. Any other value is
92
+ * refused with an `EInvoiceParsingError`: no day is guessed, and none is read
93
+ * in the zone of the machine.
86
94
  */
87
95
  parseRequiredCIIDate(dateStr, format) {
88
96
  const value = dateStr.trim();
@@ -91,25 +99,29 @@ export class BaseDecoder {
91
99
  format: 'cii',
92
100
  });
93
101
  }
94
- if (format === '102') {
95
- const match = /^(\d{4})(\d{2})(\d{2})$/.exec(value);
96
- if (!match) {
97
- throw this.createInvalidDateError(value, 'CII format 102');
102
+ const formatCode = (format ?? '').trim();
103
+ if (formatCode === '102' || formatCode === '203' || formatCode === '204') {
104
+ const day = parseCiiDateDay(value, formatCode);
105
+ if (day === undefined) {
106
+ throw this.createInvalidDateError(value, `CII format ${formatCode}`);
98
107
  }
99
- return this.createStrictUtcTimestamp(Number(match[1]), Number(match[2]), Number(match[3]), value, 'CII format 102');
108
+ return day;
100
109
  }
101
- if (format === '610') {
110
+ if (formatCode === '610') {
102
111
  const match = /^(\d{4})(\d{2})$/.exec(value);
103
112
  if (!match) {
104
113
  throw this.createInvalidDateError(value, 'CII format 610');
105
114
  }
106
115
  return this.createStrictUtcTimestamp(Number(match[1]), Number(match[2]), 1, value, 'CII format 610');
107
116
  }
108
- const parsed = Date.parse(value);
109
- if (Number.isNaN(parsed)) {
117
+ if (formatCode) {
118
+ throw this.createInvalidDateError(value, `CII format ${formatCode}`);
119
+ }
120
+ const day = parseIsoDateTimeDay(value);
121
+ if (day === undefined) {
110
122
  throw this.createInvalidDateError(value, 'CII');
111
123
  }
112
- return parsed;
124
+ return day;
113
125
  }
114
126
  /**
115
127
  * Parses a required UBL xsd:date value strictly: the calendar day it states,
@@ -142,4 +154,4 @@ export class BaseDecoder {
142
154
  return new EInvoiceParsingError(`Invalid required invoice issue date "${value}" for ${format}`, { format });
143
155
  }
144
156
  }
145
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYmFzZS5kZWNvZGVyLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vLi4vdHMvZm9ybWF0cy9iYXNlL2Jhc2UuZGVjb2Rlci50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFDQSxPQUFPLEVBQUUsZUFBZSxFQUFFLE1BQU0sNEJBQTRCLENBQUM7QUFFN0QsT0FBTyxFQUFFLG9CQUFvQixFQUFFLE1BQU0saUJBQWlCLENBQUM7QUFDdkQsT0FBTyxFQUFFLGVBQWUsRUFBRSxNQUFNLHdCQUF3QixDQUFDO0FBRXpELE9BQU8sRUFBRSx5QkFBeUIsRUFBMEIsTUFBTSwyQkFBMkIsQ0FBQztBQUU5Rjs7O0dBR0c7QUFDSCxNQUFNLE9BQWdCLFdBQVc7SUFDckIsR0FBRyxDQUFTO0lBQ1osY0FBYyxDQUFVO0lBRWxDLFlBQVksR0FBVyxFQUFFLGlCQUEwQixLQUFLO1FBQ3RELElBQUksQ0FBQyxHQUFHLEdBQUcsR0FBRyxDQUFDO1FBQ2YsSUFBSSxDQUFDLGNBQWMsR0FBRyxjQUFjLENBQUM7SUFDdkMsQ0FBQztJQVFEOzs7T0FHRztJQUNJLEtBQUssQ0FBQyxhQUFhO1FBQ3hCLE9BQU8sSUFBSSxDQUFDLE1BQU0sRUFBRSxDQUFDO0lBQ3ZCLENBQUM7SUFFRDs7O09BR0c7SUFDSSxNQUFNO1FBQ1gsT0FBTyxJQUFJLENBQUMsR0FBRyxDQUFDO0lBQ2xCLENBQUM7SUFFRDs7T0FFRztJQUNJLGlCQUFpQjtRQUN0QixPQUFPLFNBQVMsQ0FBQztJQUNuQixDQUFDO0lBRUQ7Ozs7T0FJRztJQUNJLGVBQWU7UUFDcEIsT0FBTyxTQUFTLENBQUM7SUFDbkIsQ0FBQztJQUVEOzs7OztPQUtHO0lBQ08sMEJBQTBCLENBQUMsSUFBdUIsRUFBRSxNQUFjO1FBQzFFLE1BQU0sQ0FBQyxPQUFPLENBQUMsR0FBRyxJQUFJLENBQUMsUUFBUSxDQUFDO1FBQ2hDLElBQUksT0FBTyxFQUFFLENBQUM7WUFDWixNQUFNLElBQUksb0JBQW9CLENBQUMseUJBQXlCLENB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
157
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYmFzZS5kZWNvZGVyLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vLi4vdHMvZm9ybWF0cy9iYXNlL2Jhc2UuZGVjb2Rlci50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFDQSxPQUFPLEVBQUUsZUFBZSxFQUFFLE1BQU0sNEJBQTRCLENBQUM7QUFFN0QsT0FBTyxFQUFFLG9CQUFvQixFQUFFLE1BQU0saUJBQWlCLENBQUM7QUFDdkQsT0FBTyxFQUFFLGVBQWUsRUFBRSxtQkFBbUIsRUFBRSxlQUFlLEVBQUUsTUFBTSx3QkFBd0IsQ0FBQztBQUUvRixPQUFPLEVBQUUseUJBQXlCLEVBQTBCLE1BQU0sMkJBQTJCLENBQUM7QUFFOUY7OztHQUdHO0FBQ0gsTUFBTSxPQUFnQixXQUFXO0lBQ3JCLEdBQUcsQ0FBUztJQUNaLGNBQWMsQ0FBVTtJQUVsQyxZQUFZLEdBQVcsRUFBRSxpQkFBMEIsS0FBSztRQUN0RCxJQUFJLENBQUMsR0FBRyxHQUFHLEdBQUcsQ0FBQztRQUNmLElBQUksQ0FBQyxjQUFjLEdBQUcsY0FBYyxDQUFDO0lBQ3ZDLENBQUM7SUFRRDs7O09BR0c7SUFDSSxLQUFLLENBQUMsYUFBYTtRQUN4QixPQUFPLElBQUksQ0FBQyxNQUFNLEVBQUUsQ0FBQztJQUN2QixDQUFDO0lBRUQ7OztPQUdHO0lBQ0ksTUFBTTtRQUNYLE9BQU8sSUFBSSxDQUFDLEdBQUcsQ0FBQztJQUNsQixDQUFDO0lBRUQ7O09BRUc7SUFDSSxpQkFBaUI7UUFDdEIsT0FBTyxTQUFTLENBQUM7SUFDbkIsQ0FBQztJQUVEOzs7O09BSUc7SUFDSSxlQUFlO1FBQ3BCLE9BQU8sU0FBUyxDQUFDO0lBQ25CLENBQUM7SUFFRDs7Ozs7T0FLRztJQUNPLDBCQUEwQixDQUFDLElBQXVCLEVBQUUsTUFBYztRQUMxRSxNQUFNLENBQUMsT0FBTyxDQUFDLEdBQUcsSUFBSSxDQUFDLFFBQVEsQ0FBQztRQUNoQyxJQUFJLE9BQU8sRUFBRSxDQUFDO1lBQ1osTUFBTSxJQUFJ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
@@ -37,3 +37,27 @@ export declare const getWritableDueDate: (issueTimestamp: unknown, dueInDays: un
37
37
  * @param value The date as the document states it
38
38
  */
39
39
  export declare const parseXsdDateDay: (value: string) => number | undefined;
40
+ /**
41
+ * The calendar day an ISO 8601 date, or date and time, names, as the UTC
42
+ * midnight of that day; undefined when the value is no such date. A CII date
43
+ * without a format code may be stated so. The value is `YYYY-MM-DD`,
44
+ * optionally followed by a time of day `Thh:mm`, `Thh:mm:ss` or
45
+ * `Thh:mm:ss.fff`, and then optionally by a time zone: `Z`, or `+hh:mm` /
46
+ * `-hh:mm` of at most 14:00. Neither the time of day nor the zone moves the
47
+ * day the document states: `2026-09-11T00:30:00+02:00` is 22:30 UTC on
48
+ * 10 September, but the document states 11 September, and that is the day an
49
+ * invoice is dated with (§ 14 Abs. 4 Satz 1 Nr. 3 UStG asks for the issue date,
50
+ * a calendar day).
51
+ * @param value The date as the document states it
52
+ */
53
+ export declare const parseIsoDateTimeDay: (value: string) => number | undefined;
54
+ /**
55
+ * The calendar day a CII date in format 102 (`YYYYMMDD`), 203
56
+ * (`YYYYMMDDhhmm`) or 204 (`YYYYMMDDhhmmss`) names, as the UTC midnight of that
57
+ * day; undefined when the value is no such date in that format. A time of day
58
+ * in formats 203 and 204 does not move the day stated; the formats carry no
59
+ * time zone.
60
+ * @param value The date as the document states it
61
+ * @param format The format code: '102', '203' or '204'
62
+ */
63
+ export declare const parseCiiDateDay: (value: string, format: "102" | "203" | "204") => number | undefined;
@@ -47,6 +47,37 @@ export const getWritableDueDate = (issueTimestamp, dueInDays, targetFormat) => {
47
47
  dueDate.setUTCDate(dueDate.getUTCDate() + dueInDays);
48
48
  return dueDate;
49
49
  };
50
+ /**
51
+ * The UTC midnight of a calendar day, or undefined when there is no such day
52
+ * @param year The year, 1 or later
53
+ * @param month The month, 1 to 12
54
+ * @param day The day of the month
55
+ */
56
+ const utcMidnightOf = (year, month, day) => {
57
+ if (year < 1) {
58
+ return undefined;
59
+ }
60
+ const date = new Date(0);
61
+ date.setUTCFullYear(year, month - 1, day);
62
+ if (date.getUTCFullYear() !== year || date.getUTCMonth() !== month - 1 || date.getUTCDate() !== day) {
63
+ return undefined;
64
+ }
65
+ return date.getTime();
66
+ };
67
+ /**
68
+ * Whether a time of day is one: hours 0 to 23, minutes and seconds 0 to 59
69
+ * @param hours The hours
70
+ * @param minutes The minutes
71
+ * @param seconds The seconds, 0 when not stated
72
+ */
73
+ const isTimeOfDay = (hours, minutes, seconds) => hours <= 23 && minutes <= 59 && seconds <= 59;
74
+ /**
75
+ * Whether a time zone offset is one an `xsd:date` or `xsd:dateTime` may carry:
76
+ * at most 14:00
77
+ * @param hours The hours of the offset
78
+ * @param minutes The minutes of the offset
79
+ */
80
+ const isZoneOffset = (hours, minutes) => hours < 14 ? minutes <= 59 : hours === 14 && minutes === 0;
50
81
  /**
51
82
  * The calendar day a UBL `xsd:date` names (BT-2, BT-9, BT-26, BT-72, BT-73,
52
83
  * BT-74), as the UTC midnight of that day; undefined when the value is no such
@@ -62,21 +93,55 @@ export const parseXsdDateDay = (value) => {
62
93
  if (!match) {
63
94
  return undefined;
64
95
  }
65
- const [year, month, day] = [Number(match[1]), Number(match[2]), Number(match[3])];
66
- if (match[4] !== undefined) {
67
- const [zoneHours, zoneMinutes] = [Number(match[4]), Number(match[5])];
68
- if (zoneHours > 14 || zoneMinutes > 59 || (zoneHours === 14 && zoneMinutes !== 0)) {
69
- return undefined;
70
- }
96
+ if (match[4] !== undefined && !isZoneOffset(Number(match[4]), Number(match[5]))) {
97
+ return undefined;
71
98
  }
72
- if (year < 1) {
99
+ return utcMidnightOf(Number(match[1]), Number(match[2]), Number(match[3]));
100
+ };
101
+ /**
102
+ * The calendar day an ISO 8601 date, or date and time, names, as the UTC
103
+ * midnight of that day; undefined when the value is no such date. A CII date
104
+ * without a format code may be stated so. The value is `YYYY-MM-DD`,
105
+ * optionally followed by a time of day `Thh:mm`, `Thh:mm:ss` or
106
+ * `Thh:mm:ss.fff`, and then optionally by a time zone: `Z`, or `+hh:mm` /
107
+ * `-hh:mm` of at most 14:00. Neither the time of day nor the zone moves the
108
+ * day the document states: `2026-09-11T00:30:00+02:00` is 22:30 UTC on
109
+ * 10 September, but the document states 11 September, and that is the day an
110
+ * invoice is dated with (§ 14 Abs. 4 Satz 1 Nr. 3 UStG asks for the issue date,
111
+ * a calendar day).
112
+ * @param value The date as the document states it
113
+ */
114
+ export const parseIsoDateTimeDay = (value) => {
115
+ const match = /^(\d{4})-(\d{2})-(\d{2})(?:T(\d{2}):(\d{2})(?::(\d{2})(?:\.\d+)?)?)?(?:Z|[+-](\d{2}):(\d{2}))?$/.exec(value.trim());
116
+ if (!match) {
73
117
  return undefined;
74
118
  }
75
- const date = new Date(0);
76
- date.setUTCFullYear(year, month - 1, day);
77
- if (date.getUTCFullYear() !== year || date.getUTCMonth() !== month - 1 || date.getUTCDate() !== day) {
119
+ if (match[4] !== undefined && !isTimeOfDay(Number(match[4]), Number(match[5]), Number(match[6] ?? 0))) {
78
120
  return undefined;
79
121
  }
80
- return date.getTime();
122
+ if (match[7] !== undefined && !isZoneOffset(Number(match[7]), Number(match[8]))) {
123
+ return undefined;
124
+ }
125
+ return utcMidnightOf(Number(match[1]), Number(match[2]), Number(match[3]));
126
+ };
127
+ /**
128
+ * The calendar day a CII date in format 102 (`YYYYMMDD`), 203
129
+ * (`YYYYMMDDhhmm`) or 204 (`YYYYMMDDhhmmss`) names, as the UTC midnight of that
130
+ * day; undefined when the value is no such date in that format. A time of day
131
+ * in formats 203 and 204 does not move the day stated; the formats carry no
132
+ * time zone.
133
+ * @param value The date as the document states it
134
+ * @param format The format code: '102', '203' or '204'
135
+ */
136
+ export const parseCiiDateDay = (value, format) => {
137
+ const timeDigits = { '102': '', '203': '(\\d{2})(\\d{2})', '204': '(\\d{2})(\\d{2})(\\d{2})' }[format];
138
+ const match = new RegExp(`^(\\d{4})(\\d{2})(\\d{2})${timeDigits}$`).exec(value.trim());
139
+ if (!match) {
140
+ return undefined;
141
+ }
142
+ if (format !== '102' && !isTimeOfDay(Number(match[4]), Number(match[5]), Number(match[6] ?? 0))) {
143
+ return undefined;
144
+ }
145
+ return utcMidnightOf(Number(match[1]), Number(match[2]), Number(match[3]));
81
146
  };
82
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZGF0ZS52YWx1ZS5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uL3RzL2Zvcm1hdHMvdXRpbHMvZGF0ZS52YWx1ZS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEVBQUUsbUJBQW1CLEVBQUUsTUFBTSxpQkFBaUIsQ0FBQztBQUV0RDs7Ozs7OztHQU9HO0FBRUg7Ozs7Ozs7O0dBUUc7QUFDSCxNQUFNLENBQUMsTUFBTSxlQUFlLEdBQUcsQ0FBQyxTQUFrQixFQUFFLEtBQWEsRUFBRSxZQUFvQixFQUFRLEVBQUU7SUFDL0YsTUFBTSxJQUFJLEdBQUcsT0FBTyxTQUFTLEtBQUssUUFBUSxJQUFJLE1BQU0sQ0FBQyxRQUFRLENBQUMsU0FBUyxDQUFDLENBQUMsQ0FBQyxDQUFDLElBQUksSUFBSSxDQUFDLFNBQVMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxTQUFTLENBQUM7SUFDM0csSUFBSSxDQUFDLElBQUksSUFBSSxNQUFNLENBQUMsS0FBSyxDQUFDLElBQUksQ0FBQyxPQUFPLEVBQUUsQ0FBQyxFQUFFLENBQUM7UUFDMUMsTUFBTSxJQUFJLG1CQUFtQixDQUFDLEdBQUcsS0FBSyxzQkFBc0IsTUFBTSxDQUFDLFNBQVMsQ0FBQyxFQUFFLEVBQUU7WUFDL0UsWUFBWTtZQUNaLG1CQUFtQixFQUFFLENBQUMsS0FBSyxDQUFDO1NBQzdCLENBQUMsQ0FBQztJQUNMLENBQUM7SUFDRCxPQUFPLElBQUksQ0FBQztBQUNkLENBQUMsQ0FBQztBQUVGOzs7Ozs7OztHQVFHO0FBQ0gsTUFBTSxDQUFDLE1BQU0sa0JBQWtCLEdBQUcsQ0FBQyxjQUF1QixFQUFFLFNBQWtCLEVBQUUsWUFBb0IsRUFBUSxFQUFFO0lBQzVHLE1BQU0sT0FBTyxHQUFHLGVBQWUsQ0FBQyxjQUFjLEVBQUUseUJBQXlCLEVBQUUsWUFBWSxDQUFDLENBQUM7SUFDekYsSUFBSSxPQUFPLFNBQVMsS0FBSyxRQUFRLElBQUksQ0FBQyxNQUFNLENBQUMsU0FBUyxDQUFDLFNBQVMsQ0FBQyxFQUFFLENBQUM7UUFDbEUsTUFBTSxJQUFJLG1CQUFtQixDQUFDLGdFQUFnRSxNQUFNLENBQUMsU0FBUyxDQUFDLEVBQUUsRUFBRTtZQUNqSCxZQUFZO1lBQ1osbUJBQW1CLEVBQUUsQ0FBQyx1QkFBdUIsQ0FBQztTQUMvQyxDQUFDLENBQUM7SUFDTCxDQUFDO0lBQ0QsK0ZBQStGO0lBQy9GLE9BQU8sQ0FBQyxVQUFVLENBQUMsT0FBTyxDQUFDLFVBQVUsRUFBRSxHQUFHLFNBQVMsQ0FBQyxDQUFDO0lBQ3JELE9BQU8sT0FBTyxDQUFDO0FBQ2pCLENBQUMsQ0FBQztBQUVGOzs7Ozs7Ozs7R0FTRztBQUNILE1BQU0sQ0FBQyxNQUFNLGVBQWUsR0FBRyxDQUFDLEtBQWEsRUFBc0IsRUFBRTtJQUNuRSxNQUFNLEtBQUssR0FBRyxxREFBcUQsQ0FBQyxJQUFJLENBQUMsS0FBSyxDQUFDLElBQUksRUFBRSxDQUFDLENBQUM7SUFDdkYsSUFBSSxDQUFDLEtBQUssRUFBRSxDQUFDO1FBQ1gsT0FBTyxTQUFTLENBQUM7SUFDbkIsQ0FBQztJQUNELE1BQU0sQ0FBQyxJQUFJLEVBQUUsS0FBSyxFQUFFLEdBQUcsQ0FBQyxHQUFHLENBQUMsTUFBTSxDQUFDLEtBQUssQ0FBQyxDQUFDLENBQUMsQ0FBQyxFQUFFLE1BQU0sQ0FBQyxLQUFLLENBQUMsQ0FBQyxDQUFDLENBQUMsRUFBRSxNQUFNLENBQUMsS0FBSyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQztJQUNsRixJQUFJLEtBQUssQ0FBQyxDQUFDLENBQUMsS0FBSyxTQUFTLEVBQUUsQ0FBQztRQUMzQixNQUFNLENBQUMsU0FBUyxFQUFFLFdBQVcsQ0FBQyxHQUFHLENBQUMsTUFBTSxDQUFDLEtBQUssQ0FBQyxDQUFDLENBQUMsQ0FBQyxFQUFFLE1BQU0sQ0FBQyxLQUFLLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDO1FBQ3RFLElBQUksU0FBUyxHQUFHLEVBQUUsSUFBSSxXQUFXLEdBQUcsRUFBRSxJQUFJLENBQUMsU0FBUyxLQUFLLEVBQUUsSUFBSSxXQUFXLEtBQUssQ0FBQyxDQUFDLEVBQUUsQ0FBQztZQUNsRixPQUFPLFNBQVMsQ0FBQztRQUNuQixDQUFDO0lBQ0gsQ0FBQztJQUNELElBQUksSUFBSSxHQUFHLENBQUMsRUFBRSxDQUFDO1FBQ2IsT0FBTyxTQUFTLENBQUM7SUFDbkIsQ0FBQztJQUNELE1BQU0sSUFBSSxHQUFHLElBQUksSUFBSSxDQUFDLENBQUMsQ0FBQyxDQUFDO0lBQ3pCLElBQUksQ0FBQyxjQUFjLENBQUMsSUFBSSxFQUFFLEtBQUssR0FBRyxDQUFDLEVBQUUsR0FBRyxDQUFDLENBQUM7SUFDMUMsSUFBSSxJQUFJLENBQUMsY0FBYyxFQUFFLEtBQUssSUFBSSxJQUFJLElBQUksQ0FBQyxXQUFXLEVBQUUsS0FBSyxLQUFLLEdBQUcsQ0FBQyxJQUFJLElBQUksQ0FBQyxVQUFVLEVBQUUsS0FBSyxHQUFHLEVBQUUsQ0FBQztRQUNwRyxPQUFPLFNBQVMsQ0FBQztJQUNuQixDQUFDO0lBQ0QsT0FBTyxJQUFJLENBQUMsT0FBTyxFQUFFLENBQUM7QUFDeEIsQ0FBQyxDQUFDIn0=
147
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZGF0ZS52YWx1ZS5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uL3RzL2Zvcm1hdHMvdXRpbHMvZGF0ZS52YWx1ZS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEVBQUUsbUJBQW1CLEVBQUUsTUFBTSxpQkFBaUIsQ0FBQztBQUV0RDs7Ozs7OztHQU9HO0FBRUg7Ozs7Ozs7O0dBUUc7QUFDSCxNQUFNLENBQUMsTUFBTSxlQUFlLEdBQUcsQ0FBQyxTQUFrQixFQUFFLEtBQWEsRUFBRSxZQUFvQixFQUFRLEVBQUU7SUFDL0YsTUFBTSxJQUFJLEdBQUcsT0FBTyxTQUFTLEtBQUssUUFBUSxJQUFJLE1BQU0sQ0FBQyxRQUFRLENBQUMsU0FBUyxDQUFDLENBQUMsQ0FBQyxDQUFDLElBQUksSUFBSSxDQUFDLFNBQVMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxTQUFTLENBQUM7SUFDM0csSUFBSSxDQUFDLElBQUksSUFBSSxNQUFNLENBQUMsS0FBSyxDQUFDLElBQUksQ0FBQyxPQUFPLEVBQUUsQ0FBQyxFQUFFLENBQUM7UUFDMUMsTUFBTSxJQUFJLG1CQUFtQixDQUFDLEdBQUcsS0FBSyxzQkFBc0IsTUFBTSxDQUFDLFNBQVMsQ0FBQyxFQUFFLEVBQUU7WUFDL0UsWUFBWTtZQUNaLG1CQUFtQixFQUFFLENBQUMsS0FBSyxDQUFDO1NBQzdCLENBQUMsQ0FBQztJQUNMLENBQUM7SUFDRCxPQUFPLElBQUksQ0FBQztBQUNkLENBQUMsQ0FBQztBQUVGOzs7Ozs7OztHQVFHO0FBQ0gsTUFBTSxDQUFDLE1BQU0sa0JBQWtCLEdBQUcsQ0FBQyxjQUF1QixFQUFFLFNBQWtCLEVBQUUsWUFBb0IsRUFBUSxFQUFFO0lBQzVHLE1BQU0sT0FBTyxHQUFHLGVBQWUsQ0FBQyxjQUFjLEVBQUUseUJBQXlCLEVBQUUsWUFBWSxDQUFDLENBQUM7SUFDekYsSUFBSSxPQUFPLFNBQVMsS0FBSyxRQUFRLElBQUksQ0FBQyxNQUFNLENBQUMsU0FBUyxDQUFD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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fin.cx/einvoice",
3
- "version": "10.0.0",
3
+ "version": "10.0.1",
4
4
  "private": false,
5
5
  "description": "A TypeScript module for creating, manipulating, and embedding XML data within PDF files specifically tailored for electronic invoice (einvoice) packages.",
6
6
  "main": "dist_ts/index.js",
@@ -16,9 +16,9 @@
16
16
  "@git.zone/tsbuild": "^5.0.0",
17
17
  "@git.zone/tsbundle": "^2.15.0",
18
18
  "@git.zone/tsrun": "^3.0.0",
19
- "@git.zone/tstest": "^6.2.0",
19
+ "@git.zone/tstest": "^6.3.1",
20
20
  "@push.rocks/smartfile": "^13.1.3",
21
- "@types/node": "^26.6.1"
21
+ "@types/node": "^26.6.2"
22
22
  },
23
23
  "dependencies": {
24
24
  "@push.rocks/smartxml": "^2.1.0",
@@ -67,7 +67,7 @@
67
67
  ],
68
68
  "scripts": {
69
69
  "test": "pnpm run test:unit",
70
- "test:unit": "tstest test/test.cii-xrechnung.node.ts --verbose --logfile --timeout 60 && tstest test/test.einvoice.ts --verbose --logfile --timeout 60 && tstest test/test.decimal.ts --verbose --logfile --timeout 60 && tstest test/test.decimal-currency-calculator.ts --verbose --logfile --timeout 60 && tstest test/test.currency-utils.ts --verbose --logfile --timeout 60 && tstest test/test.en16931-validators.ts --verbose --logfile --timeout 60 && tstest test/test.schematron-validator.ts --verbose --logfile --timeout 60 && tstest test/test.xrechnung-creditnote.node.ts --verbose --logfile --timeout 60 && tstest test/test.accountingdoc-types.node.ts --verbose --logfile --timeout 60 && tstest test/test.corrected-invoice.node.ts --verbose --logfile --timeout 60 && tstest test/test.xrechnung-3-due-date.node.ts --verbose --logfile --timeout 60 && tstest test/test.calendar-dates.node.ts --verbose --logfile --timeout 60 && tstest test/test.document-totals.node.ts --verbose --logfile --timeout 60 && tstest test/test.reverse-charge.node.ts --verbose --logfile --timeout 60 && tstest test/test.cii-schema-order.node.ts --verbose --logfile --timeout 60 && tstest test/test.tax-number.node.ts --verbose --logfile --timeout 60 && tstest test/test.country-codes.node.ts --verbose --logfile --timeout 60 && tstest test/test.paid-amount.node.ts --verbose --logfile --timeout 60 && tstest test/test.en16931-unitcodes.node.ts --verbose --logfile --timeout 60 && tstest test/test.en16931-lineunit.node.ts --verbose --logfile --timeout 60 && tstest test/test.cii-profile.node.ts --verbose --logfile --timeout 60 && tstest test/test.validator-rule-ids.node.ts --verbose --logfile --timeout 60 && tstest test/test.cii-period-address.node.ts --verbose --logfile --timeout 60 && tstest test/test.amount-due.node.ts --verbose --logfile --timeout 60 && tstest test/test.ubl-zoned-dates.node.ts --verbose --logfile --timeout 60 && tstest test/test.vat-categories.node.ts --verbose --logfile --timeout 60 && tstest test/test.xrechnung-buyer-reference.node.ts --verbose --logfile --timeout 60 && tstest test/test.specification-identifier.node.ts --verbose --logfile --timeout 60 && tstest test/test.seller-identifier.node.ts --verbose --logfile --timeout 60 && tstest test/test.stated-values.node.ts --verbose --logfile --timeout 60 && tstest test/test.vat-category-limits.node.ts --verbose --logfile --timeout 60 && tstest test/test.received-conformance.node.ts --verbose --logfile --timeout 60",
70
+ "test:unit": "tstest test/test.cii-xrechnung.node.ts --verbose --logfile --timeout 60 && tstest test/test.einvoice.ts --verbose --logfile --timeout 60 && tstest test/test.decimal.ts --verbose --logfile --timeout 60 && tstest test/test.decimal-currency-calculator.ts --verbose --logfile --timeout 60 && tstest test/test.currency-utils.ts --verbose --logfile --timeout 60 && tstest test/test.en16931-validators.ts --verbose --logfile --timeout 60 && tstest test/test.schematron-validator.ts --verbose --logfile --timeout 60 && tstest test/test.xrechnung-creditnote.node.ts --verbose --logfile --timeout 60 && tstest test/test.accountingdoc-types.node.ts --verbose --logfile --timeout 60 && tstest test/test.corrected-invoice.node.ts --verbose --logfile --timeout 60 && tstest test/test.xrechnung-3-due-date.node.ts --verbose --logfile --timeout 60 && tstest test/test.calendar-dates.node.ts --verbose --logfile --timeout 60 && tstest test/test.document-totals.node.ts --verbose --logfile --timeout 60 && tstest test/test.reverse-charge.node.ts --verbose --logfile --timeout 60 && tstest test/test.cii-schema-order.node.ts --verbose --logfile --timeout 60 && tstest test/test.tax-number.node.ts --verbose --logfile --timeout 60 && tstest test/test.country-codes.node.ts --verbose --logfile --timeout 60 && tstest test/test.paid-amount.node.ts --verbose --logfile --timeout 60 && tstest test/test.en16931-unitcodes.node.ts --verbose --logfile --timeout 60 && tstest test/test.en16931-lineunit.node.ts --verbose --logfile --timeout 60 && tstest test/test.cii-profile.node.ts --verbose --logfile --timeout 60 && tstest test/test.validator-rule-ids.node.ts --verbose --logfile --timeout 60 && tstest test/test.cii-period-address.node.ts --verbose --logfile --timeout 60 && tstest test/test.amount-due.node.ts --verbose --logfile --timeout 60 && tstest test/test.ubl-zoned-dates.node.ts --verbose --logfile --timeout 60 && tstest test/test.vat-categories.node.ts --verbose --logfile --timeout 60 && tstest test/test.xrechnung-buyer-reference.node.ts --verbose --logfile --timeout 60 && tstest test/test.specification-identifier.node.ts --verbose --logfile --timeout 60 && tstest test/test.seller-identifier.node.ts --verbose --logfile --timeout 60 && tstest test/test.stated-values.node.ts --verbose --logfile --timeout 60 && tstest test/test.vat-category-limits.node.ts --verbose --logfile --timeout 60 && tstest test/test.received-conformance.node.ts --verbose --logfile --timeout 60 && tstest test/test.cii-stated-dates.node.ts --verbose --logfile --timeout 60",
71
71
  "test:format": "tstest test/test.format-detection.ts --verbose --logfile --timeout 60",
72
72
  "test:corpus": "tstest test/suite/einvoice_corpus-validation/ --verbose --logfile --timeout 60",
73
73
  "test:performance": "NODE_OPTIONS=--expose-gc tstest test/suite/einvoice_performance/ --verbose --logfile --timeout 60",
package/readme.md CHANGED
@@ -509,7 +509,12 @@ the UTC midnight of that day, `Date.UTC(2026, 8, 20)`.
509
509
  - The decoders return that timestamp. A UBL date may carry a time zone
510
510
  (`2026-09-20+02:00`, `2026-09-20Z`); the zone does not move the day, so the
511
511
  decoders return the UTC midnight of the stated day, and every format writes
512
- that day.
512
+ that day. A CII date stated with a time of day, in format 203
513
+ (`202609202330`) or 204 (`20260920233000`), or without a format code as an
514
+ ISO 8601 date or date and time (`2026-09-20T00:30:00+02:00`), is read the
515
+ same way: the day it states, whatever the time of day and the zone. A CII
516
+ date in another form, or in a format that names no day, is refused with an
517
+ `EInvoiceParsingError`; no day is guessed.
513
518
  - The encoders write the day of that timestamp in UTC and count the payment
514
519
  term in UTC days, so what they write does not depend on the time zone of the
515
520
  machine that writes it.
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@fin.cx/einvoice',
6
- version: '10.0.0',
6
+ version: '10.0.1',
7
7
  description: 'A TypeScript module for creating, manipulating, and embedding XML data within PDF files specifically tailored for electronic invoice (einvoice) packages.'
8
8
  }
@@ -2,7 +2,7 @@ import type { TAccountingDoc } from '../../interfaces/common.js';
2
2
  import { ValidationLevel } from '../../interfaces/common.js';
3
3
  import type { ValidationResult } from '../../interfaces/common.js';
4
4
  import { EInvoiceParsingError } from '../../errors.js';
5
- import { parseXsdDateDay } from '../utils/date.value.js';
5
+ import { parseCiiDateDay, parseIsoDateTimeDay, parseXsdDateDay } from '../utils/date.value.js';
6
6
  import type { IEInvoiceStatedValues } from '../../interfaces/stated.values.js';
7
7
  import { statedValueProblemMessage, type IStatedValuesRead } from '../utils/stated.values.js';
8
8
 
@@ -102,7 +102,15 @@ export abstract class BaseDecoder {
102
102
  }
103
103
 
104
104
  /**
105
- * Parses a required CII date without inventing a fallback value.
105
+ * Parses a required CII date without inventing a fallback value: the
106
+ * calendar day it states, as the UTC midnight of that day. Format 102
107
+ * (`YYYYMMDD`) is the one EN 16931 asks for; a date stated with a time of
108
+ * day in format 203 (`YYYYMMDDhhmm`) or 204 (`YYYYMMDDhhmmss`), or without a
109
+ * format code as an ISO 8601 date or date and time (`parseIsoDateTimeDay`),
110
+ * is read as the day it states, whatever the time of day and the zone.
111
+ * Format 610 (`YYYYMM`) is read as the 1st of the month. Any other value is
112
+ * refused with an `EInvoiceParsingError`: no day is guessed, and none is read
113
+ * in the zone of the machine.
106
114
  */
107
115
  protected parseRequiredCIIDate(dateStr: string, format?: string): number {
108
116
  const value = dateStr.trim();
@@ -111,22 +119,17 @@ export abstract class BaseDecoder {
111
119
  format: 'cii',
112
120
  });
113
121
  }
122
+ const formatCode = (format ?? '').trim();
114
123
 
115
- if (format === '102') {
116
- const match = /^(\d{4})(\d{2})(\d{2})$/.exec(value);
117
- if (!match) {
118
- throw this.createInvalidDateError(value, 'CII format 102');
124
+ if (formatCode === '102' || formatCode === '203' || formatCode === '204') {
125
+ const day = parseCiiDateDay(value, formatCode);
126
+ if (day === undefined) {
127
+ throw this.createInvalidDateError(value, `CII format ${formatCode}`);
119
128
  }
120
- return this.createStrictUtcTimestamp(
121
- Number(match[1]),
122
- Number(match[2]),
123
- Number(match[3]),
124
- value,
125
- 'CII format 102',
126
- );
129
+ return day;
127
130
  }
128
131
 
129
- if (format === '610') {
132
+ if (formatCode === '610') {
130
133
  const match = /^(\d{4})(\d{2})$/.exec(value);
131
134
  if (!match) {
132
135
  throw this.createInvalidDateError(value, 'CII format 610');
@@ -140,11 +143,15 @@ export abstract class BaseDecoder {
140
143
  );
141
144
  }
142
145
 
143
- const parsed = Date.parse(value);
144
- if (Number.isNaN(parsed)) {
146
+ if (formatCode) {
147
+ throw this.createInvalidDateError(value, `CII format ${formatCode}`);
148
+ }
149
+
150
+ const day = parseIsoDateTimeDay(value);
151
+ if (day === undefined) {
145
152
  throw this.createInvalidDateError(value, 'CII');
146
153
  }
147
- return parsed;
154
+ return day;
148
155
  }
149
156
 
150
157
  /**
@@ -51,6 +51,42 @@ export const getWritableDueDate = (issueTimestamp: unknown, dueInDays: unknown,
51
51
  return dueDate;
52
52
  };
53
53
 
54
+ /**
55
+ * The UTC midnight of a calendar day, or undefined when there is no such day
56
+ * @param year The year, 1 or later
57
+ * @param month The month, 1 to 12
58
+ * @param day The day of the month
59
+ */
60
+ const utcMidnightOf = (year: number, month: number, day: number): number | undefined => {
61
+ if (year < 1) {
62
+ return undefined;
63
+ }
64
+ const date = new Date(0);
65
+ date.setUTCFullYear(year, month - 1, day);
66
+ if (date.getUTCFullYear() !== year || date.getUTCMonth() !== month - 1 || date.getUTCDate() !== day) {
67
+ return undefined;
68
+ }
69
+ return date.getTime();
70
+ };
71
+
72
+ /**
73
+ * Whether a time of day is one: hours 0 to 23, minutes and seconds 0 to 59
74
+ * @param hours The hours
75
+ * @param minutes The minutes
76
+ * @param seconds The seconds, 0 when not stated
77
+ */
78
+ const isTimeOfDay = (hours: number, minutes: number, seconds: number): boolean =>
79
+ hours <= 23 && minutes <= 59 && seconds <= 59;
80
+
81
+ /**
82
+ * Whether a time zone offset is one an `xsd:date` or `xsd:dateTime` may carry:
83
+ * at most 14:00
84
+ * @param hours The hours of the offset
85
+ * @param minutes The minutes of the offset
86
+ */
87
+ const isZoneOffset = (hours: number, minutes: number): boolean =>
88
+ hours < 14 ? minutes <= 59 : hours === 14 && minutes === 0;
89
+
54
90
  /**
55
91
  * The calendar day a UBL `xsd:date` names (BT-2, BT-9, BT-26, BT-72, BT-73,
56
92
  * BT-74), as the UTC midnight of that day; undefined when the value is no such
@@ -66,20 +102,57 @@ export const parseXsdDateDay = (value: string): number | undefined => {
66
102
  if (!match) {
67
103
  return undefined;
68
104
  }
69
- const [year, month, day] = [Number(match[1]), Number(match[2]), Number(match[3])];
70
- if (match[4] !== undefined) {
71
- const [zoneHours, zoneMinutes] = [Number(match[4]), Number(match[5])];
72
- if (zoneHours > 14 || zoneMinutes > 59 || (zoneHours === 14 && zoneMinutes !== 0)) {
73
- return undefined;
74
- }
105
+ if (match[4] !== undefined && !isZoneOffset(Number(match[4]), Number(match[5]))) {
106
+ return undefined;
75
107
  }
76
- if (year < 1) {
108
+ return utcMidnightOf(Number(match[1]), Number(match[2]), Number(match[3]));
109
+ };
110
+
111
+ /**
112
+ * The calendar day an ISO 8601 date, or date and time, names, as the UTC
113
+ * midnight of that day; undefined when the value is no such date. A CII date
114
+ * without a format code may be stated so. The value is `YYYY-MM-DD`,
115
+ * optionally followed by a time of day `Thh:mm`, `Thh:mm:ss` or
116
+ * `Thh:mm:ss.fff`, and then optionally by a time zone: `Z`, or `+hh:mm` /
117
+ * `-hh:mm` of at most 14:00. Neither the time of day nor the zone moves the
118
+ * day the document states: `2026-09-11T00:30:00+02:00` is 22:30 UTC on
119
+ * 10 September, but the document states 11 September, and that is the day an
120
+ * invoice is dated with (§ 14 Abs. 4 Satz 1 Nr. 3 UStG asks for the issue date,
121
+ * a calendar day).
122
+ * @param value The date as the document states it
123
+ */
124
+ export const parseIsoDateTimeDay = (value: string): number | undefined => {
125
+ const match =
126
+ /^(\d{4})-(\d{2})-(\d{2})(?:T(\d{2}):(\d{2})(?::(\d{2})(?:\.\d+)?)?)?(?:Z|[+-](\d{2}):(\d{2}))?$/.exec(value.trim());
127
+ if (!match) {
77
128
  return undefined;
78
129
  }
79
- const date = new Date(0);
80
- date.setUTCFullYear(year, month - 1, day);
81
- if (date.getUTCFullYear() !== year || date.getUTCMonth() !== month - 1 || date.getUTCDate() !== day) {
130
+ if (match[4] !== undefined && !isTimeOfDay(Number(match[4]), Number(match[5]), Number(match[6] ?? 0))) {
82
131
  return undefined;
83
132
  }
84
- return date.getTime();
133
+ if (match[7] !== undefined && !isZoneOffset(Number(match[7]), Number(match[8]))) {
134
+ return undefined;
135
+ }
136
+ return utcMidnightOf(Number(match[1]), Number(match[2]), Number(match[3]));
137
+ };
138
+
139
+ /**
140
+ * The calendar day a CII date in format 102 (`YYYYMMDD`), 203
141
+ * (`YYYYMMDDhhmm`) or 204 (`YYYYMMDDhhmmss`) names, as the UTC midnight of that
142
+ * day; undefined when the value is no such date in that format. A time of day
143
+ * in formats 203 and 204 does not move the day stated; the formats carry no
144
+ * time zone.
145
+ * @param value The date as the document states it
146
+ * @param format The format code: '102', '203' or '204'
147
+ */
148
+ export const parseCiiDateDay = (value: string, format: '102' | '203' | '204'): number | undefined => {
149
+ const timeDigits = { '102': '', '203': '(\\d{2})(\\d{2})', '204': '(\\d{2})(\\d{2})(\\d{2})' }[format];
150
+ const match = new RegExp(`^(\\d{4})(\\d{2})(\\d{2})${timeDigits}$`).exec(value.trim());
151
+ if (!match) {
152
+ return undefined;
153
+ }
154
+ if (format !== '102' && !isTimeOfDay(Number(match[4]), Number(match[5]), Number(match[6] ?? 0))) {
155
+ return undefined;
156
+ }
157
+ return utcMidnightOf(Number(match[1]), Number(match[2]), Number(match[3]));
85
158
  };