diff --git a/.docs/README.md b/.docs/README.md index c054fd7..da3d1c4 100644 --- a/.docs/README.md +++ b/.docs/README.md @@ -8,7 +8,7 @@ ## Requirements -You need your bank account and token generated in your e-banking. There is no special token for testing. So you will have to use the same token for both testing and production. It is recommended to generate two tokens per account. One for sending payments to bank and the other one for downloading payments from bank (We only included sending payments feature yet.). +You need your bank account and token generated in your e-banking. There is no special token for testing. So you will have to use the same token for both testing and production. It is recommended to generate two tokens per account. One for sending payments to bank and the other one for downloading payments from bank. * **accountNumber** * **accountToken** @@ -31,7 +31,7 @@ fio: ## Usage -As this package is under development we haven't included downloading payments from bank yet. You can use our package for simply sending domestic payments to bank. You can send multiple payments at once but you can query bank api only once in 30 seconds (if you ping servers in shorter intervals it will return error). This package only supports domestic transactions but it is very easy to implement other types of payments. You only need to extend `Contributte\Fio\Entity\Transaction\Transaction` class and use it same way as Domestic Transaction class which has these mandatory properties: +You can use our package for sending domestic payments to bank and for downloading account movements (statements). You can send multiple payments at once but you can query bank api only once in 30 seconds (if you ping servers in shorter intervals it will return error). This package only supports domestic transactions but it is very easy to implement other types of payments. You only need to extend `Contributte\Fio\Entity\Transaction\Transaction` class and use it same way as Domestic Transaction class which has these mandatory properties: * Sender account (automatically supplier from config) * Currency @@ -109,3 +109,38 @@ final class SendPaymentsControl extends BaseControl } ``` + +### Downloading movements + +From `FioManager` you get `AccountService` which downloads account movements in Fio XML format and parses them into `AccountStatement` (account info + list of `Movement` objects): + +* `movementsForPeriod(DateTimeInterface $from, DateTimeInterface $to)` - movements for given period +* `movementsFromLastRequest()` - movements since the last download (bank moves the cursor automatically) +* `movementsForStatement(int $year, int $id)` - official statement by year and number +* `setLastId(int $id)` - sets the cursor to the ID of the last successfully downloaded movement +* `setLastDate(DateTimeInterface $date)` - sets the cursor to a date + +```php +createAccountService('czk-read'); + +$statement = $accountService->movementsForPeriod( + new DateTimeImmutable('2023-08-01'), + new DateTimeImmutable('2023-08-31') +); + +echo $statement->getOpeningBalance(); +echo $statement->getClosingBalance(); + +foreach ($statement as $movement) { + echo $movement->getId(); + echo $movement->getDate()->format('Y-m-d'); + echo $movement->getAmount(); + echo $movement->getCurrency(); + echo $movement->getType(); + echo $movement->getVs(); +} +``` + +Data older than 90 days require temporarily unlocking full history in your e-banking (Settings → API). diff --git a/src/Entity/Statement/AccountStatement.php b/src/Entity/Statement/AccountStatement.php new file mode 100644 index 0000000..66aef2d --- /dev/null +++ b/src/Entity/Statement/AccountStatement.php @@ -0,0 +1,201 @@ + + */ +class AccountStatement implements IteratorAggregate +{ + + private string $accountId; + + private string $currency; + + private string $iban; + + private string $bic; + + private float $openingBalance; + + private float $closingBalance; + + private ?string $bankId = null; + + private ?DateTimeImmutable $dateStart = null; + + private ?DateTimeImmutable $dateEnd = null; + + private ?int $yearList = null; + + private ?int $idList = null; + + private ?int $idFrom = null; + + private ?int $idTo = null; + + private ?int $idLastDownload = null; + + /** @var Movement[] */ + private array $movements = []; + + public function __construct(string $accountId, string $currency, string $iban, string $bic, float $openingBalance, float $closingBalance) + { + $this->accountId = $accountId; + $this->currency = $currency; + $this->iban = $iban; + $this->bic = $bic; + $this->openingBalance = $openingBalance; + $this->closingBalance = $closingBalance; + } + + /** + * @throws InvalidResponseException when XML has unexpected structure + */ + public static function fromXml(string $xml): self + { + // Catch errors differently + $prev = libxml_use_internal_errors(true); + + try { + $sxe = new SimpleXMLElement($xml); + } catch (Throwable $e) { + throw new InvalidResponseException($e->getMessage(), (int) $e->getCode(), $e, $xml); + } finally { + libxml_clear_errors(); + libxml_use_internal_errors($prev); + } + + if (!isset($sxe->Info)) { + throw new InvalidResponseException('Unexpected XML structure.', 0, null, $xml); + } + + $info = $sxe->Info; + + $accountId = XmlValue::toStringOrNull($info, 'accountId'); + $currency = XmlValue::toStringOrNull($info, 'currency'); + $iban = XmlValue::toStringOrNull($info, 'iban'); + $bic = XmlValue::toStringOrNull($info, 'bic'); + $openingBalance = XmlValue::toFloatOrNull($info, 'openingBalance'); + $closingBalance = XmlValue::toFloatOrNull($info, 'closingBalance'); + + if ($accountId === null || $currency === null || $iban === null || $bic === null || $openingBalance === null || $closingBalance === null) { + throw new InvalidResponseException('Unexpected XML structure.', 0, null, $xml); + } + + $statement = new self($accountId, $currency, $iban, $bic, $openingBalance, $closingBalance); + $statement->bankId = XmlValue::toStringOrNull($info, 'bankId'); + $statement->dateStart = XmlValue::toDateOrNull($info, 'dateStart'); + $statement->dateEnd = XmlValue::toDateOrNull($info, 'dateEnd'); + $statement->yearList = XmlValue::toIntOrNull($info, 'yearList'); + $statement->idList = XmlValue::toIntOrNull($info, 'idList'); + $statement->idFrom = XmlValue::toIntOrNull($info, 'idFrom'); + $statement->idTo = XmlValue::toIntOrNull($info, 'idTo'); + $statement->idLastDownload = XmlValue::toIntOrNull($info, 'idLastDownload'); + + if (isset($sxe->TransactionList->Transaction)) { + foreach ($sxe->TransactionList->Transaction as $transaction) { + $statement->movements[] = Movement::fromXml($transaction); + } + } + + return $statement; + } + + public function getAccountId(): string + { + return $this->accountId; + } + + public function getCurrency(): string + { + return $this->currency; + } + + public function getIban(): string + { + return $this->iban; + } + + public function getBic(): string + { + return $this->bic; + } + + public function getOpeningBalance(): float + { + return $this->openingBalance; + } + + public function getClosingBalance(): float + { + return $this->closingBalance; + } + + public function getBankId(): ?string + { + return $this->bankId; + } + + public function getDateStart(): ?DateTimeImmutable + { + return $this->dateStart; + } + + public function getDateEnd(): ?DateTimeImmutable + { + return $this->dateEnd; + } + + public function getYearList(): ?int + { + return $this->yearList; + } + + public function getIdList(): ?int + { + return $this->idList; + } + + public function getIdFrom(): ?int + { + return $this->idFrom; + } + + public function getIdTo(): ?int + { + return $this->idTo; + } + + public function getIdLastDownload(): ?int + { + return $this->idLastDownload; + } + + /** + * @return Movement[] + */ + public function getMovements(): array + { + return $this->movements; + } + + /** + * @return ArrayIterator + */ + public function getIterator(): ArrayIterator + { + return new ArrayIterator($this->movements); + } + +} diff --git a/src/Entity/Statement/Movement.php b/src/Entity/Statement/Movement.php new file mode 100644 index 0000000..a733ec5 --- /dev/null +++ b/src/Entity/Statement/Movement.php @@ -0,0 +1,200 @@ +id = $id; + $this->date = $date; + $this->amount = $amount; + $this->currency = $currency; + $this->type = $type; + } + + /** + * @throws InvalidResponseException when mandatory columns are missing + */ + public static function fromXml(SimpleXMLElement $transaction): self + { + $id = XmlValue::toIntOrNull($transaction, 'column_22'); + $date = XmlValue::toDateOrNull($transaction, 'column_0'); + $amount = XmlValue::toFloatOrNull($transaction, 'column_1'); + $currency = XmlValue::toStringOrNull($transaction, 'column_14'); + $type = XmlValue::toStringOrNull($transaction, 'column_8'); + + if ($id === null || $date === null || $amount === null || $currency === null || $type === null) { + throw new InvalidResponseException('Movement is missing mandatory columns.'); + } + + $movement = new self($id, $date, $amount, $currency, $type); + $movement->counterAccount = XmlValue::toStringOrNull($transaction, 'column_2'); + $movement->counterAccountName = XmlValue::toStringOrNull($transaction, 'column_10'); + $movement->bankCode = XmlValue::toStringOrNull($transaction, 'column_3'); + $movement->bankName = XmlValue::toStringOrNull($transaction, 'column_12'); + $movement->ks = XmlValue::toStringOrNull($transaction, 'column_4'); + $movement->vs = XmlValue::toStringOrNull($transaction, 'column_5'); + $movement->ss = XmlValue::toStringOrNull($transaction, 'column_6'); + $movement->userIdentification = XmlValue::toStringOrNull($transaction, 'column_7'); + $movement->messageForRecipient = XmlValue::toStringOrNull($transaction, 'column_16'); + $movement->performedBy = XmlValue::toStringOrNull($transaction, 'column_9'); + $movement->specification = XmlValue::toStringOrNull($transaction, 'column_18'); + $movement->comment = XmlValue::toStringOrNull($transaction, 'column_25'); + $movement->bic = XmlValue::toStringOrNull($transaction, 'column_26'); + $movement->instructionId = XmlValue::toIntOrNull($transaction, 'column_17'); + $movement->payerReference = XmlValue::toStringOrNull($transaction, 'column_27'); + + return $movement; + } + + public function getId(): int + { + return $this->id; + } + + public function getDate(): DateTimeImmutable + { + return $this->date; + } + + public function getAmount(): float + { + return $this->amount; + } + + public function getCurrency(): string + { + return $this->currency; + } + + public function getType(): string + { + return $this->type; + } + + public function getCounterAccount(): ?string + { + return $this->counterAccount; + } + + public function getCounterAccountName(): ?string + { + return $this->counterAccountName; + } + + public function getBankCode(): ?string + { + return $this->bankCode; + } + + public function getBankName(): ?string + { + return $this->bankName; + } + + public function getKs(): ?string + { + return $this->ks; + } + + public function getVs(): ?string + { + return $this->vs; + } + + public function getSs(): ?string + { + return $this->ss; + } + + public function getUserIdentification(): ?string + { + return $this->userIdentification; + } + + public function getMessageForRecipient(): ?string + { + return $this->messageForRecipient; + } + + public function getPerformedBy(): ?string + { + return $this->performedBy; + } + + public function getSpecification(): ?string + { + return $this->specification; + } + + public function getComment(): ?string + { + return $this->comment; + } + + public function getBic(): ?string + { + return $this->bic; + } + + public function getInstructionId(): ?int + { + return $this->instructionId; + } + + public function getPayerReference(): ?string + { + return $this->payerReference; + } + +} diff --git a/src/Exceptions/HttpStatusException.php b/src/Exceptions/HttpStatusException.php new file mode 100644 index 0000000..af6e802 --- /dev/null +++ b/src/Exceptions/HttpStatusException.php @@ -0,0 +1,46 @@ + 'Check URL parameters of the request.', + 409 => 'Minimum interval of 30 seconds between requests with the same token was not respected.', + 413 => 'Too many movements requested (limit is 50 000). Narrow the date range or move the cursor to a newer movement.', + 422 => 'Data older than 90 days requested without unlocking full history in the internetbanking (Settings - API).', + 500 => 'Token does not exist or is inactive. Check the token in the internetbanking.', + ]; + + protected ?string $result = null; + + public static function fromStatusCode(int $statusCode, ?string $result = null): self + { + $message = sprintf('Server returned HTTP status %d.', $statusCode); + + if (isset(self::STATUS_HINTS[$statusCode])) { + $message .= ' ' . self::STATUS_HINTS[$statusCode]; + } + + $exception = new self($message, $statusCode); + $exception->result = $result; + + return $exception; + } + + public function getStatusCode(): int + { + return $this->getCode(); + } + + public function getResult(): ?string + { + return $this->result; + } + +} diff --git a/src/FioManager.php b/src/FioManager.php index 06fe4a0..05ecd22 100644 --- a/src/FioManager.php +++ b/src/FioManager.php @@ -3,6 +3,7 @@ namespace Contributte\Fio; use Contributte\Fio\Http\IHttpClient; +use Contributte\Fio\Services\AccountService; use Contributte\Fio\Services\PaymentService; /** @@ -29,4 +30,12 @@ public function createPaymentService(string $accountName): PaymentService return new PaymentService($account, $this->httpClient); } + public function createAccountService(string $accountName): AccountService + { + // We get account by name + $account = $this->config->getAccountByName($accountName); + + return new AccountService($account, $this->httpClient); + } + } diff --git a/src/Http/HttpClient.php b/src/Http/HttpClient.php index ae04394..66eccef 100644 --- a/src/Http/HttpClient.php +++ b/src/Http/HttpClient.php @@ -2,6 +2,7 @@ namespace Contributte\Fio\Http; +use Contributte\Fio\Exceptions\HttpStatusException; use Contributte\Fio\Exceptions\IOException; use CURLFile; @@ -66,6 +67,9 @@ public function sendRequest(Request $request): string throw new IOException(curl_strerror(curl_errno($ch))); } + // HTTP status + $statusCode = curl_getinfo($ch, CURLINFO_RESPONSE_CODE); + // Close curl_close($ch); @@ -74,6 +78,11 @@ public function sendRequest(Request $request): string fclose($xmlFile); } + // Error statuses carry no parseable payload, throw with hint from API docs + if ($statusCode >= 400) { + throw HttpStatusException::fromStatusCode($statusCode, is_string($result) ? $result : null); + } + return $result; } diff --git a/src/Services/AccountService.php b/src/Services/AccountService.php index d799f89..573af92 100644 --- a/src/Services/AccountService.php +++ b/src/Services/AccountService.php @@ -2,7 +2,11 @@ namespace Contributte\Fio\Services; +use Contributte\Fio\Entity\Statement\AccountStatement; +use Contributte\Fio\Exceptions\InvalidResponseException; +use Contributte\Fio\Exceptions\IOException; use Contributte\Fio\Http\Request; +use DateTimeInterface; /** * AccountService @@ -10,21 +14,98 @@ class AccountService extends Service { - public function movementsForPeriod(string $from, string $to): void + private const URL = 'https://fioapi.fio.cz/v1/rest/'; + + /** + * Downloads movements for given period + * + * @throws InvalidResponseException|IOException + */ + public function movementsForPeriod(DateTimeInterface $from, DateTimeInterface $to): AccountStatement + { + $url = sprintf( + '%speriods/%s/%s/%s/transactions.xml', + self::URL, + $this->account->getToken(), + $from->format('Y-m-d'), + $to->format('Y-m-d') + ); + + return AccountStatement::fromXml($this->createRequest(new Request($url, $this->account->getToken()))); + } + + /** + * Downloads movements since the last download (moves the server-side cursor) + * + * @throws InvalidResponseException|IOException + */ + public function movementsFromLastRequest(): AccountStatement { - // Tady se vytvori request a $this->sendRequest() + $url = sprintf( + '%slast/%s/transactions.xml', + self::URL, + $this->account->getToken() + ); + + return AccountStatement::fromXml($this->createRequest(new Request($url, $this->account->getToken()))); } - public function movementsFromLastRequest(): void + /** + * Downloads official statement by year and number + * + * @throws InvalidResponseException|IOException + */ + public function movementsForStatement(int $year, int $id): AccountStatement { - // Tady se vytvori request a $this->sendRequest() - // a dalsi tyto metody + $url = sprintf( + '%sby-id/%s/%d/%d/transactions.xml', + self::URL, + $this->account->getToken(), + $year, + $id + ); + + return AccountStatement::fromXml($this->createRequest(new Request($url, $this->account->getToken()))); + } + + /** + * Sets the cursor to the ID of the last successfully downloaded movement + * + * @throws IOException + */ + public function setLastId(int $id): void + { + $url = sprintf( + '%sset-last-id/%s/%d/', + self::URL, + $this->account->getToken(), + $id + ); + + $this->createRequest(new Request($url, $this->account->getToken())); + } + + /** + * Sets the cursor to the date of the last unsuccessfully downloaded day + * + * @throws IOException + */ + public function setLastDate(DateTimeInterface $date): void + { + $url = sprintf( + '%sset-last-date/%s/%s/', + self::URL, + $this->account->getToken(), + $date->format('Y-m-d') + ); + + $this->createRequest(new Request($url, $this->account->getToken())); } protected function createRequest(Request $request): string { - // TODO: Implement sendRequest() method. - return ''; + // Ask HttpClient to execute request + return $this->httpClient->sendRequest($request); } } diff --git a/src/Utils/XmlValue.php b/src/Utils/XmlValue.php new file mode 100644 index 0000000..1d0a5c6 --- /dev/null +++ b/src/Utils/XmlValue.php @@ -0,0 +1,65 @@ +{$child})) { + return null; + } + + return (string) $parent->{$child}; + } + + public static function toFloatOrNull(SimpleXMLElement $parent, string $child): ?float + { + $value = self::toStringOrNull($parent, $child); + + if ($value === null || !is_numeric($value)) { + return null; + } + + return (float) $value; + } + + public static function toIntOrNull(SimpleXMLElement $parent, string $child): ?int + { + $value = self::toStringOrNull($parent, $child); + + if ($value === null || !is_numeric($value)) { + return null; + } + + return (int) $value; + } + + public static function toDateOrNull(SimpleXMLElement $parent, string $child): ?DateTimeImmutable + { + $value = self::toStringOrNull($parent, $child); + + if ($value === null) { + return null; + } + + // Date with timezone offset, e.g. 2012-07-27+02:00 or 2012-07-27+0200 + foreach (['!Y-m-dP', '!Y-m-dO', '!Y-m-d'] as $format) { + $date = DateTimeImmutable::createFromFormat($format, $value); + + if ($date !== false) { + return $date; + } + } + + return null; + } + +} diff --git a/tests/Toolkit/SpyHttpClient.php b/tests/Toolkit/SpyHttpClient.php new file mode 100644 index 0000000..28da87d --- /dev/null +++ b/tests/Toolkit/SpyHttpClient.php @@ -0,0 +1,27 @@ +response = $response; + } + + public function sendRequest(Request $request): string + { + $this->requestedUrl = $request->getUrl(); + + return $this->response; + } + +} diff --git a/tests/cases/Entity/Statement/AccountStatementTest.phpt b/tests/cases/Entity/Statement/AccountStatementTest.phpt new file mode 100644 index 0000000..45c188a --- /dev/null +++ b/tests/cases/Entity/Statement/AccountStatementTest.phpt @@ -0,0 +1,81 @@ +getAccountId()); + Assert::same('2010', $statement->getBankId()); + Assert::same('CZK', $statement->getCurrency()); + Assert::same('CZ7920100000002400222222', $statement->getIban()); + Assert::same('FIOBCZPPXXX', $statement->getBic()); + Assert::same(195.00, $statement->getOpeningBalance()); + Assert::same(195.01, $statement->getClosingBalance()); + Assert::same(1148734530, $statement->getIdFrom()); + Assert::same(1149190193, $statement->getIdTo()); + Assert::same(1149190192, $statement->getIdLastDownload()); + Assert::null($statement->getYearList()); + Assert::null($statement->getIdList()); + Assert::same('2012-06-26', $statement->getDateStart()?->format('Y-m-d')); + Assert::same('2012-06-30', $statement->getDateEnd()?->format('Y-m-d')); + + $movements = $statement->getMovements(); + Assert::count(2, $movements); + + $first = $movements[0]; + Assert::same(1148734530, $first->getId()); + Assert::same('2012-06-26', $first->getDate()->format('Y-m-d')); + Assert::same(1.00, $first->getAmount()); + Assert::same('CZK', $first->getCurrency()); + Assert::same('Příjem převodem uvnitř banky', $first->getType()); + Assert::same('2900233333', $first->getCounterAccount()); + Assert::same('Pavel, Novák', $first->getCounterAccountName()); + Assert::same('2010', $first->getBankCode()); + Assert::same('Fio banka, a.s.', $first->getBankName()); + Assert::same('0558', $first->getKs()); + Assert::same('1234567890', $first->getVs()); + Assert::null($first->getSs()); + Assert::same(2105685816, $first->getInstructionId()); + + $second = $movements[1]; + Assert::same(1149190193, $second->getId()); + Assert::same(0.01, $second->getAmount()); + Assert::same('Připsaný úrok', $second->getType()); + Assert::null($second->getCounterAccount()); + Assert::null($second->getBankCode()); + + // Statement is iterable over movements + Assert::same($movements, iterator_to_array($statement)); +}); + +// Empty transaction list (only info header) +Toolkit::test(function (): void { + $xml = '24002222222010CZKCZ7920100000002400222222FIOBCZPPXXX1.01.0'; + $statement = AccountStatement::fromXml($xml); + + Assert::count(0, $statement->getMovements()); +}); + +// Invalid XML +Toolkit::test(function (): void { + Assert::throws(function (): void { + AccountStatement::fromXml('this is not xml'); + }, InvalidResponseException::class); +}); + +// Unexpected structure +Toolkit::test(function (): void { + Assert::throws(function (): void { + AccountStatement::fromXml('bar'); + }, InvalidResponseException::class, 'Unexpected XML structure.'); +}); diff --git a/tests/cases/Exceptions/HttpStatusExceptionTest.phpt b/tests/cases/Exceptions/HttpStatusExceptionTest.phpt new file mode 100644 index 0000000..3b82a15 --- /dev/null +++ b/tests/cases/Exceptions/HttpStatusExceptionTest.phpt @@ -0,0 +1,28 @@ +getStatusCode()); + Assert::same(409, $e->getCode()); + Assert::same('body', $e->getResult()); + Assert::same('Server returned HTTP status 409. Minimum interval of 30 seconds between requests with the same token was not respected.', $e->getMessage()); +}); + +// Unknown status without hint +Toolkit::test(function (): void { + $e = HttpStatusException::fromStatusCode(503); + + Assert::same(503, $e->getStatusCode()); + Assert::null($e->getResult()); + Assert::same('Server returned HTTP status 503.', $e->getMessage()); +}); diff --git a/tests/cases/Services/AccountServiceTest.phpt b/tests/cases/Services/AccountServiceTest.phpt new file mode 100644 index 0000000..8547b70 --- /dev/null +++ b/tests/cases/Services/AccountServiceTest.phpt @@ -0,0 +1,93 @@ +movementsForPeriod(new DateTimeImmutable('2012-06-26'), new DateTimeImmutable('2012-06-30')); + + Assert::same( + sprintf('https://fioapi.fio.cz/v1/rest/periods/%s/2012-06-26/2012-06-30/transactions.xml', TOKEN), + $http->requestedUrl + ); + Assert::count(2, $statement->getMovements()); +}); + +// Movements from last request +Toolkit::test(function (): void { + $xml = (string) file_get_contents(__DIR__ . '/../../fixtures/movements.xml'); + $http = new SpyHttpClient($xml); + + createService($http)->movementsFromLastRequest(); + + Assert::same( + sprintf('https://fioapi.fio.cz/v1/rest/last/%s/transactions.xml', TOKEN), + $http->requestedUrl + ); +}); + +// Movements for official statement +Toolkit::test(function (): void { + $xml = (string) file_get_contents(__DIR__ . '/../../fixtures/movements.xml'); + $http = new SpyHttpClient($xml); + + createService($http)->movementsForStatement(2012, 4); + + Assert::same( + sprintf('https://fioapi.fio.cz/v1/rest/by-id/%s/2012/4/transactions.xml', TOKEN), + $http->requestedUrl + ); +}); + +// Set cursor to last downloaded movement ID +Toolkit::test(function (): void { + $http = new SpyHttpClient(); + + createService($http)->setLastId(1147608196); + + Assert::same( + sprintf('https://fioapi.fio.cz/v1/rest/set-last-id/%s/1147608196/', TOKEN), + $http->requestedUrl + ); +}); + +// Set cursor to date +Toolkit::test(function (): void { + $http = new SpyHttpClient(); + + createService($http)->setLastDate(new DateTimeImmutable('2023-07-27')); + + Assert::same( + sprintf('https://fioapi.fio.cz/v1/rest/set-last-date/%s/2023-07-27/', TOKEN), + $http->requestedUrl + ); +}); + +// Invalid response +Toolkit::test(function (): void { + $http = new SpyHttpClient('not an xml'); + + Assert::throws(function () use ($http): void { + createService($http)->movementsFromLastRequest(); + }, InvalidResponseException::class); +}); diff --git a/tests/fixtures/movements.xml b/tests/fixtures/movements.xml new file mode 100644 index 0000000..5760455 --- /dev/null +++ b/tests/fixtures/movements.xml @@ -0,0 +1,41 @@ + + + + 2400222222 + 2010 + CZK + CZ7920100000002400222222 + FIOBCZPPXXX + 195.00 + 195.01 + 2012-06-26+02:00 + 2012-06-30+02:00 + 1148734530 + 1149190193 + 1149190192 + + + + 1148734530 + 2012-06-26+02:00 + 1.00 + CZK + 2900233333 + Pavel, Novák + 2010 + Fio banka, a.s. + 0558 + 1234567890 + Příjem převodem uvnitř banky + 2105685816 + + + 1149190193 + 2012-06-30+02:00 + 0.01 + CZK + Připsaný úrok + 2107642322 + + +