660 lines
16 KiB
TypeScript
660 lines
16 KiB
TypeScript
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.metadata = {
|
|
...invoice.metadata,
|
|
extensions: {
|
|
...invoice.metadata?.extensions,
|
|
paymentAccount: {
|
|
iban: 'DE89370400440532013000',
|
|
institutionName: 'Test Bank'
|
|
}
|
|
}
|
|
};
|
|
|
|
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(); |