1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace Fhp; |
4
|
|
|
|
5
|
|
|
use Fhp\Adapter\AdapterInterface; |
6
|
|
|
use Fhp\Adapter\Curl; |
7
|
|
|
use Fhp\DataTypes\Kik; |
8
|
|
|
use Fhp\DataTypes\Kti; |
9
|
|
|
use Fhp\DataTypes\Ktv; |
10
|
|
|
use Fhp\Dialog\Dialog; |
11
|
|
|
use Fhp\Message\AbstractMessage; |
12
|
|
|
use Fhp\Message\Message; |
13
|
|
|
use Fhp\Model\SEPAAccount; |
14
|
|
|
use Fhp\Response\GetAccounts; |
15
|
|
|
use Fhp\Response\GetSaldo; |
16
|
|
|
use Fhp\Response\GetSEPAAccounts; |
17
|
|
|
use Fhp\Response\GetStatementOfAccount; |
18
|
|
|
use Fhp\Segment\HKKAZ; |
19
|
|
|
use Fhp\Segment\HKSAL; |
20
|
|
|
use Fhp\Segment\HKSPA; |
21
|
|
|
use Psr\Log\LoggerInterface; |
22
|
|
|
use Psr\Log\NullLogger; |
23
|
|
|
|
24
|
|
|
/** |
25
|
|
|
* Class FinTs. |
26
|
|
|
* |
27
|
|
|
* @package Fhp |
28
|
|
|
*/ |
29
|
|
|
class FinTs |
30
|
|
|
{ |
31
|
|
|
const DEFAULT_COUNTRY_CODE = 280; |
32
|
|
|
|
33
|
|
|
/** @var LoggerInterface */ |
34
|
|
|
protected $logger; |
35
|
|
|
/** @var string */ |
36
|
|
|
protected $server; |
37
|
|
|
/** @var int */ |
38
|
|
|
protected $port; |
39
|
|
|
/** @var string */ |
40
|
|
|
protected $bankCode; |
41
|
|
|
/** @var string */ |
42
|
|
|
protected $username; |
43
|
|
|
/** @var string */ |
44
|
|
|
protected $pin; |
45
|
|
|
/** @var Connection */ |
46
|
|
|
protected $connection; |
47
|
|
|
/** @var AdapterInterface */ |
48
|
|
|
protected $adapter; |
49
|
|
|
/** @var int */ |
50
|
|
|
protected $systemId = 0; |
51
|
|
|
/** @var string */ |
52
|
|
|
protected $bankName; |
53
|
|
|
/** @var string */ |
54
|
|
|
protected $productName; |
55
|
|
|
/** @var string */ |
56
|
|
|
protected $productVersion; |
57
|
|
|
|
58
|
|
|
/** |
59
|
|
|
* FinTs constructor. |
60
|
|
|
* @param string $server Base URL (should start with https://) where the bank's server can be reached. |
61
|
|
|
* @param int $port The port to use for the connection. Use 443 for HTTPS. |
62
|
|
|
* @param string $bankCode The bank's identifier (Bankleitzahl). |
63
|
|
|
* @param string $username The username for authentication. This is assigned by the bank initially, but most banks |
64
|
|
|
* allow users to customize it. |
65
|
|
|
* @param string $pin The PIN for authentication. |
66
|
|
|
* @param string $productName Identifies the product (i.e. the application in which the fints-hbci-php library is |
67
|
|
|
* being used). This is used to show users which products/applications have access to their bank account. Note |
68
|
|
|
* that this shouldn't just be an arbitrary string, but rather the registration number obtained from the |
69
|
|
|
* registration on https://www.hbci-zka.de/register/prod_register.htm. |
70
|
|
|
* @param string $productVersion The product version, which can be an arbitrary string, though if your the |
71
|
|
|
* application displays a version number somewhere on its own user interface, it should match that. |
72
|
|
|
* @param LoggerInterface|null $logger |
73
|
|
|
*/ |
74
|
|
|
public function __construct( |
75
|
|
|
$server, |
76
|
|
|
$port, |
77
|
|
|
$bankCode, |
78
|
|
|
$username, |
79
|
|
|
$pin, |
80
|
|
|
$productName, |
81
|
|
|
$productVersion, |
82
|
|
|
LoggerInterface $logger = null |
83
|
|
|
) { |
84
|
|
|
$this->server = $server; |
85
|
|
|
$this->port = $port; |
86
|
|
|
$this->logger = null == $logger ? new NullLogger() : $logger; |
87
|
|
|
|
88
|
|
|
// escaping of bank code not really needed here as it should |
89
|
|
|
// never have special chars. But we just do it to ensure |
90
|
|
|
// that the request will not get messed up and the user |
91
|
|
|
// can receive a valid error response from the HBCI server. |
92
|
|
|
$this->bankCode = $this->escapeString($bankCode); |
93
|
|
|
|
94
|
|
|
// Here, escaping is needed for usernames or pins with |
95
|
|
|
// HBCI special chars. |
96
|
|
|
$this->username = $this->escapeString($username); |
97
|
|
|
$this->pin = $this->escapeString($pin); |
98
|
|
|
|
99
|
|
|
$this->productName = $productName; |
100
|
|
|
$this->productVersion = $productVersion; |
101
|
|
|
|
102
|
|
|
$this->adapter = new Curl($this->server, $this->port); |
103
|
|
|
$this->connection = new Connection($this->adapter); |
104
|
|
|
} |
105
|
|
|
|
106
|
|
|
/** |
107
|
|
|
* Sets the adapter to use. |
108
|
|
|
* |
109
|
|
|
* @param AdapterInterface $adapter |
110
|
|
|
*/ |
111
|
|
|
public function setAdapter(AdapterInterface $adapter) |
112
|
|
|
{ |
113
|
|
|
$this->adapter = $adapter; |
114
|
|
|
$this->connection = new Connection($this->adapter); |
115
|
|
|
} |
116
|
|
|
|
117
|
|
|
/** |
118
|
|
|
* Gets array of all accounts. |
119
|
|
|
* |
120
|
|
|
* @return Model\Account[] |
121
|
|
|
*/ |
122
|
|
|
public function getAccounts() |
123
|
|
|
{ |
124
|
|
|
$dialog = $this->getDialog(); |
125
|
|
|
$result = $dialog->syncDialog(); |
126
|
|
|
$this->bankName = $dialog->getBankName(); |
127
|
|
|
$accounts = new GetAccounts($result); |
128
|
|
|
|
129
|
|
|
return $accounts->getAccountsArray(); |
130
|
|
|
} |
131
|
|
|
|
132
|
|
|
/** |
133
|
|
|
* Gets array of all SEPA Accounts. |
134
|
|
|
* |
135
|
|
|
* @return Model\SEPAAccount[] |
136
|
|
|
* @throws Adapter\Exception\AdapterException |
137
|
|
|
* @throws Adapter\Exception\CurlException |
138
|
|
|
*/ |
139
|
|
|
public function getSEPAAccounts() |
140
|
|
|
{ |
141
|
|
|
$dialog = $this->getDialog(); |
142
|
|
|
$dialog->syncDialog(); |
143
|
|
|
$dialog->initDialog(); |
144
|
|
|
|
145
|
|
|
$message = $this->getNewMessage( |
146
|
|
|
$dialog, |
147
|
|
|
array(new HKSPA(3)), |
148
|
|
|
array(AbstractMessage::OPT_PINTAN_MECH => $dialog->getSupportedPinTanMechanisms()) |
149
|
|
|
); |
150
|
|
|
|
151
|
|
|
$result = $dialog->sendMessage($message); |
152
|
|
|
$dialog->endDialog(); |
153
|
|
|
$sepaAccounts = new GetSEPAAccounts($result->rawResponse); |
154
|
|
|
|
155
|
|
|
return $sepaAccounts->getSEPAAccountsArray(); |
156
|
|
|
} |
157
|
|
|
|
158
|
|
|
/** |
159
|
|
|
* Gets the bank name. |
160
|
|
|
* |
161
|
|
|
* @return string |
162
|
|
|
*/ |
163
|
|
|
public function getBankName() |
164
|
|
|
{ |
165
|
|
|
if (null == $this->bankName) { |
166
|
|
|
$this->getDialog()->syncDialog(); |
167
|
|
|
} |
168
|
|
|
|
169
|
|
|
return $this->bankName; |
170
|
|
|
} |
171
|
|
|
|
172
|
|
|
/** |
173
|
|
|
* Gets statement of account. |
174
|
|
|
* |
175
|
|
|
* @param SEPAAccount $account |
176
|
|
|
* @param \DateTime $from |
177
|
|
|
* @param \DateTime $to |
178
|
|
|
* @return Model\StatementOfAccount\StatementOfAccount|null |
179
|
|
|
* @throws \Exception |
180
|
|
|
*/ |
181
|
|
|
public function getStatementOfAccount(SEPAAccount $account, \DateTime $from, \DateTime $to) |
182
|
|
|
{ |
183
|
|
|
$responses = array(); |
184
|
|
|
|
185
|
|
|
$this->logger->info('Start fetching statement of account results'); |
186
|
|
|
$this->logger->info('Start date: ' . $from->format('Y-m-d')); |
187
|
|
|
$this->logger->info('End date : ' . $to->format('Y-m-d')); |
188
|
|
|
|
189
|
|
|
$dialog = $this->getDialog(); |
190
|
|
|
$dialog->syncDialog(); |
191
|
|
|
$dialog->initDialog(); |
192
|
|
|
|
193
|
|
|
$message = $this->createStateOfAccountMessage($dialog, $account, $from, $to, null); |
194
|
|
|
$response = $dialog->sendMessage($message); |
195
|
|
|
$touchdowns = $response->getTouchdowns($message); |
196
|
|
|
$soaResponse = new GetStatementOfAccount($response->rawResponse); |
197
|
|
|
$responses[] = $soaResponse->getRawMt940(); |
198
|
|
|
|
199
|
|
|
$touchdownCounter = 1; |
200
|
|
|
while (isset($touchdowns[HKKAZ::NAME])) { |
201
|
|
|
$this->logger->info('Fetching more statement of account results (' . $touchdownCounter++ . ') ...'); |
202
|
|
|
$message = $this->createStateOfAccountMessage( |
203
|
|
|
$dialog, |
204
|
|
|
$account, |
205
|
|
|
$from, |
206
|
|
|
$to, |
207
|
|
|
$touchdowns[HKKAZ::NAME] |
208
|
|
|
); |
209
|
|
|
|
210
|
|
|
$r = $dialog->sendMessage($message); |
211
|
|
|
$touchdowns = $r->getTouchDowns($message); |
212
|
|
|
$soaResponse = new GetStatementOfAccount($r->rawResponse); |
213
|
|
|
$responses[] = $soaResponse->getRawMt940(); |
214
|
|
|
} |
215
|
|
|
|
216
|
|
|
$this->logger->info('Fetching of ' . $touchdownCounter . ' pages done.'); |
217
|
|
|
$this->logger->debug('HKKAZ response:'); |
218
|
|
|
|
219
|
|
|
$dialog->endDialog(); |
220
|
|
|
|
221
|
|
|
return GetStatementOfAccount::createModelFromRawMt940(implode('', $responses)); |
222
|
|
|
} |
223
|
|
|
|
224
|
|
|
/** |
225
|
|
|
* Helper method to create a "Statement of Account Message". |
226
|
|
|
* |
227
|
|
|
* @param Dialog $dialog |
228
|
|
|
* @param SEPAAccount $account |
229
|
|
|
* @param \DateTime $from |
230
|
|
|
* @param \DateTime $to |
231
|
|
|
* @param string|null $touchdown |
232
|
|
|
* @return Message |
233
|
|
|
* @throws \Exception |
234
|
|
|
*/ |
235
|
|
|
protected function createStateOfAccountMessage( |
236
|
|
|
Dialog $dialog, |
237
|
|
|
SepaAccount $account, |
238
|
|
|
\DateTime $from, |
239
|
|
|
\DateTime $to, |
240
|
|
|
$touchdown = null |
241
|
|
|
) { |
242
|
|
|
// version 4, 5, 6, 7 |
243
|
|
|
|
244
|
|
|
// version 5 |
245
|
|
|
/* |
246
|
|
|
1 Segmentkopf DEG M 1 |
247
|
|
|
2 Kontoverbindung Auftraggeber DEG ktv # M 1 |
248
|
|
|
3 Alle Konten DE jn # M 1 |
249
|
|
|
4 Von Datum DE dat # K 1 |
250
|
|
|
5 Bis Datum DE dat # K 1 |
251
|
|
|
6 Maximale Anzahl Einträge DE num ..4 K 1 >0 |
252
|
|
|
7 Aufsetzpunkt DE an ..35 K 1 |
253
|
|
|
*/ |
254
|
|
|
|
255
|
|
|
// version 6 |
256
|
|
|
/* |
257
|
|
|
1 Segmentkopf 1 DEG M 1 |
258
|
|
|
2 Kontoverbindung Auftraggeber 2 DEG ktv # M 1 |
259
|
|
|
3 Alle Konten 1 DE jn # M 1 |
260
|
|
|
4 Von Datum 1 DE dat # O 1 |
261
|
|
|
5 Bis Datum 1 DE dat # O 1 |
262
|
|
|
6 Maximale Anzahl Einträge 1 DE num ..4 C 1 >0 |
263
|
|
|
7 Aufsetzpunkt 1 DE an ..35 C 1 |
264
|
|
|
*/ |
265
|
|
|
|
266
|
|
|
// version 7 |
267
|
|
|
/* |
268
|
|
|
1 Segmentkopf 1 DEG M 1 |
269
|
|
|
2 Kontoverbindung international 1 DEG kti # M 1 |
270
|
|
|
3 Alle Konten 1 DE jn # M 1 |
271
|
|
|
4 Von Datum 1 DE dat # O 1 |
272
|
|
|
5 Bis Datum 1 DE dat # O 1 |
273
|
|
|
6 Maximale Anzahl Einträge 1 DE num ..4 C 1 >0 |
274
|
|
|
7 Aufsetzpunkt 1 DE an ..35 C 1 |
275
|
|
|
*/ |
276
|
|
|
|
277
|
|
|
switch ($dialog->getHkkazMaxVersion()) { |
278
|
|
|
case 4: |
279
|
|
|
case 5: |
280
|
|
|
$konto = new Deg(); |
281
|
|
|
$konto->addDataElement($account->getAccountNumber()); |
282
|
|
|
$konto->addDataElement($account->getSubAccount()); |
283
|
|
|
$konto->addDataElement(static::DEFAULT_COUNTRY_CODE); |
284
|
|
|
$konto->addDataElement($account->getBlz()); |
285
|
|
|
break; |
286
|
|
View Code Duplication |
case 6: |
|
|
|
|
287
|
|
|
$konto = new Ktv( |
288
|
|
|
$account->getAccountNumber(), |
289
|
|
|
$account->getSubAccount(), |
290
|
|
|
new Kik(280, $account->getBlz()) |
291
|
|
|
); |
292
|
|
|
break; |
293
|
|
View Code Duplication |
case 7: |
|
|
|
|
294
|
|
|
$konto = new Kti( |
295
|
|
|
$account->getIban(), |
296
|
|
|
$account->getBic(), |
297
|
|
|
$account->getAccountNumber(), |
298
|
|
|
$account->getSubAccount(), |
299
|
|
|
new Kik(280, $account->getBlz()) |
300
|
|
|
); |
301
|
|
|
break; |
302
|
|
|
default: |
303
|
|
|
throw new \Exception('Unsupported HKKAZ version: ' . $dialog->getHkkazMaxVersion()); |
304
|
|
|
} |
305
|
|
|
|
306
|
|
|
$message = $this->getNewMessage( |
307
|
|
|
$dialog, |
308
|
|
|
array( |
309
|
|
|
new HKKAZ( |
310
|
|
|
$dialog->getHkkazMaxVersion(), |
311
|
|
|
3, |
312
|
|
|
$konto, |
313
|
|
|
HKKAZ::ALL_ACCOUNTS_N, |
314
|
|
|
$from, |
315
|
|
|
$to, |
316
|
|
|
$touchdown |
317
|
|
|
) |
318
|
|
|
), |
319
|
|
|
array(AbstractMessage::OPT_PINTAN_MECH => $dialog->getSupportedPinTanMechanisms()) |
320
|
|
|
); |
321
|
|
|
|
322
|
|
|
return $message; |
323
|
|
|
} |
324
|
|
|
|
325
|
|
|
/** |
326
|
|
|
* Gets the saldo of given SEPAAccount. |
327
|
|
|
* |
328
|
|
|
* @param SEPAAccount $account |
329
|
|
|
* @return Model\Saldo|null |
330
|
|
|
* @throws Adapter\Exception\AdapterException |
331
|
|
|
* @throws Adapter\Exception\CurlException |
332
|
|
|
* @throws \Exception |
333
|
|
|
*/ |
334
|
|
|
public function getSaldo(SEPAAccount $account) |
335
|
|
|
{ |
336
|
|
|
$dialog = $this->getDialog(); |
337
|
|
|
$dialog->syncDialog(); |
338
|
|
|
$dialog->initDialog(); |
339
|
|
|
|
340
|
|
|
switch ((int) $dialog->getHksalMaxVersion()) { |
341
|
|
|
case 4: |
342
|
|
|
case 5: |
343
|
|
|
$hksalAccount = new Deg( |
344
|
|
|
$account->getAccountNumber(), |
|
|
|
|
345
|
|
|
$account->getSubAccount(), |
346
|
|
|
static::DEFAULT_COUNTRY_CODE, $account->getBlz() |
347
|
|
|
); |
348
|
|
|
$hksalAccount->addDataElement($account->getAccountNumber()); |
349
|
|
|
$hksalAccount->addDataElement($account->getSubAccount()); |
350
|
|
|
$hksalAccount->addDataElement(static::DEFAULT_COUNTRY_CODE); |
351
|
|
|
$hksalAccount->addDataElement($account->getBlz()); |
352
|
|
|
break; |
353
|
|
View Code Duplication |
case 6: |
|
|
|
|
354
|
|
|
$hksalAccount = new Ktv( |
355
|
|
|
$account->getAccountNumber(), |
356
|
|
|
$account->getSubAccount(), |
357
|
|
|
new Kik(280, $account->getBlz()) |
358
|
|
|
); |
359
|
|
|
break; |
360
|
|
View Code Duplication |
case 7: |
|
|
|
|
361
|
|
|
$hksalAccount = new Kti( |
362
|
|
|
$account->getIban(), |
363
|
|
|
$account->getBic(), |
364
|
|
|
$account->getAccountNumber(), |
365
|
|
|
$account->getSubAccount(), |
366
|
|
|
new Kik(280, $account->getBlz()) |
367
|
|
|
); |
368
|
|
|
break; |
369
|
|
|
default: |
370
|
|
|
throw new \Exception('Unsupported HKSAL version: ' . $dialog->getHksalMaxVersion()); |
371
|
|
|
} |
372
|
|
|
|
373
|
|
|
$message = new Message( |
374
|
|
|
$this->bankCode, |
375
|
|
|
$this->username, |
376
|
|
|
$this->pin, |
377
|
|
|
$dialog->getSystemId(), |
378
|
|
|
$dialog->getDialogId(), |
379
|
|
|
$dialog->getMessageNumber(), |
380
|
|
|
array( |
381
|
|
|
new HKSAL($dialog->getHksalMaxVersion(), 3, $hksalAccount, HKSAL::ALL_ACCOUNTS_N) |
382
|
|
|
), |
383
|
|
|
array( |
384
|
|
|
AbstractMessage::OPT_PINTAN_MECH => $dialog->getSupportedPinTanMechanisms() |
385
|
|
|
) |
386
|
|
|
); |
387
|
|
|
|
388
|
|
|
$response = $dialog->sendMessage($message); |
389
|
|
|
$response = new GetSaldo($response->rawResponse); |
390
|
|
|
|
391
|
|
|
return $response->getSaldoModel(); |
392
|
|
|
} |
393
|
|
|
|
394
|
|
|
/** |
395
|
|
|
* Helper method to retrieve a pre configured message object. |
396
|
|
|
* Factory for poor people :) |
397
|
|
|
* |
398
|
|
|
* @param Dialog $dialog |
399
|
|
|
* @param array $segments |
400
|
|
|
* @param array $options |
401
|
|
|
* @return Message |
402
|
|
|
*/ |
403
|
|
|
protected function getNewMessage(Dialog $dialog, array $segments, array $options) |
404
|
|
|
{ |
405
|
|
|
return new Message( |
406
|
|
|
$this->bankCode, |
407
|
|
|
$this->username, |
408
|
|
|
$this->pin, |
409
|
|
|
$dialog->getSystemId(), |
410
|
|
|
$dialog->getDialogId(), |
411
|
|
|
$dialog->getMessageNumber(), |
412
|
|
|
$segments, |
413
|
|
|
$options |
414
|
|
|
); |
415
|
|
|
} |
416
|
|
|
|
417
|
|
|
/** |
418
|
|
|
* Helper method to retrieve a pre configured dialog object. |
419
|
|
|
* Factory for poor people :) |
420
|
|
|
* |
421
|
|
|
* @return Dialog |
422
|
|
|
*/ |
423
|
|
|
protected function getDialog() |
424
|
|
|
{ |
425
|
|
|
return new Dialog( |
426
|
|
|
$this->connection, |
427
|
|
|
$this->bankCode, |
428
|
|
|
$this->username, |
429
|
|
|
$this->pin, |
430
|
|
|
$this->systemId, |
431
|
|
|
$this->productName, |
432
|
|
|
$this->productVersion, |
433
|
|
|
$this->logger |
434
|
|
|
); |
435
|
|
|
} |
436
|
|
|
|
437
|
|
|
/** |
438
|
|
|
* Needed for escaping userdata. |
439
|
|
|
* HBCI escape char is "?" |
440
|
|
|
* |
441
|
|
|
* @param string $string |
442
|
|
|
* @return string |
443
|
|
|
*/ |
444
|
10 |
|
protected function escapeString($string) |
445
|
|
|
{ |
446
|
10 |
|
return str_replace( |
447
|
10 |
|
array('?', '@', ':', '+', '\''), |
448
|
10 |
|
array('??', '?@', '?:', '?+', '?\''), |
449
|
|
|
$string |
450
|
10 |
|
); |
451
|
|
|
} |
452
|
|
|
} |
453
|
|
|
|
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.