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