einvoice/test/suite/einvoice_conversion/test.conv-05.mandatory-fields.ts
2025-05-25 19:45:37 +00:00

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