Added proper check for Debtor Mandate Signature Date.
[memberdb.git] / js / sepa-CORE.js
1 // stolen from https://gist.github.com/panzi/1857360
2
3 var XML_CHAR_MAP = {
4   '<': '&lt;',
5   '>': '&gt;',
6   '&': '&amp;',
7   '"': '&quot;',
8   "'": '&apos;'
9 };
10
11 function escapeXml (s) {
12   return s.replace(/[<>&"']/g, function (ch) {
13     return XML_CHAR_MAP[ch];
14   });
15 }
16
17 //
18 // PLEASE DO NOT HARM YOURSELF: DO NOT TRY TO IMPROVE THIS QUICK AND DIRTY HACK!!!
19 // PLEASE CONSIDER A PROPER REIMPLEMENTATION OF pain.008.003.02 IN JS INSTEAD.
20 //
21
22 // TODO: check for valid characters instead of just escapeXml!
23
24 var SEPACORE = {
25
26   creditorname: null,
27   creditoridentifier: null,
28   creditoriban: null,
29   creditorbic: null,
30   
31   directdebittxs: { 'FRST': [], 'RCUR': [] },
32   directdebittxssums: { 'FRST': 0, 'RCUR': 0 },
33   
34   creationdate: new Date(),
35   // only one collectiondate for FRST & RCUR is supported
36   collectiondate: null,
37   
38   errormsg: '',
39   
40   init: function(collectiondate, creditoridentifier, creditorname, creditoriban, creditorbic) {
41     
42     var errors = [];
43     var argscntOK = true;
44     
45     if (arguments.length != 4 && arguments.length != 5) {
46       this.errors.push('initSEPACORE mit falscher Parameteranzahl aufgerufen (Soll: 4 oder 5; Ist: ' + arguments.length + ').');
47       argscntOK = false;
48     }
49     
50     if (argscntOK && (creditorname.length == 0 || creditorname.length > 70)) {
51       errors.push('Name des Zahlungsempfängers muss zwischen 1 und 70 Zeichen lang sein (nicht ' + creditorname.length + ').');
52     }
53     this.creditorname = creditorname;
54
55     if (argscntOK && (creditoriban.length < 15 || creditoriban.length > 32)) {
56       errors.push('IBAN des Zahlungsempfängers muss zwischen 15 und 32 Zeichen lang sein (nicht ' + creditoriban.length + ').');
57     }
58     this.creditoriban = creditoriban;
59
60     if (argscntOK && creditorbic != null && creditorbic.length != 8 && creditorbic.length != 11) {
61       errors.push('BIC des Zahlungsempfängers muss 8 oder 11 Zeichen lang oder nicht gesetzt sein (nicht ' + creditorbic.length + ').');
62     }
63     this.creditorbic = creditorbic;
64
65     if (argscntOK && (creditoridentifier.length == 0 || creditoridentifier.length > 35)) {
66       errors.push('Gläubiger-ID für den Zahlungspflichtigen muss zwischen 1 und 35 Zeichen lang sein (nicht ' + creditormndtid.length + ').');
67     }
68     this.creditoridentifier = creditoridentifier;
69     
70     this.collectiondate = collectiondate;
71     
72     
73     this.directdebittxs = { 'FRST': [], 'RCUR': [] };
74     this.directdebittxssums = { 'FRST': 0, 'RCUR': 0 };
75   
76     this.creationdate = new Date(),
77     this.errormsg = '';
78     
79     return true;
80   },
81   
82   addDDTx: function(ddtype, debtorname, debtoriban, debtorbic, debtormndtid, debtormndtdate, amountcent, purpose, e2eid) {
83     
84     var errors = [];
85     var argscntOK = true;
86     
87     if (arguments.length != 9 && arguments.length != 8) {
88       this.errors.push('addDDTx mit falscher Parameteranzahl aufgerufen (Soll: 8 oder 9; Ist: ' + arguments.length + ').');
89       argscntOK = false;
90     }      
91     
92     if (argscntOK && ddtype != 'FRST' && ddtype != 'RCUR') {
93       errors.push('Sequenztyp (ddtype) muss FRST oder RCUR sein.');
94     }
95     
96     if (argscntOK && (debtorname.length == 0 || debtorname.length > 70)) {
97       errors.push('Name des Zahlungspflichtigen muss zwischen 1 und 70 Zeichen lang sein (nicht ' + debtorname.length + ').');
98     }
99
100     if (argscntOK && (debtoriban.length < 15 || debtoriban.length > 32)) {
101       errors.push('IBAN des Zahlungspflichtigen muss zwischen 15 und 32 Zeichen lang sein (nicht ' + debtoriban.length + ').');
102     }
103
104     if (argscntOK && debtorbic != null && debtorbic.length != 8 && debtorbic.length != 11) {
105       errors.push('BIC des Zahlungspflichtigen muss 8 oder 11 Zeichen lang oder nicht gesetzt sein (nicht ' + debtorbic.length + ').');
106     }
107
108     if (argscntOK && (debtormndtid.length == 0 || debtormndtid.length > 35)) {
109       errors.push('Mandatsreferenz für den Zahlungspflichtigen muss zwischen 1 und 35 Zeichen lang sein (nicht ' + debtormndtid.length + ').');
110     }
111     
112     if (argscntOK && debtormndtdate.length != 10) {
113       errors.push('Datum der Mandatsunterschrift muss 10 Zeichen lang sein (nicht ' + debtormndtdate.length + ').');
114     }
115     
116     if (! /^[-]?\d{4}-(0[1-9]|1[0-2])-(0[0-9]|[1-5][0-9]|60)[Z\-]?\d*:?\d*$/.test(debtormndtdate) ) {
117       errors.push('Datum der Mandatsunterschrift ist nicht in Ordnung (' + debtormndtdate + ').');
118     }      
119     
120     if (argscntOK && (isNaN(amountcent) || amountcent < 0)) {
121       errors.push('Betrag muss eine Zahl und darf nicht negativ (' + amountcent + ' cent) sein.');
122     }
123     amountcent = parseInt(amountcent);
124     
125     purpose = purpose || '';
126     if (argscntOK && purpose.length > 140) {
127       errors.push('Verwendungszweck muss zwischen 0 und 140 Zeichen lang sein (nicht ' + purpose.length + ').');
128     }
129     
130     e2eid = e2eid || 'NOTPROVIDED';
131     if (argscntOK && purpose.length > 35) {
132       errors.push('End-to-End ID muss zwischen 0 und 35 Zeichen lang sein (nicht ' + purpose.length + ').');
133     }
134     if (e2eid == '') {
135       e2eid = 'NOTPROVIDED';
136     }
137     
138     if (debtorbic != null) {
139       bicstr = '<DbtrAgt><FinInstnId><BIC>' + escapeXml(debtorbic) + '</BIC></FinInstnId></DbtrAgt>';
140     } else {
141       bicstr = '<!-- no BIC for DbtrAgt supplied -->';
142     }
143     
144     if (purpose != '') {
145       purposestr = '<RmtInf><Ustrd>' + escapeXml(purpose) + '</Ustrd></RmtInf>';
146     } else {
147       purposestr = '<!-- no Ustrd for RmtInf supplied -->';
148     }
149     
150     if (errors.length == 0) {
151       this.directdebittxs[ddtype].push([      
152         '      <DrctDbtTxInf>',
153         '        <PmtId><EndToEndId>' + escapeXml(e2eid) + '</EndToEndId></PmtId>',
154         '        <InstdAmt Ccy="EUR">' + centToEur(amountcent) + '</InstdAmt>',
155         '        <DrctDbtTx><MndtRltdInf>',
156         '          <MndtId>' + escapeXml(debtormndtid) + '</MndtId>',
157         '          <DtOfSgntr>' + escapeXml(debtormndtdate) + '</DtOfSgntr>',
158         '          <AmdmntInd>false</AmdmntInd>',
159         '        </MndtRltdInf></DrctDbtTx>',
160         '        ' + bicstr,
161         '        <Dbtr><Nm>' + escapeXml(debtorname) + '</Nm></Dbtr>',
162         '        <DbtrAcct><Id><IBAN>' + escapeXml(debtoriban) + '</IBAN></Id></DbtrAcct>',
163         '        ' + purposestr,
164         '      </DrctDbtTxInf>'].join('\n'));
165       this.directdebittxssums[ddtype] += amountcent;
166       return true;
167     }
168     
169     this.errormsg = 'Fehler beim Hinzufügen eines Empfängers:\n';
170     for (var i = 0; i < errors.length; i++) {
171       this.errormsg += ' - ' + errors[i]  +'\n';
172     }
173     return false;
174   },
175   
176   getGroupHeaderBlock: function(ddtype) {
177
178     if (ddtype != 'FRST' && ddtype != 'RCUR') {
179       this.errormsg += ' - getGroupHeaderBlock: Sequenztyp (ddtype) muss FRST oder RCUR sein.\n';
180       return;
181     }
182     
183     createdatestr = [
184       this.creationdate.getUTCFullYear(), '-',
185         str_pad_left(this.creationdate.getUTCMonth()+1, 2, '0'), '-',
186         str_pad_left(this.creationdate.getUTCDate(), 2, '0'), 'T',
187         str_pad_left(this.creationdate.getUTCHours(), 2, '0'), ':',
188         str_pad_left(this.creationdate.getUTCMinutes(), 2, '0'), ':',
189         str_pad_left(this.creationdate.getUTCSeconds(), 2, '0'), '.000Z' ].join('');
190
191     return [
192       '    <GrpHdr>',
193       '      <MsgId>/V:1/MSG:' + parseInt(this.creationdate.getTime()/1000) + '/S:' + ddtype[0] + '/</MsgId>',
194       '      <CreDtTm>' + createdatestr + '</CreDtTm>',
195       '      <NbOfTxs>' + this.directdebittxs[ddtype].length + '</NbOfTxs>',
196       '      <InitgPty><Nm>' + escapeXml(this.creditorname) + '</Nm></InitgPty>',
197       '    </GrpHdr>'].join('\n');
198   },
199     
200   getPaymentInformationHeaderBlock: function(ddtype) {
201     
202     if (ddtype != 'FRST' && ddtype != 'RCUR') {
203       this.errormsg += ' - getPaymentInformationHeaderBlock: Sequenztyp (ddtype) muss FRST oder RCUR sein.\n';
204       return;
205     }
206
207     // Localtime or UTC???
208     collectiondatestr = [ this.collectiondate.getFullYear(),
209                           str_pad_left(this.collectiondate.getMonth()+1, 2, '0'),
210                           str_pad_left(this.collectiondate.getDate(), 2, '0')
211                           ].join('-');
212         
213     return [
214       '      <PmtInfId>/V:1/PMT:' + parseInt(this.creationdate.getTime()/1000) + '/S:' + ddtype[0] + '/</PmtInfId>',
215       '      <PmtMtd>DD</PmtMtd>',
216       '      <NbOfTxs>' + this.directdebittxs[ddtype].length + '</NbOfTxs>',
217       '      <CtrlSum>' + centToEur(this.directdebittxssums[ddtype]) + '</CtrlSum>',
218       '      <PmtTpInf><SvcLvl><Cd>SEPA</Cd></SvcLvl>',
219       '        <LclInstrm><Cd>CORE</Cd></LclInstrm>',
220       '        <SeqTp>' + ddtype + '</SeqTp>',
221       '      </PmtTpInf>',
222       '      <ReqdColltnDt>' + collectiondatestr + '</ReqdColltnDt>'
223       ].join('\n');
224   },
225
226   getCreditorBlock: function() {
227     if (this.creditorbic != null) {
228       bicstr = '<CdtrAgt><FinInstnId><BIC>' + escapeXml(this.creditorbic) + '</BIC></FinInstnId></CdtrAgt>';
229     } else {
230       bicstr = '<!-- no BIC for CdtrAgt supplied -->';
231     }
232     return [
233       '      <Cdtr><Nm>' + escapeXml(this.creditorname) + '</Nm> </Cdtr>',
234       '      <CdtrAcct><Id><IBAN>' + escapeXml(this.creditoriban) + '</IBAN></Id></CdtrAcct>',
235       '      ' + bicstr ,
236       '      <ChrgBr>SLEV</ChrgBr>',
237       '      <CdtrSchmeId><Id><PrvtId><Othr>',
238       '        <Id>' + escapeXml(this.creditoridentifier) + '</Id>',
239       '        <SchmeNm><Prtry>SEPA</Prtry></SchmeNm>',
240       '      </Othr></PrvtId></Id></CdtrSchmeId>'
241       ].join('\n');
242   },
243   
244   getDirectDebitBlock: function(ddtype) {
245     if (ddtype != 'FRST' && ddtype != 'RCUR') {
246       this.errormsg += ' - getDirectDebitBlock: Sequenztyp (ddtype) muss FRST oder RCUR sein.\n';
247       return;
248     }
249     return this.directdebittxs[ddtype].join('\n');
250   },
251
252   getXMLContent: function(ddtype) {
253     if (ddtype != 'FRST' && ddtype != 'RCUR') {
254       this.errormsg += ' - getXMLContent: Sequenztyp (ddtype) muss FRST oder RCUR sein.\n';
255       return;
256     }
257     if (this.directdebittxs[ddtype].length == 0) {
258       return '<!-- Keine ' + ((ddtype=='FRST') ? 'SEPA Ersteinzüge' : 'wiederkehrenden SEPA Einzüge') + ' -->';
259     } else {
260       return [
261         '<?xml version="1.0" encoding="UTF-8"?>',
262         '<Document xmlns="urn:iso:std:iso:20022:tech:xsd:pain.008.003.02" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="urn:iso:std:iso:20022:tech:xsd:pain.008.003.02 pain.008.003.02.xsd">',
263         '  <CstmrDrctDbtInitn>',
264         this.getGroupHeaderBlock(ddtype),
265         '    <PmtInf>',
266         this.getPaymentInformationHeaderBlock(ddtype),
267         this.getCreditorBlock(),
268         this.getDirectDebitBlock(ddtype),
269         '    </PmtInf>',
270         '  </CstmrDrctDbtInitn>',
271         '</Document>'].join('\n');
272     }  
273   },
274   
275 }