Added inital SEPA CORE (Direct Debit) support. Needs testing!
[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 des Mandatsunterschrift muss 10 Zeichen lang sein (nicht ' + debtormndtdate.length + ').');
114     }
115     
116     
117     
118     if (argscntOK && (isNaN(amountcent) || amountcent < 0)) {
119       errors.push('Betrag muss eine Zahl und darf nicht negativ (' + amountcent + ' cent) sein.');
120     }
121     amountcent = parseInt(amountcent);
122     
123     purpose = purpose || '';
124     if (argscntOK && purpose.length > 140) {
125       errors.push('Verwendungszweck muss zwischen 0 und 140 Zeichen lang sein (nicht ' + purpose.length + ').');
126     }
127     
128     e2eid = e2eid || 'NOTPROVIDED';
129     if (argscntOK && purpose.length > 35) {
130       errors.push('End-to-End ID muss zwischen 0 und 35 Zeichen lang sein (nicht ' + purpose.length + ').');
131     }
132     if (e2eid == '') {
133       e2eid = 'NOTPROVIDED';
134     }
135     
136     if (debtorbic != null) {
137       bicstr = '<DbtrAgt><FinInstnId><BIC>' + escapeXml(debtorbic) + '</BIC></FinInstnId></DbtrAgt>';
138     } else {
139       bicstr = '<!-- no BIC for DbtrAgt supplied -->';
140     }
141     
142     if (purpose != '') {
143       purposestr = '<RmtInf><Ustrd>' + escapeXml(purpose) + '</Ustrd></RmtInf>';
144     } else {
145       purposestr = '<!-- no Ustrd for RmtInf supplied -->';
146     }
147     
148     if (errors.length == 0) {
149       this.directdebittxs[ddtype].push([      
150         '      <DrctDbtTxInf>',
151         '        <PmtId><EndToEndId>' + escapeXml(e2eid) + '</EndToEndId></PmtId>',
152         '        <InstdAmt Ccy="EUR">' + centToEur(amountcent) + '</InstdAmt>',
153         '        <DrctDbtTx><MndtRltdInf>',
154         '          <MndtId>' + escapeXml(debtormndtid) + '</MndtId>',
155         '          <DtOfSgntr>' + escapeXml(debtormndtdate) + '</DtOfSgntr>',
156         '          <AmdmntInd>false</AmdmntInd>',
157         '        </MndtRltdInf></DrctDbtTx>',
158         '        ' + bicstr,
159         '        <Dbtr><Nm>' + escapeXml(debtorname) + '</Nm></Dbtr>',
160         '        <DbtrAcct><Id><IBAN>' + escapeXml(debtoriban) + '</IBAN></Id></DbtrAcct>',
161         '        ' + purposestr,
162         '      </DrctDbtTxInf>'].join('\n'));
163       this.directdebittxssums[ddtype] += amountcent;
164       return true;
165     }
166     
167     this.errormsg = 'Fehler beim Hinzufügen eines Empfängers:\n';
168     for (var i = 0; i < errors.length; i++) {
169       this.errormsg += ' - ' + errors[i]  +'\n';
170     }
171     return false;
172   },
173   
174   getGroupHeaderBlock: function(ddtype) {
175
176     if (ddtype != 'FRST' && ddtype != 'RCUR') {
177       this.errormsg += ' - getGroupHeaderBlock: Sequenztyp (ddtype) muss FRST oder RCUR sein.\n';
178       return;
179     }
180     
181     createdatestr = [
182       this.creationdate.getUTCFullYear(), '-',
183         str_pad_left(this.creationdate.getUTCMonth()+1, 2, '0'), '-',
184         str_pad_left(this.creationdate.getUTCDate(), 2, '0'), 'T',
185         str_pad_left(this.creationdate.getUTCHours(), 2, '0'), ':',
186         str_pad_left(this.creationdate.getUTCMinutes(), 2, '0'), ':',
187         str_pad_left(this.creationdate.getUTCSeconds(), 2, '0'), '.000Z' ].join('');
188
189     return [
190       '    <GrpHdr>',
191       '      <MsgId>/V:1/MSG:' + parseInt(this.creationdate.getTime()/1000) + '/S:' + ddtype[0] + '/</MsgId>',
192       '      <CreDtTm>' + createdatestr + '</CreDtTm>',
193       '      <NbOfTxs>' + this.directdebittxs[ddtype].length + '</NbOfTxs>',
194       '      <InitgPty><Nm>' + escapeXml(this.creditorname) + '</Nm></InitgPty>',
195       '    </GrpHdr>'].join('\n');
196   },
197     
198   getPaymentInformationHeaderBlock: function(ddtype) {
199     
200     if (ddtype != 'FRST' && ddtype != 'RCUR') {
201       this.errormsg += ' - getPaymentInformationHeaderBlock: Sequenztyp (ddtype) muss FRST oder RCUR sein.\n';
202       return;
203     }
204
205     // Localtime or UTC???
206     collectiondatestr = [ this.collectiondate.getFullYear(),
207                           str_pad_left(this.collectiondate.getMonth()+1, 2, '0'),
208                           str_pad_left(this.collectiondate.getDate(), 2, '0')
209                           ].join('-');
210         
211     return [
212       '      <PmtInfId>/V:1/PMT:' + parseInt(this.creationdate.getTime()/1000) + '/S:' + ddtype[0] + '/</PmtInfId>',
213       '      <PmtMtd>DD</PmtMtd>',
214       '      <NbOfTxs>' + this.directdebittxs[ddtype].length + '</NbOfTxs>',
215       '      <CtrlSum>' + centToEur(this.directdebittxssums[ddtype]) + '</CtrlSum>',
216       '      <PmtTpInf><SvcLvl><Cd>SEPA</Cd></SvcLvl>',
217       '        <LclInstrm><Cd>CORE</Cd></LclInstrm>',
218       '        <SeqTp>' + ddtype + '</SeqTp>',
219       '      </PmtTpInf>',
220       '      <ReqdColltnDt>' + collectiondatestr + '</ReqdColltnDt>'
221       ].join('\n');
222   },
223
224   getCreditorBlock: function() {
225     if (this.creditorbic != null) {
226       bicstr = '<CdtrAgt><FinInstnId><BIC>' + escapeXml(this.creditorbic) + '</BIC></FinInstnId></CdtrAgt>';
227     } else {
228       bicstr = '<!-- no BIC for CdtrAgt supplied -->';
229     }
230     return [
231       '      <Cdtr><Nm>' + escapeXml(this.creditorname) + '</Nm> </Cdtr>',
232       '      <CdtrAcct><Id><IBAN>' + escapeXml(this.creditoriban) + '</IBAN></Id></CdtrAcct>',
233       '      ' + bicstr ,
234       '      <ChrgBr>SLEV</ChrgBr>',
235       '      <CdtrSchmeId><Id><PrvtId><Othr>',
236       '        <Id>' + escapeXml(this.creditoridentifier) + '</Id>',
237       '        <SchmeNm><Prtry>SEPA</Prtry></SchmeNm>',
238       '      </Othr></PrvtId></Id></CdtrSchmeId>'
239       ].join('\n');
240   },
241   
242   getDirectDebitBlock: function(ddtype) {
243     if (ddtype != 'FRST' && ddtype != 'RCUR') {
244       this.errormsg += ' - getDirectDebitBlock: Sequenztyp (ddtype) muss FRST oder RCUR sein.\n';
245       return;
246     }
247     return this.directdebittxs[ddtype].join('\n');
248   },
249
250   getXMLContent: function(ddtype) {
251     if (ddtype != 'FRST' && ddtype != 'RCUR') {
252       this.errormsg += ' - getXMLContent: Sequenztyp (ddtype) muss FRST oder RCUR sein.\n';
253       return;
254     }
255     if (this.directdebittxs[ddtype].length == 0) {
256       return '<!-- Keine ' + ((ddtype=='FRST') ? 'SEPA Ersteinzüge' : 'wiederkehrenden SEPA Einzüge') + ' -->';
257     } else {
258       return [
259         '<?xml version="1.0" encoding="UTF-8"?>',
260         '<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">',
261         '  <CstmrDrctDbtInitn>',
262         this.getGroupHeaderBlock(ddtype),
263         '    <PmtInf>',
264         this.getPaymentInformationHeaderBlock(ddtype),
265         this.getCreditorBlock(),
266         this.getDirectDebitBlock(ddtype),
267         '    </PmtInf>',
268         '  </CstmrDrctDbtInitn>',
269         '</Document>'].join('\n');
270     }  
271   },
272   
273 }