Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 37 additions & 2 deletions .docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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**
Expand All @@ -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
Expand Down Expand Up @@ -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
<?php

$accountService = $fioManager->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).
201 changes: 201 additions & 0 deletions src/Entity/Statement/AccountStatement.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
<?php declare(strict_types = 1);

namespace Contributte\Fio\Entity\Statement;

use ArrayIterator;
use Contributte\Fio\Exceptions\InvalidResponseException;
use Contributte\Fio\Utils\XmlValue;
use DateTimeImmutable;
use IteratorAggregate;
use SimpleXMLElement;
use Throwable;

/**
* AccountStatement (info + list of movements)
*
* @implements IteratorAggregate<int, Movement>
*/
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<int, Movement>
*/
public function getIterator(): ArrayIterator
{
return new ArrayIterator($this->movements);
}

}
Loading