Skip to content
Merged
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
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ try {
| `getPackList(?DateTimeInterface $date = null): ?array` | `GET GetPackList` | список идентификаторов контейнеров, готовых к получению |
| `getPackListResponse(DateTimeInterface\|string\|null $since = null)` | `GET GetPackList` | список контейнеров и отметка времени последнего из них |
| `getPack(string $id): Packet` | `GET GetPack` | транспортный контейнер |
| `getSettings(string $inn, string $bic, ?string $account = null): Settings` | `POST GetSettings` | настройки обмена с банком |

Отметка времени для списка контейнеров задаётся по часам сервера банка и передаётся
в формате `dd.MM.yyyy HH:mm:ss`. Чтобы получать только новые контейнеры, передавайте
Expand All @@ -116,6 +117,22 @@ foreach ($list->getPacketID() as $id) {
$lastTimestamp = $list->getTimeStampLastPacket() ?? $lastTimestamp; // сохранить до следующего запроса
```

### Настройки обмена

Настройки обмена с банком можно получить автоматически. Если идентификатор клиента в банке ещё
неизвестен, в `customerId` передаётся `'0'`; из настроек его можно взять для дальнейшей работы:

```php
$settings = (new Client(['customerId' => '0'] + $credentials))
->getSettings(inn: '7705260699', bic: '044525888', account: '40702810813123123222');

$data = $settings->getData();
$data->getCustomerID(); // идентификатор клиента в банке
$data->getBankServerAddress(); // адрес ресурса банка
$data->getDocKinds(); // виды документов, которыми возможен обмен: ['03', '05', '10', ...]
$data->getLogon()->getLogin()?->getUser();
```

### Ошибки

- `TTBooking\DirectBank\Exceptions\ClientException` — банк вернул ошибку (`ResultBank/Error`),
Expand Down
30 changes: 30 additions & 0 deletions src/Client.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
use TTBooking\DirectBank\Objects\GetPacketListResponseType;
use TTBooking\DirectBank\Objects\Packet;
use TTBooking\DirectBank\Objects\ResultBank;
use TTBooking\DirectBank\Objects\Settings;

class Client implements ClientInterface
{
Expand Down Expand Up @@ -125,6 +126,35 @@ protected function requestPackList(?\DateTimeInterface $since): ?GetPacketListRe
return $result->getSuccess()->getGetPacketListResponse();
}

/**
* Настройки обмена с банком в автоматическом режиме (GetSettings).
* Если идентификатор клиента в банке ещё неизвестен, в настройке customerId передаётся '0'.
*
* @param string $inn ИНН организации, для которой запрашиваются настройки
* @param string $bic БИК банка
* @param string|null $account номер расчётного счёта; для зарплатных проектов может отсутствовать
*/
public function getSettings(string $inn, string $bic, ?string $account = null): Settings
{
$headers = array_filter([
'inn' => $inn,
'bic' => $bic,
'account' => $account,
'availableapiversion' => $this->settings['availableApiVersion'],
], fn($value) => $value !== null);

$result = $this->invoke('POST', 'GetSettings', headers: $headers);

$response = $result->getSuccess()->getGetSettingsResponse()
?? throw new UnexpectedResponseException('Bank response to GetSettings has no GetSettingsResponse.');

try {
return $response->getSettings();
} catch (\Throwable $e) {
throw new UnexpectedResponseException('Unable to parse settings from GetSettings response: ' . $e->getMessage(), 0, $e);
}
}

public function getPack(string $uid): Packet
{
$result = $this->invoke('GET', 'GetPack', query: ['id' => $uid]);
Expand Down
3 changes: 3 additions & 0 deletions src/ClientInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

use TTBooking\DirectBank\Objects\GetPacketListResponseType;
use TTBooking\DirectBank\Objects\Packet;
use TTBooking\DirectBank\Objects\Settings;

interface ClientInterface
{
Expand All @@ -19,4 +20,6 @@ public function getPackList(?\DateTimeInterface $dateTime = null): ?array;
public function getPackListResponse(\DateTimeInterface|string|null $since = null): GetPacketListResponseType;

public function getPack(string $uid) : Packet;

public function getSettings(string $inn, string $bic, ?string $account = null): Settings;
}
31 changes: 31 additions & 0 deletions src/Objects/GetSettingsDataType.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);

namespace TTBooking\DirectBank\Objects;


/**
* Настройки обмена в ответе на GetSettings (GetSettingsResponse/Data): base64 с видом ЭД
*/
class GetSettingsDataType
{
/**
* @xmlNodeValue
*/
protected string $value;

/**
* @xmlAttribute
*/
protected string $dockind;

public function getValue(): string
{
return $this->value;
}

public function getDockind(): string
{
return $this->dockind;
}
}
77 changes: 77 additions & 0 deletions src/Objects/GetSettingsResponseType.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
<?php
declare(strict_types=1);

namespace TTBooking\DirectBank\Objects;


/**
* Ответ на запрос настроек обмена (GetSettings)
*/
class GetSettingsResponseType
{
/**
* @xmlAttribute
*/
protected string $id;

/**
* @xmlAttribute
*/
protected string $formatVersion;

/**
* @xmlAttribute
*/
protected string $creationDate;

/**
* @xmlAttribute
*/
protected ?string $userAgent = null;

protected GetSettingsDataType $Data;

public function getId(): string
{
return $this->id;
}

public function getFormatVersion(): string
{
return $this->formatVersion;
}

public function getCreationDate(): string
{
return $this->creationDate;
}

public function getUserAgent(): ?string
{
return $this->userAgent;
}

/**
* Настройки обмена в base64
*/
public function getData(): string
{
return $this->Data->getValue();
}

public function getDockind(): string
{
return $this->Data->getDockind();
}

/**
* Разобранные настройки обмена
*/
public function getSettings(): Settings
{
$settings = new Settings();
$settings->mapFromXml(base64_decode($this->getData(), true) ?: throw new \UnexpectedValueException('Settings data is not valid base64.'));

return $settings;
}
}
7 changes: 7 additions & 0 deletions src/Objects/SuccessResultType.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ class SuccessResultType

protected ?Packet $GetPacketResponse = null;

protected ?GetSettingsResponseType $GetSettingsResponse = null;

/**
* @return \TTBooking\DirectBank\Objects\LogonResponseType|null
*/
Expand All @@ -36,4 +38,9 @@ public function getGetPacketResponse(): ?Packet
{
return $this->GetPacketResponse;
}

public function getGetSettingsResponse(): ?GetSettingsResponseType
{
return $this->GetSettingsResponse;
}
}
75 changes: 75 additions & 0 deletions tests/ClientHttpTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use GuzzleHttp\Psr7\Response;
use PHPUnit\Framework\TestCase;
use Psr\Http\Message\RequestInterface;
use TTBooking\DirectBank\Dictionary\ErrorCode;
use TTBooking\DirectBank\Exceptions\ClientException;
use TTBooking\DirectBank\Exceptions\OtpRequiredException;
use TTBooking\DirectBank\Exceptions\UnexpectedResponseException;
Expand Down Expand Up @@ -455,6 +456,80 @@ public function testCreateSessionStoresSessionId()
$this->assertSame('SID-1', $this->requests[1]->getHeaderLine('SID'));
}

public function testGetSettings()
{
$this->mock->append(
self::logon(),
new Response(200, [], file_get_contents(__DIR__ . '/Fixture/xml/1c/GetSettingsResponse.xml')),
);

$settings = $this->createClient(['customerId' => '0'])->getSettings('761700021132', '044525888', '40802810200000099888');

$this->assertSame('EFD857B5-7FA8-4195-8666-2CCADBC3C8DE', $settings->getId());
$this->assertSame('2806', $settings->getData()->getCustomerID());
$this->assertSame('https://dbogate.demobank.ru/', $settings->getData()->getBankServerAddress());
$this->assertSame('user_login', $settings->getData()->getLogon()->getLogin()->getUser());
$this->assertSame(['03', '05', '10', '11', '14', '30'], $settings->getData()->getDocKinds());

[$logon, $request] = $this->requests;

$this->assertStringEndsWith('/Logon', $logon->getUri()->getPath());
$this->assertSame('POST', $request->getMethod());
$this->assertStringEndsWith('/GetSettings', $request->getUri()->getPath());
$this->assertSame('0', $request->getHeaderLine('CustomerID'));
$this->assertSame('SID-1', $request->getHeaderLine('SID'));
$this->assertSame('761700021132', $request->getHeaderLine('Inn'));
$this->assertSame('044525888', $request->getHeaderLine('Bic'));
$this->assertSame('40802810200000099888', $request->getHeaderLine('Account'));
$this->assertSame('2.2.2', $request->getHeaderLine('APIVersion'));
$this->assertSame('2.2.2', $request->getHeaderLine('AvailableAPIVersion'));
$this->assertFalse($request->hasHeader('Authorization'));
$this->assertSame('', (string) $request->getBody());
}

public function testGetSettingsWithoutAccount()
{
$this->mock->append(new Response(200, [], file_get_contents(__DIR__ . '/Fixture/xml/1c/GetSettingsResponse.xml')));

$this->createClient(['sessionId' => 'SID-0', 'availableApiVersion' => null])->getSettings('7705260699', '044525888');

$this->assertFalse($this->requests[0]->hasHeader('Account'));
$this->assertFalse($this->requests[0]->hasHeader('AvailableAPIVersion'));
}

public function testGetSettingsError()
{
$this->mock->append(self::error('1012', 'Не удалось получить настройки обмена с банком'));

try {
$this->createClient(['sessionId' => 'SID-0'])->getSettings('7705260699', '044525888');
$this->fail('ClientException expected');
} catch (ClientException $e) {
$this->assertSame(ErrorCode::SETTINGS_UNAVAILABLE, $e->getBankCode());
}
}

public function invalidSettingsResponseProvider(): array
{
return [
'no GetSettingsResponse' => [self::success('<SendPacketResponse><ID>PACK-1</ID></SendPacketResponse>')],
'not base64' => [self::success('<GetSettingsResponse id="1" formatVersion="2.2.2" creationDate="2026-09-23T10:00:00"><Data dockind="06">!!!</Data></GetSettingsResponse>')],
'not settings' => [self::success('<GetSettingsResponse id="1" formatVersion="2.2.2" creationDate="2026-09-23T10:00:00"><Data dockind="06">' . base64_encode('not xml') . '</Data></GetSettingsResponse>')],
];
}

/**
* @dataProvider invalidSettingsResponseProvider
*/
public function testGetSettingsUnexpectedResponse(Response $response)
{
$this->mock->append($response);

$this->expectException(UnexpectedResponseException::class);

$this->createClient(['sessionId' => 'SID-0'])->getSettings('7705260699', '044525888');
}

public function testBankErrorWithHttpErrorStatus()
{
$this->mock->append(self::error('1009', 'Ошибка приемного сервиса', 500));
Expand Down
26 changes: 26 additions & 0 deletions tests/Fixture/xml/1c/GetSettingsResponse.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<ResultBank xmlns ="http://directbank.1c.ru/XMLSchema" formatVersion="2.2.1">
<Success>
<GetSettingsResponse creationDate="2015-02-19T11:21:02" formatVersion="2.2.1" id="502036">
<Data dockind="06">PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPFNldHRpbmdzIHhtbG5zPSJo
dHRwOi8vZGlyZWN0YmFuay4xYy5ydS9YTUxTY2hlbWEiCiAgICB4bWxuczp4c2k9Imh0dHA6Ly93
d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hLWluc3RhbmNlIgogICAgeG1sbnM6eHNkPSJodHRwOi8v
d3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIKICAgIGlkPSJFRkQ4NTdCNS03RkE4LTQxOTUtODY2
Ni0yQ0NBREJDM0M4REUiCiAgICBmb3JtYXRWZXJzaW9uPSIyLjIuMSIKICAgIGNyZWF0aW9uRGF0
ZT0iMjAxNi0wNC0yMlQwOTozODo1MSIgCiAgICB1c2VyQWdlbnQ9IkRlbW9CYW5rU2VydmljZSI+
Cgk8U2VuZGVyIGJpYz0iMDQ0NTI1ODg4IiBuYW1lPSLQlNCV0JzQni3QkdCQ0J3QmiIgLz4KICAg
IDxSZWNpcGllbnQgaWQ9IjI4MDYiIG5hbWU9ItCi0L7RgNCz0L7QstGL0Lkg0LTQvtC8INCa0L7Q
vNC/0LvQtdC60YHQvdGL0LkiIGlubj0iNzcwNTI2MDY5OSIga3BwPSI3NzA1MDEwMDEiIC8+CiAg
ICA8RGF0YT4KICAgICAgICA8Q3VzdG9tZXJJRD4yODA2PC9DdXN0b21lcklEPgogICAgICAgIDxC
YW5rU2VydmVyQWRkcmVzcz5odHRwczovL2Rib2dhdGUuZGVtb2JhbmsucnUvPC9CYW5rU2VydmVy
QWRkcmVzcz4KICAgICAgICA8Rm9ybWF0VmVyc2lvbj4yLjIuMTwvRm9ybWF0VmVyc2lvbj4KICAg
ICAgICA8RW5jb2Rpbmc+VVRGLTg8L0VuY29kaW5nPgogICAgICAgIDxMb2dvbj4KICAgICAgICAg
ICAgPExvZ2luPgogICAgICAgICAgICAgICAgPFVzZXI+dXNlcl9sb2dpbjwvVXNlcj4KICAgICAg
ICAgICAgPC9Mb2dpbj4KICAgICAgICA8L0xvZ29uPgogICAgICAgIDxEb2N1bWVudCBkb2NLaW5k
PSIwMyIgLz4KICAgICAgICA8RG9jdW1lbnQgZG9jS2luZD0iMDUiIC8+CiAgICAgICAgPERvY3Vt
ZW50IGRvY0tpbmQ9IjEwIiAvPgogICAgICAgIDxEb2N1bWVudCBkb2NLaW5kPSIxMSIgLz4JCQog
ICAgICAgIDxEb2N1bWVudCBkb2NLaW5kPSIxNCIgLz4KCQk8RG9jdW1lbnQgZG9jS2luZD0iMzAi
IC8+CiAgICA8L0RhdGE+CjwvU2V0dGluZ3M+</Data>
</GetSettingsResponse>
</Success>
</ResultBank>
1 change: 1 addition & 0 deletions tests/Fixture/xml/1c/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,6 @@
- Электронные документы — <https://github.com/1C-Company/DirectBank/tree/2.2.2/doc/application-layer>:
`CancelationRequest.xml`, `PayDocRu.xml`, `PayRequest.xml`, `Probe.xml`, `Settings.xml`, `Settings_Logon_Certificate.xml`,
`Statement.xml`, `StatementRequest.xml`, `StatusDocNotice.xml`, `StatusRequest.xml`
- `GetSettingsResponse.xml` — пример ответа на GetSettings из <https://github.com/1C-Company/DirectBank/blob/2.2.2/doc/transport-api/readme.md>
- `Packet.xml` — пример запроса SendPack из <https://github.com/1C-Company/DirectBank/blob/2.2.2/doc/transport-api/readme.md>.
Подпись в примере обрезана и не является корректным base64, поэтому XSD файл не проходит.
Loading