739 lines
23 KiB
TypeScript
739 lines
23 KiB
TypeScript
import { tap } from '@git.zone/tstest/tapbundle';
|
||
import * as plugins from '../plugins.js';
|
||
import { EInvoice } from '../../../ts/index.js';
|
||
import { PerformanceTracker } from '../performance.tracker.js';
|
||
import { CorpusLoader } from '../corpus.loader.js';
|
||
|
||
const performanceTracker = new PerformanceTracker('STD-01: EN16931 Core Compliance');
|
||
|
||
tap.test('STD-01: EN16931 Core Compliance - should validate EN16931 core standard compliance', async (t) => {
|
||
const einvoice = new EInvoice();
|
||
const corpusLoader = new CorpusLoader();
|
||
|
||
// Test 1: Mandatory fields validation
|
||
const mandatoryFieldsValidation = await performanceTracker.measureAsync(
|
||
'mandatory-fields-validation',
|
||
async () => {
|
||
const mandatoryFields = [
|
||
'BT-1', // Invoice number
|
||
'BT-2', // Invoice issue date
|
||
'BT-5', // Invoice currency code
|
||
'BT-6', // VAT accounting currency code
|
||
'BT-9', // Payment due date
|
||
'BT-24', // Specification identifier
|
||
'BT-27', // Buyer name
|
||
'BT-44', // Seller name
|
||
'BT-109', // Invoice line net amount
|
||
'BT-112', // Invoice total amount without VAT
|
||
'BT-115', // Amount due for payment
|
||
];
|
||
|
||
const testInvoices = [
|
||
{
|
||
name: 'complete-invoice',
|
||
xml: createCompleteEN16931Invoice()
|
||
},
|
||
{
|
||
name: 'missing-bt1',
|
||
xml: createEN16931InvoiceWithout('BT-1')
|
||
},
|
||
{
|
||
name: 'missing-bt27',
|
||
xml: createEN16931InvoiceWithout('BT-27')
|
||
},
|
||
{
|
||
name: 'missing-multiple',
|
||
xml: createEN16931InvoiceWithout(['BT-5', 'BT-44'])
|
||
}
|
||
];
|
||
|
||
const results = [];
|
||
|
||
for (const test of testInvoices) {
|
||
try {
|
||
const parsed = await einvoice.parseDocument(test.xml);
|
||
const validation = await einvoice.validateEN16931(parsed);
|
||
|
||
results.push({
|
||
invoice: test.name,
|
||
valid: validation?.isValid || false,
|
||
missingMandatory: validation?.missingMandatoryFields || [],
|
||
errors: validation?.errors || []
|
||
});
|
||
} catch (error) {
|
||
results.push({
|
||
invoice: test.name,
|
||
valid: false,
|
||
error: error.message
|
||
});
|
||
}
|
||
}
|
||
|
||
return results;
|
||
}
|
||
);
|
||
|
||
// Check complete invoice is valid
|
||
const completeInvoice = mandatoryFieldsValidation.find(r => r.invoice === 'complete-invoice');
|
||
t.ok(completeInvoice?.valid, 'Complete EN16931 invoice should be valid');
|
||
|
||
// Check missing fields are detected
|
||
mandatoryFieldsValidation.filter(r => r.invoice !== 'complete-invoice').forEach(result => {
|
||
t.notOk(result.valid, `Invoice ${result.invoice} should be invalid`);
|
||
t.ok(result.missingMandatory?.length > 0, 'Missing mandatory fields should be detected');
|
||
});
|
||
|
||
// Test 2: Business rules validation
|
||
const businessRulesValidation = await performanceTracker.measureAsync(
|
||
'business-rules-validation',
|
||
async () => {
|
||
const businessRuleTests = [
|
||
{
|
||
rule: 'BR-1',
|
||
description: 'Invoice shall have Specification identifier',
|
||
xml: createInvoiceViolatingBR('BR-1')
|
||
},
|
||
{
|
||
rule: 'BR-2',
|
||
description: 'Invoice shall have Invoice number',
|
||
xml: createInvoiceViolatingBR('BR-2')
|
||
},
|
||
{
|
||
rule: 'BR-3',
|
||
description: 'Invoice shall have Issue date',
|
||
xml: createInvoiceViolatingBR('BR-3')
|
||
},
|
||
{
|
||
rule: 'BR-CO-10',
|
||
description: 'Sum of line net amounts = Total without VAT',
|
||
xml: createInvoiceViolatingBR('BR-CO-10')
|
||
},
|
||
{
|
||
rule: 'BR-CO-15',
|
||
description: 'Total with VAT = Total without VAT + VAT',
|
||
xml: createInvoiceViolatingBR('BR-CO-15')
|
||
}
|
||
];
|
||
|
||
const results = [];
|
||
|
||
for (const test of businessRuleTests) {
|
||
try {
|
||
const parsed = await einvoice.parseDocument(test.xml);
|
||
const validation = await einvoice.validateEN16931BusinessRules(parsed);
|
||
|
||
const ruleViolated = validation?.violations?.find(v => v.rule === test.rule);
|
||
|
||
results.push({
|
||
rule: test.rule,
|
||
description: test.description,
|
||
violated: !!ruleViolated,
|
||
severity: ruleViolated?.severity || 'unknown',
|
||
message: ruleViolated?.message
|
||
});
|
||
} catch (error) {
|
||
results.push({
|
||
rule: test.rule,
|
||
error: error.message
|
||
});
|
||
}
|
||
}
|
||
|
||
return results;
|
||
}
|
||
);
|
||
|
||
businessRulesValidation.forEach(result => {
|
||
t.ok(result.violated, `Business rule ${result.rule} violation should be detected`);
|
||
});
|
||
|
||
// Test 3: Syntax bindings compliance
|
||
const syntaxBindingsCompliance = await performanceTracker.measureAsync(
|
||
'syntax-bindings-compliance',
|
||
async () => {
|
||
const syntaxTests = [
|
||
{
|
||
syntax: 'UBL',
|
||
version: '2.1',
|
||
xml: createUBLEN16931Invoice()
|
||
},
|
||
{
|
||
syntax: 'CII',
|
||
version: 'D16B',
|
||
xml: createCIIEN16931Invoice()
|
||
}
|
||
];
|
||
|
||
const results = [];
|
||
|
||
for (const test of syntaxTests) {
|
||
try {
|
||
const parsed = await einvoice.parseDocument(test.xml);
|
||
const compliance = await einvoice.checkEN16931SyntaxBinding(parsed, {
|
||
syntax: test.syntax,
|
||
version: test.version
|
||
});
|
||
|
||
results.push({
|
||
syntax: test.syntax,
|
||
version: test.version,
|
||
compliant: compliance?.isCompliant || false,
|
||
mappingComplete: compliance?.allFieldsMapped || false,
|
||
unmappedFields: compliance?.unmappedFields || [],
|
||
syntaxSpecificRules: compliance?.syntaxRulesPassed || false
|
||
});
|
||
} catch (error) {
|
||
results.push({
|
||
syntax: test.syntax,
|
||
version: test.version,
|
||
compliant: false,
|
||
error: error.message
|
||
});
|
||
}
|
||
}
|
||
|
||
return results;
|
||
}
|
||
);
|
||
|
||
syntaxBindingsCompliance.forEach(result => {
|
||
t.ok(result.compliant, `${result.syntax} syntax binding should be compliant`);
|
||
t.ok(result.mappingComplete, `All EN16931 fields should map to ${result.syntax}`);
|
||
});
|
||
|
||
// Test 4: Code list validation
|
||
const codeListValidation = await performanceTracker.measureAsync(
|
||
'code-list-validation',
|
||
async () => {
|
||
const codeListTests = [
|
||
{
|
||
field: 'BT-5',
|
||
name: 'Currency code',
|
||
validCode: 'EUR',
|
||
invalidCode: 'XXX'
|
||
},
|
||
{
|
||
field: 'BT-40',
|
||
name: 'Country code',
|
||
validCode: 'DE',
|
||
invalidCode: 'ZZ'
|
||
},
|
||
{
|
||
field: 'BT-48',
|
||
name: 'VAT category code',
|
||
validCode: 'S',
|
||
invalidCode: 'X'
|
||
},
|
||
{
|
||
field: 'BT-81',
|
||
name: 'Payment means code',
|
||
validCode: '30',
|
||
invalidCode: '99'
|
||
},
|
||
{
|
||
field: 'BT-130',
|
||
name: 'Unit of measure',
|
||
validCode: 'C62',
|
||
invalidCode: 'XXX'
|
||
}
|
||
];
|
||
|
||
const results = [];
|
||
|
||
for (const test of codeListTests) {
|
||
// Test valid code
|
||
const validInvoice = createInvoiceWithCode(test.field, test.validCode);
|
||
const validResult = await einvoice.validateEN16931CodeLists(validInvoice);
|
||
|
||
// Test invalid code
|
||
const invalidInvoice = createInvoiceWithCode(test.field, test.invalidCode);
|
||
const invalidResult = await einvoice.validateEN16931CodeLists(invalidInvoice);
|
||
|
||
results.push({
|
||
field: test.field,
|
||
name: test.name,
|
||
validCodeAccepted: validResult?.isValid || false,
|
||
invalidCodeRejected: !invalidResult?.isValid,
|
||
codeListUsed: validResult?.codeListVersion
|
||
});
|
||
}
|
||
|
||
return results;
|
||
}
|
||
);
|
||
|
||
codeListValidation.forEach(result => {
|
||
t.ok(result.validCodeAccepted, `Valid ${result.name} should be accepted`);
|
||
t.ok(result.invalidCodeRejected, `Invalid ${result.name} should be rejected`);
|
||
});
|
||
|
||
// Test 5: Calculation rules
|
||
const calculationRules = await performanceTracker.measureAsync(
|
||
'calculation-rules-validation',
|
||
async () => {
|
||
const calculationTests = [
|
||
{
|
||
name: 'line-extension-amount',
|
||
rule: 'BT-131 = BT-129 × BT-130',
|
||
values: { quantity: 10, price: 50.00, expected: 500.00 }
|
||
},
|
||
{
|
||
name: 'invoice-total-without-vat',
|
||
rule: 'BT-109 sum = BT-112',
|
||
lineAmounts: [100.00, 200.00, 150.00],
|
||
expected: 450.00
|
||
},
|
||
{
|
||
name: 'invoice-total-with-vat',
|
||
rule: 'BT-112 + BT-110 = BT-113',
|
||
values: { netTotal: 1000.00, vatAmount: 190.00, expected: 1190.00 }
|
||
},
|
||
{
|
||
name: 'vat-calculation',
|
||
rule: 'BT-116 × (BT-119/100) = BT-117',
|
||
values: { taxableAmount: 1000.00, vatRate: 19.00, expected: 190.00 }
|
||
}
|
||
];
|
||
|
||
const results = [];
|
||
|
||
for (const test of calculationTests) {
|
||
const invoice = createInvoiceWithCalculation(test);
|
||
const validation = await einvoice.validateEN16931Calculations(invoice);
|
||
|
||
const calculationResult = validation?.calculations?.find(c => c.rule === test.rule);
|
||
|
||
results.push({
|
||
name: test.name,
|
||
rule: test.rule,
|
||
correct: calculationResult?.isCorrect || false,
|
||
calculated: calculationResult?.calculatedValue,
|
||
expected: calculationResult?.expectedValue,
|
||
tolerance: calculationResult?.tolerance || 0.01
|
||
});
|
||
}
|
||
|
||
return results;
|
||
}
|
||
);
|
||
|
||
calculationRules.forEach(result => {
|
||
t.ok(result.correct, `Calculation ${result.name} should be correct`);
|
||
});
|
||
|
||
// Test 6: Conditional fields
|
||
const conditionalFields = await performanceTracker.measureAsync(
|
||
'conditional-fields-validation',
|
||
async () => {
|
||
const conditionalTests = [
|
||
{
|
||
condition: 'If BT-31 exists, then BT-32 is mandatory',
|
||
scenario: 'seller-tax-representative',
|
||
fields: { 'BT-31': 'Tax Rep Name', 'BT-32': null }
|
||
},
|
||
{
|
||
condition: 'If BT-7 != BT-2, then BT-7 is allowed',
|
||
scenario: 'tax-point-date',
|
||
fields: { 'BT-2': '2024-01-15', 'BT-7': '2024-01-20' }
|
||
},
|
||
{
|
||
condition: 'If credit note, BT-3 must be 381',
|
||
scenario: 'credit-note-type',
|
||
fields: { 'BT-3': '380', isCreditNote: true }
|
||
}
|
||
];
|
||
|
||
const results = [];
|
||
|
||
for (const test of conditionalTests) {
|
||
const invoice = createInvoiceWithConditional(test);
|
||
const validation = await einvoice.validateEN16931Conditionals(invoice);
|
||
|
||
results.push({
|
||
condition: test.condition,
|
||
scenario: test.scenario,
|
||
valid: validation?.isValid || false,
|
||
conditionMet: validation?.conditionsMet?.includes(test.condition),
|
||
errors: validation?.conditionalErrors || []
|
||
});
|
||
}
|
||
|
||
return results;
|
||
}
|
||
);
|
||
|
||
conditionalFields.forEach(result => {
|
||
if (result.scenario === 'tax-point-date') {
|
||
t.ok(result.valid, 'Valid conditional field should be accepted');
|
||
} else {
|
||
t.notOk(result.valid, `Invalid conditional ${result.scenario} should be rejected`);
|
||
}
|
||
});
|
||
|
||
// Test 7: Corpus EN16931 compliance testing
|
||
const corpusCompliance = await performanceTracker.measureAsync(
|
||
'corpus-en16931-compliance',
|
||
async () => {
|
||
const en16931Files = await corpusLoader.getFilesByPattern('**/EN16931*.xml');
|
||
const sampleSize = Math.min(10, en16931Files.length);
|
||
const samples = en16931Files.slice(0, sampleSize);
|
||
|
||
const results = {
|
||
total: samples.length,
|
||
compliant: 0,
|
||
nonCompliant: 0,
|
||
errors: []
|
||
};
|
||
|
||
for (const file of samples) {
|
||
try {
|
||
const content = await corpusLoader.readFile(file);
|
||
const parsed = await einvoice.parseDocument(content);
|
||
const validation = await einvoice.validateEN16931(parsed);
|
||
|
||
if (validation?.isValid) {
|
||
results.compliant++;
|
||
} else {
|
||
results.nonCompliant++;
|
||
results.errors.push({
|
||
file: file.name,
|
||
errors: validation?.errors?.slice(0, 3) // First 3 errors
|
||
});
|
||
}
|
||
} catch (error) {
|
||
results.errors.push({
|
||
file: file.name,
|
||
error: error.message
|
||
});
|
||
}
|
||
}
|
||
|
||
return results;
|
||
}
|
||
);
|
||
|
||
t.ok(corpusCompliance.compliant > 0, 'Some corpus files should be EN16931 compliant');
|
||
|
||
// Test 8: Profile validation
|
||
const profileValidation = await performanceTracker.measureAsync(
|
||
'en16931-profile-validation',
|
||
async () => {
|
||
const profiles = [
|
||
{
|
||
name: 'BASIC',
|
||
level: 'Minimum',
|
||
requiredFields: ['BT-1', 'BT-2', 'BT-5', 'BT-27', 'BT-44']
|
||
},
|
||
{
|
||
name: 'COMFORT',
|
||
level: 'Basic+',
|
||
requiredFields: ['BT-1', 'BT-2', 'BT-5', 'BT-27', 'BT-44', 'BT-50', 'BT-51']
|
||
},
|
||
{
|
||
name: 'EXTENDED',
|
||
level: 'Full',
|
||
requiredFields: null // All fields allowed
|
||
}
|
||
];
|
||
|
||
const results = [];
|
||
|
||
for (const profile of profiles) {
|
||
const invoice = createEN16931InvoiceForProfile(profile.name);
|
||
const validation = await einvoice.validateEN16931Profile(invoice, profile.name);
|
||
|
||
results.push({
|
||
profile: profile.name,
|
||
level: profile.level,
|
||
valid: validation?.isValid || false,
|
||
profileCompliant: validation?.profileCompliant || false,
|
||
fieldCoverage: validation?.fieldCoverage || 0
|
||
});
|
||
}
|
||
|
||
return results;
|
||
}
|
||
);
|
||
|
||
profileValidation.forEach(result => {
|
||
t.ok(result.valid, `Profile ${result.profile} should validate`);
|
||
});
|
||
|
||
// Test 9: Extension handling
|
||
const extensionHandling = await performanceTracker.measureAsync(
|
||
'en16931-extension-handling',
|
||
async () => {
|
||
const extensionTests = [
|
||
{
|
||
name: 'national-extension',
|
||
type: 'DE-specific',
|
||
xml: createEN16931WithExtension('national')
|
||
},
|
||
{
|
||
name: 'sector-extension',
|
||
type: 'Construction',
|
||
xml: createEN16931WithExtension('sector')
|
||
},
|
||
{
|
||
name: 'custom-extension',
|
||
type: 'Company-specific',
|
||
xml: createEN16931WithExtension('custom')
|
||
}
|
||
];
|
||
|
||
const results = [];
|
||
|
||
for (const test of extensionTests) {
|
||
try {
|
||
const parsed = await einvoice.parseDocument(test.xml);
|
||
const validation = await einvoice.validateEN16931WithExtensions(parsed);
|
||
|
||
results.push({
|
||
extension: test.name,
|
||
type: test.type,
|
||
coreValid: validation?.coreCompliant || false,
|
||
extensionValid: validation?.extensionValid || false,
|
||
extensionPreserved: validation?.extensionDataPreserved || false
|
||
});
|
||
} catch (error) {
|
||
results.push({
|
||
extension: test.name,
|
||
type: test.type,
|
||
error: error.message
|
||
});
|
||
}
|
||
}
|
||
|
||
return results;
|
||
}
|
||
);
|
||
|
||
extensionHandling.forEach(result => {
|
||
t.ok(result.coreValid, `Core EN16931 should remain valid with ${result.extension}`);
|
||
t.ok(result.extensionPreserved, 'Extension data should be preserved');
|
||
});
|
||
|
||
// Test 10: Semantic validation
|
||
const semanticValidation = await performanceTracker.measureAsync(
|
||
'en16931-semantic-validation',
|
||
async () => {
|
||
const semanticTests = [
|
||
{
|
||
name: 'date-logic',
|
||
test: 'Issue date before due date',
|
||
valid: { issueDate: '2024-01-15', dueDate: '2024-02-15' },
|
||
invalid: { issueDate: '2024-02-15', dueDate: '2024-01-15' }
|
||
},
|
||
{
|
||
name: 'amount-signs',
|
||
test: 'Credit note amounts negative',
|
||
valid: { type: '381', amount: -100.00 },
|
||
invalid: { type: '381', amount: 100.00 }
|
||
},
|
||
{
|
||
name: 'tax-logic',
|
||
test: 'VAT rate matches category',
|
||
valid: { category: 'S', rate: 19.00 },
|
||
invalid: { category: 'Z', rate: 19.00 }
|
||
}
|
||
];
|
||
|
||
const results = [];
|
||
|
||
for (const test of semanticTests) {
|
||
// Test valid scenario
|
||
const validInvoice = createInvoiceWithSemantic(test.valid);
|
||
const validResult = await einvoice.validateEN16931Semantics(validInvoice);
|
||
|
||
// Test invalid scenario
|
||
const invalidInvoice = createInvoiceWithSemantic(test.invalid);
|
||
const invalidResult = await einvoice.validateEN16931Semantics(invalidInvoice);
|
||
|
||
results.push({
|
||
name: test.name,
|
||
test: test.test,
|
||
validAccepted: validResult?.isValid || false,
|
||
invalidRejected: !invalidResult?.isValid,
|
||
semanticErrors: invalidResult?.semanticErrors || []
|
||
});
|
||
}
|
||
|
||
return results;
|
||
}
|
||
);
|
||
|
||
semanticValidation.forEach(result => {
|
||
t.ok(result.validAccepted, `Valid semantic ${result.name} should be accepted`);
|
||
t.ok(result.invalidRejected, `Invalid semantic ${result.name} should be rejected`);
|
||
});
|
||
|
||
// Print performance summary
|
||
performanceTracker.printSummary();
|
||
});
|
||
|
||
// Helper functions
|
||
function createCompleteEN16931Invoice(): string {
|
||
return `<?xml version="1.0" encoding="UTF-8"?>
|
||
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"
|
||
xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2"
|
||
xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2">
|
||
<cbc:CustomizationID>urn:cen.eu:en16931:2017</cbc:CustomizationID>
|
||
<cbc:ID>INV-001</cbc:ID>
|
||
<cbc:IssueDate>2024-01-15</cbc:IssueDate>
|
||
<cbc:DueDate>2024-02-15</cbc:DueDate>
|
||
<cbc:InvoiceTypeCode>380</cbc:InvoiceTypeCode>
|
||
<cbc:DocumentCurrencyCode>EUR</cbc:DocumentCurrencyCode>
|
||
|
||
<cac:AccountingSupplierParty>
|
||
<cac:Party>
|
||
<cac:PartyName>
|
||
<cbc:Name>Seller Company</cbc:Name>
|
||
</cac:PartyName>
|
||
</cac:Party>
|
||
</cac:AccountingSupplierParty>
|
||
|
||
<cac:AccountingCustomerParty>
|
||
<cac:Party>
|
||
<cac:PartyName>
|
||
<cbc:Name>Buyer Company</cbc:Name>
|
||
</cac:PartyName>
|
||
</cac:Party>
|
||
</cac:AccountingCustomerParty>
|
||
|
||
<cac:LegalMonetaryTotal>
|
||
<cbc:LineExtensionAmount currencyID="EUR">1000.00</cbc:LineExtensionAmount>
|
||
<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">10</cbc:InvoicedQuantity>
|
||
<cbc:LineExtensionAmount currencyID="EUR">1000.00</cbc:LineExtensionAmount>
|
||
<cac:Price>
|
||
<cbc:PriceAmount currencyID="EUR">100.00</cbc:PriceAmount>
|
||
</cac:Price>
|
||
</cac:InvoiceLine>
|
||
</Invoice>`;
|
||
}
|
||
|
||
function createEN16931InvoiceWithout(fields: string | string[]): string {
|
||
// Create invoice missing specified fields
|
||
const fieldsToOmit = Array.isArray(fields) ? fields : [fields];
|
||
let invoice = createCompleteEN16931Invoice();
|
||
|
||
// Remove fields based on BT codes
|
||
if (fieldsToOmit.includes('BT-1')) {
|
||
invoice = invoice.replace(/<cbc:ID>.*?<\/cbc:ID>/, '');
|
||
}
|
||
if (fieldsToOmit.includes('BT-5')) {
|
||
invoice = invoice.replace(/<cbc:DocumentCurrencyCode>.*?<\/cbc:DocumentCurrencyCode>/, '');
|
||
}
|
||
// ... etc
|
||
|
||
return invoice;
|
||
}
|
||
|
||
function createInvoiceViolatingBR(rule: string): string {
|
||
// Create invoices that violate specific business rules
|
||
const base = createCompleteEN16931Invoice();
|
||
|
||
switch (rule) {
|
||
case 'BR-CO-10':
|
||
// Sum of lines != total
|
||
return base.replace('<cbc:LineExtensionAmount currencyID="EUR">1000.00</cbc:LineExtensionAmount>',
|
||
'<cbc:LineExtensionAmount currencyID="EUR">900.00</cbc:LineExtensionAmount>');
|
||
case 'BR-CO-15':
|
||
// Total with VAT != Total without VAT + VAT
|
||
return base.replace('<cbc:TaxInclusiveAmount currencyID="EUR">1190.00</cbc:TaxInclusiveAmount>',
|
||
'<cbc:TaxInclusiveAmount currencyID="EUR">1200.00</cbc:TaxInclusiveAmount>');
|
||
default:
|
||
return base;
|
||
}
|
||
}
|
||
|
||
function createUBLEN16931Invoice(): string {
|
||
return createCompleteEN16931Invoice();
|
||
}
|
||
|
||
function createCIIEN16931Invoice(): string {
|
||
return `<?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">
|
||
<rsm:ExchangedDocumentContext>
|
||
<ram:GuidelineSpecifiedDocumentContextParameter>
|
||
<ram:ID>urn:cen.eu:en16931:2017</ram:ID>
|
||
</ram:GuidelineSpecifiedDocumentContextParameter>
|
||
</rsm:ExchangedDocumentContext>
|
||
<rsm:ExchangedDocument>
|
||
<ram:ID>INV-001</ram:ID>
|
||
<ram:TypeCode>380</ram:TypeCode>
|
||
<ram:IssueDateTime>
|
||
<udt:DateTimeString format="102">20240115</udt:DateTimeString>
|
||
</ram:IssueDateTime>
|
||
</rsm:ExchangedDocument>
|
||
</rsm:CrossIndustryInvoice>`;
|
||
}
|
||
|
||
function createInvoiceWithCode(field: string, code: string): any {
|
||
// Return invoice object with specific code
|
||
return {
|
||
currencyCode: field === 'BT-5' ? code : 'EUR',
|
||
countryCode: field === 'BT-40' ? code : 'DE',
|
||
vatCategoryCode: field === 'BT-48' ? code : 'S',
|
||
paymentMeansCode: field === 'BT-81' ? code : '30',
|
||
unitCode: field === 'BT-130' ? code : 'C62'
|
||
};
|
||
}
|
||
|
||
function createInvoiceWithCalculation(test: any): any {
|
||
// Create invoice with specific calculation scenario
|
||
return {
|
||
lines: test.lineAmounts?.map(amount => ({ netAmount: amount })),
|
||
totals: {
|
||
netTotal: test.values?.netTotal,
|
||
vatAmount: test.values?.vatAmount,
|
||
grossTotal: test.values?.expected
|
||
}
|
||
};
|
||
}
|
||
|
||
function createInvoiceWithConditional(test: any): any {
|
||
// Create invoice with conditional field scenario
|
||
return {
|
||
...test.fields,
|
||
documentType: test.isCreditNote ? 'CreditNote' : 'Invoice'
|
||
};
|
||
}
|
||
|
||
function createEN16931InvoiceForProfile(profile: string): string {
|
||
// Create invoice matching specific profile requirements
|
||
if (profile === 'BASIC') {
|
||
return createEN16931InvoiceWithout(['BT-50', 'BT-51']); // Remove optional fields
|
||
}
|
||
return createCompleteEN16931Invoice();
|
||
}
|
||
|
||
function createEN16931WithExtension(type: string): string {
|
||
const base = createCompleteEN16931Invoice();
|
||
const extension = type === 'national' ?
|
||
'<ext:GermanSpecificField>Value</ext:GermanSpecificField>' :
|
||
'<ext:CustomField>Value</ext:CustomField>';
|
||
|
||
return base.replace('</Invoice>', `${extension}</Invoice>`);
|
||
}
|
||
|
||
function createInvoiceWithSemantic(scenario: any): any {
|
||
return {
|
||
issueDate: scenario.issueDate,
|
||
dueDate: scenario.dueDate,
|
||
documentType: scenario.type,
|
||
totalAmount: scenario.amount,
|
||
vatCategory: scenario.category,
|
||
vatRate: scenario.rate
|
||
};
|
||
}
|
||
|
||
// Run the test
|
||
tap.start(); |