feat: Implement PEPPOL and XRechnung validators for compliance with e-invoice specifications
- Added PeppolValidator class to validate PEPPOL BIS 3.0 invoices, including checks for endpoint IDs, document type IDs, process IDs, party identification, and business rules. - Implemented validation for GLN check digits, document types, and transport protocols specific to PEPPOL. - Added XRechnungValidator class to validate XRechnung 3.0 invoices, focusing on German-specific requirements such as Leitweg-ID, payment details, seller contact, and tax registration. - Included validation for IBAN and BIC formats, ensuring compliance with SEPA regulations. - Established methods for checking B2G invoice indicators and validating mandatory fields for both validators.
This commit is contained in:
509
ts/formats/utils/decimal.ts
Normal file
509
ts/formats/utils/decimal.ts
Normal file
@@ -0,0 +1,509 @@
|
||||
/**
|
||||
* Decimal Arithmetic Library for EN16931 Compliance
|
||||
* Provides arbitrary precision decimal arithmetic to avoid floating-point errors
|
||||
*
|
||||
* Based on EN16931 requirements for financial calculations:
|
||||
* - All monetary amounts must be calculated with sufficient precision
|
||||
* - Rounding must be consistent and predictable
|
||||
* - No loss of precision in intermediate calculations
|
||||
*/
|
||||
|
||||
/**
|
||||
* Decimal class for arbitrary precision arithmetic
|
||||
* Internally stores the value as an integer with a scale factor
|
||||
*/
|
||||
export class Decimal {
|
||||
private readonly value: bigint;
|
||||
private readonly scale: number;
|
||||
|
||||
// Constants - initialized lazily to avoid initialization issues
|
||||
private static _ZERO: Decimal | undefined;
|
||||
private static _ONE: Decimal | undefined;
|
||||
private static _TEN: Decimal | undefined;
|
||||
private static _HUNDRED: Decimal | undefined;
|
||||
|
||||
static get ZERO(): Decimal {
|
||||
if (!this._ZERO) this._ZERO = new Decimal(0);
|
||||
return this._ZERO;
|
||||
}
|
||||
|
||||
static get ONE(): Decimal {
|
||||
if (!this._ONE) this._ONE = new Decimal(1);
|
||||
return this._ONE;
|
||||
}
|
||||
|
||||
static get TEN(): Decimal {
|
||||
if (!this._TEN) this._TEN = new Decimal(10);
|
||||
return this._TEN;
|
||||
}
|
||||
|
||||
static get HUNDRED(): Decimal {
|
||||
if (!this._HUNDRED) this._HUNDRED = new Decimal(100);
|
||||
return this._HUNDRED;
|
||||
}
|
||||
|
||||
// Default scale for monetary calculations (4 decimal places for intermediate calculations)
|
||||
private static readonly DEFAULT_SCALE = 4;
|
||||
|
||||
/**
|
||||
* Create a new Decimal from various input types
|
||||
*/
|
||||
constructor(value: string | number | bigint | Decimal, scale?: number) {
|
||||
if (value instanceof Decimal) {
|
||||
this.value = value.value;
|
||||
this.scale = value.scale;
|
||||
return;
|
||||
}
|
||||
|
||||
// Special handling for direct bigint with scale (internal use)
|
||||
if (typeof value === 'bigint' && scale !== undefined) {
|
||||
this.value = value;
|
||||
this.scale = scale;
|
||||
return;
|
||||
}
|
||||
|
||||
// Determine scale if not provided
|
||||
if (scale === undefined) {
|
||||
if (typeof value === 'string') {
|
||||
const parts = value.split('.');
|
||||
scale = parts.length > 1 ? parts[1].length : 0;
|
||||
} else {
|
||||
scale = Decimal.DEFAULT_SCALE;
|
||||
}
|
||||
}
|
||||
|
||||
this.scale = scale;
|
||||
|
||||
// Convert to scaled integer
|
||||
if (typeof value === 'string') {
|
||||
// Remove any formatting
|
||||
value = value.replace(/[^\d.-]/g, '');
|
||||
const parts = value.split('.');
|
||||
const integerPart = parts[0] || '0';
|
||||
const decimalPart = (parts[1] || '').padEnd(scale, '0').slice(0, scale);
|
||||
this.value = BigInt(integerPart + decimalPart);
|
||||
} else if (typeof value === 'number') {
|
||||
// Handle floating point numbers
|
||||
if (!isFinite(value)) {
|
||||
throw new Error(`Invalid number value: ${value}`);
|
||||
}
|
||||
const multiplier = Math.pow(10, scale);
|
||||
this.value = BigInt(Math.round(value * multiplier));
|
||||
} else {
|
||||
// bigint
|
||||
this.value = value * BigInt(Math.pow(10, scale));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert to string representation
|
||||
*/
|
||||
toString(decimalPlaces?: number): string {
|
||||
const absValue = this.value < 0n ? -this.value : this.value;
|
||||
const str = absValue.toString().padStart(this.scale + 1, '0');
|
||||
const integerPart = this.scale > 0 ? (str.slice(0, -this.scale) || '0') : str;
|
||||
let decimalPart = this.scale > 0 ? str.slice(-this.scale) : '';
|
||||
|
||||
// Apply decimal places if specified
|
||||
if (decimalPlaces !== undefined) {
|
||||
if (decimalPlaces === 0) {
|
||||
return (this.value < 0n ? '-' : '') + integerPart;
|
||||
}
|
||||
decimalPart = decimalPart.padEnd(decimalPlaces, '0').slice(0, decimalPlaces);
|
||||
}
|
||||
|
||||
// Remove trailing zeros if no specific decimal places requested
|
||||
if (decimalPlaces === undefined) {
|
||||
decimalPart = decimalPart.replace(/0+$/, '');
|
||||
}
|
||||
|
||||
const result = decimalPart ? `${integerPart}.${decimalPart}` : integerPart;
|
||||
return this.value < 0n ? '-' + result : result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert to number (may lose precision)
|
||||
*/
|
||||
toNumber(): number {
|
||||
return Number(this.value) / Math.pow(10, this.scale);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert to fixed decimal places string
|
||||
*/
|
||||
toFixed(decimalPlaces: number): string {
|
||||
return this.round(decimalPlaces).toString(decimalPlaces);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add two decimals
|
||||
*/
|
||||
add(other: Decimal | number | string): Decimal {
|
||||
const otherDecimal = other instanceof Decimal ? other : new Decimal(other);
|
||||
|
||||
// Align scales
|
||||
if (this.scale === otherDecimal.scale) {
|
||||
return new Decimal(this.value + otherDecimal.value, this.scale);
|
||||
}
|
||||
|
||||
const maxScale = Math.max(this.scale, otherDecimal.scale);
|
||||
const thisScaled = this.rescale(maxScale);
|
||||
const otherScaled = otherDecimal.rescale(maxScale);
|
||||
|
||||
return new Decimal(thisScaled.value + otherScaled.value, maxScale);
|
||||
}
|
||||
|
||||
/**
|
||||
* Subtract another decimal
|
||||
*/
|
||||
subtract(other: Decimal | number | string): Decimal {
|
||||
const otherDecimal = other instanceof Decimal ? other : new Decimal(other);
|
||||
|
||||
// Align scales
|
||||
if (this.scale === otherDecimal.scale) {
|
||||
return new Decimal(this.value - otherDecimal.value, this.scale);
|
||||
}
|
||||
|
||||
const maxScale = Math.max(this.scale, otherDecimal.scale);
|
||||
const thisScaled = this.rescale(maxScale);
|
||||
const otherScaled = otherDecimal.rescale(maxScale);
|
||||
|
||||
return new Decimal(thisScaled.value - otherScaled.value, maxScale);
|
||||
}
|
||||
|
||||
/**
|
||||
* Multiply by another decimal
|
||||
*/
|
||||
multiply(other: Decimal | number | string): Decimal {
|
||||
const otherDecimal = other instanceof Decimal ? other : new Decimal(other);
|
||||
|
||||
// Multiply values and add scales
|
||||
const newValue = this.value * otherDecimal.value;
|
||||
const newScale = this.scale + otherDecimal.scale;
|
||||
|
||||
// Reduce scale if possible to avoid overflow
|
||||
const result = new Decimal(newValue, newScale);
|
||||
return result.normalize();
|
||||
}
|
||||
|
||||
/**
|
||||
* Divide by another decimal
|
||||
*/
|
||||
divide(other: Decimal | number | string, precision: number = 10): Decimal {
|
||||
const otherDecimal = other instanceof Decimal ? other : new Decimal(other);
|
||||
|
||||
if (otherDecimal.value === 0n) {
|
||||
throw new Error('Division by zero');
|
||||
}
|
||||
|
||||
// Scale up the dividend to maintain precision
|
||||
const scaledDividend = this.value * BigInt(Math.pow(10, precision));
|
||||
const quotient = scaledDividend / otherDecimal.value;
|
||||
|
||||
return new Decimal(quotient, this.scale + precision - otherDecimal.scale).normalize();
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate percentage (this * rate / 100)
|
||||
*/
|
||||
percentage(rate: Decimal | number | string): Decimal {
|
||||
const rateDecimal = rate instanceof Decimal ? rate : new Decimal(rate);
|
||||
return this.multiply(rateDecimal).divide(100);
|
||||
}
|
||||
|
||||
/**
|
||||
* Round to specified decimal places using a specific rounding mode
|
||||
*/
|
||||
round(decimalPlaces: number, mode: 'HALF_UP' | 'HALF_DOWN' | 'HALF_EVEN' | 'UP' | 'DOWN' | 'CEILING' | 'FLOOR' = 'HALF_UP'): Decimal {
|
||||
if (decimalPlaces === this.scale) {
|
||||
return this;
|
||||
}
|
||||
|
||||
if (decimalPlaces > this.scale) {
|
||||
// Just add zeros
|
||||
return this.rescale(decimalPlaces);
|
||||
}
|
||||
|
||||
// Need to round
|
||||
const factor = BigInt(Math.pow(10, this.scale - decimalPlaces));
|
||||
const halfFactor = factor / 2n;
|
||||
|
||||
let rounded: bigint;
|
||||
const isNegative = this.value < 0n;
|
||||
const absValue = isNegative ? -this.value : this.value;
|
||||
|
||||
switch (mode) {
|
||||
case 'HALF_UP':
|
||||
// Round half away from zero
|
||||
rounded = (absValue + halfFactor) / factor;
|
||||
break;
|
||||
|
||||
case 'HALF_DOWN':
|
||||
// Round half toward zero
|
||||
rounded = (absValue + halfFactor - 1n) / factor;
|
||||
break;
|
||||
|
||||
case 'HALF_EVEN':
|
||||
// Banker's rounding
|
||||
const quotient = absValue / factor;
|
||||
const remainder = absValue % factor;
|
||||
if (remainder > halfFactor || (remainder === halfFactor && quotient % 2n === 1n)) {
|
||||
rounded = quotient + 1n;
|
||||
} else {
|
||||
rounded = quotient;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'UP':
|
||||
// Round away from zero
|
||||
rounded = (absValue + factor - 1n) / factor;
|
||||
break;
|
||||
|
||||
case 'DOWN':
|
||||
// Round toward zero
|
||||
rounded = absValue / factor;
|
||||
break;
|
||||
|
||||
case 'CEILING':
|
||||
// Round toward positive infinity
|
||||
if (isNegative) {
|
||||
rounded = absValue / factor;
|
||||
} else {
|
||||
rounded = (absValue + factor - 1n) / factor;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'FLOOR':
|
||||
// Round toward negative infinity
|
||||
if (isNegative) {
|
||||
rounded = (absValue + factor - 1n) / factor;
|
||||
} else {
|
||||
rounded = absValue / factor;
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Error(`Unknown rounding mode: ${mode}`);
|
||||
}
|
||||
|
||||
const finalValue = isNegative ? -rounded : rounded;
|
||||
return new Decimal(finalValue, decimalPlaces);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare with another decimal
|
||||
*/
|
||||
compareTo(other: Decimal | number | string): number {
|
||||
const otherDecimal = other instanceof Decimal ? other : new Decimal(other);
|
||||
|
||||
// Align scales for comparison
|
||||
if (this.scale === otherDecimal.scale) {
|
||||
if (this.value < otherDecimal.value) return -1;
|
||||
if (this.value > otherDecimal.value) return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
const maxScale = Math.max(this.scale, otherDecimal.scale);
|
||||
const thisScaled = this.rescale(maxScale);
|
||||
const otherScaled = otherDecimal.rescale(maxScale);
|
||||
|
||||
if (thisScaled.value < otherScaled.value) return -1;
|
||||
if (thisScaled.value > otherScaled.value) return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check equality
|
||||
*/
|
||||
equals(other: Decimal | number | string, tolerance?: Decimal | number | string): boolean {
|
||||
if (tolerance) {
|
||||
const toleranceDecimal = tolerance instanceof Decimal ? tolerance : new Decimal(tolerance);
|
||||
const diff = this.subtract(other);
|
||||
const absDiff = diff.abs();
|
||||
return absDiff.compareTo(toleranceDecimal) <= 0;
|
||||
}
|
||||
return this.compareTo(other) === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if less than
|
||||
*/
|
||||
lessThan(other: Decimal | number | string): boolean {
|
||||
return this.compareTo(other) < 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if less than or equal
|
||||
*/
|
||||
lessThanOrEqual(other: Decimal | number | string): boolean {
|
||||
return this.compareTo(other) <= 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if greater than
|
||||
*/
|
||||
greaterThan(other: Decimal | number | string): boolean {
|
||||
return this.compareTo(other) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if greater than or equal
|
||||
*/
|
||||
greaterThanOrEqual(other: Decimal | number | string): boolean {
|
||||
return this.compareTo(other) >= 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get absolute value
|
||||
*/
|
||||
abs(): Decimal {
|
||||
return this.value < 0n ? new Decimal(-this.value, this.scale) : this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Negate the value
|
||||
*/
|
||||
negate(): Decimal {
|
||||
return new Decimal(-this.value, this.scale);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if zero
|
||||
*/
|
||||
isZero(): boolean {
|
||||
return this.value === 0n;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if negative
|
||||
*/
|
||||
isNegative(): boolean {
|
||||
return this.value < 0n;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if positive
|
||||
*/
|
||||
isPositive(): boolean {
|
||||
return this.value > 0n;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rescale to a different number of decimal places
|
||||
*/
|
||||
private rescale(newScale: number): Decimal {
|
||||
if (newScale === this.scale) {
|
||||
return this;
|
||||
}
|
||||
|
||||
if (newScale > this.scale) {
|
||||
// Add zeros
|
||||
const factor = BigInt(Math.pow(10, newScale - this.scale));
|
||||
return new Decimal(this.value * factor, newScale);
|
||||
}
|
||||
|
||||
// This would lose precision, use round() instead
|
||||
throw new Error('Use round() to reduce scale');
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize by removing trailing zeros
|
||||
*/
|
||||
private normalize(): Decimal {
|
||||
if (this.value === 0n) {
|
||||
return new Decimal(0n, 0);
|
||||
}
|
||||
|
||||
let value = this.value;
|
||||
let scale = this.scale;
|
||||
|
||||
while (scale > 0 && value % 10n === 0n) {
|
||||
value = value / 10n;
|
||||
scale--;
|
||||
}
|
||||
|
||||
return new Decimal(value, scale);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a Decimal from a percentage string (e.g., "19%" -> 0.19)
|
||||
*/
|
||||
static fromPercentage(value: string): Decimal {
|
||||
const cleaned = value.replace('%', '').trim();
|
||||
return new Decimal(cleaned).divide(100);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sum an array of decimals
|
||||
*/
|
||||
static sum(values: (Decimal | number | string)[]): Decimal {
|
||||
return values.reduce<Decimal>((acc, val) => {
|
||||
const decimal = val instanceof Decimal ? val : new Decimal(val);
|
||||
return acc.add(decimal);
|
||||
}, Decimal.ZERO);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the minimum value
|
||||
*/
|
||||
static min(...values: (Decimal | number | string)[]): Decimal {
|
||||
if (values.length === 0) {
|
||||
throw new Error('No values provided');
|
||||
}
|
||||
|
||||
let min = values[0] instanceof Decimal ? values[0] : new Decimal(values[0]);
|
||||
|
||||
for (let i = 1; i < values.length; i++) {
|
||||
const current = values[i] instanceof Decimal ? values[i] : new Decimal(values[i]);
|
||||
const currentDecimal = current instanceof Decimal ? current : new Decimal(current);
|
||||
if (currentDecimal.lessThan(min)) {
|
||||
min = currentDecimal;
|
||||
}
|
||||
}
|
||||
|
||||
return min;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the maximum value
|
||||
*/
|
||||
static max(...values: (Decimal | number | string)[]): Decimal {
|
||||
if (values.length === 0) {
|
||||
throw new Error('No values provided');
|
||||
}
|
||||
|
||||
let max = values[0] instanceof Decimal ? values[0] : new Decimal(values[0]);
|
||||
|
||||
for (let i = 1; i < values.length; i++) {
|
||||
const current = values[i] instanceof Decimal ? values[i] : new Decimal(values[i]);
|
||||
const currentDecimal = current instanceof Decimal ? current : new Decimal(current);
|
||||
if (currentDecimal.greaterThan(max)) {
|
||||
max = currentDecimal;
|
||||
}
|
||||
}
|
||||
|
||||
return max;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to create a Decimal
|
||||
*/
|
||||
export function decimal(value: string | number | bigint | Decimal): Decimal {
|
||||
return new Decimal(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Export commonly used rounding modes
|
||||
*/
|
||||
export const RoundingMode = {
|
||||
HALF_UP: 'HALF_UP' as const,
|
||||
HALF_DOWN: 'HALF_DOWN' as const,
|
||||
HALF_EVEN: 'HALF_EVEN' as const,
|
||||
UP: 'UP' as const,
|
||||
DOWN: 'DOWN' as const,
|
||||
CEILING: 'CEILING' as const,
|
||||
FLOOR: 'FLOOR' as const
|
||||
} as const;
|
||||
|
||||
export type RoundingMode = typeof RoundingMode[keyof typeof RoundingMode];
|
Reference in New Issue
Block a user