Skip to content

Commit 124d91a

Browse files
authored
Merge pull request #533 from ticktoo/master
Add CAMT XML parser with automatic MT940 fallback
2 parents fa15bd5 + a109333 commit 124d91a

File tree

4 files changed

+862
-16
lines changed

4 files changed

+862
-16
lines changed

Samples/statementOfAccountXML.php

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
<?php
2+
3+
/** @noinspection PhpUnhandledExceptionInspection */
4+
5+
/**
6+
* SAMPLE - Displays the statement of account using XML format (CAMT).
7+
* This sample demonstrates how to use GetStatementOfAccountXML directly when you need
8+
* raw XML access, or shows that GetStatementOfAccount now automatically falls back to
9+
* XML format when MT940 is not available.
10+
*/
11+
12+
// See login.php, it returns a FinTs instance that is already logged in.
13+
/** @var \Fhp\FinTs $fints */
14+
$fints = require_once 'login.php';
15+
16+
// Just pick the first account, for demonstration purposes. You could also have the user choose, or have SEPAAccount
17+
// hard-coded and not call getSEPAAccounts() at all.
18+
$getSepaAccounts = \Fhp\Action\GetSEPAAccounts::create();
19+
$fints->execute($getSepaAccounts);
20+
if ($getSepaAccounts->needsTan()) {
21+
handleStrongAuthentication($getSepaAccounts); // See login.php for the implementation.
22+
}
23+
$oneAccount = $getSepaAccounts->getAccounts()[0];
24+
25+
$from = new \DateTime('2022-07-15');
26+
$to = new \DateTime();
27+
28+
// Option 1: Use GetStatementOfAccount - it will automatically use XML if MT940 is not available
29+
$getStatement = \Fhp\Action\GetStatementOfAccount::create($oneAccount, $from, $to, false, true);
30+
$fints->execute($getStatement);
31+
if ($getStatement->needsTan()) {
32+
handleStrongAuthentication($getStatement); // See login.php for the implementation.
33+
}
34+
35+
$soa = $getStatement->getStatement();
36+
foreach ($soa->getStatements() as $statement) {
37+
echo $statement->getDate()->format('Y-m-d') . ': Start Saldo: '
38+
. ($statement->getCreditDebit() == \Fhp\Model\StatementOfAccount\Statement::CD_DEBIT ? '-' : '')
39+
. $statement->getStartBalance() . PHP_EOL;
40+
echo 'Transactions:' . PHP_EOL;
41+
echo '=======================================' . PHP_EOL;
42+
foreach ($statement->getTransactions() as $transaction) {
43+
echo "Booked : " . ($transaction->getBooked() ? "true" : "false") . PHP_EOL;
44+
echo 'Amount : ' . ($transaction->getCreditDebit() == \Fhp\Model\StatementOfAccount\Transaction::CD_DEBIT ? '-' : '') . $transaction->getAmount() . PHP_EOL;
45+
echo 'Booking text: ' . $transaction->getBookingText() . PHP_EOL;
46+
echo 'Name : ' . $transaction->getName() . PHP_EOL;
47+
echo 'Description : ' . $transaction->getMainDescription() . PHP_EOL;
48+
echo 'EREF : ' . $transaction->getEndToEndID() . PHP_EOL;
49+
echo '=======================================' . PHP_EOL . PHP_EOL;
50+
}
51+
}
52+
echo 'Found ' . count($soa->getStatements()) . ' statements.' . PHP_EOL;
53+
54+
echo PHP_EOL . PHP_EOL;
55+
echo '========================================' . PHP_EOL;
56+
echo 'Option 2: Direct XML access if needed' . PHP_EOL;
57+
echo '========================================' . PHP_EOL;
58+
59+
// Option 2: Use GetStatementOfAccountXML directly if you need raw XML access
60+
$getStatementXML = \Fhp\Action\GetStatementOfAccountXML::create($oneAccount, $from, $to);
61+
$fints->execute($getStatementXML);
62+
if ($getStatementXML->needsTan()) {
63+
handleStrongAuthentication($getStatementXML); // See login.php for the implementation.
64+
}
65+
66+
$xmlStrings = $getStatementXML->getBookedXML();
67+
foreach ($xmlStrings as $index => $xml) {
68+
echo "XML Document " . ($index + 1) . ":" . PHP_EOL;
69+
// You can now parse the XML manually if needed
70+
$doc = simplexml_load_string($xml);
71+
if ($doc !== false) {
72+
echo "Successfully loaded XML document" . PHP_EOL;
73+
}
74+
}

lib/Fhp/Action/GetStatementOfAccount.php

Lines changed: 61 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace Fhp\Action;
44

5+
use Fhp\CAMT\CAMT;
56
use Fhp\Model\SEPAAccount;
67
use Fhp\Model\StatementOfAccount\StatementOfAccount;
78
use Fhp\MT940\Dialect\PostbankMT940;
@@ -47,6 +48,10 @@ class GetStatementOfAccount extends PaginateableAction
4748
/** @var string */
4849
private $bankName;
4950

51+
// Internal action for XML fallback
52+
/** @var GetStatementOfAccountXML|null */
53+
private $xmlAction;
54+
5055
// Response
5156
/** @var string */
5257
private $rawMT940 = '';
@@ -151,29 +156,47 @@ protected function createRequest(BPD $bpd, ?UPD $upd)
151156
{
152157
$this->bankName = $bpd->getBankName();
153158

154-
/** @var HIKAZS $hikazs */
155-
$hikazs = $bpd->requireLatestSupportedParameters('HIKAZS');
156-
if ($this->allAccounts && !$hikazs->getParameter()->getAlleKontenErlaubt()) {
157-
throw new \InvalidArgumentException('The bank do not permit the use of allAccounts=true');
158-
}
159-
switch ($hikazs->getVersion()) {
160-
case 4:
161-
return HKKAZv4::create(Kto::fromAccount($this->account), $this->from, $this->to);
162-
case 5:
163-
return HKKAZv5::create(KtvV3::fromAccount($this->account), $this->allAccounts, $this->from, $this->to);
164-
case 6:
165-
return HKKAZv6::create(KtvV3::fromAccount($this->account), $this->allAccounts, $this->from, $this->to);
166-
case 7:
167-
return HKKAZv7::create(Kti::fromAccount($this->account), $this->allAccounts, $this->from, $this->to);
168-
default:
169-
throw new UnsupportedException('Unsupported HKKAZ version: ' . $hikazs->getVersion());
159+
// Try to use MT940 format (HIKAZS) if supported
160+
try {
161+
/** @var HIKAZS $hikazs */
162+
$hikazs = $bpd->requireLatestSupportedParameters('HIKAZS');
163+
if ($this->allAccounts && !$hikazs->getParameter()->getAlleKontenErlaubt()) {
164+
throw new \InvalidArgumentException('The bank do not permit the use of allAccounts=true');
165+
}
166+
switch ($hikazs->getVersion()) {
167+
case 4:
168+
return HKKAZv4::create(Kto::fromAccount($this->account), $this->from, $this->to);
169+
case 5:
170+
return HKKAZv5::create(KtvV3::fromAccount($this->account), $this->allAccounts, $this->from, $this->to);
171+
case 6:
172+
return HKKAZv6::create(KtvV3::fromAccount($this->account), $this->allAccounts, $this->from, $this->to);
173+
case 7:
174+
return HKKAZv7::create(Kti::fromAccount($this->account), $this->allAccounts, $this->from, $this->to);
175+
default:
176+
throw new UnsupportedException('Unsupported HKKAZ version: ' . $hikazs->getVersion());
177+
}
178+
} catch (UnexpectedResponseException | UnsupportedException $e) {
179+
// MT940 format not supported, fall back to XML format (HICAZS)
180+
$this->xmlAction = GetStatementOfAccountXML::create($this->account, $this->from, $this->to, null, $this->allAccounts);
181+
return $this->xmlAction->createRequest($bpd, $upd);
170182
}
171183
}
172184

173185
public function processResponse(Message $response)
174186
{
175187
parent::processResponse($response);
176188

189+
// If we're using XML fallback, delegate to the XML action
190+
if ($this->xmlAction !== null) {
191+
$this->xmlAction->processResponse($response);
192+
193+
// Parse XML and convert to StatementOfAccount once all pages are received
194+
if (!$this->hasMorePages()) {
195+
$this->parseXml();
196+
}
197+
return;
198+
}
199+
177200
// Banks send just 3010 and no HIKAZ in case there are no transactions.
178201
$isUnavailable = $response->findRueckmeldung(Rueckmeldungscode::NICHT_VERFUEGBAR) !== null;
179202
$responseHikaz = $response->findSegments(HIKAZ::class);
@@ -218,4 +241,26 @@ private function parseMt940()
218241
throw new \InvalidArgumentException('Invalid MT940 data', 0, $e);
219242
}
220243
}
244+
245+
private function parseXml()
246+
{
247+
if ($this->xmlAction === null) {
248+
throw new \RuntimeException('XML action not initialized');
249+
}
250+
251+
$xmlStrings = $this->xmlAction->getBookedXML();
252+
if (empty($xmlStrings)) {
253+
// No transactions available
254+
$this->statement = new StatementOfAccount();
255+
return;
256+
}
257+
258+
try {
259+
$parser = new CAMT();
260+
$parsedCAMT = $parser->parse($xmlStrings);
261+
$this->statement = StatementOfAccount::fromCAMTArray($parsedCAMT);
262+
} catch (\Exception $e) {
263+
throw new \InvalidArgumentException('Invalid CAMT XML data', 0, $e);
264+
}
265+
}
221266
}

0 commit comments

Comments
 (0)