Files
einvoice/test/test.semantic-model.ts

660 lines
16 KiB
TypeScript
Raw Normal View History

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();