sepa
Version:
Create SEPA XML for business transactions
926 lines (783 loc) • 30.8 kB
JavaScript
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
* Portions Copyright (C) Philipp Kewisch, 2014-2015 */
/**
* This is sepa.js. Its module exports the following functions:
*
* SEPA.Document -- class for creating SEPA XML Documents
* SEPA.PaymentInfo -- class for SEPA payment information blocks
* SEPA.Transaction -- class for generic transactions
* SEPA.validateIBAN -- function to validate an IBAN
* SEPA.checksumIBAN -- function to calculate the IBAN checksum
* SEPA.validateCreditorID -- function to validate a creditor id
* SEPA.checksumCreditorID -- function to calculate the creditor id checksum
* SEPA.setIDSeparator -- function to customize the ID separator when needed (defaults to '.')
* SEPA.enableValidations -- function to enable/disable fields validation
*/
(function(exports) {
var XSI_NAMESPACE = 'http://www.w3.org/2001/XMLSchema-instance';
var XSI_NS = 'urn:iso:std:iso:20022:tech:xsd:';
var DEFAULT_XML_VERSION = '1.0';
var DEFAULT_XML_ENCODING = 'UTF-8';
var DEFAULT_PAIN_FORMAT = 'pain.008.001.02';
var ID_SEPARATOR = '.';
function setIDSeparator(seperator) {
ID_SEPARATOR = seperator;
}
var VALIDATIONS_ENABLED = true;
var CHARSET_VALIDATION_ENABLED = true;
/**
* Controls the validation that is conducted when SepaDocuments are serialized.
*
* @param {boolean} enabled - Whether the validation should be conducted
* @param {boolean} [charsetValidationsEnabled=true] - If validation is enabled, whether fields
* should be checked for the limited SEPA character set. You want to set this to false, e.g.,
* if you are using this library to handle communication within Greece or Finnland, where
* more characters are allowed.
*/
function enableValidations(enabled, charsetValidationsEnabled = true) {
VALIDATIONS_ENABLED = !!enabled;
CHARSET_VALIDATION_ENABLED = !!charsetValidationsEnabled;
}
var SEPATypes = {
'pain.001.001.02': 'pain.001.001.02',
'pain.001.001.03': 'CstmrCdtTrfInitn',
'pain.001.001.08': 'CstmrCdtTrfInitn',
'pain.001.001.09': 'CstmrCdtTrfInitn',
'pain.008.001.02': 'CstmrDrctDbtInitn',
'pain.008.001.08': 'CstmrDrctDbtInitn',
};
function getPainXMLVersion(painFormat) {
var inc = painFormat.indexOf('pain.008') === 0 ? 1 : 0;
return parseInt(painFormat.substr(-2), 10) + inc;
}
function SepaDocument(painFormat) {
this._painFormat = painFormat || DEFAULT_PAIN_FORMAT;
this._type = SEPATypes[this._painFormat];
this._paymentInfo = [];
this._xmlVersion = DEFAULT_XML_VERSION;
this._xmlEncoding = DEFAULT_XML_ENCODING;
this.grpHdr = new SepaGroupHeader(this._painFormat);
}
SepaDocument.Types = SEPATypes;
SepaDocument.prototype = {
/** Pain Format used */
_painFormat: null,
/** Group Header object */
grpHdr: null,
/** Payment Info array */
_paymentInfo: [],
/** SEPA Document type setting, contains the root element */
_type: null,
/** Xml version */
_xmlVersion: null,
/** Xml encoding */
_xmlEncoding: null,
/**
* Adds a Sepa.PaymentInfo block to this document. Its id will be
* automatically prefixed with the group header id.
*
* @param pi The payment info block.
*/
addPaymentInfo: function(pi) {
if (!(pi instanceof SepaPaymentInfo)) {
throw new Error('Given payment is not member of the PaymentInfo class');
}
if (pi.id) {
pi.id = this.grpHdr.id + ID_SEPARATOR + pi.id;
} else {
pi.id = this.grpHdr.id + ID_SEPARATOR + this._paymentInfo.length;
}
this._paymentInfo.push(pi);
},
/**
* Factory method for PI
*/
createPaymentInfo: function() {
return new SepaPaymentInfo(this._painFormat);
},
/**
* Normalize fields like the control sum or transaction count. This will be
* called automatically when serialized to XML.
*/
normalize: function() {
var controlSum = 0;
var txCount = 0;
for (var i = 0, l = this._paymentInfo.length; i < l; ++i) {
this._paymentInfo[i].normalize();
controlSum += this._paymentInfo[i].controlSum;
txCount += this._paymentInfo[i].transactionCount;
}
this.grpHdr.controlSum = controlSum;
this.grpHdr.transactionCount = txCount;
},
/**
* Serialize this document to a DOM Document.
*
* @return The DOM Document.
*/
toXML: function() {
this.normalize();
var docNS = XSI_NS + this._painFormat;
var doc = createDocument(docNS, 'Document');
var body = doc.documentElement;
body.setAttribute('xmlns:xsi', XSI_NAMESPACE);
body.setAttribute('xsi:schemaLocation', XSI_NS + this._painFormat + ' ' + this._painFormat + '.xsd');
var rootElement = doc.createElementNS(docNS, this._type);
rootElement.appendChild(this.grpHdr.toXML(doc));
for (var i = 0, l = this._paymentInfo.length; i < l; ++i) {
rootElement.appendChild(this._paymentInfo[i].toXML(doc));
}
doc.documentElement.appendChild(rootElement);
return doc;
},
/**
* Serialize this document to an XML string.
*
* @return String The XML string of this document.
*/
toString: function() {
var doc = this.toXML();
// as some banks require the document declaration string and it is not provided by the XMLSerializer, it is added here.
var docDeclaration = '<?xml version="' + this._xmlVersion + '" encoding="' + this._xmlEncoding + '"?>';
return docDeclaration + serializeToString(doc);
}
};
/**
* Wrapper class for the SEPA <GrpHdr> element.
*/
function SepaGroupHeader(painFormat) {
this._painFormat = painFormat;
}
SepaGroupHeader.prototype = {
_painFormat: null,
id: '',
created: '',
transactionCount: 0,
initiatorName: '',
controlSum: 0,
batchBooking: false,
grouping: 'MIXD',
/*
* Serialize this document to a DOM Element.
*
* @return The DOM <GrpHdr> Element.
*/
toXML: function(doc) {
var r = createXMLHelper(doc, true, true);
var grpHdr = doc.createElementNS(doc.documentElement.namespaceURI, 'GrpHdr');
var painVersion = getPainXMLVersion(this._painFormat);
r(grpHdr, 'MsgId', this.id);
r(grpHdr, 'CreDtTm', this.created.toISOString().substr(0,19));
// XML >= v2 formats, add batch booking nodes
if (painVersion === 2) {
r(grpHdr, 'BtchBookg', this.batchBooking.toString());
}
r(grpHdr, 'NbOfTxs', this.transactionCount);
r(grpHdr, 'CtrlSum', this.controlSum.toFixed(2));
// XML v2 formats, add grouping booking nodes
if (painVersion === 2) {
r(grpHdr, 'Grpg', this.grouping);
}
r(grpHdr, 'InitgPty', 'Nm', this.initiatorName);
return grpHdr;
},
/**
* Serialize this element to an XML string.
*
* @return The XML string of this element.
*/
toString: function() {
return serializeToString(this.toXML());
}
};
var PaymentInfoTypes = {
DirectDebit: 'DD',
Transfer: 'TRF'
};
/**
* Wrapper class for the SEPA <PmtInf> Element
*/
function SepaPaymentInfo(painFormat) {
this._painFormat = painFormat;
this.method = painFormat.indexOf('pain.001') === 0 ? PaymentInfoTypes.Transfer : PaymentInfoTypes.DirectDebit;
this._payments = [];
}
SepaPaymentInfo.PaymentInfoTypes = PaymentInfoTypes;
SepaPaymentInfo.prototype = {
_painFormat: null,
/** Transaction array */
_payments: null,
id: '',
/** SEPA payment method. */
method: null,
/** If true, booking will appear as one entry on your statement */
batchBooking: false,
/** Grouping, defines structure handling for XML file */
grouping: 'MIXD',
/** Sum of all payments, will be automatically set */
controlSum: 0,
/* Instrumentation code:
* 'CORE' - Standard Transfer
* 'COR1' - Expedited Transfer
* 'B2B' - Business Transfer
*/
localInstrumentation: 'CORE',
/**
* 'FRST' - First transfer
* 'RCUR' - Subsequent transfer
* 'OOFF' - One Off transfer
* 'FNAL' - Final transfer
*/
sequenceType: 'FRST',
/** Requested collection date */
collectionDate: null,
/** Execution date of the SEPA order */
requestedExecutionDate: null,
/** Id assigned to the creditor */
creditorId: '',
/** Name, Address, IBAN and BIC of the creditor */
creditorName: '',
creditorStreet: null,
creditorCity: null,
creditorCountry: null,
creditorIBAN: '',
creditorBIC: '',
/** Id assigned to the debtor for Transfer payments */
debtorId: '',
/** Name, Address, IBAN and BIC of the debtor */
debtorName: '',
debtorStreet: null,
debtorCity: null,
debtorCountry: null,
debtorIBAN: '',
debtorBIC: '',
/** SEPA order priority, can be HIGH or NORM */
instructionPriority: 'NORM',
/** Number of transactions in this payment info block */
get transactionCount() {
return this._payments.length;
},
/**
* Normalize fields like the control sum or transaction count. This will
* _NOT_ be called when serialized to XML and must be called manually.
*/
normalize: function() {
var controlSum = 0;
for (var i = 0, l = this._payments.length; i < l; ++i) {
controlSum += this._payments[i].amount;
}
this.controlSum = controlSum;
},
/**
* Adds a transaction to this payment. The transaction id will be prefixed
* by the payment info id.
*
* @param pmt The Transacation to add.
*/
addTransaction: function(pmt) {
if (!(pmt instanceof SepaTransaction)) {
throw new Error('Given Transaction is not member of the SepaTransaction class');
}
if (pmt.id) {
pmt.id = this.id + ID_SEPARATOR + pmt.id;
} else {
pmt.id = this.id + ID_SEPARATOR + this._payments.length;
}
this._payments.push(pmt);
},
createTransaction: function() {
return new SepaTransaction(this._painFormat);
},
validate: function() {
// TODO consider using getters/setters instead
var pullFrom = this.method === PaymentInfoTypes.DirectDebit ? 'creditor' : 'debtor';
assert_fixed(this.localInstrumentation, ['CORE', 'COR1', 'B2B'], 'localInstrumentation');
assert_fixed(this.sequenceType, ['FRST', 'RCUR', 'OOFF', 'FNAL'], 'sequenceType');
if (this.method === PaymentInfoTypes.DirectDebit) {
assert_date(this.collectionDate, 'collectionDate');
}
else {
assert_date(this.requestedExecutionDate, 'requestedExecutionDate');
}
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');
assert_length(this[pullFrom + 'City'], null, 70, pullFrom + 'City');
assert_length(this[pullFrom + 'Country'], null, 2, pullFrom + 'Country');
assert_iban(this[pullFrom + 'IBAN'], pullFrom + 'IBAN');
assert_length(this[pullFrom + 'BIC'], [0,8,11], pullFrom + 'BIC');
var countryMatches = (this[pullFrom + 'BIC'].length === 0 || this[pullFrom + 'BIC'].substr(4, 2) === this[pullFrom + 'IBAN'].substr(0, 2));
assert(countryMatches, 'country mismatch in BIC/IBAN');
assert_length(this._payments.length, 1, null, '_payments');
},
/*
* Serialize this document to a DOM Element.
*
* @return The DOM <PmtInf> Element.
*/
toXML: function(doc) {
if (VALIDATIONS_ENABLED) {
this.validate();
}
var n = createXMLHelper(doc, true, false);
//var o = createXMLHelper(doc, false, true);
var r = createXMLHelper(doc, true, true);
var pmtInf = doc.createElementNS(doc.documentElement.namespaceURI, 'PmtInf');
r(pmtInf, 'PmtInfId', this.id);
r(pmtInf, 'PmtMtd', this.method);
// XML v3 formats, add grouping + batch booking nodes
if (getPainXMLVersion(this._painFormat) >= 3) {
r(pmtInf, 'BtchBookg', this.batchBooking.toString());
r(pmtInf, 'NbOfTxs', this.transactionCount);
r(pmtInf, 'CtrlSum', this.controlSum.toFixed(2));
}
var pmtTpInf = n(pmtInf, 'PmtTpInf');
r(pmtTpInf, 'SvcLvl', 'Cd', 'SEPA');
if (this.method === PaymentInfoTypes.DirectDebit) {
r(pmtTpInf, 'LclInstrm', 'Cd', this.localInstrumentation);
r(pmtTpInf, 'SeqTp', this.sequenceType);
r(pmtInf, 'ReqdColltnDt', this.collectionDate.toISOString().substr(0, 10));
}
else {
if (getPainXMLVersion(this._painFormat) >= 8) {
var reqdExctnDt = n(pmtInf, 'ReqdExctnDt');
r(reqdExctnDt, 'Dt', this.requestedExecutionDate.toISOString().substr(0, 10));
} else {
r(pmtInf, 'ReqdExctnDt', this.requestedExecutionDate.toISOString().substr(0, 10));
}
}
var pullFrom = this.method === PaymentInfoTypes.DirectDebit ? 'creditor' : 'debtor';
var emitterNodeName = this.method === PaymentInfoTypes.DirectDebit ? 'Cdtr' : 'Dbtr';
var emitter = n(pmtInf, emitterNodeName);
r(emitter, 'Nm', this[pullFrom + 'Name']);
if (this[pullFrom + 'Street'] && this[pullFrom + 'City'] && this[pullFrom + 'Country']) {
var pstl = n(emitter, 'PstlAdr');
r(pstl, 'Ctry', this[pullFrom + 'Country']);
r(pstl, 'AdrLine', this[pullFrom + 'Street']);
r(pstl, 'AdrLine', this[pullFrom + 'City']);
}
r(pmtInf, emitterNodeName + 'Acct', 'Id', 'IBAN', this[pullFrom + 'IBAN']);
if (this[pullFrom + 'BIC']) {
r(pmtInf, emitterNodeName + 'Agt', 'FinInstnId', getPainXMLVersion(this._painFormat) >= 8 ? 'BICFI' : 'BIC', this[pullFrom + 'BIC']);
} else {
r(pmtInf, emitterNodeName + 'Agt', 'FinInstnId', 'Othr', 'Id', 'NOTPROVIDED');
}
r(pmtInf, 'ChrgBr', 'SLEV');
if (this.method === PaymentInfoTypes.DirectDebit) {
var creditorScheme = n(pmtInf, 'CdtrSchmeId', 'Id', 'PrvtId', 'Othr');
r(creditorScheme, 'Id', this.creditorId);
r(creditorScheme, 'SchmeNm', 'Prtry', 'SEPA');
}
for (var i = 0, l = this._payments.length; i < l; ++i) {
pmtInf.appendChild(this._payments[i].toXML(doc));
}
return pmtInf;
},
/**
* Serialize this element to an XML string.
*
* @return The XML string of this element.
*/
toString: function() {
return serializeToString(this.toXML());
}
};
/**
* Generic Transaction class
*/
var TransactionTypes = {
DirectDebit: 'DrctDbtTxInf',
Transfer: 'CdtTrfTxInf'
};
function SepaTransaction(painFormat) {
this._painFormat = painFormat;
this._type = painFormat.indexOf('pain.001') === 0 ? TransactionTypes.Transfer : TransactionTypes.DirectDebit;
}
SepaTransaction.TransactionTypes = TransactionTypes;
SepaTransaction.prototype = {
/** Generic Transaction Type */
_type: TransactionTypes.DirectDebit,
/** The unique transaction id */
id: '',
/** The End-To-End id */
end2endId: '',
/** The currency to transfer */
currency: 'EUR',
/** The amount to transfer */
amount: 0,
/** (optional) The purpose code to use */
purposeCode: null,
/** The mandate id of the debtor */
mandateId: '',
/** The signature date of the mandate */
mandateSignatureDate: null,
/** Name, Address, IBAN and BIC of the debtor */
debtorName: '',
debtorStreet: null,
debtorCity: null,
debtorCountry: null,
debtorIBAN: '',
debtorBIC: '',
/** Unstructured Remittance Info */
remittanceInfo: '',
/** Structured Remittance Info */
structuredRemittanceInfo: {
typeCode: '',
issuer: '',
reference: ''
},
/** Name, Address, IBAN and BIC of the creditor */
creditorName: '',
creditorStreet: null,
creditorCity: null,
creditorCountry: null,
creditorIBAN: '',
creditorBIC: '',
validate: function() {
var pullFrom = this._type === TransactionTypes.Transfer ? 'creditor' : 'debtor';
assert_valid_sepa_id(this.end2endId, 35, 'end2endId', CHARSET_VALIDATION_ENABLED);
assert_range(this.amount, 0.01, 999999999.99, 'amount');
assert(this.amount == this.amount.toFixed(2), 'amount has too many fractional digits');
assert_length(this.purposeCode, 1, 4, 'purposeCode');
if(this._type === TransactionTypes.DirectDebit) {
assert_valid_sepa_id(this.mandateId, 35, 'mandateId', CHARSET_VALIDATION_ENABLED);
assert_date(this.mandateSignatureDate, 'mandateSignatureDate');
}
assert_length(this[pullFrom + 'Name'], null, 70, pullFrom + 'Name');
assert_length(this[pullFrom + 'Street'], null, 70, pullFrom + 'Street');
assert_length(this[pullFrom + 'City'], null, 70, pullFrom + 'City');
assert_length(this[pullFrom + 'Country'], null, 2, pullFrom + 'Country');
assert_iban(this[pullFrom + 'IBAN'], pullFrom + 'IBAN');
assert_fixed(this[pullFrom + 'BIC'].length, [0, 8, 11], pullFrom + 'BIC');
var countryMatches = (this[pullFrom + 'BIC'].length === 0 || this[pullFrom + 'BIC'].substr(4, 2) === this[pullFrom + 'IBAN'].substr(0, 2));
assert(countryMatches, 'country mismatch in BIC/IBAN');
assert_length(this.remittanceInfo, null, 140, 'remittanceInfo');
// validate structured remittance information
if (this.structuredRemittanceInfo.reference) {
assert_length(this.structuredRemittanceInfo.typeCode, null, 35, 'typeCode');
assert_length(this.structuredRemittanceInfo.issuer, null, 35, 'issuer');
assert_length(this.structuredRemittanceInfo.reference, null, 35, 'reference');
}
},
toXML: function(doc) {
if (VALIDATIONS_ENABLED) {
this.validate();
}
var pullFrom = this._type === TransactionTypes.Transfer ? 'creditor' : 'debtor';
var recieverNodeName = this._type === TransactionTypes.Transfer ? 'Cdtr' : 'Dbtr';
var n = createXMLHelper(doc, true, false);
var o = createXMLHelper(doc, false, true);
var r = createXMLHelper(doc, true, true);
var txInf = doc.createElementNS(doc.documentElement.namespaceURI, this._type);
var paymentId = n(txInf, 'PmtId');
r(paymentId, 'InstrId', this.id);
r(paymentId, 'EndToEndId', this.end2endId);
if (this._type === TransactionTypes.DirectDebit) {
r(txInf, 'InstdAmt', this.amount.toFixed(2)).setAttribute('Ccy', this.currency);
var mandate = n(txInf, 'DrctDbtTx', 'MndtRltdInf');
r(mandate, 'MndtId', this.mandateId);
r(mandate, 'DtOfSgntr', this.mandateSignatureDate.toISOString().substr(0, 10));
if (this.ammendment) {
r(mandate, 'AmdmntInd', 'true');
r(mandate, 'AmdmnInfDtls', this.ammendment);
} else {
r(mandate, 'AmdmntInd', 'false');
}
}
else {
r(txInf, 'Amt', 'InstdAmt', this.amount.toFixed(2)).setAttribute('Ccy', this.currency);
}
if (this[pullFrom + 'BIC']) {
r(txInf, recieverNodeName + 'Agt', 'FinInstnId', getPainXMLVersion(this._painFormat) >= 8 ? 'BICFI' : 'BIC', this[pullFrom + 'BIC']);
} else {
r(txInf, recieverNodeName + 'Agt', 'FinInstnId', 'Othr', 'Id', 'NOTPROVIDED');
}
var reciever = n(txInf, recieverNodeName);
r(reciever, 'Nm', this[pullFrom + 'Name']);
if (this[pullFrom + 'Street'] && this[pullFrom + 'City'] && this[pullFrom + 'Country']) {
var pstl = n(reciever, 'PstlAdr');
r(pstl, 'Ctry', this[pullFrom + 'Country']);
r(pstl, 'AdrLine', this[pullFrom + 'Street']);
r(pstl, 'AdrLine', this[pullFrom + 'City']);
}
r(txInf, recieverNodeName + 'Acct', 'Id', 'IBAN', this[pullFrom + 'IBAN']);
var remittance = n(txInf, 'RmtInf');
if (this.structuredRemittanceInfo.reference) {
var structured = n(remittance, 'Strd');
var creditorRefInf = n(structured, 'CdtrRefInf');
var cdOrPrtry = n(creditorRefInf, 'Tp', 'CdOrPrtry');
r(cdOrPrtry, 'Cd', this.structuredRemittanceInfo.typeCode);
r(creditorRefInf, 'Issr', this.structuredRemittanceInfo.issuer);
r(creditorRefInf, 'Ref', this.structuredRemittanceInfo.reference);
} else {
r(remittance, 'Ustrd', this.remittanceInfo);
}
o(txInf, 'Purp', 'Cd', this.purposeCode);
return txInf;
}
};
/**
* Replace letters with numbers using the SEPA scheme A=10, B=11, ...
* Non-alphanumerical characters are dropped.
*
* @param str The alphanumerical input string
* @return The input string with letters replaced
*/
function _replaceChars(str) {
var res = '';
for (var i = 0, l = str.length; i < l; ++i) {
var cc = str.charCodeAt(i);
if (cc >= 65 && cc <= 90) {
res += (cc - 55).toString();
} else if (cc >= 97 && cc <= 122) {
res += (cc - 87).toString();
} else if (cc >= 48 && cc <= 57) {
res += str[i];
}
}
return res;
}
/**
* mod97 function for large numbers
*
* @param str The number as a string.
* @return The number mod 97.
*/
function _txtMod97(str) {
var res = 0;
for (var i = 0, l = str.length; i < l; ++i) {
res = (res * 10 + parseInt(str[i], 10)) % 97;
}
return res;
}
/**
* Checks whether the given ascii code corresponds to an uppercase letter
*
* @param {number} charCode
*/
function isUppercaseLetter(charCode) {
return (charCode >= 65 && charCode <= 90);
}
/**
* Checks whether the given ascii code corresponds to a digit
*
* @param {number} charCode
*/
function isDigit(charCode) {
return charCode >= 48 && charCode <= 57;
}
/**
* Checks if an IBAN is valid (no country specific checks are done).
*
* @param iban The IBAN to check.
* @return True, if the IBAN is valid.
*/
function validateIBAN(iban) {
// the first two positions are used for the country code and must be letters
if (!isUppercaseLetter(iban.charCodeAt(0)) || !isUppercaseLetter(iban.charCodeAt(1))) {
return false;
}
// positions three and four are used for the checksum and must be digits
if (!isDigit(iban.charCodeAt(2)) || !isDigit(iban.charCodeAt(3))) {
return false;
}
var ibrev = iban.substr(4) + iban.substr(0, 4);
return _txtMod97(_replaceChars(ibrev)) === 1;
}
/**
* Calculates the checksum for the given IBAN. The input IBAN should pass 00
* as the checksum digits, a full iban with the corrected checksum will be
* returned.
*
* Example: DE00123456781234567890 -> DE87123456781234567890
*
* @param iban The IBAN to calculate the checksum for.
* @return The corrected IBAN.
*/
function checksumIBAN(iban) {
var ibrev = iban.substr(4) + iban.substr(0, 2) + '00';
var mod = _txtMod97(_replaceChars(ibrev));
return iban.substr(0, 2) + ('0' + (98 - mod)).substr(-2,2) + iban.substr(4);
}
/**
* Checks if a Creditor ID is valid (no country specific checks are done).
*
* @param iban The Creditor ID to check.
* @return True, if the Creditor IDis valid.
*/
function validateCreditorID(cid) {
var cidrev = cid.substr(7) + cid.substr(0, 4);
return _txtMod97(_replaceChars(cidrev)) === 1;
}
/**
* Calculates the checksum for the given Creditor ID . The input Creditor ID
* should pass 00 as the checksum digits, a full Creditor ID with the
* corrected checksum will be returned.
*
* Example: DE00ZZZ09999999999 -> DE98ZZZ09999999999
*
* @param iban The IBAN to calculate the checksum for.
* @return The corrected IBAN.
*/
function checksumCreditorID(cid) {
var cidrev = cid.substr(7) + cid.substr(0, 2) + '00';
var mod = _txtMod97(_replaceChars(cidrev));
return cid.substr(0, 2) + ('0' + (98 - mod)).substr(-2,2) + cid.substr(4);
}
// --- Various private functions follow --- //
/** Assert that |cond| is true, otherwise throw an error with |msg| */
function assert(cond, msg) {
if (!cond) {
throw new Error(msg);
}
}
/** Assert that |val| is one of |choices| */
function assert_fixed(val, choices, member) {
if (choices.indexOf(val) < 0) {
throw new Error(member + ' must have any value of: ' + choices.join(' ') + '(found: ' + val + ')');
}
}
/** assert that |str| has a length between min and max (either may be null) */
function assert_length(str, min, max, member) {
if ((min !== null && str && str.length < min) ||
(max !== null && str && str.length > max)) {
throw new Error(member + ' has invalid string length, expected ' + min + ' < ' + str + ' < ' + max);
}
}
/** assert that |num| is in the range between |min| and |max| */
function assert_range(num, min, max, member) {
if (num < min || num > max) {
throw new Error(member + ' does not match range ' + min + ' < ' + num + ' < ' + max);
}
}
/** assert that |str| is an IBAN */
function assert_iban(str, member) {
if (!validateIBAN(str)) {
throw new Error(member + ' has invalid IBAN "' + str + '"');
}
}
/** assert that |str| is a creditor id */
function assert_cid(str, member) {
if (!validateCreditorID(str)) {
throw new Error(member + ' is invalid "' + str + '"');
}
}
/** assert an iso date */
function assert_date(dt, member) {
if (!dt || isNaN(dt.getTime())) {
throw new Error(member + ' has invalid date ' + dt);
}
}
/**
* Checks whether the given string is a valid SEPA id.
*
* @param {string} str - The id to check
* @param {number} maxLength - The maximum length of the id
* @param {string} member - The name of the field that is validated
* @param {boolean} validateCharset - If the character set should be validated
*/
function assert_valid_sepa_id(str, maxLength, member, validateCharset) {
assert_length(str, null, maxLength, member);
if (validateCharset) {
if (str && !str.match(/([A-Za-z0-9]|[+|?|/|\-|:|(|)|.|,|' ]){1,35}/)) {
throw new Error(`${member} contains characters which are not in the SEPA character set (found: "${str}")`);
}
}
if (str && str.length > 1 && str.charAt(0) === '/') {
throw new Error(`${member} is an id and hence must not start with a "/". (found "${str}"`);
}
if (str && str.match(/\/\//)) {
throw new Error(`${member} is an id and hence must not contain "//". (found "${str}"`);
}
}
/**
* Creates a DOM Document, either using the browser document, or node.js xmldom.
*
* @param nsURI The namespace URI.
* @param qname Qualified name for the root tag.
* @return The created DOM document.
*/
function createDocument(nsURI, qname) {
if (typeof document !== 'undefined' && typeof document.implementation !== 'undefined') {
return document.implementation.createDocument(nsURI, qname);
} else {
var DOMImplementation = require('@xmldom/xmldom').DOMImplementation;
return new DOMImplementation().createDocument(nsURI, qname);
}
}
/**
* Serializes a dom element or document to string, using either the builtin
* XMLSerializer or the one from node.js xmldom.
*
* @param doc The document or element to serialize
* @return The serialized XML document.
*/
function serializeToString(doc) {
var s;
if (typeof window === 'undefined') {
var XMLSerializer = require('@xmldom/xmldom').XMLSerializer;
s = new XMLSerializer();
} else {
s = new window.XMLSerializer();
}
return s.serializeToString(doc);
}
/**
* Returns a helper for creating XML nodes. There are three intended calls
* for this helper. The first parameter for the returned function is always
* the parent element, followed by a variable number of element names. The
* last parameter may be the text content value, as shown below. The
* innermost node is always returned.
*
* // This helper creates a node without a contained value
* // Usage: n(rootNode, 'foo', 'bar')
* // Result: <root><foo><bar/></foo></root>
* var n = createXMLHelper(doc, true, false);
*
* // This helper creates a node with an optional value. If the value is
* // null, then the node is not added to the parent.
* // Usage: o(rootNode, 'foo', 'bar', myValue)
* // Result (if myValue is not null): <root><foo><bar>myValue</bar></foo></root>
* var o = createXMLHelper(doc, false, true);
*
* // This helper creates a node with a required value. It is added
* // regardless of if its null or not.
* // Usage: r(rootNode, 'foo', 'bar', myValue)
* // Result: <root><foo><bar>myValue</bar></foo></root>
* var r = createXMLHelper(doc, true, true);
*
* @param doc The document to create nodes with
* @param required If false, nodes with null values will not be added to the parent.
* @param withVal If true, the last parameter of the returned function is set as textContent.
*/
function createXMLHelper(doc, required, withVal) {
return function() {
var node = arguments[0];
var val = withVal && arguments[arguments.length - 1];
var maxarg = (withVal ? arguments.length - 1 : arguments.length);
if (required || val || val === 0) {
for (var i = 1; i < maxarg; ++i) {
node = node.appendChild(doc.createElementNS(doc.documentElement.namespaceURI, arguments[i]));
}
if (withVal) {
node.textContent = val;
}
return node;
} else {
return null;
}
};
}
// --- Module Exports follow --- //
exports.Document = SepaDocument;
exports.validateIBAN = validateIBAN;
exports.checksumIBAN = checksumIBAN;
exports.validateCreditorID = validateCreditorID;
exports.checksumCreditorID = checksumCreditorID;
exports.setIDSeparator = setIDSeparator;
exports.enableValidations = enableValidations;
})(typeof exports === 'undefined' ? this.SEPA = {} : exports);