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:
2025-08-11 18:07:01 +00:00
parent 10e14af85b
commit cbb297b0b1
24 changed files with 7714 additions and 98 deletions

View File

@@ -1,4 +1,4 @@
import { expect, tap } from '@git.zone/tstest/tapbundle/index.js';
import { expect, tap } from '@git.zone/tstest/tapbundle';
import * as path from 'path';
import * as fs from 'fs';

View File

@@ -0,0 +1,184 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import { DecimalCurrencyCalculator } from '../ts/formats/utils/currency.calculator.decimal.js';
import { Decimal } from '../ts/formats/utils/decimal.js';
tap.test('DecimalCurrencyCalculator - EUR calculations', async () => {
const calculator = new DecimalCurrencyCalculator('EUR');
// Line calculation
const lineNet = calculator.calculateLineNet('3', '33.333', '0');
expect(lineNet.toString()).toEqual('100'); // calculateLineNet rounds the result
// VAT calculation
const vat = calculator.calculateVAT('100', '19');
expect(vat.toString()).toEqual('19');
// Gross amount
const gross = calculator.calculateGrossAmount('100', '19');
expect(gross.toString()).toEqual('119');
});
tap.test('DecimalCurrencyCalculator - JPY calculations (no decimals)', async () => {
const calculator = new DecimalCurrencyCalculator('JPY');
// Should round to 0 decimal places
const amount = calculator.round('1234.56');
expect(amount.toString()).toEqual('1235');
// VAT calculation
const vat = calculator.calculateVAT('1000', '10');
expect(vat.toString()).toEqual('100');
});
tap.test('DecimalCurrencyCalculator - KWD calculations (3 decimals)', async () => {
const calculator = new DecimalCurrencyCalculator('KWD');
// Should maintain 3 decimal places
const amount = calculator.round('123.4567');
expect(amount.toString()).toEqual('123.457');
// VAT calculation
const vat = calculator.calculateVAT('100.000', '5');
expect(vat.toString()).toEqual('5');
});
tap.test('DecimalCurrencyCalculator - sum line items', async () => {
const calculator = new DecimalCurrencyCalculator('EUR');
const items = [
{ quantity: '2', unitPrice: '50.00', discount: '5.00' },
{ quantity: '3', unitPrice: '33.33', discount: '0' },
{ quantity: '1', unitPrice: '100.00', discount: '10.00' }
];
const total = calculator.sumLineItems(items);
expect(total.toString()).toEqual('284.99');
});
tap.test('DecimalCurrencyCalculator - VAT breakdown', async () => {
const calculator = new DecimalCurrencyCalculator('EUR');
const items = [
{ netAmount: '100.00', vatRate: '19' },
{ netAmount: '50.00', vatRate: '19' },
{ netAmount: '200.00', vatRate: '7' }
];
const breakdown = calculator.calculateVATBreakdown(items);
expect(breakdown).toHaveLength(2);
const vat19 = breakdown.find(b => b.rate.toString() === '19');
expect(vat19?.baseAmount.toString()).toEqual('150');
expect(vat19?.vatAmount.toString()).toEqual('28.5');
const vat7 = breakdown.find(b => b.rate.toString() === '7');
expect(vat7?.baseAmount.toString()).toEqual('200');
expect(vat7?.vatAmount.toString()).toEqual('14');
});
tap.test('DecimalCurrencyCalculator - distribute amount', async () => {
const calculator = new DecimalCurrencyCalculator('EUR');
// Distribute 100 EUR across three items
const items = [
{ value: '30' }, // 30%
{ value: '50' }, // 50%
{ value: '20' } // 20%
];
const distributed = calculator.distributeAmount('100', items);
expect(distributed[0].toString()).toEqual('30');
expect(distributed[1].toString()).toEqual('50');
expect(distributed[2].toString()).toEqual('20');
// Sum should equal total
const sum = Decimal.sum(distributed);
expect(sum.toString()).toEqual('100');
});
tap.test('DecimalCurrencyCalculator - compound adjustments', async () => {
const calculator = new DecimalCurrencyCalculator('EUR');
const adjustments = [
{ type: 'allowance' as const, value: '10', isPercentage: true }, // -10%
{ type: 'charge' as const, value: '5', isPercentage: false }, // +5 EUR
{ type: 'allowance' as const, value: '2', isPercentage: false } // -2 EUR
];
const result = calculator.calculateCompoundAmount('100', adjustments);
// 100 - 10% = 90, + 5 = 95, - 2 = 93
expect(result.toString()).toEqual('93');
});
tap.test('DecimalCurrencyCalculator - validation', async () => {
const calculator = new DecimalCurrencyCalculator('EUR');
// Valid calculation
const result1 = calculator.validateCalculation('119.00', '119.00', 'BR-CO-15');
expect(result1.valid).toBeTrue();
expect(result1.expected).toEqual('119.00');
expect(result1.calculated).toEqual('119.00');
// Invalid calculation
const result2 = calculator.validateCalculation('119.00', '118.99', 'BR-CO-15');
expect(result2.valid).toBeFalse();
expect(result2.difference).toEqual('0.01');
});
tap.test('DecimalCurrencyCalculator - different rounding modes', async () => {
// HALF_DOWN for specific requirements
const calculator = new DecimalCurrencyCalculator('EUR', 'HALF_DOWN');
const amount1 = calculator.round('10.125'); // Should round down
expect(amount1.toString()).toEqual('10.12');
const amount2 = calculator.round('10.135'); // Should round down with HALF_DOWN
expect(amount2.toString()).toEqual('10.13');
// HALF_EVEN (Banker's rounding) for statistical accuracy
const bankerCalc = new DecimalCurrencyCalculator('EUR', 'HALF_EVEN');
const amount3 = bankerCalc.round('10.125'); // Round to even (down)
expect(amount3.toString()).toEqual('10.12');
const amount4 = bankerCalc.round('10.135'); // Round to even (up)
expect(amount4.toString()).toEqual('10.14');
});
tap.test('DecimalCurrencyCalculator - real invoice scenario', async () => {
const calculator = new DecimalCurrencyCalculator('EUR');
// Invoice lines
const lines = [
{ quantity: '2.5', unitPrice: '45.60', discount: '5.00' },
{ quantity: '10', unitPrice: '12.34', discount: '0' },
{ quantity: '1', unitPrice: '250.00', discount: '25.00' }
];
// Calculate line totals
const lineTotal = calculator.sumLineItems(lines);
expect(lineTotal.toString()).toEqual('457.4');
// Apply document-level allowance (2%)
const allowance = calculator.calculatePaymentDiscount(lineTotal, '2');
expect(allowance.toString()).toEqual('9.15');
const netAfterAllowance = lineTotal.subtract(allowance);
expect(calculator.round(netAfterAllowance).toString()).toEqual('448.25');
// Calculate VAT at 19%
const vat = calculator.calculateVAT(netAfterAllowance, '19');
expect(vat.toString()).toEqual('85.17');
// Total with VAT
const total = calculator.calculateGrossAmount(netAfterAllowance, vat);
expect(total.toString()).toEqual('533.42');
// Format for display
const formatted = calculator.formatAmount(total);
expect(formatted).toEqual('533.42 EUR');
});
export default tap.start();

257
test/test.decimal.ts Normal file
View 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();

View File

@@ -196,4 +196,4 @@ tap.test('EInvoice should export XML correctly', async () => {
});
// Run the tests
tap.start();
export default tap.start();

View File

@@ -0,0 +1,453 @@
import { tap, expect } from '@git.zone/tstest/tapbundle';
import { FacturXValidator, FacturXProfile } from '../ts/formats/validation/facturx.validator.js';
import type { EInvoice } from '../ts/einvoice.js';
tap.test('Factur-X Validator - basic instantiation', async () => {
const validator = FacturXValidator.create();
expect(validator).toBeInstanceOf(FacturXValidator);
// Singleton pattern
const validator2 = FacturXValidator.create();
expect(validator2).toEqual(validator);
});
tap.test('Factur-X Validator - profile detection', async () => {
const validator = FacturXValidator.create();
// MINIMUM profile
const minInvoice: Partial<EInvoice> = {
metadata: {
profileId: 'urn:facturx:minimum:2017'
}
};
expect(validator.detectProfile(minInvoice as EInvoice)).toEqual(FacturXProfile.MINIMUM);
// BASIC profile
const basicInvoice: Partial<EInvoice> = {
metadata: {
profileId: 'urn:facturx:basic:2017'
}
};
expect(validator.detectProfile(basicInvoice as EInvoice)).toEqual(FacturXProfile.BASIC);
// EN16931 profile (Comfort)
const en16931Invoice: Partial<EInvoice> = {
metadata: {
profileId: 'urn:facturx:comfort:2017'
}
};
expect(validator.detectProfile(en16931Invoice as EInvoice)).toEqual(FacturXProfile.EN16931);
// EXTENDED profile
const extendedInvoice: Partial<EInvoice> = {
metadata: {
profileId: 'urn:facturx:extended:2017'
}
};
expect(validator.detectProfile(extendedInvoice as EInvoice)).toEqual(FacturXProfile.EXTENDED);
// Non-Factur-X invoice
const otherInvoice: Partial<EInvoice> = {
metadata: {
profileId: 'urn:cen.eu:en16931:2017'
}
};
expect(validator.detectProfile(otherInvoice as EInvoice)).toEqual(null);
});
tap.test('Factur-X Validator - MINIMUM profile validation', async () => {
const validator = FacturXValidator.create();
const invoice: Partial<EInvoice> = {
metadata: {
profileId: 'urn:facturx:minimum:2017'
},
accountingDocId: 'INV-2025-001',
issueDate: new Date('2025-01-11'),
accountingDocType: 'invoice',
currency: 'EUR',
from: {
type: 'company',
name: 'Test Seller',
vatNumber: 'DE123456789'
},
to: {
type: 'company',
name: 'Test Buyer'
},
totalInvoiceAmount: 119.00,
totalNetAmount: 100.00,
totalVatAmount: 19.00
};
const results = validator.validateFacturX(invoice as EInvoice, FacturXProfile.MINIMUM);
const errors = results.filter(r => r.severity === 'error');
console.log('MINIMUM profile validation errors:', errors);
expect(errors.length).toEqual(0);
});
tap.test('Factur-X Validator - MINIMUM profile missing fields', async () => {
const validator = FacturXValidator.create();
const invoice: Partial<EInvoice> = {
metadata: {
profileId: 'urn:facturx:minimum:2017'
},
accountingDocId: 'INV-2025-001',
issueDate: new Date('2025-01-11'),
// Missing required fields for MINIMUM
};
const results = validator.validateFacturX(invoice as EInvoice, FacturXProfile.MINIMUM);
const errors = results.filter(r => r.severity === 'error');
expect(errors.length).toBeGreaterThan(0);
expect(errors.some(e => e.field === 'currency')).toBeTrue();
expect(errors.some(e => e.field === 'from.name')).toBeTrue();
});
tap.test('Factur-X Validator - BASIC profile validation', async () => {
const validator = FacturXValidator.create();
const invoice: Partial<EInvoice> = {
metadata: {
profileId: 'urn:facturx:basic:2017'
},
accountingDocId: 'INV-2025-001',
issueDate: new Date('2025-01-11'),
accountingDocType: 'invoice',
currency: 'EUR',
dueDate: new Date('2025-02-11'),
from: {
type: 'company',
name: 'Test Seller',
vatNumber: 'DE123456789',
address: 'Test Street 1',
country: 'DE'
},
to: {
type: 'company',
name: 'Test Buyer',
address: 'Buyer Street 1',
country: 'FR'
},
items: [
{
position: 1,
name: 'Test Product',
unitQuantity: 1,
unitNetPrice: 100.00,
unitType: 'C62',
vatPercentage: 19,
articleNumber: 'ART-001'
}
],
totalInvoiceAmount: 119.00,
totalNetAmount: 100.00,
totalVatAmount: 19.00
};
const results = validator.validateFacturX(invoice as EInvoice, FacturXProfile.BASIC);
const errors = results.filter(r => r.severity === 'error');
console.log('BASIC profile validation errors:', errors);
expect(errors.length).toEqual(0);
});
tap.test('Factur-X Validator - BASIC profile missing line items', async () => {
const validator = FacturXValidator.create();
const invoice: Partial<EInvoice> = {
metadata: {
profileId: 'urn:facturx:basic:2017'
},
accountingDocId: 'INV-2025-001',
issueDate: new Date('2025-01-11'),
accountingDocType: 'invoice',
currency: 'EUR',
dueDate: new Date('2025-02-11'),
from: {
type: 'company',
name: 'Test Seller',
vatNumber: 'DE123456789',
address: 'Test Street 1',
country: 'DE'
},
to: {
type: 'company',
name: 'Test Buyer',
address: 'Buyer Street 1',
country: 'FR'
},
// Missing items
totalInvoiceAmount: 119.00,
totalNetAmount: 100.00,
totalVatAmount: 19.00
};
const results = validator.validateFacturX(invoice as EInvoice);
const errors = results.filter(r => r.severity === 'error');
expect(errors.length).toBeGreaterThan(0);
expect(errors.some(e => e.ruleId === 'FX-BAS-02')).toBeTrue();
});
tap.test('Factur-X Validator - BASIC_WL profile (without lines)', async () => {
const validator = FacturXValidator.create();
const invoice: Partial<EInvoice> = {
metadata: {
profileId: 'urn:facturx:basicwl:2017'
},
accountingDocId: 'INV-2025-001',
issueDate: new Date('2025-01-11'),
accountingDocType: 'invoice',
currency: 'EUR',
dueDate: new Date('2025-02-11'),
from: {
type: 'company',
name: 'Test Seller',
vatNumber: 'DE123456789',
address: 'Test Street 1',
country: 'DE'
},
to: {
type: 'company',
name: 'Test Buyer',
address: 'Buyer Street 1',
country: 'FR'
},
// No items required for BASIC_WL
totalInvoiceAmount: 119.00,
totalNetAmount: 100.00,
totalVatAmount: 19.00
};
const results = validator.validateFacturX(invoice as EInvoice, FacturXProfile.BASIC_WL);
const errors = results.filter(r => r.severity === 'error');
console.log('BASIC_WL profile validation errors:', errors);
expect(errors.length).toEqual(0);
});
tap.test('Factur-X Validator - EN16931 profile validation', async () => {
const validator = FacturXValidator.create();
const invoice: Partial<EInvoice> = {
metadata: {
profileId: 'urn:facturx:en16931:2017',
buyerReference: 'REF-12345'
},
accountingDocId: 'INV-2025-001',
issueDate: new Date('2025-01-11'),
accountingDocType: 'invoice',
currency: 'EUR',
dueDate: new Date('2025-02-11'),
from: {
type: 'company',
name: 'Test Seller',
vatNumber: 'DE123456789',
address: 'Test Street 1',
city: 'Berlin',
postalCode: '10115',
country: 'DE'
},
to: {
type: 'company',
name: 'Test Buyer',
address: 'Buyer Street 1',
city: 'Paris',
postalCode: '75001',
country: 'FR'
},
items: [
{
position: 1,
name: 'Test Product',
unitQuantity: 1,
unitNetPrice: 100.00,
unitType: 'C62',
vatPercentage: 19,
articleNumber: 'ART-001'
}
],
totalInvoiceAmount: 119.00,
totalNetAmount: 100.00,
totalVatAmount: 19.00
};
const results = validator.validateFacturX(invoice as EInvoice, FacturXProfile.EN16931);
const errors = results.filter(r => r.severity === 'error');
console.log('EN16931 profile validation errors:', errors);
expect(errors.length).toEqual(0);
});
tap.test('Factur-X Validator - EN16931 missing buyer reference', async () => {
const validator = FacturXValidator.create();
const invoice: Partial<EInvoice> = {
metadata: {
profileId: 'urn:facturx:en16931:2017',
// Missing buyerReference or purchaseOrderReference
},
accountingDocId: 'INV-2025-001',
issueDate: new Date('2025-01-11'),
accountingDocType: 'invoice',
currency: 'EUR',
from: {
type: 'company',
name: 'Test Seller',
vatNumber: 'DE123456789',
address: 'Test Street 1',
city: 'Berlin',
postalCode: '10115',
country: 'DE'
},
to: {
type: 'company',
name: 'Test Buyer',
address: 'Buyer Street 1',
city: 'Paris',
postalCode: '75001',
country: 'FR'
},
items: [],
totalInvoiceAmount: 0,
totalNetAmount: 0,
totalVatAmount: 0,
dueDate: new Date('2025-02-11')
};
const results = validator.validateFacturX(invoice as EInvoice);
const errors = results.filter(r => r.severity === 'error');
expect(errors.some(e => e.ruleId === 'FX-EN-01')).toBeTrue();
});
tap.test('Factur-X Validator - EXTENDED profile validation', async () => {
const validator = FacturXValidator.create();
const invoice: Partial<EInvoice> = {
metadata: {
profileId: 'urn:facturx:extended:2017',
extensions: {
attachments: [
{
filename: 'invoice.pdf',
mimeType: 'application/pdf',
data: 'base64encodeddata'
}
]
}
},
accountingDocId: 'INV-2025-001',
issueDate: new Date('2025-01-11'),
accountingDocType: 'invoice',
currency: 'EUR',
from: {
type: 'company',
name: 'Test Seller',
vatNumber: 'DE123456789'
},
to: {
type: 'company',
name: 'Test Buyer'
},
totalInvoiceAmount: 119.00
};
const results = validator.validateFacturX(invoice as EInvoice, FacturXProfile.EXTENDED);
const errors = results.filter(r => r.severity === 'error');
console.log('EXTENDED profile validation errors:', errors);
expect(errors.length).toEqual(0);
});
tap.test('Factur-X Validator - EXTENDED profile attachment validation', async () => {
const validator = FacturXValidator.create();
const invoice: Partial<EInvoice> = {
metadata: {
profileId: 'urn:facturx:extended:2017',
extensions: {
attachments: [
{
// Missing filename and mimeType
data: 'base64encodeddata'
}
]
}
},
accountingDocId: 'INV-2025-001',
issueDate: new Date('2025-01-11'),
accountingDocType: 'invoice',
currency: 'EUR',
from: {
type: 'company',
name: 'Test Seller',
vatNumber: 'DE123456789'
},
to: {
type: 'company',
name: 'Test Buyer'
},
totalInvoiceAmount: 119.00
};
const results = validator.validateFacturX(invoice as EInvoice);
const warnings = results.filter(r => r.severity === 'warning');
expect(warnings.some(w => w.ruleId === 'FX-EXT-01')).toBeTrue();
});
tap.test('Factur-X Validator - ZUGFeRD compatibility', async () => {
const validator = FacturXValidator.create();
const invoice: Partial<EInvoice> = {
metadata: {
profileId: 'urn:zugferd:basic:2017' // ZUGFeRD format
}
};
// Should detect as Factur-X (ZUGFeRD is the German name)
const profile = validator.detectProfile(invoice as EInvoice);
expect(profile).toEqual(FacturXProfile.BASIC);
});
tap.test('Factur-X Validator - profile display names', async () => {
const validator = FacturXValidator.create();
expect(validator.getProfileDisplayName(FacturXProfile.MINIMUM)).toEqual('Factur-X MINIMUM');
expect(validator.getProfileDisplayName(FacturXProfile.BASIC)).toEqual('Factur-X BASIC');
expect(validator.getProfileDisplayName(FacturXProfile.BASIC_WL)).toEqual('Factur-X BASIC WL');
expect(validator.getProfileDisplayName(FacturXProfile.EN16931)).toEqual('Factur-X EN16931');
expect(validator.getProfileDisplayName(FacturXProfile.EXTENDED)).toEqual('Factur-X EXTENDED');
});
tap.test('Factur-X Validator - profile compliance levels', async () => {
const validator = FacturXValidator.create();
expect(validator.getProfileComplianceLevel(FacturXProfile.MINIMUM)).toEqual(1);
expect(validator.getProfileComplianceLevel(FacturXProfile.BASIC_WL)).toEqual(2);
expect(validator.getProfileComplianceLevel(FacturXProfile.BASIC)).toEqual(3);
expect(validator.getProfileComplianceLevel(FacturXProfile.EN16931)).toEqual(4);
expect(validator.getProfileComplianceLevel(FacturXProfile.EXTENDED)).toEqual(5);
});
tap.test('Factur-X Validator - non-Factur-X invoice skips validation', async () => {
const validator = FacturXValidator.create();
const invoice: Partial<EInvoice> = {
metadata: {
profileId: 'urn:cen.eu:en16931:2017' // Not Factur-X
}
};
const results = validator.validateFacturX(invoice as EInvoice);
expect(results.length).toEqual(0);
});
export default tap.start();

View File

@@ -0,0 +1,219 @@
import { tap, expect } from '@git.zone/tstest/tapbundle';
import { MainValidator, createValidator } from '../ts/formats/validation/integrated.validator.js';
import { EInvoice } from '../ts/einvoice.js';
import * as fs from 'fs';
import * as path from 'path';
tap.test('Integrated Validator - Basic validation', async () => {
const validator = new MainValidator();
const invoice = new EInvoice();
invoice.invoiceNumber = 'TEST-001';
invoice.issueDate = new Date('2025-01-11');
invoice.from = {
type: 'company',
name: 'Test Seller',
address: {
streetName: 'Test Street',
city: 'Berlin',
postalCode: '10115',
countryCode: 'DE'
}
};
invoice.to = {
name: 'Test Buyer',
address: {
streetName: 'Buyer Street',
city: 'Munich',
postalCode: '80331',
countryCode: 'DE'
}
};
const report = await validator.validate(invoice);
console.log('Basic validation report:');
console.log(` Valid: ${report.valid}`);
console.log(` Errors: ${report.errorCount}`);
console.log(` Warnings: ${report.warningCount}`);
console.log(` Coverage: ${report.coverage.toFixed(1)}%`);
expect(report).toBeDefined();
expect(report.errorCount).toBeGreaterThan(0); // Should have errors (missing required fields)
});
tap.test('Integrated Validator - XRechnung detection', async () => {
const validator = new MainValidator();
const invoice = new EInvoice();
invoice.metadata = {
profileId: 'urn:cen.eu:en16931:2017#compliant#urn:xeinkauf.de:kosit:xrechnung_3.0',
buyerReference: '991-12345678901-23' // Leitweg-ID
};
invoice.invoiceNumber = 'XR-2025-001';
invoice.issueDate = new Date('2025-01-11');
const report = await validator.validate(invoice);
console.log('XRechnung validation report:');
console.log(` Profile: ${report.profile}`);
console.log(` XRechnung errors found: ${
report.results.filter(r => r.source === 'XRECHNUNG').length
}`);
expect(report.profile).toInclude('XRECHNUNG');
// Check for XRechnung-specific validation
const xrErrors = report.results.filter(r => r.source === 'XRECHNUNG');
expect(xrErrors.length).toBeGreaterThan(0);
});
tap.test('Integrated Validator - Complete valid invoice', async () => {
const validator = await createValidator({ enableSchematron: false });
const invoice = new EInvoice();
invoice.accountingDocId = 'INV-2025-001';
invoice.accountingDocType = '380';
invoice.invoiceNumber = 'INV-2025-001';
invoice.issueDate = new Date('2025-01-11');
invoice.currencyCode = 'EUR';
invoice.from = {
type: 'company',
name: 'Example GmbH',
address: {
streetName: 'Hauptstraße 1',
city: 'Berlin',
postalCode: '10115',
countryCode: 'DE'
},
registrationDetails: {
vatId: 'DE123456789'
}
};
invoice.to = {
name: 'Customer AG',
address: {
streetName: 'Kundenweg 42',
city: 'Munich',
postalCode: '80331',
countryCode: 'DE'
}
};
invoice.items = [{
title: 'Consulting Services',
description: 'Professional consulting',
quantity: 10,
unitPrice: 100,
netAmount: 1000,
vatRate: 19,
vatAmount: 190,
grossAmount: 1190
}];
invoice.metadata = {
customizationId: 'urn:cen.eu:en16931:2017',
profileId: 'urn:cen.eu:en16931:2017',
taxDetails: [{
taxPercent: 19,
netAmount: 1000,
taxAmount: 190
}],
totals: {
lineExtensionAmount: 1000,
taxExclusiveAmount: 1000,
taxInclusiveAmount: 1190,
payableAmount: 1190
}
};
const report = await validator.validate(invoice);
console.log('\nComplete invoice validation:');
console.log(validator.formatReport(report));
// Should have fewer errors with more complete data
expect(report.errorCount).toBeLessThan(10);
});
tap.test('Integrated Validator - With XML content', async () => {
const validator = await createValidator();
// Load a sample XML file if available
const xmlPath = path.join(
process.cwd(),
'corpus/xml-rechnung/3.1/ubl/01-01a-INVOICE_ubl.xml'
);
if (fs.existsSync(xmlPath)) {
const xmlContent = fs.readFileSync(xmlPath, 'utf-8');
const invoice = await EInvoice.fromXML(xmlContent);
const report = await validator.validateAuto(invoice, xmlContent);
console.log('\nXML validation with Schematron:');
console.log(` Format detected: ${report.format}`);
console.log(` Schematron enabled: ${report.schematronEnabled}`);
console.log(` Validation sources: ${
[...new Set(report.results.map(r => r.source))].join(', ')
}`);
expect(report.format).toBeDefined();
} else {
console.log('Sample XML not found, skipping XML validation test');
}
});
tap.test('Integrated Validator - Capabilities check', async () => {
const validator = new MainValidator();
const capabilities = validator.getCapabilities();
console.log('\nValidator capabilities:');
console.log(` Schematron: ${capabilities.schematron ? '✅' : '❌'}`);
console.log(` XRechnung: ${capabilities.xrechnung ? '✅' : '❌'}`);
console.log(` PEPPOL: ${capabilities.peppol ? '✅' : '❌'}`);
console.log(` Calculations: ${capabilities.calculations ? '✅' : '❌'}`);
console.log(` Code Lists: ${capabilities.codeLists ? '✅' : '❌'}`);
expect(capabilities.xrechnung).toBeTrue();
expect(capabilities.calculations).toBeTrue();
expect(capabilities.codeLists).toBeTrue();
});
tap.test('Integrated Validator - Deduplication', async () => {
const validator = new MainValidator();
// Create invoice that will trigger duplicate errors
const invoice = new EInvoice();
invoice.invoiceNumber = 'TEST-DUP';
const report = await validator.validate(invoice);
// Check that duplicates are removed
const ruleIds = report.results.map(r => r.ruleId);
const uniqueRuleIds = [...new Set(ruleIds)];
console.log(`\nDeduplication test:`);
console.log(` Total results: ${report.results.length}`);
console.log(` Unique rule IDs: ${uniqueRuleIds.length}`);
// Each rule+field combination should appear only once
const combinations = new Set();
let duplicates = 0;
for (const result of report.results) {
const key = `${result.ruleId}|${result.field || ''}`;
if (combinations.has(key)) {
duplicates++;
}
combinations.add(key);
}
console.log(` Duplicate combinations: ${duplicates}`);
expect(duplicates).toEqual(0);
});
export default tap.start();

View File

@@ -0,0 +1,328 @@
import { tap, expect } from '@git.zone/tstest/tapbundle';
import { PeppolValidator } from '../ts/formats/validation/peppol.validator.js';
import type { EInvoice } from '../ts/einvoice.js';
tap.test('PEPPOL Validator - basic instantiation', async () => {
const validator = PeppolValidator.create();
expect(validator).toBeInstanceOf(PeppolValidator);
// Singleton pattern
const validator2 = PeppolValidator.create();
expect(validator2).toEqual(validator);
});
tap.test('PEPPOL Validator - endpoint ID validation', async () => {
const validator = PeppolValidator.create();
const invoice: Partial<EInvoice> = {
metadata: {
profileId: 'urn:fdc:peppol.eu:2017:poacc:billing:3.0',
extensions: {
sellerEndpointId: '0088:1234567890128', // Valid GLN
buyerEndpointId: '0192:123456789' // Valid Norwegian org
}
}
};
const results = validator.validatePeppol(invoice as EInvoice);
const endpointErrors = results.filter(r => r.ruleId.startsWith('PEPPOL-T00'));
console.log('Endpoint validation results:', endpointErrors);
expect(endpointErrors.length).toEqual(0);
});
tap.test('PEPPOL Validator - invalid GLN endpoint', async () => {
const validator = PeppolValidator.create();
const invoice: Partial<EInvoice> = {
metadata: {
profileId: 'urn:fdc:peppol.eu:2017:poacc:billing:3.0',
extensions: {
sellerEndpointId: '0088:123456789012', // Invalid GLN (wrong check digit)
}
}
};
const results = validator.validatePeppol(invoice as EInvoice);
const endpointErrors = results.filter(r => r.ruleId === 'PEPPOL-T001');
expect(endpointErrors.length).toBeGreaterThan(0);
expect(endpointErrors[0].message).toInclude('Invalid seller endpoint ID');
});
tap.test('PEPPOL Validator - invalid endpoint format', async () => {
const validator = PeppolValidator.create();
const invoice: Partial<EInvoice> = {
metadata: {
profileId: 'urn:fdc:peppol.eu:2017:poacc:billing:3.0',
extensions: {
sellerEndpointId: 'invalid-format', // No scheme
}
}
};
const results = validator.validatePeppol(invoice as EInvoice);
const endpointErrors = results.filter(r => r.ruleId === 'PEPPOL-T001');
expect(endpointErrors.length).toBeGreaterThan(0);
expect(endpointErrors[0].severity).toEqual('error');
});
tap.test('PEPPOL Validator - document type validation', async () => {
const validator = PeppolValidator.create();
const invoice: Partial<EInvoice> = {
metadata: {
profileId: 'urn:fdc:peppol.eu:2017:poacc:billing:3.0',
extensions: {
documentTypeId: 'urn:oasis:names:specification:ubl:schema:xsd:Invoice-2::Invoice##urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0::2.1'
}
}
};
const results = validator.validatePeppol(invoice as EInvoice);
const docTypeErrors = results.filter(r => r.ruleId === 'PEPPOL-T003');
expect(docTypeErrors.length).toEqual(0);
});
tap.test('PEPPOL Validator - process ID validation', async () => {
const validator = PeppolValidator.create();
const invoice: Partial<EInvoice> = {
metadata: {
profileId: 'urn:fdc:peppol.eu:2017:poacc:billing:3.0',
extensions: {
processId: 'urn:fdc:peppol.eu:2017:poacc:billing:01:1.0'
}
}
};
const results = validator.validatePeppol(invoice as EInvoice);
const processErrors = results.filter(r => r.ruleId === 'PEPPOL-T004');
expect(processErrors.length).toEqual(0);
});
tap.test('PEPPOL Validator - invalid process ID', async () => {
const validator = PeppolValidator.create();
const invoice: Partial<EInvoice> = {
metadata: {
profileId: 'urn:fdc:peppol.eu:2017:poacc:billing:3.0',
extensions: {
processId: 'invalid:process:id'
}
}
};
const results = validator.validatePeppol(invoice as EInvoice);
const processErrors = results.filter(r => r.ruleId === 'PEPPOL-T004');
expect(processErrors.length).toBeGreaterThan(0);
expect(processErrors[0].severity).toEqual('warning');
});
tap.test('PEPPOL Validator - business rules', async () => {
const validator = PeppolValidator.create();
const invoice: Partial<EInvoice> = {
metadata: {
profileId: 'urn:fdc:peppol.eu:2017:poacc:billing:3.0',
// Missing both buyer reference and purchase order reference
},
from: {
type: 'company',
name: 'Test Company'
// Missing email
}
};
const results = validator.validatePeppol(invoice as EInvoice);
// Should have error for missing buyer reference
const buyerRefErrors = results.filter(r => r.ruleId === 'PEPPOL-B-01');
expect(buyerRefErrors.length).toBeGreaterThan(0);
// Should have warning for missing seller email
const emailWarnings = results.filter(r => r.ruleId === 'PEPPOL-B-02');
expect(emailWarnings.length).toBeGreaterThan(0);
});
tap.test('PEPPOL Validator - buyer reference present', async () => {
const validator = PeppolValidator.create();
const invoice: Partial<EInvoice> = {
metadata: {
profileId: 'urn:fdc:peppol.eu:2017:poacc:billing:3.0',
buyerReference: 'REF-12345'
}
};
const results = validator.validatePeppol(invoice as EInvoice);
const buyerRefErrors = results.filter(r => r.ruleId === 'PEPPOL-B-01');
expect(buyerRefErrors.length).toEqual(0);
});
tap.test('PEPPOL Validator - purchase order reference present', async () => {
const validator = PeppolValidator.create();
const invoice: Partial<EInvoice> = {
metadata: {
profileId: 'urn:fdc:peppol.eu:2017:poacc:billing:3.0',
extensions: {
purchaseOrderReference: 'PO-2025-001'
}
}
};
const results = validator.validatePeppol(invoice as EInvoice);
const buyerRefErrors = results.filter(r => r.ruleId === 'PEPPOL-B-01');
expect(buyerRefErrors.length).toEqual(0);
});
tap.test('PEPPOL Validator - payment means validation', async () => {
const validator = PeppolValidator.create();
const invoice: Partial<EInvoice> = {
metadata: {
profileId: 'urn:fdc:peppol.eu:2017:poacc:billing:3.0',
extensions: {
paymentMeans: {
paymentMeansCode: '30' // Valid code for credit transfer
}
}
}
};
const results = validator.validatePeppol(invoice as EInvoice);
const paymentErrors = results.filter(r => r.ruleId === 'PEPPOL-B-04');
expect(paymentErrors.length).toEqual(0);
});
tap.test('PEPPOL Validator - invalid payment means', async () => {
const validator = PeppolValidator.create();
const invoice: Partial<EInvoice> = {
metadata: {
profileId: 'urn:fdc:peppol.eu:2017:poacc:billing:3.0',
extensions: {
paymentMeans: {
paymentMeansCode: '999' // Invalid code
}
}
}
};
const results = validator.validatePeppol(invoice as EInvoice);
const paymentErrors = results.filter(r => r.ruleId === 'PEPPOL-B-04');
expect(paymentErrors.length).toBeGreaterThan(0);
expect(paymentErrors[0].severity).toEqual('error');
});
tap.test('PEPPOL Validator - non-PEPPOL invoice skips validation', async () => {
const validator = PeppolValidator.create();
const invoice: Partial<EInvoice> = {
metadata: {
profileId: 'urn:cen.eu:en16931:2017', // Not PEPPOL
}
};
const results = validator.validatePeppol(invoice as EInvoice);
expect(results.length).toEqual(0);
});
tap.test('PEPPOL Validator - scheme ID validation', async () => {
const validator = PeppolValidator.create();
const invoice: Partial<EInvoice> = {
metadata: {
profileId: 'urn:fdc:peppol.eu:2017:poacc:billing:3.0',
extensions: {
buyerPartyId: {
schemeId: '0088', // Valid GLN scheme
id: '1234567890128'
}
}
},
from: {
type: 'company',
name: 'Test Company',
registrationDetails: {
partyIdentification: {
schemeId: '9906', // Valid IT:VAT scheme
id: 'IT12345678901'
}
}
} as any
};
const results = validator.validatePeppol(invoice as EInvoice);
const schemeErrors = results.filter(r =>
r.ruleId === 'PEPPOL-T005' || r.ruleId === 'PEPPOL-T006'
);
expect(schemeErrors.length).toEqual(0);
});
tap.test('PEPPOL Validator - invalid scheme ID', async () => {
const validator = PeppolValidator.create();
const invoice: Partial<EInvoice> = {
metadata: {
profileId: 'urn:fdc:peppol.eu:2017:poacc:billing:3.0',
extensions: {
buyerPartyId: {
schemeId: '9999', // Invalid scheme
id: '12345'
}
}
}
};
const results = validator.validatePeppol(invoice as EInvoice);
const schemeErrors = results.filter(r => r.ruleId === 'PEPPOL-T006');
expect(schemeErrors.length).toBeGreaterThan(0);
expect(schemeErrors[0].severity).toEqual('warning');
});
tap.test('PEPPOL Validator - B2G detection', async () => {
const validator = PeppolValidator.create();
const invoice: Partial<EInvoice> = {
metadata: {
profileId: 'urn:fdc:peppol.eu:2017:poacc:billing:3.0',
extensions: {
buyerPartyId: {
schemeId: '0204', // German government Leitweg-ID
id: '991-12345-01'
},
buyerCategory: 'government'
}
},
to: {
type: 'company',
name: 'Government Agency'
}
};
const results = validator.validatePeppol(invoice as EInvoice);
// B2G should require endpoint IDs
const endpointErrors = results.filter(r =>
r.ruleId === 'PEPPOL-T001' || r.ruleId === 'PEPPOL-T002'
);
expect(endpointErrors.length).toBeGreaterThan(0);
expect(endpointErrors[0].message).toInclude('mandatory for PEPPOL B2G');
});
export default tap.start();

654
test/test.semantic-model.ts Normal file
View File

@@ -0,0 +1,654 @@
import { tap, expect } from '@git.zone/tstest/tapbundle';
import { SemanticModelValidator } from '../ts/formats/semantic/semantic.validator.js';
import { SemanticModelAdapter } from '../ts/formats/semantic/semantic.adapter.js';
import { EInvoice } from '../ts/einvoice.js';
import type { EN16931SemanticModel } from '../ts/formats/semantic/bt-bg.model.js';
tap.test('Semantic Model - adapter instantiation', async () => {
const adapter = new SemanticModelAdapter();
expect(adapter).toBeInstanceOf(SemanticModelAdapter);
const validator = new SemanticModelValidator();
expect(validator).toBeInstanceOf(SemanticModelValidator);
});
tap.test('Semantic Model - EInvoice to semantic model conversion', async () => {
const adapter = new SemanticModelAdapter();
const invoice = new EInvoice();
invoice.accountingDocId = 'INV-2025-001';
invoice.issueDate = new Date('2025-01-11');
invoice.accountingDocType = 'invoice';
invoice.currency = 'EUR';
invoice.from = {
type: 'company',
name: 'Test Seller GmbH',
address: {
streetName: 'Hauptstrasse 1',
houseNumber: '',
city: 'Berlin',
postalCode: '10115',
country: 'DE'
},
registrationDetails: {
vatId: 'DE123456789',
registrationId: '',
registrationName: 'Test Seller GmbH'
},
status: 'active',
foundedDate: {
year: 2024,
month: 1,
day: 1
}
} as any;
invoice.to = {
type: 'company',
name: 'Test Buyer SAS',
address: {
streetName: 'Rue de la Paix 10',
houseNumber: '',
city: 'Paris',
postalCode: '75001',
country: 'FR'
},
registrationDetails: {
vatId: 'FR987654321',
registrationId: '',
registrationName: 'Test Buyer SAS'
},
status: 'active',
foundedDate: {
year: 2024,
month: 1,
day: 1
}
} as any;
invoice.items = [{
position: 1,
name: 'Consulting Service',
unitQuantity: 10,
unitNetPrice: 100,
vatPercentage: 19,
unitType: 'HUR',
articleNumber: '',
description: 'Professional consulting services'
}];
const model = adapter.toSemanticModel(invoice);
// Verify core fields
expect(model.documentInformation.invoiceNumber).toEqual('INV-2025-001');
expect(model.documentInformation.currencyCode).toEqual('EUR');
expect(model.documentInformation.typeCode).toEqual('380'); // Invoice type code
// Verify seller
expect(model.seller.name).toEqual('Test Seller GmbH');
expect(model.seller.vatIdentifier).toEqual('DE123456789');
expect(model.seller.postalAddress.countryCode).toEqual('DE');
// Verify buyer
expect(model.buyer.name).toEqual('Test Buyer SAS');
expect(model.buyer.vatIdentifier).toEqual('FR987654321');
expect(model.buyer.postalAddress.countryCode).toEqual('FR');
// Verify lines
expect(model.invoiceLines.length).toEqual(1);
expect(model.invoiceLines[0].itemInformation.name).toEqual('Consulting Service');
expect(model.invoiceLines[0].invoicedQuantity).toEqual(10);
});
tap.test('Semantic Model - semantic model to EInvoice conversion', async () => {
const adapter = new SemanticModelAdapter();
const model: EN16931SemanticModel = {
documentInformation: {
invoiceNumber: 'INV-2025-002',
issueDate: new Date('2025-01-11'),
typeCode: '380',
currencyCode: 'USD'
},
seller: {
name: 'US Seller Inc',
vatIdentifier: 'US123456789',
postalAddress: {
addressLine1: '123 Main St',
city: 'New York',
postCode: '10001',
countryCode: 'US'
}
},
buyer: {
name: 'Canadian Buyer Ltd',
vatIdentifier: 'CA987654321',
postalAddress: {
addressLine1: '456 Queen St',
city: 'Toronto',
postCode: 'M5H 2N2',
countryCode: 'CA'
}
},
paymentInstructions: {
paymentMeansTypeCode: '30',
paymentAccountIdentifier: 'US12345678901234567890'
},
documentTotals: {
lineExtensionAmount: 1000,
taxExclusiveAmount: 1000,
taxInclusiveAmount: 1100,
payableAmount: 1100
},
invoiceLines: [{
identifier: '1',
invoicedQuantity: 5,
invoicedQuantityUnitOfMeasureCode: 'C62',
lineExtensionAmount: 1000,
priceDetails: {
itemNetPrice: 200
},
vatInformation: {
categoryCode: 'S',
rate: 10
},
itemInformation: {
name: 'Product A',
description: 'High quality product'
}
}]
};
const invoice = adapter.fromSemanticModel(model);
expect(invoice.accountingDocId).toEqual('INV-2025-002');
expect(invoice.currency).toEqual('USD');
expect(invoice.accountingDocType).toEqual('invoice');
expect(invoice.from.name).toEqual('US Seller Inc');
expect(invoice.to.name).toEqual('Canadian Buyer Ltd');
expect(invoice.items.length).toEqual(1);
expect(invoice.items[0].name).toEqual('Product A');
});
tap.test('Semantic Model - validation of mandatory business terms', async () => {
const validator = new SemanticModelValidator();
// Invalid invoice missing mandatory fields
const invoice = new EInvoice();
invoice.accountingDocId = ''; // Missing invoice number
invoice.issueDate = new Date('2025-01-11');
invoice.accountingDocType = 'invoice';
invoice.currency = 'EUR';
invoice.from = {
type: 'company',
name: 'Test Seller',
address: {
streetName: '',
houseNumber: '',
city: '',
postalCode: '',
country: 'DE'
},
registrationDetails: {
vatId: '',
registrationId: '',
registrationName: 'Test Seller'
},
status: 'active',
foundedDate: {
year: 2024,
month: 1,
day: 1
}
} as any;
invoice.to = {
type: 'company',
name: 'Test Buyer',
address: {
streetName: '',
houseNumber: '',
city: '',
postalCode: '',
country: 'FR'
},
registrationDetails: {
vatId: '',
registrationId: '',
registrationName: 'Test Buyer'
},
status: 'active',
foundedDate: {
year: 2024,
month: 1,
day: 1
}
} as any;
invoice.items = [];
const results = validator.validate(invoice);
// Should have errors for missing mandatory fields
const errors = results.filter(r => r.severity === 'error');
expect(errors.length).toBeGreaterThan(0);
// Check for specific BT errors
expect(errors.some(e => e.btReference === 'BT-1')).toBeTrue(); // Invoice number
expect(errors.some(e => e.bgReference === 'BG-25')).toBeTrue(); // Invoice lines
});
tap.test('Semantic Model - validation of valid invoice', async () => {
const validator = new SemanticModelValidator();
const invoice = new EInvoice();
invoice.accountingDocId = 'INV-2025-003';
invoice.issueDate = new Date('2025-01-11');
invoice.accountingDocType = 'invoice';
invoice.currency = 'EUR';
invoice.from = {
type: 'company',
name: 'Valid Seller GmbH',
address: {
streetName: 'Hauptstrasse 1',
houseNumber: '',
city: 'Berlin',
postalCode: '10115',
country: 'DE'
},
registrationDetails: {
vatId: 'DE123456789',
registrationId: '',
registrationName: 'Valid Seller GmbH'
},
status: 'active',
foundedDate: {
year: 2024,
month: 1,
day: 1
}
} as any;
invoice.to = {
type: 'company',
name: 'Valid Buyer SAS',
address: {
streetName: 'Rue de la Paix 10',
houseNumber: '',
city: 'Paris',
postalCode: '75001',
country: 'FR'
},
registrationDetails: {
vatId: 'FR987654321',
registrationId: '',
registrationName: 'Valid Buyer SAS'
},
status: 'active',
foundedDate: {
year: 2024,
month: 1,
day: 1
}
} as any;
invoice.items = [{
position: 1,
name: 'Consulting Service',
unitQuantity: 10,
unitNetPrice: 100,
vatPercentage: 19,
unitType: 'HUR',
articleNumber: '',
description: 'Professional consulting services'
}];
invoice.paymentAccount = {
iban: 'DE89370400440532013000',
institutionName: 'Test Bank'
} as any;
const results = validator.validate(invoice);
const errors = results.filter(r => r.severity === 'error');
console.log('Validation errors:', errors);
// Should have minimal or no errors for a valid invoice
expect(errors.length).toBeLessThanOrEqual(1); // Allow for payment means type code
});
tap.test('Semantic Model - BT/BG mapping', async () => {
const validator = new SemanticModelValidator();
const invoice = new EInvoice();
invoice.accountingDocId = 'INV-2025-004';
invoice.issueDate = new Date('2025-01-11');
invoice.accountingDocType = 'invoice';
invoice.currency = 'EUR';
invoice.from = {
type: 'company',
name: 'Mapping Test Seller',
address: {
streetName: '',
houseNumber: '',
city: '',
postalCode: '',
country: 'DE'
},
registrationDetails: {
vatId: '',
registrationId: '',
registrationName: 'Mapping Test Seller'
},
status: 'active',
foundedDate: {
year: 2024,
month: 1,
day: 1
}
} as any;
invoice.to = {
type: 'company',
name: 'Mapping Test Buyer',
address: {
streetName: '',
houseNumber: '',
city: '',
postalCode: '',
country: 'FR'
},
registrationDetails: {
vatId: '',
registrationId: '',
registrationName: 'Mapping Test Buyer'
},
status: 'active',
foundedDate: {
year: 2024,
month: 1,
day: 1
}
} as any;
invoice.items = [{
position: 1,
name: 'Test Item',
unitQuantity: 1,
unitNetPrice: 100,
vatPercentage: 19,
unitType: 'C62',
articleNumber: '',
description: 'Test item description'
}];
const mapping = validator.getBusinessTermMapping(invoice);
// Verify key mappings
expect(mapping.get('BT-1')).toEqual('INV-2025-004');
expect(mapping.get('BT-5')).toEqual('EUR');
expect(mapping.get('BT-27')).toEqual('Mapping Test Seller');
expect(mapping.get('BT-44')).toEqual('Mapping Test Buyer');
expect(mapping.has('BG-25')).toBeTrue(); // Invoice lines
const invoiceLines = mapping.get('BG-25');
expect(invoiceLines.length).toEqual(1);
});
tap.test('Semantic Model - credit note validation', async () => {
const validator = new SemanticModelValidator();
const creditNote = new EInvoice();
creditNote.accountingDocId = 'CN-2025-001';
creditNote.issueDate = new Date('2025-01-11');
creditNote.accountingDocType = 'creditNote';
creditNote.currency = 'EUR';
creditNote.from = {
type: 'company',
name: 'Credit Issuer',
address: {
streetName: '',
houseNumber: '',
city: '',
postalCode: '',
country: 'DE'
},
registrationDetails: {
vatId: '',
registrationId: '',
registrationName: 'Credit Issuer'
},
status: 'active',
foundedDate: {
year: 2024,
month: 1,
day: 1
}
} as any;
creditNote.to = {
type: 'company',
name: 'Credit Receiver',
address: {
streetName: '',
houseNumber: '',
city: '',
postalCode: '',
country: 'FR'
},
registrationDetails: {
vatId: '',
registrationId: '',
registrationName: 'Credit Receiver'
},
status: 'active',
foundedDate: {
year: 2024,
month: 1,
day: 1
}
} as any;
creditNote.items = [{
position: 1,
name: 'Refund Item',
unitQuantity: -1,
unitNetPrice: 100,
vatPercentage: 19,
unitType: 'C62',
articleNumber: '',
description: 'Refund for returned goods'
}];
const results = validator.validate(creditNote);
// Should have warning about missing preceding invoice reference
const warnings = results.filter(r => r.severity === 'warning');
expect(warnings.some(w => w.ruleId === 'COND-02')).toBeTrue();
});
tap.test('Semantic Model - VAT breakdown validation', async () => {
const adapter = new SemanticModelAdapter();
const invoice = new EInvoice();
invoice.accountingDocId = 'INV-2025-005';
invoice.issueDate = new Date('2025-01-11');
invoice.accountingDocType = 'invoice';
invoice.currency = 'EUR';
invoice.from = {
type: 'company',
name: 'VAT Test Seller',
address: {
streetName: '',
houseNumber: '',
city: '',
postalCode: '',
country: 'DE'
},
registrationDetails: {
vatId: '',
registrationId: '',
registrationName: 'VAT Test Seller'
},
status: 'active',
foundedDate: {
year: 2024,
month: 1,
day: 1
}
} as any;
invoice.to = {
type: 'company',
name: 'VAT Test Buyer',
address: {
streetName: '',
houseNumber: '',
city: '',
postalCode: '',
country: 'FR'
},
registrationDetails: {
vatId: '',
registrationId: '',
registrationName: 'VAT Test Buyer'
},
status: 'active',
foundedDate: {
year: 2024,
month: 1,
day: 1
}
} as any;
invoice.items = [
{
position: 1,
name: 'Standard Rate Item',
unitQuantity: 1,
unitNetPrice: 100,
vatPercentage: 19,
unitType: 'C62',
articleNumber: '',
description: 'Product with standard VAT rate'
},
{
position: 2,
name: 'Zero Rate Item',
unitQuantity: 1,
unitNetPrice: 50,
vatPercentage: 0,
unitType: 'C62',
articleNumber: '',
description: 'Product with zero VAT rate'
}
];
const model = adapter.toSemanticModel(invoice);
// Should create VAT breakdown
expect(model.vatBreakdown).toBeDefined();
if (model.vatBreakdown) {
// Default implementation creates single breakdown from totals
expect(model.vatBreakdown.length).toBeGreaterThan(0);
}
});
tap.test('Semantic Model - complete semantic model validation', async () => {
const adapter = new SemanticModelAdapter();
const model: EN16931SemanticModel = {
documentInformation: {
invoiceNumber: 'COMPLETE-001',
issueDate: new Date('2025-01-11'),
typeCode: '380',
currencyCode: 'EUR',
notes: [{ noteContent: 'Test invoice' }]
},
processControl: {
specificationIdentifier: 'urn:cen.eu:en16931:2017'
},
references: {
buyerReference: 'REF-12345',
purchaseOrderReference: 'PO-2025-001'
},
seller: {
name: 'Complete Seller GmbH',
vatIdentifier: 'DE123456789',
legalRegistrationIdentifier: 'HRB 12345',
postalAddress: {
addressLine1: 'Hauptstrasse 1',
city: 'Berlin',
postCode: '10115',
countryCode: 'DE'
},
contact: {
contactPoint: 'John Doe',
telephoneNumber: '+49 30 12345678',
emailAddress: 'john@seller.de'
}
},
buyer: {
name: 'Complete Buyer SAS',
vatIdentifier: 'FR987654321',
postalAddress: {
addressLine1: 'Rue de la Paix 10',
city: 'Paris',
postCode: '75001',
countryCode: 'FR'
}
},
delivery: {
name: 'Delivery Location',
actualDeliveryDate: new Date('2025-01-10')
},
paymentInstructions: {
paymentMeansTypeCode: '30',
paymentAccountIdentifier: 'DE89370400440532013000',
paymentServiceProviderIdentifier: 'COBADEFFXXX'
},
documentTotals: {
lineExtensionAmount: 1000,
taxExclusiveAmount: 1000,
taxInclusiveAmount: 1190,
payableAmount: 1190
},
vatBreakdown: [{
vatCategoryTaxableAmount: 1000,
vatCategoryTaxAmount: 190,
vatCategoryCode: 'S',
vatCategoryRate: 19
}],
invoiceLines: [{
identifier: '1',
invoicedQuantity: 10,
invoicedQuantityUnitOfMeasureCode: 'HUR',
lineExtensionAmount: 1000,
priceDetails: {
itemNetPrice: 100
},
vatInformation: {
categoryCode: 'S',
rate: 19
},
itemInformation: {
name: 'Professional Services',
description: 'Consulting and implementation'
}
}]
};
// Validate the model
const errors = adapter.validateSemanticModel(model);
console.log('Semantic model validation errors:', errors);
expect(errors.length).toEqual(0);
});
export default tap.start();

View File

@@ -0,0 +1,368 @@
import { tap, expect } from '@git.zone/tstest/tapbundle';
import { XRechnungValidator } from '../ts/formats/validation/xrechnung.validator.js';
import type { EInvoice } from '../ts/einvoice.js';
tap.test('XRechnungValidator - Leitweg-ID validation', async () => {
const validator = XRechnungValidator.create();
// Create test invoice with XRechnung profile
const invoice: Partial<EInvoice> = {
invoiceNumber: 'INV-2025-001',
metadata: {
profileId: 'urn:cen.eu:en16931:2017#compliant#urn:xeinkauf.de:kosit:xrechnung_3.0',
buyerReference: '04-123456789012-01'
}
};
const results = validator.validateXRechnung(invoice as EInvoice);
// Valid Leitweg-ID should pass
const leitwegErrors = results.filter(r => r.ruleId === 'XR-DE-01');
expect(leitwegErrors).toHaveLength(0);
});
tap.test('XRechnungValidator - Invalid Leitweg-ID', async () => {
const validator = XRechnungValidator.create();
const invoice: Partial<EInvoice> = {
invoiceNumber: 'INV-2025-002',
metadata: {
profileId: 'urn:cen.eu:en16931:2017#compliant#urn:xeinkauf.de:kosit:xrechnung_3.0',
buyerReference: '4-12345-1' // Invalid format
}
};
const results = validator.validateXRechnung(invoice as EInvoice);
// Should have Leitweg-ID format error
const leitwegErrors = results.filter(r => r.ruleId === 'XR-DE-01');
expect(leitwegErrors).toHaveLength(1);
expect(leitwegErrors[0].severity).toEqual('error');
});
tap.test('XRechnungValidator - IBAN validation', async () => {
const validator = XRechnungValidator.create();
const invoice: Partial<EInvoice> = {
invoiceNumber: 'INV-2025-003',
metadata: {
profileId: 'urn:cen.eu:en16931:2017#compliant#urn:xeinkauf.de:kosit:xrechnung_3.0',
buyerReference: 'REF-123',
extensions: {
paymentMeans: [
{
type: 'SEPA',
iban: 'DE89370400440532013000', // Valid German IBAN
bic: 'COBADEFFXXX'
}
]
}
}
};
const results = validator.validateXRechnung(invoice as EInvoice);
// Valid IBAN should pass
const ibanErrors = results.filter(r => r.ruleId === 'XR-DE-19');
expect(ibanErrors).toHaveLength(0);
});
tap.test('XRechnungValidator - Invalid IBAN checksum', async () => {
const validator = XRechnungValidator.create();
const invoice: Partial<EInvoice> = {
invoiceNumber: 'INV-2025-004',
metadata: {
profileId: 'urn:cen.eu:en16931:2017#compliant#urn:xeinkauf.de:kosit:xrechnung_3.0',
buyerReference: 'REF-124',
extensions: {
paymentMeans: [
{
type: 'SEPA',
iban: 'DE89370400440532013001' // Invalid checksum
}
]
}
}
};
const results = validator.validateXRechnung(invoice as EInvoice);
// Should have IBAN checksum error
const ibanErrors = results.filter(r => r.ruleId === 'XR-DE-19');
expect(ibanErrors).toHaveLength(1);
expect(ibanErrors[0].message).toInclude('Invalid IBAN checksum');
});
tap.test('XRechnungValidator - BIC validation', async () => {
const validator = XRechnungValidator.create();
const invoice: Partial<EInvoice> = {
invoiceNumber: 'INV-2025-005',
metadata: {
profileId: 'urn:cen.eu:en16931:2017#compliant#urn:xeinkauf.de:kosit:xrechnung_3.0',
buyerReference: 'REF-125',
extensions: {
paymentMeans: [
{
type: 'SEPA',
iban: 'DE89370400440532013000',
bic: 'COBADEFF' // Valid 8-character BIC
}
]
}
}
};
const results = validator.validateXRechnung(invoice as EInvoice);
// Valid BIC should pass
const bicErrors = results.filter(r => r.ruleId === 'XR-DE-20');
expect(bicErrors).toHaveLength(0);
});
tap.test('XRechnungValidator - Invalid BIC format', async () => {
const validator = XRechnungValidator.create();
const invoice: Partial<EInvoice> = {
invoiceNumber: 'INV-2025-006',
metadata: {
profileId: 'urn:cen.eu:en16931:2017#compliant#urn:xeinkauf.de:kosit:xrechnung_3.0',
buyerReference: 'REF-126',
extensions: {
paymentMeans: [
{
type: 'SEPA',
iban: 'DE89370400440532013000',
bic: 'INVALID' // Invalid BIC format
}
]
}
}
};
const results = validator.validateXRechnung(invoice as EInvoice);
// Should have BIC format error
const bicErrors = results.filter(r => r.ruleId === 'XR-DE-20');
expect(bicErrors).toHaveLength(1);
expect(bicErrors[0].message).toInclude('Invalid BIC format');
});
tap.test('XRechnungValidator - Mandatory buyer reference', async () => {
const validator = XRechnungValidator.create();
const invoice: Partial<EInvoice> = {
invoiceNumber: 'INV-2025-007',
metadata: {
profileId: 'urn:cen.eu:en16931:2017#compliant#urn:xeinkauf.de:kosit:xrechnung_3.0'
// Missing buyerReference
}
};
const results = validator.validateXRechnung(invoice as EInvoice);
// Should have mandatory buyer reference error
const refErrors = results.filter(r => r.ruleId === 'XR-DE-15');
expect(refErrors).toHaveLength(1);
expect(refErrors[0].severity).toEqual('error');
});
tap.test('XRechnungValidator - Seller contact validation', async () => {
const validator = XRechnungValidator.create();
const invoice: Partial<EInvoice> = {
invoiceNumber: 'INV-2025-008',
metadata: {
profileId: 'urn:cen.eu:en16931:2017#compliant#urn:xeinkauf.de:kosit:xrechnung_3.0',
buyerReference: 'REF-127',
extensions: {
sellerContact: {
name: 'John Doe',
email: 'john.doe@example.com',
phone: '+49 30 12345678'
}
}
}
};
const results = validator.validateXRechnung(invoice as EInvoice);
// Valid seller contact should pass
const contactErrors = results.filter(r => r.ruleId === 'XR-DE-02');
expect(contactErrors).toHaveLength(0);
});
tap.test('XRechnungValidator - Missing seller contact', async () => {
const validator = XRechnungValidator.create();
const invoice: Partial<EInvoice> = {
invoiceNumber: 'INV-2025-009',
metadata: {
profileId: 'urn:cen.eu:en16931:2017#compliant#urn:xeinkauf.de:kosit:xrechnung_3.0',
buyerReference: 'REF-128'
// Missing sellerContact
}
};
const results = validator.validateXRechnung(invoice as EInvoice);
// Should have missing seller contact error
const contactErrors = results.filter(r => r.ruleId === 'XR-DE-02');
expect(contactErrors).toHaveLength(1);
expect(contactErrors[0].severity).toEqual('error');
});
tap.test('XRechnungValidator - German VAT ID validation', async () => {
const validator = XRechnungValidator.create();
const invoice: Partial<EInvoice> = {
invoiceNumber: 'INV-2025-010',
from: {
type: 'company' as const,
name: 'Test Company',
registrationDetails: {
vatId: 'DE123456789' // Valid German VAT ID format
}
},
metadata: {
profileId: 'urn:cen.eu:en16931:2017#compliant#urn:xeinkauf.de:kosit:xrechnung_3.0',
buyerReference: 'REF-129',
sellerTaxId: 'DE123456789'
}
};
const results = validator.validateXRechnung(invoice as EInvoice);
// Valid German VAT ID should pass
const vatErrors = results.filter(r => r.ruleId === 'XR-DE-04');
expect(vatErrors).toHaveLength(0);
});
tap.test('XRechnungValidator - Invalid German VAT ID', async () => {
const validator = XRechnungValidator.create();
const invoice: Partial<EInvoice> = {
invoiceNumber: 'INV-2025-011',
metadata: {
profileId: 'urn:cen.eu:en16931:2017#compliant#urn:xeinkauf.de:kosit:xrechnung_3.0',
buyerReference: 'REF-130',
sellerTaxId: 'DE12345' // Invalid - too short
}
};
const results = validator.validateXRechnung(invoice as EInvoice);
// Should have invalid VAT ID error
const vatErrors = results.filter(r => r.ruleId === 'XR-DE-04');
expect(vatErrors).toHaveLength(1);
expect(vatErrors[0].message).toInclude('Invalid German VAT ID format');
});
tap.test('XRechnungValidator - Non-XRechnung invoice', async () => {
const validator = XRechnungValidator.create();
const invoice: Partial<EInvoice> = {
invoiceNumber: 'INV-2025-012',
metadata: {
profileId: 'urn:cen.eu:en16931:2017' // Not XRechnung
}
};
const results = validator.validateXRechnung(invoice as EInvoice);
// Should not validate non-XRechnung invoices
expect(results).toHaveLength(0);
});
tap.test('XRechnungValidator - SEPA country validation', async () => {
const validator = XRechnungValidator.create();
const invoice: Partial<EInvoice> = {
invoiceNumber: 'INV-2025-013',
metadata: {
profileId: 'urn:cen.eu:en16931:2017#compliant#urn:xeinkauf.de:kosit:xrechnung_3.0',
buyerReference: 'REF-131',
extensions: {
paymentMeans: [
{
type: 'SEPA',
iban: 'US12345678901234567890123456789' // Non-SEPA country
}
]
}
}
};
const results = validator.validateXRechnung(invoice as EInvoice);
// Should have warning for non-SEPA country
const sepaWarnings = results.filter(r => r.ruleId === 'XR-DE-19' && r.severity === 'warning');
expect(sepaWarnings.length).toBeGreaterThan(0);
expect(sepaWarnings[0].message).toInclude('not in SEPA zone');
});
tap.test('XRechnungValidator - B2G Leitweg-ID requirement', async () => {
const validator = XRechnungValidator.create();
const invoice: Partial<EInvoice> = {
invoiceNumber: 'INV-2025-014',
to: {
name: 'Bundesamt für Migration' // Public entity
},
metadata: {
profileId: 'urn:cen.eu:en16931:2017#compliant#urn:xeinkauf.de:kosit:xrechnung_3.0',
// Missing buyerReference for B2G
}
};
const results = validator.validateXRechnung(invoice as EInvoice);
// Should require Leitweg-ID for B2G
const b2gErrors = results.filter(r => r.ruleId === 'XR-DE-15');
expect(b2gErrors).toHaveLength(1);
expect(b2gErrors[0].message).toInclude('mandatory for B2G invoices');
});
tap.test('XRechnungValidator - Complete valid XRechnung invoice', async () => {
const validator = XRechnungValidator.create();
const invoice: Partial<EInvoice> = {
invoiceNumber: 'INV-2025-015',
from: {
type: 'company' as const,
name: 'Example GmbH',
registrationDetails: {
vatId: 'DE123456789'
}
},
metadata: {
profileId: 'urn:cen.eu:en16931:2017#compliant#urn:xeinkauf.de:kosit:xrechnung_3.0',
buyerReference: '991-12345678901-23',
sellerTaxId: 'DE123456789',
extensions: {
sellerContact: {
name: 'Sales Department',
email: 'sales@example.de',
phone: '+49 30 98765432'
},
paymentMeans: [
{
type: 'SEPA',
iban: 'DE89370400440532013000',
bic: 'COBADEFFXXX',
accountName: 'Example GmbH'
}
]
}
}
};
const results = validator.validateXRechnung(invoice as EInvoice);
// Complete valid invoice should have no errors
const errors = results.filter(r => r.severity === 'error');
expect(errors).toHaveLength(0);
});
export default tap.start();