From d1284aa1df5911e76b6e9b0fae19bfb07cc5f89e Mon Sep 17 00:00:00 2001 From: Tobias Lippert Date: Fri, 24 Nov 2023 22:58:50 +0100 Subject: [PATCH] fix: Debitor ids are optional Also: Actually include the creditor/debitor ids when they are set Fixes kewisch#58 --- lib/sepa.js | 8 ++- lib/sepa.test.js | 138 ++++++++++++++++++++++++++++++++++++++++++++++ package-lock.json | 12 +++- package.json | 3 +- 4 files changed, 158 insertions(+), 3 deletions(-) diff --git a/lib/sepa.js b/lib/sepa.js index c32d214..fb3f79f 100644 --- a/lib/sepa.js +++ b/lib/sepa.js @@ -356,7 +356,9 @@ assert_date(this.requestedExecutionDate, 'requestedExecutionDate'); } - assert_cid(this[pullFrom + 'Id'], pullFrom + 'Id'); + if (this[pullFrom + 'Id']) { + assert_cid(this[pullFrom + 'Id'], pullFrom + 'Id'); + } assert_length(this[pullFrom + 'Name'], null, 70, pullFrom + 'Name'); assert_length(this[pullFrom + 'Street'], null, 70, pullFrom + 'Street'); @@ -411,6 +413,10 @@ var emitter = n(pmtInf, emitterNodeName); r(emitter, 'Nm', this[pullFrom + 'Name']); + if (this[pullFrom + 'Id']) { + r(emitter, 'Id', this[pullFrom + 'Id']); + } + if (this[pullFrom + 'Street'] && this[pullFrom + 'City'] && this[pullFrom + 'Country']) { var pstl = n(emitter, 'PstlAdr'); r(pstl, 'Ctry', this[pullFrom + 'Country']); diff --git a/lib/sepa.test.js b/lib/sepa.test.js index 592d0ea..2774407 100644 --- a/lib/sepa.test.js +++ b/lib/sepa.test.js @@ -1,4 +1,10 @@ var SEPA = require('./sepa.js'); +var xpath = require('xpath'); + +// The following debtor id is provided by German Bundesbank for testing purposes. +const A_VALID_CREDITOR_ID = 'DE98ZZZ09999999999'; + +const A_VALID_IBAN = 'DE43500105178994141576'; describe('IBAN tests', () => { @@ -30,3 +36,135 @@ describe('IBAN tests', expect(SEPA.validateIBAN('DE1Zsantander')).toBe(false); }); }); + +describe('xml generation for transfer documents', () => { + const PAIN_FOR_TRANSFERS = 'pain.001.003.03'; + + function validTransferDocument({debtorId=A_VALID_CREDITOR_ID, debtorName='default-debtor-name'}) { + const doc = new SEPA.Document(PAIN_FOR_TRANSFERS); + doc.grpHdr.created = new Date(); + + const info = doc.createPaymentInfo(); + info.collectionDate = new Date(); + info.debtorIBAN = A_VALID_IBAN; + info.debtorName = debtorName; + info.debtorId = debtorId; + info.requestedExecutionDate = new Date(); + doc.addPaymentInfo(info); + + const tx = info.createTransaction(); + tx.creditorName = 'creditor-name'; + tx.creditorIBAN = A_VALID_IBAN; + tx.amount = 1.0; + tx.mandateSignatureDate = new Date(); + info.addTransaction(tx); + return doc; + } + + test('debtor id is optional for transfer documents', () => { + // GIVEN + const doc = validTransferDocument({debtorId: null}); + // WHEN + const dom = doc.toXML(); + // THEN + const select = xpath.useNamespaces({ + p: `urn:iso:std:iso:20022:tech:xsd:${PAIN_FOR_TRANSFERS}`, + }); + const debtorId = select('/p:Document/p:CstmrCdtTrfInitn/p:PmtInf/p:Dbtr/p:Id', dom, true); + expect(debtorId).toBeUndefined(); + }); + + test('debtor name and id are included in transfer documents when they are set', () => { + // GIVEN + const doc = validTransferDocument({debtorId: 'FR72ZZZ123456', debtorName: 'debtor-name'}); + + // WHEN + const dom = doc.toXML(); + + // THEN + const select = xpath.useNamespaces({ + p: `urn:iso:std:iso:20022:tech:xsd:${PAIN_FOR_TRANSFERS}`, + }); + + const debtorName = select('/p:Document/p:CstmrCdtTrfInitn/p:PmtInf/p:Dbtr/p:Nm', dom, true); + expect(debtorName).not.toBeUndefined(); + expect(debtorName.textContent).toBe('debtor-name'); + + const debtorId = select('/p:Document/p:CstmrCdtTrfInitn/p:PmtInf/p:Dbtr/p:Id', dom, true); + expect(debtorId).not.toBeUndefined(); + expect(debtorId.textContent).toBe('FR72ZZZ123456'); + }); + + test('dutch debtor ids are accepted', () => { + // GIVEN + const validDutchCreditorId = 'NL79ZZZ999999990000'; + const doc = validTransferDocument({debtorId: validDutchCreditorId}); + + // WHEN + const dom = doc.toXML(); + + // THEN + const select = xpath.useNamespaces({ + p: `urn:iso:std:iso:20022:tech:xsd:${PAIN_FOR_TRANSFERS}`, + }); + + const debtorId = select('/p:Document/p:CstmrCdtTrfInitn/p:PmtInf/p:Dbtr/p:Id', dom, true); + expect(debtorId).not.toBeUndefined(); + expect(debtorId.textContent).toBe(validDutchCreditorId); + }); +}); + +describe('xml generation for direct debit documents', () => { + const PAIN_FOR_DIRECT_DEBIT = 'pain.008.001.02'; + function validDirectDebitDocument({creditorId=A_VALID_CREDITOR_ID}) { + var doc = new SEPA.Document(PAIN_FOR_DIRECT_DEBIT); + doc.grpHdr.created = new Date(); + + var info = doc.createPaymentInfo(); + info.collectionDate = new Date(); + info.creditorIBAN = A_VALID_IBAN; + info.creditorId = creditorId; + doc.addPaymentInfo(info); + + var tx = info.createTransaction(); + tx.debtorIBAN = A_VALID_IBAN; + tx.mandateSignatureDate = new Date('2014-02-01'); + tx.amount = 50.23; + info.addTransaction(tx); + + return doc; + } + + test('includes creditor id when set', () => { + // GIVEN + const doc = validDirectDebitDocument({creditorId: 'IT66ZZZA1B2C3D4E5F6G7H8'}); + + // WHEN + const dom = doc.toXML(); + + // THEN + const select = xpath.useNamespaces({ + p: `urn:iso:std:iso:20022:tech:xsd:${PAIN_FOR_DIRECT_DEBIT}`, + }); + + const creditorId = select('/p:Document/p:CstmrDrctDbtInitn/p:PmtInf/p:Cdtr/p:Id', dom, true); + expect(creditorId).not.toBeUndefined(); + expect(creditorId.textContent).toBe('IT66ZZZA1B2C3D4E5F6G7H8'); + }); + + test('Works without setting creditor id', () => { + // GIVEN + const doc = validDirectDebitDocument({creditorId: null}); + + // WHEN + const dom = doc.toXML(); + + // THEN + const select = xpath.useNamespaces({ + p: `urn:iso:std:iso:20022:tech:xsd:${PAIN_FOR_DIRECT_DEBIT}`, + }); + + const creditorId = select('/p:Document/p:CstmrDrctDbtInitn/p:PmtInf/p:Cdtr/p:Id', dom, true); + expect(creditorId).toBeUndefined(); + }); +}); \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 8f0656e..d4d8ebc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,7 +16,8 @@ "eslint-plugin-jest": "^27.6.0", "jest": "^29.7.0", "pre-commit": "*", - "uglify-js": "^3.3.20" + "uglify-js": "^3.3.20", + "xpath": "^0.0.33" }, "engines": { "node": "*" @@ -4935,6 +4936,15 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, + "node_modules/xpath": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/xpath/-/xpath-0.0.33.tgz", + "integrity": "sha512-NNXnzrkDrAzalLhIUc01jO2mOzXGXh1JwPgkihcLLzw98c0WgYDmmjSh1Kl3wzaxSVWMuA+fe0WTWOBDWCBmNA==", + "dev": true, + "engines": { + "node": ">=0.6.0" + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/package.json b/package.json index 4b8b538..4e9c7cf 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,8 @@ "eslint-plugin-jest": "^27.6.0", "jest": "^29.7.0", "pre-commit": "*", - "uglify-js": "^3.3.20" + "uglify-js": "^3.3.20", + "xpath": "^0.0.33" }, "engines": { "node": "*"