368 lines
11 KiB
TypeScript
368 lines
11 KiB
TypeScript
|
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();
|