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:
257
test/test.decimal.ts
Normal file
257
test/test.decimal.ts
Normal file
@@ -0,0 +1,257 @@
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import { Decimal, decimal, RoundingMode } from '../ts/formats/utils/decimal.js';
|
||||
|
||||
tap.test('Decimal - basic construction', async () => {
|
||||
// From string
|
||||
const d1 = new Decimal('123.456');
|
||||
expect(d1.toString()).toEqual('123.456');
|
||||
|
||||
// From number
|
||||
const d2 = new Decimal(123.456);
|
||||
expect(d2.toString()).toEqual('123.456');
|
||||
|
||||
// From bigint
|
||||
const d3 = new Decimal(123n);
|
||||
expect(d3.toString()).toEqual('123');
|
||||
|
||||
// From another Decimal
|
||||
const d4 = new Decimal(d1);
|
||||
expect(d4.toString()).toEqual('123.456');
|
||||
|
||||
// Negative values
|
||||
const d5 = new Decimal('-123.456');
|
||||
expect(d5.toString()).toEqual('-123.456');
|
||||
});
|
||||
|
||||
tap.test('Decimal - arithmetic operations', async () => {
|
||||
const a = new Decimal('10.50');
|
||||
const b = new Decimal('3.25');
|
||||
|
||||
// Addition
|
||||
expect(a.add(b).toString()).toEqual('13.75');
|
||||
|
||||
// Subtraction
|
||||
expect(a.subtract(b).toString()).toEqual('7.25');
|
||||
|
||||
// Multiplication
|
||||
expect(a.multiply(b).toString()).toEqual('34.125');
|
||||
|
||||
// Division
|
||||
expect(a.divide(b).toString()).toEqual('3.2307692307');
|
||||
|
||||
// Percentage
|
||||
const amount = new Decimal('100');
|
||||
const rate = new Decimal('19');
|
||||
expect(amount.percentage(rate).toString()).toEqual('19');
|
||||
});
|
||||
|
||||
tap.test('Decimal - rounding modes', async () => {
|
||||
// HALF_UP (default)
|
||||
expect(new Decimal('2.5').round(0, 'HALF_UP').toString()).toEqual('3');
|
||||
expect(new Decimal('2.4').round(0, 'HALF_UP').toString()).toEqual('2');
|
||||
expect(new Decimal('-2.5').round(0, 'HALF_UP').toString()).toEqual('-3');
|
||||
|
||||
// HALF_DOWN
|
||||
expect(new Decimal('2.5').round(0, 'HALF_DOWN').toString()).toEqual('2');
|
||||
expect(new Decimal('2.6').round(0, 'HALF_DOWN').toString()).toEqual('3');
|
||||
expect(new Decimal('-2.5').round(0, 'HALF_DOWN').toString()).toEqual('-2');
|
||||
|
||||
// HALF_EVEN (Banker's rounding)
|
||||
expect(new Decimal('2.5').round(0, 'HALF_EVEN').toString()).toEqual('2');
|
||||
expect(new Decimal('3.5').round(0, 'HALF_EVEN').toString()).toEqual('4');
|
||||
expect(new Decimal('2.4').round(0, 'HALF_EVEN').toString()).toEqual('2');
|
||||
expect(new Decimal('2.6').round(0, 'HALF_EVEN').toString()).toEqual('3');
|
||||
|
||||
// UP (away from zero)
|
||||
expect(new Decimal('2.1').round(0, 'UP').toString()).toEqual('3');
|
||||
expect(new Decimal('-2.1').round(0, 'UP').toString()).toEqual('-3');
|
||||
|
||||
// DOWN (toward zero)
|
||||
expect(new Decimal('2.9').round(0, 'DOWN').toString()).toEqual('2');
|
||||
expect(new Decimal('-2.9').round(0, 'DOWN').toString()).toEqual('-2');
|
||||
|
||||
// CEILING (toward positive infinity)
|
||||
expect(new Decimal('2.1').round(0, 'CEILING').toString()).toEqual('3');
|
||||
expect(new Decimal('-2.9').round(0, 'CEILING').toString()).toEqual('-2');
|
||||
|
||||
// FLOOR (toward negative infinity)
|
||||
expect(new Decimal('2.9').round(0, 'FLOOR').toString()).toEqual('2');
|
||||
expect(new Decimal('-2.1').round(0, 'FLOOR').toString()).toEqual('-3');
|
||||
});
|
||||
|
||||
tap.test('Decimal - EN16931 calculation scenarios', async () => {
|
||||
// Line item calculation
|
||||
const quantity = new Decimal('3');
|
||||
const unitPrice = new Decimal('33.333333');
|
||||
const lineTotal = quantity.multiply(unitPrice);
|
||||
expect(lineTotal.round(2).toString()).toEqual('100');
|
||||
|
||||
// VAT calculation
|
||||
const netAmount = new Decimal('100');
|
||||
const vatRate = new Decimal('19');
|
||||
const vatAmount = netAmount.percentage(vatRate);
|
||||
expect(vatAmount.toString()).toEqual('19');
|
||||
|
||||
// Total with VAT
|
||||
const grossAmount = netAmount.add(vatAmount);
|
||||
expect(grossAmount.toString()).toEqual('119');
|
||||
|
||||
// Complex calculation with allowances
|
||||
const lineExtension = new Decimal('150.00');
|
||||
const allowance = new Decimal('10.00');
|
||||
const charge = new Decimal('5.00');
|
||||
const taxExclusive = lineExtension.subtract(allowance).add(charge);
|
||||
expect(taxExclusive.toString()).toEqual('145');
|
||||
|
||||
const vat = taxExclusive.percentage(new Decimal('19'));
|
||||
expect(vat.round(2).toString()).toEqual('27.55');
|
||||
|
||||
const total = taxExclusive.add(vat);
|
||||
expect(total.round(2).toString()).toEqual('172.55');
|
||||
});
|
||||
|
||||
tap.test('Decimal - comparisons', async () => {
|
||||
const a = new Decimal('10.50');
|
||||
const b = new Decimal('10.50');
|
||||
const c = new Decimal('10.51');
|
||||
|
||||
// Equality
|
||||
expect(a.equals(b)).toBeTrue();
|
||||
expect(a.equals(c)).toBeFalse();
|
||||
|
||||
// With tolerance
|
||||
expect(a.equals(c, '0.01')).toBeTrue();
|
||||
expect(a.equals(c, '0.005')).toBeFalse();
|
||||
|
||||
// Comparisons
|
||||
expect(a.lessThan(c)).toBeTrue();
|
||||
expect(c.greaterThan(a)).toBeTrue();
|
||||
expect(a.lessThanOrEqual(b)).toBeTrue();
|
||||
expect(a.greaterThanOrEqual(b)).toBeTrue();
|
||||
});
|
||||
|
||||
tap.test('Decimal - edge cases', async () => {
|
||||
// Very small numbers
|
||||
const tiny = new Decimal('0.0000000001');
|
||||
expect(tiny.multiply(new Decimal('1000000000')).toString()).toEqual('0.1');
|
||||
|
||||
// Very large numbers
|
||||
const huge = new Decimal('999999999999999999');
|
||||
expect(huge.add(new Decimal('1')).toString()).toEqual('1000000000000000000');
|
||||
|
||||
// Division by zero
|
||||
const zero = new Decimal('0');
|
||||
const one = new Decimal('1');
|
||||
let errorThrown = false;
|
||||
try {
|
||||
one.divide(zero);
|
||||
} catch (e) {
|
||||
errorThrown = true;
|
||||
expect(e.message).toEqual('Division by zero');
|
||||
}
|
||||
expect(errorThrown).toBeTrue();
|
||||
|
||||
// Zero operations
|
||||
expect(zero.add(one).toString()).toEqual('1');
|
||||
expect(zero.multiply(one).toString()).toEqual('0');
|
||||
expect(zero.isZero()).toBeTrue();
|
||||
expect(one.isZero()).toBeFalse();
|
||||
});
|
||||
|
||||
tap.test('Decimal - currency calculations with different minor units', async () => {
|
||||
// EUR (2 decimal places)
|
||||
const eurAmount = new Decimal('100.00');
|
||||
const eurVat = eurAmount.percentage(new Decimal('19'));
|
||||
expect(eurVat.round(2).toString()).toEqual('19');
|
||||
|
||||
// JPY (0 decimal places)
|
||||
const jpyAmount = new Decimal('1000');
|
||||
const jpyTax = jpyAmount.percentage(new Decimal('10'));
|
||||
expect(jpyTax.round(0).toString()).toEqual('100');
|
||||
|
||||
// KWD (3 decimal places)
|
||||
const kwdAmount = new Decimal('100.000');
|
||||
const kwdTax = kwdAmount.percentage(new Decimal('5'));
|
||||
expect(kwdTax.round(3).toString()).toEqual('5');
|
||||
|
||||
// BTC (8 decimal places for satoshis)
|
||||
const btcAmount = new Decimal('0.00100000');
|
||||
const btcFee = btcAmount.percentage(new Decimal('0.1'));
|
||||
expect(btcFee.round(8).toString()).toEqual('0.000001');
|
||||
});
|
||||
|
||||
tap.test('Decimal - static methods', async () => {
|
||||
// Sum
|
||||
const values = ['10.50', '20.25', '30.75'];
|
||||
const sum = Decimal.sum(values);
|
||||
expect(sum.toString()).toEqual('61.5');
|
||||
|
||||
// Min
|
||||
const min = Decimal.min('10.50', '20.25', '5.75');
|
||||
expect(min.toString()).toEqual('5.75');
|
||||
|
||||
// Max
|
||||
const max = Decimal.max('10.50', '20.25', '5.75');
|
||||
expect(max.toString()).toEqual('20.25');
|
||||
|
||||
// From percentage
|
||||
const rate = Decimal.fromPercentage('19%');
|
||||
expect(rate.toString()).toEqual('0.19');
|
||||
});
|
||||
|
||||
tap.test('Decimal - formatting', async () => {
|
||||
const value = new Decimal('1234.567890');
|
||||
|
||||
// Fixed decimal places
|
||||
expect(value.toFixed(2)).toEqual('1234.57');
|
||||
expect(value.toFixed(0)).toEqual('1235');
|
||||
expect(value.toFixed(4)).toEqual('1234.5679');
|
||||
|
||||
// toString with decimal places
|
||||
expect(value.toString(2)).toEqual('1234.56');
|
||||
expect(value.toString(6)).toEqual('1234.567890');
|
||||
|
||||
// Automatic trailing zero removal
|
||||
const rounded = new Decimal('100.00');
|
||||
expect(rounded.toString()).toEqual('100');
|
||||
expect(rounded.toFixed(2)).toEqual('100.00');
|
||||
});
|
||||
|
||||
tap.test('Decimal - real-world invoice calculation', async () => {
|
||||
// Invoice with multiple lines and VAT rates
|
||||
const lines = [
|
||||
{ quantity: '2', unitPrice: '50.00', vatRate: '19' },
|
||||
{ quantity: '3', unitPrice: '33.33', vatRate: '19' },
|
||||
{ quantity: '1', unitPrice: '100.00', vatRate: '7' }
|
||||
];
|
||||
|
||||
let totalNet = Decimal.ZERO;
|
||||
let totalVat19 = Decimal.ZERO;
|
||||
let totalVat7 = Decimal.ZERO;
|
||||
|
||||
for (const line of lines) {
|
||||
const quantity = new Decimal(line.quantity);
|
||||
const unitPrice = new Decimal(line.unitPrice);
|
||||
const lineNet = quantity.multiply(unitPrice);
|
||||
totalNet = totalNet.add(lineNet);
|
||||
|
||||
const vatAmount = lineNet.percentage(new Decimal(line.vatRate));
|
||||
if (line.vatRate === '19') {
|
||||
totalVat19 = totalVat19.add(vatAmount);
|
||||
} else {
|
||||
totalVat7 = totalVat7.add(vatAmount);
|
||||
}
|
||||
}
|
||||
|
||||
expect(totalNet.round(2).toString()).toEqual('299.99');
|
||||
expect(totalVat19.round(2).toString()).toEqual('38');
|
||||
expect(totalVat7.round(2).toString()).toEqual('7');
|
||||
|
||||
const totalVat = totalVat19.add(totalVat7);
|
||||
const totalGross = totalNet.add(totalVat);
|
||||
|
||||
expect(totalVat.round(2).toString()).toEqual('45');
|
||||
expect(totalGross.round(2).toString()).toEqual('344.99');
|
||||
});
|
||||
|
||||
export default tap.start();
|
Reference in New Issue
Block a user