@endevops/peppol-schema 0.1.0-beta.5 → 0.1.0-beta.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{index-CfjMSrpQ.d.ts → index-CiW698HY.d.ts} +2 -3
- package/dist/{index-CfjMSrpQ.d.ts.map → index-CiW698HY.d.ts.map} +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +6 -5
- package/dist/index.js.map +1 -1
- package/dist/schematron.d.ts +1 -1
- package/dist/schematron.d.ts.map +1 -1
- package/dist/schematron.js +74 -55
- package/dist/schematron.js.map +1 -1
- package/package.json +1 -1
- package/src/decoders/fields/encode-message-party.ts +24 -11
- package/src/schemas/utils/peppol-vat-regex-schema.ts +0 -2
- package/src/schematron/gr/gr-r-006.ts +4 -4
- package/src/schematron/helpers.ts +10 -12
- package/src/schematron/peppol/cl/peppol-en16931-cl007.ts +23 -23
- package/src/schematron/peppol/f/peppol-en16931-f001.ts +17 -11
- package/src/schematron/peppol/r/peppol-en16931-r051.ts +41 -21
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://raw.githubusercontent.com/SchemaStore/schemastore/refs/heads/master/src/schemas/json/package.json",
|
|
3
3
|
"name": "@endevops/peppol-schema",
|
|
4
|
-
"version": "0.1.0-beta.
|
|
4
|
+
"version": "0.1.0-beta.6",
|
|
5
5
|
"description": "A simple example library built by tsdown",
|
|
6
6
|
"license": "Apache-2.0",
|
|
7
7
|
"contributors": [
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { Effect, Predicate } from 'effect';
|
|
2
2
|
|
|
3
|
+
import type { PeppolContact } from '#/schemas/fields/peppol-contact-schema';
|
|
4
|
+
import type { PeppolIdentifier } from '#/schemas/fields/peppol-identifier-schema';
|
|
3
5
|
import type { PeppolInvoiceDocumentResponseParty, PeppolInvoiceResponseParty } from '#/schemas/peppol-invoice-response-schema';
|
|
4
6
|
import type { PeppolMessageLevelResponseParty } from '#/schemas/peppol-message-level-response-party-schema';
|
|
5
7
|
|
|
@@ -7,18 +9,29 @@ import { encodeContact } from '#/decoders/fields/encode-contact';
|
|
|
7
9
|
import { encodeIdentifier } from '#/decoders/fields/encode-identifier';
|
|
8
10
|
import { encodePartyLegalEntity } from '#/decoders/fields/encode-party-legal-entity';
|
|
9
11
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
12
|
+
type ResponseParty = PeppolMessageLevelResponseParty | PeppolInvoiceResponseParty | PeppolInvoiceDocumentResponseParty;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @description The response party union exposes different fields per variant; reading the optional ones through a shared view keeps the encoder branch-free.
|
|
16
|
+
*/
|
|
17
|
+
interface ResponsePartyFields {
|
|
18
|
+
endpointId?: PeppolIdentifier | undefined;
|
|
19
|
+
partyIdentification?: PeppolIdentifier | undefined;
|
|
20
|
+
partyName?: { name: string } | undefined;
|
|
21
|
+
partyLegalEntity?:
|
|
22
|
+
| { registrationName?: string | undefined; companyId?: PeppolIdentifier | undefined; companyLegalForm?: string | undefined }
|
|
23
|
+
| undefined;
|
|
24
|
+
contact?: PeppolContact | undefined;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const encodeMessageParty = Effect.fn(function* (party?: ResponseParty) {
|
|
13
28
|
if (Predicate.isNullish(party)) return undefined;
|
|
29
|
+
const fields = party as ResponsePartyFields;
|
|
14
30
|
return {
|
|
15
|
-
'cbc:EndpointID':
|
|
16
|
-
'cac:PartyIdentification':
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
'cac:PartyName': 'partyName' in party && Predicate.isNotNullish(party.partyName) ? { 'cbc:Name': party.partyName.name } : undefined,
|
|
21
|
-
'cac:PartyLegalEntity': 'partyLegalEntity' in party ? yield* encodePartyLegalEntity(party.partyLegalEntity) : undefined,
|
|
22
|
-
'cac:Contact': 'contact' in party ? yield* encodeContact(party.contact) : undefined,
|
|
31
|
+
'cbc:EndpointID': yield* encodeIdentifier(fields.endpointId),
|
|
32
|
+
'cac:PartyIdentification': fields.partyIdentification ? { 'cbc:ID': yield* encodeIdentifier(fields.partyIdentification) } : undefined,
|
|
33
|
+
'cac:PartyName': fields.partyName ? { 'cbc:Name': fields.partyName.name } : undefined,
|
|
34
|
+
'cac:PartyLegalEntity': fields.partyLegalEntity ? yield* encodePartyLegalEntity(fields.partyLegalEntity) : undefined,
|
|
35
|
+
'cac:Contact': yield* encodeContact(fields.contact),
|
|
23
36
|
};
|
|
24
37
|
});
|
|
@@ -47,5 +47,3 @@ export const peppolVatRegexSchema = Schema.Union([
|
|
|
47
47
|
Schema.String.check(Schema.isStartsWith('SI'), Schema.isPattern(/^SI\d{8}$/)),
|
|
48
48
|
Schema.String.check(Schema.isStartsWith('SK'), Schema.isPattern(/^SK\d{10}$/)),
|
|
49
49
|
]);
|
|
50
|
-
|
|
51
|
-
export type VatRegex = typeof peppolVatRegexSchema.Type;
|
|
@@ -10,13 +10,13 @@ const rule = {
|
|
|
10
10
|
message: 'Greek Suppliers must provide the VAT number of the buyer, if the buyer is Greek ',
|
|
11
11
|
} as const satisfies SchematronRule;
|
|
12
12
|
|
|
13
|
+
const isGreekCountry = (country: string): boolean => country === 'GR' || country === 'EL';
|
|
14
|
+
|
|
13
15
|
function evaluateGrR006(document: PeppolDocument): boolean {
|
|
14
|
-
|
|
15
|
-
const isGreekCustomer = getCustomerCountry(document) === 'GR' || getCustomerCountry(document) === 'EL';
|
|
16
|
-
if (!isGreekSupplier || !isGreekCustomer) {
|
|
16
|
+
if (!isGreekCountry(getSupplierCountry(document)) || !isGreekCountry(getCustomerCountry(document))) {
|
|
17
17
|
return true;
|
|
18
18
|
}
|
|
19
|
-
const vatSchemes = document.accountingCustomerParty.partyTaxSchemes
|
|
19
|
+
const vatSchemes = (document.accountingCustomerParty.partyTaxSchemes ?? []).filter(scheme => scheme.taxSchemeId.id.trim().toUpperCase() === 'VAT');
|
|
20
20
|
if (vatSchemes.length !== 1) {
|
|
21
21
|
return false;
|
|
22
22
|
}
|
|
@@ -566,19 +566,17 @@ export function everyVatBreakdownTaxableMatchesRateSum(document: PeppolDocument)
|
|
|
566
566
|
* @description Returns all identifiers carrying a scheme identifier, used by the PEPPOL-COMMON-R* rules.
|
|
567
567
|
*/
|
|
568
568
|
export function getIdentifiersWithSchemeId(document: PeppolDocument): Array<{ id: string; schemeId: string }> {
|
|
569
|
-
const
|
|
570
|
-
const push = (id: string | undefined, schemeId: string | undefined) => {
|
|
571
|
-
if (id && schemeId) {
|
|
572
|
-
result.push({ id, schemeId });
|
|
573
|
-
}
|
|
574
|
-
};
|
|
569
|
+
const candidates: Array<{ id: string | undefined; schemeId: string | undefined }> = [];
|
|
575
570
|
for (const party of [document.accountingSupplierParty, document.accountingCustomerParty]) {
|
|
576
|
-
push(
|
|
577
|
-
|
|
578
|
-
|
|
571
|
+
candidates.push(
|
|
572
|
+
{ id: party.endpointId?.id, schemeId: party.endpointId?.schemeId },
|
|
573
|
+
{ id: party.partyIdentification?.id?.id, schemeId: party.partyIdentification?.id?.schemeId },
|
|
574
|
+
{ id: party.partyLegalEntity.companyId?.id, schemeId: party.partyLegalEntity.companyId?.schemeId }
|
|
575
|
+
);
|
|
579
576
|
}
|
|
580
|
-
|
|
581
|
-
|
|
577
|
+
const payeeIdentification = document.payeeParty?.partyIdentification?.id;
|
|
578
|
+
if (payeeIdentification) {
|
|
579
|
+
candidates.push({ id: payeeIdentification.id, schemeId: payeeIdentification.schemeId });
|
|
582
580
|
}
|
|
583
|
-
return
|
|
581
|
+
return candidates.filter((candidate): candidate is { id: string; schemeId: string } => Boolean(candidate.id) && Boolean(candidate.schemeId));
|
|
584
582
|
}
|
|
@@ -12,33 +12,33 @@ const rule = {
|
|
|
12
12
|
|
|
13
13
|
const isValidCurrency = (code: string): boolean => (currencyCodesKeys as ReadonlyArray<string>).includes(code);
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
const currencies: Array<string> = [document.documentCurrencyCode];
|
|
17
|
-
if (document.taxCurrencyCode) {
|
|
18
|
-
currencies.push(document.taxCurrencyCode);
|
|
19
|
-
}
|
|
20
|
-
for (const line of getLines(document)) {
|
|
21
|
-
currencies.push(line.lineExtensionAmount.currencyId);
|
|
22
|
-
currencies.push(line.price.priceAmount.currencyId);
|
|
23
|
-
}
|
|
15
|
+
const collectCurrencies = (document: PeppolDocument): Array<string> => {
|
|
24
16
|
const total = document.legalMonetaryTotal;
|
|
25
|
-
|
|
17
|
+
const optionalTotalCurrencies = [
|
|
18
|
+
total.allowanceTotalAmount?.currencyId,
|
|
19
|
+
total.chargeTotalAmount?.currencyId,
|
|
20
|
+
total.prepaidAmount?.currencyId,
|
|
21
|
+
total.payableRoundingAmount?.currencyId,
|
|
22
|
+
].filter((code): code is string => code !== undefined);
|
|
23
|
+
|
|
24
|
+
return [
|
|
25
|
+
document.documentCurrencyCode,
|
|
26
|
+
...(document.taxCurrencyCode ? [document.taxCurrencyCode] : []),
|
|
27
|
+
...getLines(document).flatMap(line => [line.lineExtensionAmount.currencyId, line.price.priceAmount.currencyId]),
|
|
26
28
|
total.lineExtensionAmount.currencyId,
|
|
27
29
|
total.taxExclusiveAmount.currencyId,
|
|
28
30
|
total.taxInclusiveAmount.currencyId,
|
|
29
|
-
total.payableAmount.currencyId
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
}
|
|
41
|
-
return currencies.every(isValidCurrency);
|
|
31
|
+
total.payableAmount.currencyId,
|
|
32
|
+
...optionalTotalCurrencies,
|
|
33
|
+
...document.taxTotals.flatMap(taxTotal => [
|
|
34
|
+
taxTotal.taxAmount.currencyId,
|
|
35
|
+
...(taxTotal.taxSubtotals ?? []).flatMap(subtotal => [subtotal.taxAmount.currencyId, subtotal.taxableAmount.currencyId]),
|
|
36
|
+
]),
|
|
37
|
+
];
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
function evaluatePeppolEn16931CL007(document: PeppolDocument): boolean {
|
|
41
|
+
return collectCurrencies(document).every(isValidCurrency);
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
export const validatePeppolEn16931CL007 = schematronRule(rule, evaluatePeppolEn16931CL007);
|
|
@@ -15,18 +15,24 @@ const isValidDate = (value: string | undefined): boolean => value === undefined
|
|
|
15
15
|
const encodeDateSync = Schema.encodeSync(Schema.Union([PeppolIsoDateString, Schema.String]));
|
|
16
16
|
const isDate = Schema.is(PeppolIsoDateString);
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
18
|
+
const collectDates = (document: PeppolDocument): Array<string> => {
|
|
19
|
+
const optionalDates = [
|
|
20
|
+
document.taxPointDate,
|
|
21
|
+
document.invoicePeriod?.startDate,
|
|
22
|
+
document.invoicePeriod?.endDate,
|
|
23
|
+
document.delivery?.actualDeliveryDate,
|
|
24
|
+
...getLines(document).flatMap(line => [line.invoicePeriod?.startDate, line.invoicePeriod?.endDate]),
|
|
25
|
+
].filter((date): date is string => date !== undefined);
|
|
26
|
+
|
|
27
|
+
const dates = [document.issueDate, ...optionalDates].map(date => encodeDateSync(date));
|
|
28
|
+
if (Predicate.hasProperty(document, 'dueDate') && isDate(document.dueDate)) {
|
|
29
|
+
dates.push(encodeDateSync(document.dueDate));
|
|
28
30
|
}
|
|
29
|
-
return dates
|
|
31
|
+
return dates;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
function evaluatePeppolEn16931F001(document: PeppolDocument): boolean {
|
|
35
|
+
return collectDates(document).every(isValidDate);
|
|
30
36
|
}
|
|
31
37
|
|
|
32
38
|
export const validatePeppolEn16931F001 = schematronRule(rule, evaluatePeppolEn16931F001);
|
|
@@ -10,20 +10,34 @@ const rule = {
|
|
|
10
10
|
'All currencyID attributes must have the same value as the invoice currency code (BT-5), except for the invoice total VAT amount in accounting currency (BT-111).',
|
|
11
11
|
} as const satisfies SchematronRule;
|
|
12
12
|
|
|
13
|
+
interface CurrencyAmount {
|
|
14
|
+
currencyId: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface AllowanceChargeAmounts {
|
|
18
|
+
amount?: CurrencyAmount | undefined;
|
|
19
|
+
baseAmount?: CurrencyAmount | undefined;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const matchesCurrency = (amount: CurrencyAmount | undefined, expected: string): boolean => amount?.currencyId === expected;
|
|
23
|
+
|
|
24
|
+
const optionalMatchesCurrency = (amount: CurrencyAmount | undefined, expected: string): boolean =>
|
|
25
|
+
amount === undefined || matchesCurrency(amount, expected);
|
|
26
|
+
|
|
27
|
+
const allowanceChargeMatchesCurrency = (allowanceCharge: AllowanceChargeAmounts, expected: string): boolean =>
|
|
28
|
+
matchesCurrency(allowanceCharge.amount, expected) && optionalMatchesCurrency(allowanceCharge.baseAmount, expected);
|
|
29
|
+
|
|
13
30
|
function evaluatePeppolEn16931R051(document: PeppolDocument): boolean {
|
|
14
|
-
const
|
|
15
|
-
const checkCurrency = (currencyId: string | undefined): boolean => currencyId === documentCurrency;
|
|
31
|
+
const currency = document.documentCurrencyCode;
|
|
16
32
|
|
|
17
|
-
const documentAllowancesOk = (document.allowanceCharges ?? []).every(
|
|
18
|
-
ac => checkCurrency(ac.amount?.currencyId) && (!ac.baseAmount || checkCurrency(ac.baseAmount.currencyId))
|
|
19
|
-
);
|
|
33
|
+
const documentAllowancesOk = (document.allowanceCharges ?? []).every(allowanceCharge => allowanceChargeMatchesCurrency(allowanceCharge, currency));
|
|
20
34
|
|
|
21
|
-
const linesOk = getLines(document).every(
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
35
|
+
const linesOk = getLines(document).every(
|
|
36
|
+
line =>
|
|
37
|
+
matchesCurrency(line.price.priceAmount, currency) &&
|
|
38
|
+
matchesCurrency(line.lineExtensionAmount, currency) &&
|
|
39
|
+
(line.allowanceCharges ?? []).every(allowanceCharge => allowanceChargeMatchesCurrency(allowanceCharge, currency))
|
|
40
|
+
);
|
|
27
41
|
|
|
28
42
|
const taxTotalsOk = document.taxTotals.every(total => {
|
|
29
43
|
// Only tax totals with subtotals are in scope for BT-110; the accounting currency total (BT-111) is excluded.
|
|
@@ -31,21 +45,27 @@ function evaluatePeppolEn16931R051(document: PeppolDocument): boolean {
|
|
|
31
45
|
return true;
|
|
32
46
|
}
|
|
33
47
|
return (
|
|
34
|
-
|
|
35
|
-
(total.taxSubtotals ?? []).every(
|
|
48
|
+
matchesCurrency(total.taxAmount, currency) &&
|
|
49
|
+
(total.taxSubtotals ?? []).every(subtotal => matchesCurrency(subtotal.taxAmount, currency) && matchesCurrency(subtotal.taxableAmount, currency))
|
|
36
50
|
);
|
|
37
51
|
});
|
|
38
52
|
|
|
39
53
|
const totals = document.legalMonetaryTotal;
|
|
54
|
+
const requiredTotals: Array<CurrencyAmount> = [
|
|
55
|
+
totals.lineExtensionAmount,
|
|
56
|
+
totals.taxExclusiveAmount,
|
|
57
|
+
totals.taxInclusiveAmount,
|
|
58
|
+
totals.payableAmount,
|
|
59
|
+
];
|
|
60
|
+
const optionalTotals: Array<CurrencyAmount | undefined> = [
|
|
61
|
+
totals.allowanceTotalAmount,
|
|
62
|
+
totals.chargeTotalAmount,
|
|
63
|
+
totals.prepaidAmount,
|
|
64
|
+
totals.payableRoundingAmount,
|
|
65
|
+
];
|
|
66
|
+
|
|
40
67
|
const totalsOk =
|
|
41
|
-
|
|
42
|
-
checkCurrency(totals.taxExclusiveAmount.currencyId) &&
|
|
43
|
-
checkCurrency(totals.taxInclusiveAmount.currencyId) &&
|
|
44
|
-
(!totals.allowanceTotalAmount || checkCurrency(totals.allowanceTotalAmount.currencyId)) &&
|
|
45
|
-
(!totals.chargeTotalAmount || checkCurrency(totals.chargeTotalAmount.currencyId)) &&
|
|
46
|
-
(!totals.prepaidAmount || checkCurrency(totals.prepaidAmount.currencyId)) &&
|
|
47
|
-
(!totals.payableRoundingAmount || checkCurrency(totals.payableRoundingAmount.currencyId)) &&
|
|
48
|
-
checkCurrency(totals.payableAmount.currencyId);
|
|
68
|
+
requiredTotals.every(amount => matchesCurrency(amount, currency)) && optionalTotals.every(amount => optionalMatchesCurrency(amount, currency));
|
|
49
69
|
|
|
50
70
|
return documentAllowancesOk && linesOk && taxTotalsOk && totalsOk;
|
|
51
71
|
}
|