Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion src/schematron/cen/ae/cen-en16931-br-ae-08.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* @description Unit tests for CEN-EN16931-BR-AE-08.
*/
import { assert, describe, it } from '@effect/vitest';
import { assert, describe, expect, it } from '@effect/vitest';
import { Effect, Result } from 'effect';

import { decodeBaseExample } from '#/test/test-utils.ts';
Expand Down Expand Up @@ -36,6 +36,13 @@ describe('CEN-EN16931-BR-AE-08', () => {
];
const result = yield* validateCenEn16931BrAe08(document).pipe(Effect.result);
assert(Result.isFailure(result));
if (Result.isFailure(result)) {
assert(result.failure.fields.length > 0);
expect(result.failure.fields).toEqual([
{ path: 'taxTotals[0].taxSubtotals[0].taxableAmount.value', expected: 2800, actual: 50 },
{ path: 'invoiceLines[0].lineExtensionAmount.value', expected: null, actual: 2800 },
]);
}
})
);
});
10 changes: 2 additions & 8 deletions src/schematron/cen/ae/cen-en16931-br-ae-08.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import type { PeppolDocument } from '#/schemas/peppol-document-schema.ts';
import type { SchematronRule } from '#/schematron/helpers.ts';

import { amountsEqual, categoryTaxableSum, getTaxSubtotalsWithCode, schematronRule } from '#/schematron/helpers.ts';
import { categoryTaxableSumFieldIssues, schematronRule } from '#/schematron/helpers.ts';

const rule = {
id: 'CEN-EN16931-BR-AE-08',
Expand All @@ -10,9 +9,4 @@ const rule = {
'In a VAT breakdown (BG-23) where the VAT category code (BT-118) is "Reverse charge" the VAT category taxable amount (BT-116) shall equal the sum of Invoice line net amounts (BT-131) minus the sum of Document level allowance amounts (BT-92) plus the sum of Document level charge amounts (BT-99) where the VAT category codes (BT-151, BT-95, BT-102) are "Reverse charge".',
} as const satisfies SchematronRule;

function evaluateCenEn16931BrAe08(document: PeppolDocument): boolean {
const passed = getTaxSubtotalsWithCode(document, 'AE').every(st => amountsEqual(st.taxableAmount, categoryTaxableSum(document, 'AE')));
return passed;
}

export const validateCenEn16931BrAe08 = schematronRule(rule, evaluateCenEn16931BrAe08);
export const validateCenEn16931BrAe08 = schematronRule(rule, document => categoryTaxableSumFieldIssues(document, 'AE'));
7 changes: 7 additions & 0 deletions src/schematron/cen/ae/cen-en16931-br-ae-09.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,13 @@ describe('CEN-EN16931-BR-AE-09', () => {
];
const result = yield* validateCenEn16931BrAe09(document).pipe(Effect.result);
assert(Result.isFailure(result));
if (Result.isFailure(result)) {
const fields = result.failure.fields ?? [];
assert(fields.length > 0);
assert(fields[0]?.path === 'taxTotals[0].taxSubtotals[0].taxAmount.value');
assert(fields[0]?.expected === 0);
assert(fields[0]?.actual === 1);
}
})
);
});
19 changes: 15 additions & 4 deletions src/schematron/cen/ae/cen-en16931-br-ae-09.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import type { PeppolDocument } from '#/schemas/peppol-document-schema.ts';
import type { SchematronRule } from '#/schematron/helpers.ts';
import type { SchematronFieldIssue } from '#/schematron/types.ts';

import { getTaxSubtotalsWithCode, schematronRule } from '#/schematron/helpers.ts';
import { fieldIssue, schematronRule } from '#/schematron/helpers.ts';

const rule = {
id: 'CEN-EN16931-BR-AE-09',
Expand All @@ -10,9 +11,19 @@ const rule = {
'The VAT category tax amount (BT-117) in a VAT breakdown (BG-23) where the VAT category code (BT-118) is "Reverse charge" shall be 0 (zero).',
} as const satisfies SchematronRule;

function evaluateCenEn16931BrAe09(document: PeppolDocument): boolean {
const passed = getTaxSubtotalsWithCode(document, 'AE').every(st => st.taxAmount === 0);
return passed;
function evaluateCenEn16931BrAe09(document: PeppolDocument): ReadonlyArray<SchematronFieldIssue> {
const issues: Array<SchematronFieldIssue> = [];

document.taxTotals.forEach((total, taxTotalIndex) => {
(total.taxSubtotals ?? []).forEach((subtotal, taxSubtotalIndex) => {
if (subtotal.taxCategory.id !== 'AE' || subtotal.taxAmount.value === 0) {
return;
}
issues.push(fieldIssue(`taxTotals[${taxTotalIndex}].taxSubtotals[${taxSubtotalIndex}].taxAmount.value`, 0, subtotal.taxAmount.value));
});
});

return issues;
}

export const validateCenEn16931BrAe09 = schematronRule(rule, evaluateCenEn16931BrAe09);
9 changes: 8 additions & 1 deletion src/schematron/cen/af/cen-en16931-br-af-08.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* @description Unit tests for CEN-EN16931-BR-AF-08.
*/
import { assert, describe, it } from '@effect/vitest';
import { assert, describe, expect, it } from '@effect/vitest';
import { Effect, Result } from 'effect';

import { decodeBaseExample } from '#/test/test-utils.ts';
Expand Down Expand Up @@ -36,6 +36,13 @@ describe('CEN-EN16931-BR-AF-08', () => {
];
const result = yield* validateCenEn16931BrAf08(document).pipe(Effect.result);
assert(Result.isFailure(result));
if (Result.isFailure(result)) {
assert(result.failure.fields.length > 0);
expect(result.failure.fields).toEqual([
{ path: 'taxTotals[0].taxSubtotals[0].taxableAmount.value', expected: 0, actual: 50 },
{ path: 'taxTotals[0].taxSubtotals[0].taxCategory.percent', expected: null, actual: 0 },
]);
}
})
);
});
10 changes: 2 additions & 8 deletions src/schematron/cen/af/cen-en16931-br-af-08.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import type { PeppolDocument } from '#/schemas/peppol-document-schema.ts';
import type { SchematronRule } from '#/schematron/helpers.ts';

import { everyVatBreakdownTaxableMatchesRateSum, schematronRule } from '#/schematron/helpers.ts';
import { schematronRule, standardRatedTaxableSumFieldIssues } from '#/schematron/helpers.ts';

const rule = {
id: 'CEN-EN16931-BR-AF-08',
Expand All @@ -10,9 +9,4 @@ const rule = {
'For each different value of VAT category rate (BT-119) where the VAT category code (BT-118) is "IGIC", the VAT category taxable amount (BT-116) in a VAT breakdown (BG-23) shall equal the sum of Invoice line net amounts (BT-131) plus the sum of document level charge amounts (BT-99) minus the sum of document level allowance amounts (BT-92) where the VAT category code (BT-151, BT-102, BT-95) is "IGIC" and the VAT rate (BT-152, BT-103, BT-96) equals the VAT category rate (BT-119).',
} as const satisfies SchematronRule;

function evaluateCenEn16931BrAf08(document: PeppolDocument): boolean {
const passed = everyVatBreakdownTaxableMatchesRateSum(document);
return passed;
}

export const validateCenEn16931BrAf08 = schematronRule(rule, evaluateCenEn16931BrAf08);
export const validateCenEn16931BrAf08 = schematronRule(rule, standardRatedTaxableSumFieldIssues);
14 changes: 14 additions & 0 deletions src/schematron/cen/af/cen-en16931-br-af-09.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,20 @@ describe('CEN-EN16931-BR-AF-09', () => {
];
const result = yield* validateCenEn16931BrAf09(document).pipe(Effect.result);
assert(Result.isFailure(result));
if (Result.isFailure(result)) {
const fields = result.failure.fields ?? [];
assert(fields.length > 0);
assert(fields[0]?.path === 'taxTotals[0].taxSubtotals[0].taxAmount.value');
assert(fields[0]?.expected === 0);
assert(fields[0]?.actual === 1);
const operands = fields.slice(1);
assert(operands.length === 2);
for (const operand of operands) {
assert(operand.expected === null);
}
assert(operands[0]?.actual === 100);
assert(operands[1]?.actual === 0);
}
})
);
});
12 changes: 2 additions & 10 deletions src/schematron/cen/af/cen-en16931-br-af-09.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import type { PeppolDocument } from '#/schemas/peppol-document-schema.ts';
import type { SchematronRule } from '#/schematron/helpers.ts';

import { getTaxSubtotalsWithCode, round2, schematronRule, withinSlackOne } from '#/schematron/helpers.ts';
import { categoryTaxAmountFieldIssues, schematronRule } from '#/schematron/helpers.ts';

const rule = {
id: 'CEN-EN16931-BR-AF-09',
Expand All @@ -10,11 +9,4 @@ const rule = {
'The VAT category tax amount (BT-117) in a VAT breakdown (BG-23) where VAT category code (BT-118) is "IGIC" shall equal the VAT category taxable amount (BT-116) multiplied by the VAT category rate (BT-119).',
} as const satisfies SchematronRule;

function evaluateCenEn16931BrAf09(document: PeppolDocument): boolean {
const passed = getTaxSubtotalsWithCode(document, 'L').every(st =>
withinSlackOne(st.taxAmount, round2((st.taxableAmount * (st.taxCategory.percent ?? 0)) / 100))
);
return passed;
}

export const validateCenEn16931BrAf09 = schematronRule(rule, evaluateCenEn16931BrAf09);
export const validateCenEn16931BrAf09 = schematronRule(rule, document => categoryTaxAmountFieldIssues(document, 'L'));
9 changes: 8 additions & 1 deletion src/schematron/cen/ag/cen-en16931-br-ag-08.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* @description Unit tests for CEN-EN16931-BR-AG-08.
*/
import { assert, describe, it } from '@effect/vitest';
import { assert, describe, expect, it } from '@effect/vitest';
import { Effect, Result } from 'effect';

import { decodeBaseExample } from '#/test/test-utils.ts';
Expand Down Expand Up @@ -36,6 +36,13 @@ describe('CEN-EN16931-BR-AG-08', () => {
];
const result = yield* validateCenEn16931BrAg08(document).pipe(Effect.result);
assert(Result.isFailure(result));
if (Result.isFailure(result)) {
assert(result.failure.fields.length > 0);
expect(result.failure.fields).toEqual([
{ path: 'taxTotals[0].taxSubtotals[0].taxableAmount.value', expected: 0, actual: 50 },
{ path: 'taxTotals[0].taxSubtotals[0].taxCategory.percent', expected: null, actual: 0 },
]);
}
})
);
});
10 changes: 2 additions & 8 deletions src/schematron/cen/ag/cen-en16931-br-ag-08.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import type { PeppolDocument } from '#/schemas/peppol-document-schema.ts';
import type { SchematronRule } from '#/schematron/helpers.ts';

import { everyVatBreakdownTaxableMatchesRateSum, schematronRule } from '#/schematron/helpers.ts';
import { schematronRule, standardRatedTaxableSumFieldIssues } from '#/schematron/helpers.ts';

const rule = {
id: 'CEN-EN16931-BR-AG-08',
Expand All @@ -10,9 +9,4 @@ const rule = {
'For each different value of VAT category rate (BT-119) where the VAT category code (BT-118) is "IPSI", the VAT category taxable amount (BT-116) in a VAT breakdown (BG-23) shall equal the sum of Invoice line net amounts (BT-131) plus the sum of document level charge amounts (BT-99) minus the sum of document level allowance amounts (BT-92) where the VAT category code (BT-151, BT-102, BT-95) is "IPSI" and the VAT rate (BT-152, BT-103, BT-96) equals the VAT category rate (BT-119).',
} as const satisfies SchematronRule;

function evaluateCenEn16931BrAg08(document: PeppolDocument): boolean {
const passed = everyVatBreakdownTaxableMatchesRateSum(document);
return passed;
}

export const validateCenEn16931BrAg08 = schematronRule(rule, evaluateCenEn16931BrAg08);
export const validateCenEn16931BrAg08 = schematronRule(rule, standardRatedTaxableSumFieldIssues);
14 changes: 14 additions & 0 deletions src/schematron/cen/ag/cen-en16931-br-ag-09.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,20 @@ describe('CEN-EN16931-BR-AG-09', () => {
];
const result = yield* validateCenEn16931BrAg09(document).pipe(Effect.result);
assert(Result.isFailure(result));
if (Result.isFailure(result)) {
const fields = result.failure.fields ?? [];
assert(fields.length > 0);
assert(fields[0]?.path === 'taxTotals[0].taxSubtotals[0].taxAmount.value');
assert(fields[0]?.expected === 0);
assert(fields[0]?.actual === 1);
const operands = fields.slice(1);
assert(operands.length === 2);
for (const operand of operands) {
assert(operand.expected === null);
}
assert(operands[0]?.actual === 100);
assert(operands[1]?.actual === 0);
}
})
);
});
12 changes: 2 additions & 10 deletions src/schematron/cen/ag/cen-en16931-br-ag-09.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import type { PeppolDocument } from '#/schemas/peppol-document-schema.ts';
import type { SchematronRule } from '#/schematron/helpers.ts';

import { getTaxSubtotalsWithCode, round2, schematronRule, withinSlackOne } from '#/schematron/helpers.ts';
import { categoryTaxAmountFieldIssues, schematronRule } from '#/schematron/helpers.ts';

const rule = {
id: 'CEN-EN16931-BR-AG-09',
Expand All @@ -10,11 +9,4 @@ const rule = {
'The VAT category tax amount (BT-117) in a VAT breakdown (BG-23) where VAT category code (BT-118) is "IPSI" shall equal the VAT category taxable amount (BT-116) multiplied by the VAT category rate (BT-119).',
} as const satisfies SchematronRule;

function evaluateCenEn16931BrAg09(document: PeppolDocument): boolean {
const passed = getTaxSubtotalsWithCode(document, 'M').every(st =>
withinSlackOne(st.taxAmount, round2((st.taxableAmount * (st.taxCategory.percent ?? 0)) / 100))
);
return passed;
}

export const validateCenEn16931BrAg09 = schematronRule(rule, evaluateCenEn16931BrAg09);
export const validateCenEn16931BrAg09 = schematronRule(rule, document => categoryTaxAmountFieldIssues(document, 'M'));
6 changes: 6 additions & 0 deletions src/schematron/cen/co/cen-en16931-br-co-10.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ describe('CEN-EN16931-BR-CO-10', () => {
document.legalMonetaryTotal.lineExtensionAmount = { ...document.legalMonetaryTotal.lineExtensionAmount, value: 1 };
const result = yield* validateCenEn16931BrCo10(document).pipe(Effect.result);
assert(Result.isFailure(result));
assert(result.failure.fields.length > 0);
assert.deepStrictEqual(result.failure.fields, [
{ path: 'legalMonetaryTotal.lineExtensionAmount.value', expected: 1300, actual: 1 },
{ path: 'invoiceLines[0].lineExtensionAmount.value', expected: null, actual: 2800 },
{ path: 'invoiceLines[1].lineExtensionAmount.value', expected: null, actual: -1500 },
]);
})
);
});
19 changes: 12 additions & 7 deletions src/schematron/cen/co/cen-en16931-br-co-10.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,25 @@
import type { PeppolDocument } from '#/schemas/peppol-document-schema.ts';
import type { SchematronRule } from '#/schematron/helpers.ts';

import { amountsEqual, getLines, round2, schematronRule } from '#/schematron/helpers.ts';
import { amountsEqual, getLines, getLinesArrayName, round2, schematronRule, sumFieldIssues } from '#/schematron/helpers.ts';

const rule = {
id: 'CEN-EN16931-BR-CO-10',
level: 'fatal',
message: 'Sum of Invoice line net amount (BT-106) = Σ Invoice line net amount (BT-131).',
} as const satisfies SchematronRule;

function evaluateCenEn16931BrCo10(document: PeppolDocument): boolean {
const passed = amountsEqual(
round2(document.legalMonetaryTotal.lineExtensionAmount.value),
round2(getLines(document).reduce((sum, line) => sum + line.lineExtensionAmount.value, 0))
);
return passed;
function evaluateCenEn16931BrCo10(document: PeppolDocument) {
const linesArrayName = getLinesArrayName(document);
return sumFieldIssues({
aggregate: { actual: document.legalMonetaryTotal.lineExtensionAmount.value, path: 'legalMonetaryTotal.lineExtensionAmount.value' },
components: getLines(document).map((line, index) => ({
actual: line.lineExtensionAmount.value,
contribution: line.lineExtensionAmount.value,
path: `${linesArrayName}[${index}].lineExtensionAmount.value`,
})),
isEqual: (actual, expected) => amountsEqual(round2(actual), round2(expected)),
});
}

export const validateCenEn16931BrCo10 = schematronRule(rule, evaluateCenEn16931BrCo10);
23 changes: 23 additions & 0 deletions src/schematron/cen/co/cen-en16931-br-co-11.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,29 @@ describe('CEN-EN16931-BR-CO-11', () => {
document.legalMonetaryTotal.allowanceTotalAmount = { currencyId: 'EUR', value: 5 };
const result = yield* validateCenEn16931BrCo11(document).pipe(Effect.result);
assert(Result.isFailure(result));
assert(result.failure.fields.length > 0);
assert.deepStrictEqual(result.failure.fields, [
{ path: 'legalMonetaryTotal.allowanceTotalAmount.value', expected: 10, actual: 5 },
{ path: 'allowanceCharges[0].amount.value', expected: null, actual: 10 },
]);
})
);

it.effect(
'fails when a zero-amount allowance is present without an allowance total',
Effect.fn(function* () {
const document = yield* Effect.promise(async () => (await decodeBaseExample()) as any);
document.legalMonetaryTotal.allowanceTotalAmount = undefined;
document.allowanceCharges = [
{ amount: { currencyId: 'EUR', value: 0 }, chargeIndicator: false, taxCategory: { id: 'S', percent: 25, taxSchemeId: { id: 'VAT' } } },
];
const result = yield* validateCenEn16931BrCo11(document).pipe(Effect.result);
assert(Result.isFailure(result));
assert(result.failure.fields.length > 0);
assert.deepStrictEqual(result.failure.fields, [
{ path: 'legalMonetaryTotal.allowanceTotalAmount.value', expected: 0, actual: null },
{ path: 'allowanceCharges[0].amount.value', expected: null, actual: 0 },
]);
})
);
});
35 changes: 23 additions & 12 deletions src/schematron/cen/co/cen-en16931-br-co-11.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,37 @@
import type { PeppolDocument } from '#/schemas/peppol-document-schema.ts';
import type { SchematronRule } from '#/schematron/helpers.ts';

import { amountsEqual, round2, schematronRule } from '#/schematron/helpers.ts';
import { amountsEqual, fieldIssue, round2, schematronRule, sumFieldIssues } from '#/schematron/helpers.ts';

const rule = {
id: 'CEN-EN16931-BR-CO-11',
level: 'fatal',
message: 'Sum of allowances on document level (BT-107) = Σ Document level allowance amount (BT-92).',
} as const satisfies SchematronRule;

function evaluateCenEn16931BrCo11(document: PeppolDocument): boolean {
const passed = (() => {
const allowances = (document.allowanceCharges ?? []).filter(ac => !ac.chargeIndicator);
if (!document.legalMonetaryTotal.allowanceTotalAmount) {
return allowances.length === 0;
function evaluateCenEn16931BrCo11(document: PeppolDocument) {
const allowanceTotalAmount = document.legalMonetaryTotal.allowanceTotalAmount;
const components = (document.allowanceCharges ?? []).flatMap((allowanceCharge, index) => {
if (allowanceCharge.chargeIndicator) {
return [];
}
return amountsEqual(
round2(document.legalMonetaryTotal.allowanceTotalAmount.value),
round2(allowances.reduce((sum, ac) => sum + (ac.amount?.value ?? 0), 0))
);
})();
return passed;
return [{ actual: allowanceCharge.amount.value, path: `allowanceCharges[${index}].amount.value` }];
});
if (allowanceTotalAmount === undefined) {
if (components.length === 0) {
return [];
}
const expected = components.reduce((sum, component) => sum + component.actual, 0);
return [
fieldIssue('legalMonetaryTotal.allowanceTotalAmount.value', expected, null),
...components.map(component => fieldIssue(component.path, null, component.actual)),
];
}
return sumFieldIssues({
aggregate: { actual: allowanceTotalAmount.value, path: 'legalMonetaryTotal.allowanceTotalAmount.value' },
components,
isEqual: (actual, expected) => amountsEqual(round2(actual), round2(expected)),
});
}

export const validateCenEn16931BrCo11 = schematronRule(rule, evaluateCenEn16931BrCo11);
Loading
Loading