691 lines
27 KiB
TypeScript
691 lines
27 KiB
TypeScript
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
|
import { EInvoice } from '../../../ts/index.js';
|
|
import { CorpusLoader } from '../../helpers/corpus.loader.js';
|
|
|
|
// CONV-05: Verify mandatory fields are maintained during format conversion
|
|
// This test ensures no required data is lost during transformation
|
|
|
|
tap.test('CONV-05: EN16931 mandatory fields in UBL', async () => {
|
|
// 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:PartyName>
|
|
<cbc:Name>Mandatory Fields Supplier AB</cbc:Name>
|
|
</cac:PartyName>
|
|
<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:PartyName>
|
|
<cbc:Name>Mandatory Fields Buyer GmbH</cbc:Name>
|
|
</cac:PartyName>
|
|
<cac:PartyLegalEntity>
|
|
<cbc:RegistrationName>Mandatory Fields Buyer GmbH</cbc:RegistrationName>
|
|
</cac:PartyLegalEntity>
|
|
<!-- BG-8: Buyer postal address (mandatory) -->
|
|
<cac:PostalAddress>
|
|
<!-- BT-50: Buyer address line 1 -->
|
|
<cbc:StreetName>Hauptstraße 123</cbc:StreetName>
|
|
<!-- BT-52: Buyer city (mandatory) -->
|
|
<cbc:CityName>Berlin</cbc:CityName>
|
|
<!-- BT-53: Buyer post code -->
|
|
<cbc:PostalZone>10115</cbc:PostalZone>
|
|
<!-- BT-55: Buyer country code (mandatory) -->
|
|
<cac:Country>
|
|
<cbc:IdentificationCode>DE</cbc:IdentificationCode>
|
|
</cac:Country>
|
|
</cac:PostalAddress>
|
|
</cac:Party>
|
|
</cac:AccountingCustomerParty>
|
|
|
|
<!-- BG-22: Document totals (mandatory) -->
|
|
<cac:LegalMonetaryTotal>
|
|
<!-- BT-109: Invoice total amount without VAT (mandatory) -->
|
|
<cbc:TaxExclusiveAmount currencyID="EUR">1000.00</cbc:TaxExclusiveAmount>
|
|
<!-- BT-112: Invoice total amount with VAT (mandatory) -->
|
|
<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-25: Invoice line (at least one mandatory) -->
|
|
<cac:InvoiceLine>
|
|
<!-- BT-126: Invoice line identifier (mandatory) -->
|
|
<cbc:ID>1</cbc:ID>
|
|
<!-- BT-129: Invoiced quantity (mandatory) -->
|
|
<cbc:InvoicedQuantity unitCode="C62">1</cbc:InvoicedQuantity>
|
|
<!-- BT-131: Invoice line net amount (mandatory) -->
|
|
<cbc:LineExtensionAmount currencyID="EUR">1000.00</cbc:LineExtensionAmount>
|
|
<!-- BG-31: Line Item information (mandatory) -->
|
|
<cac:Item>
|
|
<!-- BT-153: Item name (mandatory) -->
|
|
<cbc:Name>Mandatory Test Product</cbc:Name>
|
|
</cac:Item>
|
|
<!-- BG-29: Price details (mandatory) -->
|
|
<cac:Price>
|
|
<!-- BT-146: Item net price (mandatory) -->
|
|
<cbc:PriceAmount currencyID="EUR">1000.00</cbc:PriceAmount>
|
|
</cac:Price>
|
|
</cac:InvoiceLine>
|
|
</Invoice>`;
|
|
|
|
try {
|
|
const einvoice = new EInvoice();
|
|
await einvoice.loadXml(ublInvoice);
|
|
|
|
// Define mandatory fields to check
|
|
const mandatoryFields = [
|
|
{ field: 'id', value: einvoice.id, bt: 'BT-1' },
|
|
{ field: 'date', value: einvoice.date, bt: 'BT-2' },
|
|
{ field: 'currency', value: einvoice.currency, bt: 'BT-5' },
|
|
{ field: 'from.name', value: einvoice.from?.name, bt: 'BT-27' },
|
|
{ field: 'from.address.city', value: einvoice.from?.address?.city, bt: 'BT-37' },
|
|
{ field: 'from.address.countryCode', value: einvoice.from?.address?.countryCode, bt: 'BT-40' },
|
|
{ field: 'to.name', value: einvoice.to?.name, bt: 'BT-44' },
|
|
{ field: 'to.address.city', value: einvoice.to?.address?.city, bt: 'BT-52' },
|
|
{ field: 'to.address.countryCode', value: einvoice.to?.address?.countryCode, bt: 'BT-55' },
|
|
{ field: 'items', value: einvoice.items?.length > 0, bt: 'BG-25' }
|
|
];
|
|
|
|
// Check each mandatory field
|
|
const missingFields = mandatoryFields.filter(f => !f.value);
|
|
|
|
if (missingFields.length > 0) {
|
|
console.error('Missing mandatory fields:', missingFields.map(f => `${f.bt}: ${f.field}`));
|
|
}
|
|
|
|
expect(missingFields.length).toEqual(0);
|
|
|
|
// Test conversion to other formats
|
|
const ciiXml = await einvoice.toXmlString('cii');
|
|
expect(ciiXml.length).toBeGreaterThan(100);
|
|
|
|
// Convert back and check mandatory fields are preserved
|
|
const einvoice2 = new EInvoice();
|
|
await einvoice2.loadXml(ciiXml);
|
|
|
|
// Check key mandatory fields survived conversion
|
|
expect(einvoice2.id).toEqual('MANDATORY-UBL-001');
|
|
expect(einvoice2.currency).toEqual('EUR');
|
|
expect(einvoice2.from?.name).toBeTruthy();
|
|
expect(einvoice2.to?.name).toBeTruthy();
|
|
expect(einvoice2.items?.length).toBeGreaterThan(0);
|
|
|
|
} catch (error) {
|
|
console.error('Mandatory fields test failed:', error);
|
|
throw error;
|
|
}
|
|
});
|
|
|
|
tap.test('CONV-05: EN16931 mandatory fields in CII', async () => {
|
|
// CII invoice with all EN16931 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>
|
|
<!-- BG-25: Invoice line (at least one mandatory) -->
|
|
<ram:IncludedSupplyChainTradeLineItem>
|
|
<ram:AssociatedDocumentLineDocument>
|
|
<!-- BT-126: Invoice line identifier (mandatory) -->
|
|
<ram:LineID>1</ram:LineID>
|
|
</ram:AssociatedDocumentLineDocument>
|
|
<!-- BG-31: Line Item information (mandatory) -->
|
|
<ram:SpecifiedTradeProduct>
|
|
<!-- BT-153: Item name (mandatory) -->
|
|
<ram:Name>Mandatory Test Product</ram:Name>
|
|
</ram:SpecifiedTradeProduct>
|
|
<ram:SpecifiedLineTradeAgreement>
|
|
<!-- BG-29: Price details (mandatory) -->
|
|
<ram:NetPriceProductTradePrice>
|
|
<!-- BT-146: Item net price (mandatory) -->
|
|
<ram:ChargeAmount>1000.00</ram:ChargeAmount>
|
|
</ram:NetPriceProductTradePrice>
|
|
</ram:SpecifiedLineTradeAgreement>
|
|
<ram:SpecifiedLineTradeDelivery>
|
|
<!-- BT-129: Invoiced quantity (mandatory) -->
|
|
<ram:BilledQuantity unitCode="C62">1</ram:BilledQuantity>
|
|
</ram:SpecifiedLineTradeDelivery>
|
|
<ram:SpecifiedLineTradeSettlement>
|
|
<!-- BT-131: Invoice line net amount (mandatory) -->
|
|
<ram:SpecifiedTradeSettlementLineMonetarySummation>
|
|
<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>Mandatory Fields Supplier AB</ram:Name>
|
|
<!-- BG-5: Seller postal address (mandatory) -->
|
|
<ram:PostalTradeAddress>
|
|
<!-- BT-35: Seller address line 1 -->
|
|
<ram:LineOne>Kungsgatan 10</ram:LineOne>
|
|
<!-- BT-37: Seller city (mandatory) -->
|
|
<ram:CityName>Stockholm</ram:CityName>
|
|
<!-- BT-38: Seller post code -->
|
|
<ram:PostcodeCode>11143</ram:PostcodeCode>
|
|
<!-- BT-40: Seller country code (mandatory) -->
|
|
<ram:CountryID>SE</ram:CountryID>
|
|
</ram:PostalTradeAddress>
|
|
<!-- BT-31: Seller VAT identifier -->
|
|
<ram:SpecifiedTaxRegistration>
|
|
<ram:ID schemeID="VA">SE123456789001</ram:ID>
|
|
</ram:SpecifiedTaxRegistration>
|
|
</ram:SellerTradeParty>
|
|
<!-- BG-7: Buyer (mandatory) -->
|
|
<ram:BuyerTradeParty>
|
|
<!-- BT-44: Buyer name (mandatory) -->
|
|
<ram:Name>Mandatory Fields Buyer GmbH</ram:Name>
|
|
<!-- BG-8: Buyer postal address (mandatory) -->
|
|
<ram:PostalTradeAddress>
|
|
<!-- BT-50: Buyer address line 1 -->
|
|
<ram:LineOne>Hauptstraße 123</ram:LineOne>
|
|
<!-- BT-52: Buyer city (mandatory) -->
|
|
<ram:CityName>Berlin</ram:CityName>
|
|
<!-- BT-53: Buyer post code -->
|
|
<ram:PostcodeCode>10115</ram:PostcodeCode>
|
|
<!-- BT-55: Buyer country code (mandatory) -->
|
|
<ram:CountryID>DE</ram:CountryID>
|
|
</ram:PostalTradeAddress>
|
|
</ram:BuyerTradeParty>
|
|
</ram:ApplicableHeaderTradeAgreement>
|
|
<ram:ApplicableHeaderTradeSettlement>
|
|
<!-- BT-5: Invoice currency code (mandatory) -->
|
|
<ram:InvoiceCurrencyCode>EUR</ram:InvoiceCurrencyCode>
|
|
<!-- BG-22: Document totals (mandatory) -->
|
|
<ram:SpecifiedTradeSettlementHeaderMonetarySummation>
|
|
<!-- BT-109: Invoice total amount without VAT (mandatory) -->
|
|
<ram:TaxBasisTotalAmount>1000.00</ram:TaxBasisTotalAmount>
|
|
<!-- BT-112: Invoice total amount with VAT (mandatory) -->
|
|
<ram:GrandTotalAmount>1190.00</ram:GrandTotalAmount>
|
|
<!-- BT-115: Amount due for payment (mandatory) -->
|
|
<ram:DuePayableAmount>1190.00</ram:DuePayableAmount>
|
|
</ram:SpecifiedTradeSettlementHeaderMonetarySummation>
|
|
</ram:ApplicableHeaderTradeSettlement>
|
|
</rsm:SupplyChainTradeTransaction>
|
|
</rsm:CrossIndustryInvoice>`;
|
|
|
|
try {
|
|
const einvoice = new EInvoice();
|
|
await einvoice.loadXml(ciiInvoice);
|
|
|
|
// Check mandatory fields
|
|
expect(einvoice.id).toEqual('MANDATORY-CII-001');
|
|
expect(einvoice.date).toBeTruthy();
|
|
expect(einvoice.currency).toEqual('EUR');
|
|
expect(einvoice.from?.name).toEqual('Mandatory Fields Supplier AB');
|
|
expect(einvoice.from?.address?.city).toEqual('Stockholm');
|
|
expect(einvoice.from?.address?.countryCode).toEqual('SE');
|
|
expect(einvoice.to?.name).toEqual('Mandatory Fields Buyer GmbH');
|
|
expect(einvoice.to?.address?.city).toEqual('Berlin');
|
|
expect(einvoice.to?.address?.countryCode).toEqual('DE');
|
|
expect(einvoice.items?.length).toBeGreaterThan(0);
|
|
expect(einvoice.items?.[0]?.name).toEqual('Mandatory Test Product');
|
|
|
|
// Test conversion to UBL
|
|
const ublXml = await einvoice.toXmlString('ubl');
|
|
expect(ublXml.length).toBeGreaterThan(100);
|
|
|
|
// Verify UBL contains mandatory fields
|
|
expect(ublXml).toContain('MANDATORY-CII-001');
|
|
expect(ublXml).toContain('EUR');
|
|
expect(ublXml).toContain('Mandatory Fields Supplier AB');
|
|
expect(ublXml).toContain('Mandatory Fields Buyer GmbH');
|
|
|
|
} catch (error) {
|
|
console.error('CII mandatory fields test failed:', error);
|
|
throw error;
|
|
}
|
|
});
|
|
|
|
tap.test('CONV-05: XRechnung specific mandatory fields', async () => {
|
|
// XRechnung has additional mandatory fields beyond EN16931
|
|
const xrechnungUbl = `<?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: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>
|
|
|
|
<!-- BT-10: Buyer reference (mandatory for XRechnung) -->
|
|
<cbc:BuyerReference>XR-2025-001</cbc:BuyerReference>
|
|
|
|
<cac:AccountingSupplierParty>
|
|
<cac:Party>
|
|
<cbc:EndpointID schemeID="0088">1234567890123</cbc:EndpointID>
|
|
<cac:PartyIdentification>
|
|
<cbc:ID schemeID="0088">1234567890123</cbc:ID>
|
|
</cac:PartyIdentification>
|
|
<cac:PartyName>
|
|
<cbc:Name>XRechnung Supplier GmbH</cbc:Name>
|
|
</cac:PartyName>
|
|
<cac:PartyLegalEntity>
|
|
<cbc:RegistrationName>XRechnung Supplier GmbH</cbc:RegistrationName>
|
|
<cbc:CompanyID schemeID="0088">1234567890123</cbc:CompanyID>
|
|
</cac:PartyLegalEntity>
|
|
<cac:PostalAddress>
|
|
<cbc:StreetName>Teststraß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:PartyTaxScheme>
|
|
<cbc:CompanyID>DE123456789</cbc:CompanyID>
|
|
<cac:TaxScheme>
|
|
<cbc:ID>VAT</cbc:ID>
|
|
</cac:TaxScheme>
|
|
</cac:PartyTaxScheme>
|
|
<cac:Contact>
|
|
<cbc:Name>Max Mustermann</cbc:Name>
|
|
<cbc:Telephone>+49 30 123456</cbc:Telephone>
|
|
<cbc:ElectronicMail>max@example.com</cbc:ElectronicMail>
|
|
</cac:Contact>
|
|
</cac:Party>
|
|
</cac:AccountingSupplierParty>
|
|
|
|
<cac:AccountingCustomerParty>
|
|
<cac:Party>
|
|
<!-- Leitweg-ID (mandatory for XRechnung) -->
|
|
<cbc:EndpointID schemeID="0204">991-12345-67</cbc:EndpointID>
|
|
<cac:PartyIdentification>
|
|
<cbc:ID>991-12345-67</cbc:ID>
|
|
</cac:PartyIdentification>
|
|
<cac:PartyName>
|
|
<cbc:Name>Bundesamt für XRechnung</cbc:Name>
|
|
</cac:PartyName>
|
|
<cac:PartyLegalEntity>
|
|
<cbc:RegistrationName>Bundesamt für XRechnung</cbc:RegistrationName>
|
|
</cac:PartyLegalEntity>
|
|
<cac:PostalAddress>
|
|
<cbc:StreetName>Amtsstraße 100</cbc:StreetName>
|
|
<cbc:CityName>Berlin</cbc:CityName>
|
|
<cbc:PostalZone>10117</cbc:PostalZone>
|
|
<cac:Country>
|
|
<cbc:IdentificationCode>DE</cbc:IdentificationCode>
|
|
</cac:Country>
|
|
</cac:PostalAddress>
|
|
</cac:Party>
|
|
</cac:AccountingCustomerParty>
|
|
|
|
<cac:PaymentMeans>
|
|
<cbc:PaymentMeansCode>58</cbc:PaymentMeansCode>
|
|
<cac:PayeeFinancialAccount>
|
|
<cbc:ID>DE89370400440532013000</cbc:ID>
|
|
</cac:PayeeFinancialAccount>
|
|
</cac:PaymentMeans>
|
|
|
|
<cac:TaxTotal>
|
|
<cbc:TaxAmount currencyID="EUR">190.00</cbc:TaxAmount>
|
|
<cac:TaxSubtotal>
|
|
<cbc:TaxableAmount currencyID="EUR">1000.00</cbc:TaxableAmount>
|
|
<cbc:TaxAmount currencyID="EUR">190.00</cbc:TaxAmount>
|
|
<cac:TaxCategory>
|
|
<cbc:ID>S</cbc:ID>
|
|
<cbc:Percent>19</cbc:Percent>
|
|
<cac:TaxScheme>
|
|
<cbc:ID>VAT</cbc:ID>
|
|
</cac:TaxScheme>
|
|
</cac:TaxCategory>
|
|
</cac:TaxSubtotal>
|
|
</cac:TaxTotal>
|
|
|
|
<cac:LegalMonetaryTotal>
|
|
<cbc:TaxExclusiveAmount currencyID="EUR">1000.00</cbc:TaxExclusiveAmount>
|
|
<cbc:TaxInclusiveAmount currencyID="EUR">1190.00</cbc:TaxInclusiveAmount>
|
|
<cbc:PayableAmount currencyID="EUR">1190.00</cbc:PayableAmount>
|
|
</cac:LegalMonetaryTotal>
|
|
|
|
<cac:InvoiceLine>
|
|
<cbc:ID>1</cbc:ID>
|
|
<cbc:InvoicedQuantity unitCode="C62">1</cbc:InvoicedQuantity>
|
|
<cbc:LineExtensionAmount currencyID="EUR">1000.00</cbc:LineExtensionAmount>
|
|
<cac:Item>
|
|
<cbc:Name>XRechnung Test Product</cbc:Name>
|
|
<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>
|
|
<cac:Price>
|
|
<cbc:PriceAmount currencyID="EUR">1000.00</cbc:PriceAmount>
|
|
</cac:Price>
|
|
</cac:InvoiceLine>
|
|
</Invoice>`;
|
|
|
|
try {
|
|
const einvoice = new EInvoice();
|
|
await einvoice.loadXml(xrechnungUbl);
|
|
|
|
// Check basic mandatory fields (XRechnung-specific fields might not all be extracted yet)
|
|
expect(einvoice.id).toEqual('XRECHNUNG-001');
|
|
expect(einvoice.currency).toEqual('EUR');
|
|
expect(einvoice.from?.name).toBeTruthy();
|
|
expect(einvoice.to?.name).toBeTruthy();
|
|
|
|
// Test conversion to XRechnung format
|
|
const xrechnungXml = await einvoice.toXmlString('xrechnung');
|
|
expect(xrechnungXml.length).toBeGreaterThan(100);
|
|
|
|
// Verify XRechnung XML contains key elements
|
|
expect(xrechnungXml).toContain('XRECHNUNG-001');
|
|
expect(xrechnungXml).toContain('EUR');
|
|
|
|
// Note: Some XRechnung-specific fields like BuyerReference and Leitweg-ID
|
|
// might not be fully supported in the current implementation
|
|
|
|
} catch (error) {
|
|
console.error('XRechnung mandatory fields test failed:', error);
|
|
throw error;
|
|
}
|
|
});
|
|
|
|
tap.test('CONV-05: Mandatory fields validation errors', async () => {
|
|
// Test invoice missing mandatory fields
|
|
const invalidInvoices = [
|
|
{
|
|
name: 'Missing invoice ID',
|
|
xml: `<?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:IssueDate>2025-01-25</cbc:IssueDate>
|
|
<cbc:DocumentCurrencyCode>EUR</cbc:DocumentCurrencyCode>
|
|
</Invoice>`,
|
|
expectedError: 'invoice ID'
|
|
},
|
|
{
|
|
name: 'Missing currency',
|
|
xml: `<?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>TEST-001</cbc:ID>
|
|
<cbc:IssueDate>2025-01-25</cbc:IssueDate>
|
|
</Invoice>`,
|
|
expectedError: 'currency'
|
|
}
|
|
];
|
|
|
|
for (const testCase of invalidInvoices) {
|
|
console.log(`Testing: ${testCase.name}`);
|
|
|
|
try {
|
|
const einvoice = new EInvoice();
|
|
await einvoice.loadXml(testCase.xml);
|
|
|
|
// Check if critical fields are missing
|
|
if (!einvoice.id && testCase.expectedError.includes('ID')) {
|
|
console.log('✓ Correctly identified missing invoice ID');
|
|
}
|
|
if (!einvoice.currency && testCase.expectedError.includes('currency')) {
|
|
console.log('✓ Correctly identified missing currency');
|
|
}
|
|
|
|
} catch (error) {
|
|
// Some formats might throw errors for missing mandatory fields
|
|
console.log(`✓ Validation error for ${testCase.name}: ${error.message}`);
|
|
}
|
|
}
|
|
});
|
|
|
|
tap.test('CONV-05: Conditional mandatory fields', async () => {
|
|
// Test conditional mandatory fields (e.g., VAT details when applicable)
|
|
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:PartyName>
|
|
<cbc:Name>VAT Registered Supplier</cbc:Name>
|
|
</cac:PartyName>
|
|
<cac:PartyLegalEntity>
|
|
<cbc:RegistrationName>VAT Registered Supplier</cbc:RegistrationName>
|
|
</cac:PartyLegalEntity>
|
|
<cac:PostalAddress>
|
|
<cbc:CityName>Brussels</cbc:CityName>
|
|
<cac:Country>
|
|
<cbc:IdentificationCode>BE</cbc:IdentificationCode>
|
|
</cac:Country>
|
|
</cac:PostalAddress>
|
|
<!-- When seller is VAT registered, VAT ID becomes mandatory -->
|
|
<cac:PartyTaxScheme>
|
|
<cbc:CompanyID>BE0123456789</cbc:CompanyID>
|
|
<cac:TaxScheme>
|
|
<cbc:ID>VAT</cbc:ID>
|
|
</cac:TaxScheme>
|
|
</cac:PartyTaxScheme>
|
|
</cac:Party>
|
|
</cac:AccountingSupplierParty>
|
|
|
|
<cac:AccountingCustomerParty>
|
|
<cac:Party>
|
|
<cac:PartyName>
|
|
<cbc:Name>EU Customer</cbc:Name>
|
|
</cac:PartyName>
|
|
<cac:PartyLegalEntity>
|
|
<cbc:RegistrationName>EU Customer</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:AccountingCustomerParty>
|
|
|
|
<!-- When VAT applies, tax totals become mandatory -->
|
|
<cac:TaxTotal>
|
|
<cbc:TaxAmount currencyID="EUR">210.00</cbc:TaxAmount>
|
|
<cac:TaxSubtotal>
|
|
<cbc:TaxableAmount currencyID="EUR">1000.00</cbc:TaxableAmount>
|
|
<cbc:TaxAmount currencyID="EUR">210.00</cbc:TaxAmount>
|
|
<cac:TaxCategory>
|
|
<cbc:ID>S</cbc:ID>
|
|
<cbc:Percent>21</cbc:Percent>
|
|
<cac:TaxScheme>
|
|
<cbc:ID>VAT</cbc:ID>
|
|
</cac:TaxScheme>
|
|
</cac:TaxCategory>
|
|
</cac:TaxSubtotal>
|
|
</cac:TaxTotal>
|
|
|
|
<cac:LegalMonetaryTotal>
|
|
<cbc:TaxExclusiveAmount currencyID="EUR">1000.00</cbc:TaxExclusiveAmount>
|
|
<cbc:TaxInclusiveAmount currencyID="EUR">1210.00</cbc:TaxInclusiveAmount>
|
|
<cbc:PayableAmount currencyID="EUR">1210.00</cbc:PayableAmount>
|
|
</cac:LegalMonetaryTotal>
|
|
|
|
<cac:InvoiceLine>
|
|
<cbc:ID>1</cbc:ID>
|
|
<cbc:InvoicedQuantity unitCode="C62">1</cbc:InvoicedQuantity>
|
|
<cbc:LineExtensionAmount currencyID="EUR">1000.00</cbc:LineExtensionAmount>
|
|
<cac:Item>
|
|
<cbc:Name>Taxable Product</cbc:Name>
|
|
<!-- When item is taxable, tax category becomes mandatory -->
|
|
<cac:ClassifiedTaxCategory>
|
|
<cbc:ID>S</cbc:ID>
|
|
<cbc:Percent>21</cbc:Percent>
|
|
<cac:TaxScheme>
|
|
<cbc:ID>VAT</cbc:ID>
|
|
</cac:TaxScheme>
|
|
</cac:ClassifiedTaxCategory>
|
|
</cac:Item>
|
|
<cac:Price>
|
|
<cbc:PriceAmount currencyID="EUR">1000.00</cbc:PriceAmount>
|
|
</cac:Price>
|
|
</cac:InvoiceLine>
|
|
</Invoice>`;
|
|
|
|
try {
|
|
const einvoice = new EInvoice();
|
|
await einvoice.loadXml(conditionalInvoice);
|
|
|
|
// Check conditional mandatory fields
|
|
// When VAT applies, certain fields become mandatory
|
|
if (einvoice.from?.registrationDetails?.vatId) {
|
|
console.log('✓ VAT ID present when seller is VAT registered');
|
|
}
|
|
|
|
// Check if tax information is properly extracted
|
|
if (einvoice.items?.[0]?.vatPercentage) {
|
|
console.log('✓ VAT percentage present for taxable items');
|
|
}
|
|
|
|
// Test conversion preserves conditional fields
|
|
const ciiXml = await einvoice.toXmlString('cii');
|
|
expect(ciiXml).toContain('BE0123456789'); // VAT ID
|
|
|
|
} catch (error) {
|
|
console.error('Conditional mandatory fields test failed:', error);
|
|
throw error;
|
|
}
|
|
});
|
|
|
|
tap.test('CONV-05: Corpus mandatory fields analysis', async () => {
|
|
console.log('Analyzing mandatory fields in corpus files...');
|
|
|
|
// Get a sample of files from different formats
|
|
const corpusFiles = await CorpusLoader.createTestDataset({
|
|
formats: ['UBL', 'CII'],
|
|
categories: ['UBL_XMLRECHNUNG', 'CII_XMLRECHNUNG'],
|
|
maxFiles: 10,
|
|
validOnly: true
|
|
});
|
|
|
|
let totalFiles = 0;
|
|
let filesWithAllMandatory = 0;
|
|
const missingFieldsCount: Record<string, number> = {};
|
|
|
|
for (const file of corpusFiles) {
|
|
try {
|
|
const content = await CorpusLoader.loadFile(file.path);
|
|
const einvoice = new EInvoice();
|
|
await einvoice.loadXml(content.toString('utf-8'));
|
|
|
|
totalFiles++;
|
|
|
|
// Check EN16931 mandatory fields
|
|
const mandatoryChecks = {
|
|
'BT-1 Invoice ID': !!einvoice.id,
|
|
'BT-2 Issue Date': !!einvoice.date,
|
|
'BT-5 Currency': !!einvoice.currency,
|
|
'BT-27 Seller Name': !!einvoice.from?.name,
|
|
'BT-40 Seller Country': !!einvoice.from?.address?.countryCode,
|
|
'BT-44 Buyer Name': !!einvoice.to?.name,
|
|
'BT-55 Buyer Country': !!einvoice.to?.address?.countryCode,
|
|
'BG-25 Invoice Lines': einvoice.items?.length > 0
|
|
};
|
|
|
|
const missingFields = Object.entries(mandatoryChecks)
|
|
.filter(([_, present]) => !present)
|
|
.map(([field, _]) => field);
|
|
|
|
if (missingFields.length === 0) {
|
|
filesWithAllMandatory++;
|
|
} else {
|
|
missingFields.forEach(field => {
|
|
missingFieldsCount[field] = (missingFieldsCount[field] || 0) + 1;
|
|
});
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error(`Failed to process ${file.path}:`, error.message);
|
|
}
|
|
}
|
|
|
|
console.log(`\nMandatory fields analysis:`);
|
|
console.log(`- Total files analyzed: ${totalFiles}`);
|
|
console.log(`- Files with all mandatory fields: ${filesWithAllMandatory}`);
|
|
console.log(`- Compliance rate: ${((filesWithAllMandatory / totalFiles) * 100).toFixed(1)}%`);
|
|
|
|
if (Object.keys(missingFieldsCount).length > 0) {
|
|
console.log(`\nMost commonly missing fields:`);
|
|
Object.entries(missingFieldsCount)
|
|
.sort(([, a], [, b]) => b - a)
|
|
.slice(0, 5)
|
|
.forEach(([field, count]) => {
|
|
console.log(` - ${field}: missing in ${count} files`);
|
|
});
|
|
}
|
|
|
|
// At least 50% of valid corpus files should have all mandatory fields
|
|
// Note: Some corpus files may use different structures that aren't fully supported yet
|
|
const complianceRate = (filesWithAllMandatory / totalFiles) * 100;
|
|
expect(complianceRate).toBeGreaterThan(50);
|
|
});
|
|
|
|
tap.start(); |