668 lines
26 KiB
TypeScript
668 lines
26 KiB
TypeScript
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
|
import * as plugins from '../plugins.js';
|
|
import { EInvoice } from '../../../ts/index.js';
|
|
import { CorpusLoader } from '../corpus.loader.js';
|
|
import { PerformanceTracker } from '../performance.tracker.js';
|
|
|
|
tap.test('CONV-05: Mandatory Fields - should ensure all mandatory fields are preserved', async (t) => {
|
|
// CONV-05: Verify mandatory fields are maintained during format conversion
|
|
// This test ensures no required data is lost during transformation
|
|
|
|
const performanceTracker = new PerformanceTracker('CONV-05: Mandatory Fields');
|
|
const corpusLoader = new CorpusLoader();
|
|
|
|
t.test('EN16931 mandatory fields in UBL', async () => {
|
|
const startTime = performance.now();
|
|
|
|
// UBL invoice with all EN16931 mandatory fields
|
|
const ublInvoice = `<?xml version="1.0" encoding="UTF-8"?>
|
|
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"
|
|
xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2"
|
|
xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2">
|
|
<!-- BT-1: Invoice number (mandatory) -->
|
|
<cbc:ID>MANDATORY-UBL-001</cbc:ID>
|
|
<!-- BT-2: Invoice issue date (mandatory) -->
|
|
<cbc:IssueDate>2025-01-25</cbc:IssueDate>
|
|
<!-- BT-3: Invoice type code (mandatory) -->
|
|
<cbc:InvoiceTypeCode>380</cbc:InvoiceTypeCode>
|
|
<!-- BT-5: Invoice currency code (mandatory) -->
|
|
<cbc:DocumentCurrencyCode>EUR</cbc:DocumentCurrencyCode>
|
|
|
|
<!-- BG-4: Seller (mandatory) -->
|
|
<cac:AccountingSupplierParty>
|
|
<cac:Party>
|
|
<!-- BT-27: Seller name (mandatory) -->
|
|
<cac:PartyLegalEntity>
|
|
<cbc:RegistrationName>Mandatory Fields Supplier AB</cbc:RegistrationName>
|
|
</cac:PartyLegalEntity>
|
|
<!-- BG-5: Seller postal address (mandatory) -->
|
|
<cac:PostalAddress>
|
|
<!-- BT-35: Seller address line 1 -->
|
|
<cbc:StreetName>Kungsgatan 10</cbc:StreetName>
|
|
<!-- BT-37: Seller city (mandatory) -->
|
|
<cbc:CityName>Stockholm</cbc:CityName>
|
|
<!-- BT-38: Seller post code -->
|
|
<cbc:PostalZone>11143</cbc:PostalZone>
|
|
<!-- BT-40: Seller country code (mandatory) -->
|
|
<cac:Country>
|
|
<cbc:IdentificationCode>SE</cbc:IdentificationCode>
|
|
</cac:Country>
|
|
</cac:PostalAddress>
|
|
<!-- BT-31: Seller VAT identifier -->
|
|
<cac:PartyTaxScheme>
|
|
<cbc:CompanyID>SE123456789001</cbc:CompanyID>
|
|
<cac:TaxScheme>
|
|
<cbc:ID>VAT</cbc:ID>
|
|
</cac:TaxScheme>
|
|
</cac:PartyTaxScheme>
|
|
</cac:Party>
|
|
</cac:AccountingSupplierParty>
|
|
|
|
<!-- BG-7: Buyer (mandatory) -->
|
|
<cac:AccountingCustomerParty>
|
|
<cac:Party>
|
|
<!-- BT-44: Buyer name (mandatory) -->
|
|
<cac:PartyLegalEntity>
|
|
<cbc:RegistrationName>Mandatory Fields Customer AS</cbc:RegistrationName>
|
|
</cac:PartyLegalEntity>
|
|
<!-- BG-8: Buyer postal address (mandatory) -->
|
|
<cac:PostalAddress>
|
|
<!-- BT-50: Buyer address line 1 -->
|
|
<cbc:StreetName>Karl Johans gate 1</cbc:StreetName>
|
|
<!-- BT-52: Buyer city (mandatory) -->
|
|
<cbc:CityName>Oslo</cbc:CityName>
|
|
<!-- BT-53: Buyer post code -->
|
|
<cbc:PostalZone>0154</cbc:PostalZone>
|
|
<!-- BT-55: Buyer country code (mandatory) -->
|
|
<cac:Country>
|
|
<cbc:IdentificationCode>NO</cbc:IdentificationCode>
|
|
</cac:Country>
|
|
</cac:PostalAddress>
|
|
</cac:Party>
|
|
</cac:AccountingCustomerParty>
|
|
|
|
<!-- BG-22: Document totals (mandatory) -->
|
|
<cac:LegalMonetaryTotal>
|
|
<!-- BT-106: Sum of Invoice line net amount -->
|
|
<cbc:LineExtensionAmount currencyID="EUR">1000.00</cbc:LineExtensionAmount>
|
|
<!-- BT-109: Invoice total amount without VAT -->
|
|
<cbc:TaxExclusiveAmount currencyID="EUR">1000.00</cbc:TaxExclusiveAmount>
|
|
<!-- BT-112: Invoice total amount with VAT -->
|
|
<cbc:TaxInclusiveAmount currencyID="EUR">1190.00</cbc:TaxInclusiveAmount>
|
|
<!-- BT-115: Amount due for payment (mandatory) -->
|
|
<cbc:PayableAmount currencyID="EUR">1190.00</cbc:PayableAmount>
|
|
</cac:LegalMonetaryTotal>
|
|
|
|
<!-- BG-23: VAT breakdown (mandatory for VAT invoices) -->
|
|
<cac:TaxTotal>
|
|
<!-- BT-110: Invoice total VAT amount -->
|
|
<cbc:TaxAmount currencyID="EUR">190.00</cbc:TaxAmount>
|
|
<cac:TaxSubtotal>
|
|
<!-- BT-116: VAT category taxable amount -->
|
|
<cbc:TaxableAmount currencyID="EUR">1000.00</cbc:TaxableAmount>
|
|
<!-- BT-117: VAT category tax amount -->
|
|
<cbc:TaxAmount currencyID="EUR">190.00</cbc:TaxAmount>
|
|
<cac:TaxCategory>
|
|
<!-- BT-118: VAT category code (mandatory) -->
|
|
<cbc:ID>S</cbc:ID>
|
|
<!-- BT-119: VAT category rate -->
|
|
<cbc:Percent>19</cbc:Percent>
|
|
<cac:TaxScheme>
|
|
<cbc:ID>VAT</cbc:ID>
|
|
</cac:TaxScheme>
|
|
</cac:TaxCategory>
|
|
</cac:TaxSubtotal>
|
|
</cac:TaxTotal>
|
|
|
|
<!-- BG-25: Invoice line (mandatory - at least one) -->
|
|
<cac:InvoiceLine>
|
|
<!-- BT-126: Invoice line identifier (mandatory) -->
|
|
<cbc:ID>1</cbc:ID>
|
|
<!-- BT-129: Invoiced quantity (mandatory) -->
|
|
<cbc:InvoicedQuantity unitCode="C62">10</cbc:InvoicedQuantity>
|
|
<!-- BT-131: Invoice line net amount (mandatory) -->
|
|
<cbc:LineExtensionAmount currencyID="EUR">1000.00</cbc:LineExtensionAmount>
|
|
<!-- BT-153: Item name (mandatory) -->
|
|
<cac:Item>
|
|
<cbc:Name>Mandatory Test Product</cbc:Name>
|
|
<!-- BT-151: Item VAT category code (mandatory) -->
|
|
<cac:ClassifiedTaxCategory>
|
|
<cbc:ID>S</cbc:ID>
|
|
<cbc:Percent>19</cbc:Percent>
|
|
<cac:TaxScheme>
|
|
<cbc:ID>VAT</cbc:ID>
|
|
</cac:TaxScheme>
|
|
</cac:ClassifiedTaxCategory>
|
|
</cac:Item>
|
|
<!-- BT-146: Item net price (mandatory) -->
|
|
<cac:Price>
|
|
<cbc:PriceAmount currencyID="EUR">100.00</cbc:PriceAmount>
|
|
</cac:Price>
|
|
</cac:InvoiceLine>
|
|
</Invoice>`;
|
|
|
|
const einvoice = new EInvoice();
|
|
await einvoice.loadFromString(ublInvoice);
|
|
|
|
const xmlString = einvoice.getXmlString();
|
|
const invoiceData = einvoice.getInvoiceData();
|
|
|
|
// Verify mandatory fields are present
|
|
const mandatoryChecks = {
|
|
'Invoice number': xmlString.includes('MANDATORY-UBL-001'),
|
|
'Issue date': xmlString.includes('2025-01-25'),
|
|
'Invoice type': xmlString.includes('380'),
|
|
'Currency': xmlString.includes('EUR'),
|
|
'Seller name': xmlString.includes('Mandatory Fields Supplier'),
|
|
'Seller country': xmlString.includes('SE'),
|
|
'Buyer name': xmlString.includes('Mandatory Fields Customer'),
|
|
'Buyer country': xmlString.includes('NO'),
|
|
'Payable amount': xmlString.includes('1190.00'),
|
|
'VAT amount': xmlString.includes('190.00'),
|
|
'Line ID': xmlString.includes('<cbc:ID>1</cbc:ID>') || xmlString.includes('<ram:LineID>1</ram:LineID>'),
|
|
'Item name': xmlString.includes('Mandatory Test Product')
|
|
};
|
|
|
|
const missingFields = Object.entries(mandatoryChecks)
|
|
.filter(([field, present]) => !present)
|
|
.map(([field]) => field);
|
|
|
|
if (missingFields.length > 0) {
|
|
console.log('Missing mandatory fields:', missingFields);
|
|
} else {
|
|
console.log('All EN16931 mandatory fields preserved');
|
|
}
|
|
|
|
expect(missingFields.length).toBe(0);
|
|
|
|
const elapsed = performance.now() - startTime;
|
|
performanceTracker.addMeasurement('en16931-mandatory', elapsed);
|
|
});
|
|
|
|
t.test('EN16931 mandatory fields in CII', async () => {
|
|
const startTime = performance.now();
|
|
|
|
// CII invoice with all mandatory fields
|
|
const ciiInvoice = `<?xml version="1.0" encoding="UTF-8"?>
|
|
<rsm:CrossIndustryInvoice xmlns:rsm="urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100"
|
|
xmlns:ram="urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100"
|
|
xmlns:udt="urn:un:unece:uncefact:data:standard:UnqualifiedDataType:100">
|
|
<rsm:ExchangedDocumentContext>
|
|
<ram:GuidelineSpecifiedDocumentContextParameter>
|
|
<ram:ID>urn:cen.eu:en16931:2017</ram:ID>
|
|
</ram:GuidelineSpecifiedDocumentContextParameter>
|
|
</rsm:ExchangedDocumentContext>
|
|
|
|
<rsm:ExchangedDocument>
|
|
<!-- BT-1: Invoice number (mandatory) -->
|
|
<ram:ID>MANDATORY-CII-001</ram:ID>
|
|
<!-- BT-3: Invoice type code (mandatory) -->
|
|
<ram:TypeCode>380</ram:TypeCode>
|
|
<!-- BT-2: Invoice issue date (mandatory) -->
|
|
<ram:IssueDateTime>
|
|
<udt:DateTimeString format="102">20250125</udt:DateTimeString>
|
|
</ram:IssueDateTime>
|
|
</rsm:ExchangedDocument>
|
|
|
|
<rsm:SupplyChainTradeTransaction>
|
|
<!-- Invoice lines -->
|
|
<ram:IncludedSupplyChainTradeLineItem>
|
|
<ram:AssociatedDocumentLineDocument>
|
|
<!-- BT-126: Line ID (mandatory) -->
|
|
<ram:LineID>1</ram:LineID>
|
|
</ram:AssociatedDocumentLineDocument>
|
|
<ram:SpecifiedTradeProduct>
|
|
<!-- BT-153: Item name (mandatory) -->
|
|
<ram:Name>CII Mandatory Product</ram:Name>
|
|
</ram:SpecifiedTradeProduct>
|
|
<ram:SpecifiedLineTradeAgreement>
|
|
<ram:NetPriceProductTradePrice>
|
|
<!-- BT-146: Net price (mandatory) -->
|
|
<ram:ChargeAmount>100.00</ram:ChargeAmount>
|
|
</ram:NetPriceProductTradePrice>
|
|
</ram:SpecifiedLineTradeAgreement>
|
|
<ram:SpecifiedLineTradeDelivery>
|
|
<!-- BT-129: Quantity (mandatory) -->
|
|
<ram:BilledQuantity unitCode="C62">10</ram:BilledQuantity>
|
|
</ram:SpecifiedLineTradeDelivery>
|
|
<ram:SpecifiedLineTradeSettlement>
|
|
<ram:ApplicableTradeTax>
|
|
<ram:TypeCode>VAT</ram:TypeCode>
|
|
<!-- BT-151: VAT category (mandatory) -->
|
|
<ram:CategoryCode>S</ram:CategoryCode>
|
|
<ram:RateApplicablePercent>19</ram:RateApplicablePercent>
|
|
</ram:ApplicableTradeTax>
|
|
<ram:SpecifiedTradeSettlementLineMonetarySummation>
|
|
<!-- BT-131: Line net amount (mandatory) -->
|
|
<ram:LineTotalAmount>1000.00</ram:LineTotalAmount>
|
|
</ram:SpecifiedTradeSettlementLineMonetarySummation>
|
|
</ram:SpecifiedLineTradeSettlement>
|
|
</ram:IncludedSupplyChainTradeLineItem>
|
|
|
|
<ram:ApplicableHeaderTradeAgreement>
|
|
<!-- BG-4: Seller (mandatory) -->
|
|
<ram:SellerTradeParty>
|
|
<!-- BT-27: Seller name (mandatory) -->
|
|
<ram:Name>CII Mandatory Seller</ram:Name>
|
|
<!-- BG-5: Seller address (mandatory) -->
|
|
<ram:PostalTradeAddress>
|
|
<!-- BT-35: Address line -->
|
|
<ram:LineOne>Musterstraße 1</ram:LineOne>
|
|
<!-- BT-37: City (mandatory) -->
|
|
<ram:CityName>Berlin</ram:CityName>
|
|
<!-- BT-38: Post code -->
|
|
<ram:PostcodeCode>10115</ram:PostcodeCode>
|
|
<!-- BT-40: Country (mandatory) -->
|
|
<ram:CountryID>DE</ram:CountryID>
|
|
</ram:PostalTradeAddress>
|
|
<ram:SpecifiedTaxRegistration>
|
|
<!-- BT-31: VAT ID -->
|
|
<ram:ID schemeID="VA">DE123456789</ram:ID>
|
|
</ram:SpecifiedTaxRegistration>
|
|
</ram:SellerTradeParty>
|
|
<!-- BG-7: Buyer (mandatory) -->
|
|
<ram:BuyerTradeParty>
|
|
<!-- BT-44: Buyer name (mandatory) -->
|
|
<ram:Name>CII Mandatory Buyer</ram:Name>
|
|
<!-- BG-8: Buyer address (mandatory) -->
|
|
<ram:PostalTradeAddress>
|
|
<!-- BT-50: Address line -->
|
|
<ram:LineOne>Schulstraße 10</ram:LineOne>
|
|
<!-- BT-52: City (mandatory) -->
|
|
<ram:CityName>Hamburg</ram:CityName>
|
|
<!-- BT-53: Post code -->
|
|
<ram:PostcodeCode>20095</ram:PostcodeCode>
|
|
<!-- BT-55: Country (mandatory) -->
|
|
<ram:CountryID>DE</ram:CountryID>
|
|
</ram:PostalTradeAddress>
|
|
</ram:BuyerTradeParty>
|
|
</ram:ApplicableHeaderTradeAgreement>
|
|
|
|
<ram:ApplicableHeaderTradeSettlement>
|
|
<!-- BT-5: Currency (mandatory) -->
|
|
<ram:InvoiceCurrencyCode>EUR</ram:InvoiceCurrencyCode>
|
|
<!-- BG-23: VAT breakdown (mandatory) -->
|
|
<ram:ApplicableTradeTax>
|
|
<ram:CalculatedAmount>190.00</ram:CalculatedAmount>
|
|
<ram:TypeCode>VAT</ram:TypeCode>
|
|
<!-- BT-118: VAT category (mandatory) -->
|
|
<ram:CategoryCode>S</ram:CategoryCode>
|
|
<!-- BT-116: Taxable amount -->
|
|
<ram:BasisAmount>1000.00</ram:BasisAmount>
|
|
<!-- BT-119: VAT rate -->
|
|
<ram:RateApplicablePercent>19</ram:RateApplicablePercent>
|
|
</ram:ApplicableTradeTax>
|
|
<!-- BG-22: Totals (mandatory) -->
|
|
<ram:SpecifiedTradeSettlementHeaderMonetarySummation>
|
|
<!-- BT-106: Line total -->
|
|
<ram:LineTotalAmount>1000.00</ram:LineTotalAmount>
|
|
<!-- BT-109: Tax exclusive -->
|
|
<ram:TaxBasisTotalAmount>1000.00</ram:TaxBasisTotalAmount>
|
|
<!-- BT-110/117: Tax amount -->
|
|
<ram:TaxTotalAmount currencyID="EUR">190.00</ram:TaxTotalAmount>
|
|
<!-- BT-112: Grand total -->
|
|
<ram:GrandTotalAmount>1190.00</ram:GrandTotalAmount>
|
|
<!-- BT-115: Due payable (mandatory) -->
|
|
<ram:DuePayableAmount>1190.00</ram:DuePayableAmount>
|
|
</ram:SpecifiedTradeSettlementHeaderMonetarySummation>
|
|
</ram:ApplicableHeaderTradeSettlement>
|
|
</rsm:SupplyChainTradeTransaction>
|
|
</rsm:CrossIndustryInvoice>`;
|
|
|
|
const einvoice = new EInvoice();
|
|
await einvoice.loadFromString(ciiInvoice);
|
|
|
|
const xmlString = einvoice.getXmlString();
|
|
|
|
// Verify CII mandatory fields
|
|
const ciiMandatoryChecks = {
|
|
'Invoice ID': xmlString.includes('MANDATORY-CII-001'),
|
|
'Type code': xmlString.includes('380'),
|
|
'Issue date': xmlString.includes('20250125'),
|
|
'Currency': xmlString.includes('EUR'),
|
|
'Seller name': xmlString.includes('CII Mandatory Seller'),
|
|
'Seller country': xmlString.includes('<ram:CountryID>DE</ram:CountryID>'),
|
|
'Buyer name': xmlString.includes('CII Mandatory Buyer'),
|
|
'Line ID': xmlString.includes('<ram:LineID>1</ram:LineID>'),
|
|
'Product name': xmlString.includes('CII Mandatory Product'),
|
|
'Due amount': xmlString.includes('<ram:DuePayableAmount>1190.00</ram:DuePayableAmount>')
|
|
};
|
|
|
|
const missingCiiFields = Object.entries(ciiMandatoryChecks)
|
|
.filter(([field, present]) => !present)
|
|
.map(([field]) => field);
|
|
|
|
if (missingCiiFields.length > 0) {
|
|
console.log('Missing CII mandatory fields:', missingCiiFields);
|
|
}
|
|
|
|
expect(missingCiiFields.length).toBe(0);
|
|
|
|
const elapsed = performance.now() - startTime;
|
|
performanceTracker.addMeasurement('cii-mandatory', elapsed);
|
|
});
|
|
|
|
t.test('XRechnung specific mandatory fields', async () => {
|
|
const startTime = performance.now();
|
|
|
|
// XRechnung has additional mandatory fields
|
|
const xrechnungInvoice = `<?xml version="1.0" encoding="UTF-8"?>
|
|
<ubl:Invoice xmlns:ubl="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"
|
|
xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2"
|
|
xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2">
|
|
<cbc:CustomizationID>urn:cen.eu:en16931:2017#compliant#urn:xeinkauf.de:kosit:xrechnung_3.0</cbc:CustomizationID>
|
|
<cbc:ProfileID>urn:fdc:peppol.eu:2017:poacc:billing:01:1.0</cbc:ProfileID>
|
|
<cbc:ID>XRECHNUNG-001</cbc:ID>
|
|
<cbc:IssueDate>2025-01-25</cbc:IssueDate>
|
|
<cbc:InvoiceTypeCode>380</cbc:InvoiceTypeCode>
|
|
<cbc:DocumentCurrencyCode>EUR</cbc:DocumentCurrencyCode>
|
|
<!-- XRechnung mandatory: BT-10 Buyer reference -->
|
|
<cbc:BuyerReference>LEITWEG-ID-123456</cbc:BuyerReference>
|
|
|
|
<cac:AccountingSupplierParty>
|
|
<cac:Party>
|
|
<cbc:EndpointID schemeID="EM">seller@example.de</cbc:EndpointID>
|
|
<cac:PartyLegalEntity>
|
|
<cbc:RegistrationName>XRechnung Seller GmbH</cbc:RegistrationName>
|
|
</cac:PartyLegalEntity>
|
|
<cac:PostalAddress>
|
|
<cbc:StreetName>Berliner Straße 1</cbc:StreetName>
|
|
<cbc:CityName>Berlin</cbc:CityName>
|
|
<cbc:PostalZone>10115</cbc:PostalZone>
|
|
<cac:Country>
|
|
<cbc:IdentificationCode>DE</cbc:IdentificationCode>
|
|
</cac:Country>
|
|
</cac:PostalAddress>
|
|
<cac:Contact>
|
|
<cbc:Name>Max Mustermann</cbc:Name>
|
|
<cbc:Telephone>+49 30 12345678</cbc:Telephone>
|
|
<cbc:ElectronicMail>max@seller.de</cbc:ElectronicMail>
|
|
</cac:Contact>
|
|
</cac:Party>
|
|
</cac:AccountingSupplierParty>
|
|
|
|
<cac:AccountingCustomerParty>
|
|
<cac:Party>
|
|
<cbc:EndpointID schemeID="EM">buyer@behoerde.de</cbc:EndpointID>
|
|
<cac:PartyLegalEntity>
|
|
<cbc:RegistrationName>Bundesbehörde XY</cbc:RegistrationName>
|
|
</cac:PartyLegalEntity>
|
|
<cac:PostalAddress>
|
|
<cbc:StreetName>Amtsstraße 100</cbc:StreetName>
|
|
<cbc:CityName>Bonn</cbc:CityName>
|
|
<cbc:PostalZone>53113</cbc:PostalZone>
|
|
<cac:Country>
|
|
<cbc:IdentificationCode>DE</cbc:IdentificationCode>
|
|
</cac:Country>
|
|
</cac:PostalAddress>
|
|
</cac:Party>
|
|
</cac:AccountingCustomerParty>
|
|
|
|
<cac:PaymentMeans>
|
|
<!-- XRechnung mandatory: Payment means code -->
|
|
<cbc:PaymentMeansCode>30</cbc:PaymentMeansCode>
|
|
<cac:PayeeFinancialAccount>
|
|
<cbc:ID>DE89370400440532013000</cbc:ID>
|
|
</cac:PayeeFinancialAccount>
|
|
</cac:PaymentMeans>
|
|
|
|
<cac:LegalMonetaryTotal>
|
|
<cbc:PayableAmount currencyID="EUR">119.00</cbc:PayableAmount>
|
|
</cac:LegalMonetaryTotal>
|
|
</ubl:Invoice>`;
|
|
|
|
const einvoice = new EInvoice();
|
|
await einvoice.loadFromString(xrechnungInvoice);
|
|
|
|
const xmlString = einvoice.getXmlString();
|
|
|
|
// Check XRechnung specific mandatory fields
|
|
const xrechnungChecks = {
|
|
'Customization ID': xmlString.includes('xrechnung'),
|
|
'Buyer reference': xmlString.includes('LEITWEG-ID-123456'),
|
|
'Seller email': xmlString.includes('seller@example.de') || xmlString.includes('max@seller.de'),
|
|
'Buyer endpoint': xmlString.includes('buyer@behoerde.de'),
|
|
'Payment means': xmlString.includes('>30<')
|
|
};
|
|
|
|
const missingXrechnung = Object.entries(xrechnungChecks)
|
|
.filter(([field, present]) => !present)
|
|
.map(([field]) => field);
|
|
|
|
if (missingXrechnung.length > 0) {
|
|
console.log('Missing XRechnung fields:', missingXrechnung);
|
|
}
|
|
|
|
const elapsed = performance.now() - startTime;
|
|
performanceTracker.addMeasurement('xrechnung-mandatory', elapsed);
|
|
});
|
|
|
|
t.test('Mandatory fields validation errors', async () => {
|
|
const startTime = performance.now();
|
|
|
|
// Invoice missing mandatory fields
|
|
const incompleteInvoice = `<?xml version="1.0" encoding="UTF-8"?>
|
|
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"
|
|
xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2"
|
|
xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2">
|
|
<!-- Missing: Invoice ID -->
|
|
<cbc:IssueDate>2025-01-25</cbc:IssueDate>
|
|
<cbc:InvoiceTypeCode>380</cbc:InvoiceTypeCode>
|
|
<!-- Missing: Currency code -->
|
|
|
|
<cac:AccountingSupplierParty>
|
|
<cac:Party>
|
|
<!-- Missing: Seller name -->
|
|
<cac:PostalAddress>
|
|
<cbc:StreetName>Test Street</cbc:StreetName>
|
|
<!-- Missing: City -->
|
|
<!-- Missing: Country -->
|
|
</cac:PostalAddress>
|
|
</cac:Party>
|
|
</cac:AccountingSupplierParty>
|
|
|
|
<!-- Missing: Buyer entirely -->
|
|
|
|
<!-- Missing: Totals -->
|
|
|
|
<!-- Missing: Invoice lines -->
|
|
</Invoice>`;
|
|
|
|
const einvoice = new EInvoice();
|
|
|
|
try {
|
|
await einvoice.loadFromString(incompleteInvoice);
|
|
|
|
const validationResult = await einvoice.validate();
|
|
|
|
if (!validationResult.isValid) {
|
|
console.log('Validation detected missing mandatory fields');
|
|
|
|
// Check for specific mandatory field errors
|
|
const mandatoryErrors = validationResult.errors?.filter(err =>
|
|
err.message.toLowerCase().includes('mandatory') ||
|
|
err.message.toLowerCase().includes('required') ||
|
|
err.message.toLowerCase().includes('must')
|
|
);
|
|
|
|
if (mandatoryErrors && mandatoryErrors.length > 0) {
|
|
console.log(`Found ${mandatoryErrors.length} mandatory field errors`);
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.log('Processing incomplete invoice:', error.message);
|
|
}
|
|
|
|
const elapsed = performance.now() - startTime;
|
|
performanceTracker.addMeasurement('validation-errors', elapsed);
|
|
});
|
|
|
|
t.test('Conditional mandatory fields', async () => {
|
|
const startTime = performance.now();
|
|
|
|
// Some fields are mandatory only in certain conditions
|
|
const conditionalInvoice = `<?xml version="1.0" encoding="UTF-8"?>
|
|
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"
|
|
xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2"
|
|
xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2">
|
|
<cbc:ID>CONDITIONAL-001</cbc:ID>
|
|
<cbc:IssueDate>2025-01-25</cbc:IssueDate>
|
|
<cbc:InvoiceTypeCode>380</cbc:InvoiceTypeCode>
|
|
<cbc:DocumentCurrencyCode>EUR</cbc:DocumentCurrencyCode>
|
|
|
|
<cac:AccountingSupplierParty>
|
|
<cac:Party>
|
|
<cac:PartyLegalEntity>
|
|
<cbc:RegistrationName>VAT Exempt Supplier</cbc:RegistrationName>
|
|
</cac:PartyLegalEntity>
|
|
<cac:PostalAddress>
|
|
<cbc:CityName>Paris</cbc:CityName>
|
|
<cac:Country>
|
|
<cbc:IdentificationCode>FR</cbc:IdentificationCode>
|
|
</cac:Country>
|
|
</cac:PostalAddress>
|
|
</cac:Party>
|
|
</cac:AccountingSupplierParty>
|
|
|
|
<cac:AccountingCustomerParty>
|
|
<cac:Party>
|
|
<cac:PartyLegalEntity>
|
|
<cbc:RegistrationName>Tax Exempt Customer</cbc:RegistrationName>
|
|
</cac:PartyLegalEntity>
|
|
<cac:PostalAddress>
|
|
<cbc:CityName>Brussels</cbc:CityName>
|
|
<cac:Country>
|
|
<cbc:IdentificationCode>BE</cbc:IdentificationCode>
|
|
</cac:Country>
|
|
</cac:PostalAddress>
|
|
</cac:Party>
|
|
</cac:AccountingCustomerParty>
|
|
|
|
<!-- VAT exempt scenario -->
|
|
<cac:TaxTotal>
|
|
<cbc:TaxAmount currencyID="EUR">0.00</cbc:TaxAmount>
|
|
<cac:TaxSubtotal>
|
|
<cbc:TaxableAmount currencyID="EUR">1000.00</cbc:TaxableAmount>
|
|
<cbc:TaxAmount currencyID="EUR">0.00</cbc:TaxAmount>
|
|
<cac:TaxCategory>
|
|
<cbc:ID>E</cbc:ID>
|
|
<cbc:Percent>0</cbc:Percent>
|
|
<!-- Mandatory when tax category is E: Exemption reason -->
|
|
<cbc:TaxExemptionReasonCode>VATEX-EU-IC</cbc:TaxExemptionReasonCode>
|
|
<cbc:TaxExemptionReason>Intra-community supply</cbc:TaxExemptionReason>
|
|
<cac:TaxScheme>
|
|
<cbc:ID>VAT</cbc:ID>
|
|
</cac:TaxScheme>
|
|
</cac:TaxCategory>
|
|
</cac:TaxSubtotal>
|
|
</cac:TaxTotal>
|
|
|
|
<!-- Credit note specific mandatory fields -->
|
|
<cac:BillingReference>
|
|
<cac:InvoiceDocumentReference>
|
|
<!-- Mandatory for credit notes: Referenced invoice -->
|
|
<cbc:ID>ORIGINAL-INV-001</cbc:ID>
|
|
<cbc:IssueDate>2025-01-01</cbc:IssueDate>
|
|
</cac:InvoiceDocumentReference>
|
|
</cac:BillingReference>
|
|
|
|
<cac:LegalMonetaryTotal>
|
|
<cbc:PayableAmount currencyID="EUR">1000.00</cbc:PayableAmount>
|
|
</cac:LegalMonetaryTotal>
|
|
</Invoice>`;
|
|
|
|
const einvoice = new EInvoice();
|
|
await einvoice.loadFromString(conditionalInvoice);
|
|
|
|
const xmlString = einvoice.getXmlString();
|
|
|
|
// Check conditional mandatory fields
|
|
const conditionalChecks = {
|
|
'VAT exemption reason code': xmlString.includes('VATEX-EU-IC'),
|
|
'VAT exemption reason': xmlString.includes('Intra-community supply'),
|
|
'Referenced invoice': xmlString.includes('ORIGINAL-INV-001')
|
|
};
|
|
|
|
Object.entries(conditionalChecks).forEach(([field, present]) => {
|
|
if (present) {
|
|
console.log(`✓ Conditional mandatory field preserved: ${field}`);
|
|
}
|
|
});
|
|
|
|
const elapsed = performance.now() - startTime;
|
|
performanceTracker.addMeasurement('conditional-mandatory', elapsed);
|
|
});
|
|
|
|
t.test('Corpus mandatory fields analysis', async () => {
|
|
const startTime = performance.now();
|
|
let processedCount = 0;
|
|
const missingFieldStats: Record<string, number> = {};
|
|
|
|
const files = await corpusLoader.getAllFiles();
|
|
const xmlFiles = files.filter(f => f.endsWith('.xml') && !f.includes('.pdf'));
|
|
|
|
// Sample corpus files for mandatory field analysis
|
|
const sampleSize = Math.min(40, xmlFiles.length);
|
|
const sample = xmlFiles.slice(0, sampleSize);
|
|
|
|
for (const file of sample) {
|
|
try {
|
|
const content = await corpusLoader.readFile(file);
|
|
const einvoice = new EInvoice();
|
|
|
|
if (typeof content === 'string') {
|
|
await einvoice.loadFromString(content);
|
|
} else {
|
|
await einvoice.loadFromBuffer(content);
|
|
}
|
|
|
|
const xmlString = einvoice.getXmlString();
|
|
|
|
// Check for mandatory fields
|
|
const mandatoryFields = [
|
|
{ name: 'Invoice ID', patterns: ['<cbc:ID>', '<ram:ID>'] },
|
|
{ name: 'Issue Date', patterns: ['<cbc:IssueDate>', '<ram:IssueDateTime>'] },
|
|
{ name: 'Currency', patterns: ['<cbc:DocumentCurrencyCode>', '<ram:InvoiceCurrencyCode>'] },
|
|
{ name: 'Seller Name', patterns: ['<cbc:RegistrationName>', '<ram:Name>'] },
|
|
{ name: 'Buyer Name', patterns: ['AccountingCustomerParty', 'BuyerTradeParty'] },
|
|
{ name: 'Total Amount', patterns: ['<cbc:PayableAmount>', '<ram:DuePayableAmount>'] }
|
|
];
|
|
|
|
mandatoryFields.forEach(field => {
|
|
const hasField = field.patterns.some(pattern => xmlString.includes(pattern));
|
|
if (!hasField) {
|
|
missingFieldStats[field.name] = (missingFieldStats[field.name] || 0) + 1;
|
|
}
|
|
});
|
|
|
|
processedCount++;
|
|
} catch (error) {
|
|
console.log(`Error checking ${file}:`, error.message);
|
|
}
|
|
}
|
|
|
|
console.log(`Corpus mandatory fields analysis (${processedCount} files):`);
|
|
if (Object.keys(missingFieldStats).length > 0) {
|
|
console.log('Files missing mandatory fields:');
|
|
Object.entries(missingFieldStats)
|
|
.sort((a, b) => b[1] - a[1])
|
|
.forEach(([field, count]) => {
|
|
console.log(` ${field}: ${count} files`);
|
|
});
|
|
} else {
|
|
console.log('All sampled files have mandatory fields');
|
|
}
|
|
|
|
const elapsed = performance.now() - startTime;
|
|
performanceTracker.addMeasurement('corpus-analysis', elapsed);
|
|
});
|
|
|
|
// Print performance summary
|
|
performanceTracker.printSummary();
|
|
|
|
// Performance assertions
|
|
const avgTime = performanceTracker.getAverageTime();
|
|
expect(avgTime).toBeLessThan(300); // Mandatory field checks should be fast
|
|
});
|
|
|
|
tap.start(); |