diff --git a/README.md b/README.md
index dab5436..cdb8eac 100644
--- a/README.md
+++ b/README.md
@@ -182,6 +182,67 @@ $packet = (new Packet())
$packetId = $client->sendPack($packet);
```
+### Платёжное поручение
+
+```php
+use Ramsey\Uuid\Uuid;
+use TTBooking\DirectBank\Dictionary\DocKind;
+use TTBooking\DirectBank\Objects\{
+ BankPartyType, BankType, CustomerDetailsType, CustomerPartyType, DocumentType,
+ Packet, ParticipantType, PayDocRu, PayDocRuApp
+};
+
+$customer = (new CustomerPartyType())->setId('2806')->setInn('7705260699');
+$bank = (new BankPartyType())->setBic('044525888');
+
+$payDocRu = (new PayDocRu())
+ ->setId((string) Uuid::uuid4())
+ ->setCreationDate((new DateTimeImmutable())->format(DATE_ATOM))
+ ->setSender($customer)
+ ->setRecipient($bank)
+ ->setData(
+ (new PayDocRuApp())
+ ->setDocNo('14')
+ ->setDocDate('2026-09-22')
+ ->setSum(1234.56)
+ ->setPayer(
+ (new CustomerDetailsType())
+ ->setName('ООО "Ромашка"')->setINN('7705260699')->setKPP('770501001')
+ ->setAccount('40702810813123123222')
+ ->setBank((new BankType())->setBic('044525888'))
+ )
+ ->setPayee(
+ (new CustomerDetailsType())
+ ->setName('ООО "Лютик"')->setINN('7704596181')
+ ->setAccount('40702810401200000035')
+ ->setBank((new BankType())->setBic('044525999'))
+ )
+ ->setTransitionKind('01')
+ ->setPriority('5')
+ ->setPurpose('Оплата по счёту № 15')
+ // ->setBudgetPaymentInfo(...) — реквизиты бюджетного платежа
+ );
+// ->setDigest(new DigestType($digest, $algorithmVersion)) — если банк требует дайджест
+
+$packet = (new Packet())
+ ->setId((string) Uuid::uuid4())
+ ->setCreationDate((new DateTimeImmutable())->format(DATE_ATOM))
+ ->setSender((new ParticipantType())->setCustomer($customer))
+ ->setRecipient((new ParticipantType())->setBank($bank))
+ ->setDocument(
+ (new DocumentType())
+ ->setId($payDocRu->getId())
+ ->setDockind(DocKind::PAY_DOC_RU)
+ ->setData(base64_encode((string) $payDocRu))
+ );
+
+$client->sendPack($packet);
+```
+
+Платёжное требование собирается так же: `PayRequest` с данными `PayRequestApp` и видом `DocKind::PAY_REQUEST`.
+Исходящие документы собираются в XML через `TTBooking\DirectBank\Mapper\XmlMapper`: поля базовых
+типов идут раньше полей наследников, как требует XSD.
+
### Получение ответов банка
```php
@@ -231,8 +292,12 @@ foreach ($client->getPackList() ?? [] as $id) {
| Документ | Класс | Вид ЭД |
|---|---|---|
| Запрос о состоянии электронного документа | `StatusRequest` (`setExtID()` — ИД документа) | `DocKind::STATUS_REQUEST` |
+| Запрос об отзыве электронного документа | `CancelationRequest` (`setExtID()`, `setReason()`) | `DocKind::CANCELATION_REQUEST` |
| Запрос-зонд | `Probe` | `DocKind::PROBE` |
+Все исходящие документы принимают необязательный дайджест: `setDigest(new DigestType($data, $algorithmVersion))`.
+Как его формировать, стандарт не описывает — это делает внешняя компонента банка.
+
Входящие: `StatusPacketNotice` (`01`), `StatusDocNotice` (`02`), `Settings` (`06`), `Statement` (`15`).
## Справочники
diff --git a/src/Mapper/ConvertibleTrait.php b/src/Mapper/ConvertibleTrait.php
new file mode 100644
index 0000000..d60d98f
--- /dev/null
+++ b/src/Mapper/ConvertibleTrait.php
@@ -0,0 +1,18 @@
+unmap($this);
+ }
+}
diff --git a/src/Mapper/XmlMapper.php b/src/Mapper/XmlMapper.php
new file mode 100644
index 0000000..588dacf
--- /dev/null
+++ b/src/Mapper/XmlMapper.php
@@ -0,0 +1,79 @@
+getName();
+ $propertyValue = $property->getPropertyValue();
+ if (Validation::isEmpty($propertyValue)) {
+ continue;
+ }
+
+ if ($property->getDocBlock()->hasAnnotation(AnnotationEnum::XML_ATTRIBUTE)) {
+ $attributeKey = self::ATTR_KEY;
+ $unmappedObject->{$attributeKey}[$propertyKey] = $propertyValue;
+ continue;
+ }
+
+ if ($property->getDocBlock()->hasAnnotation(AnnotationEnum::XML_NODE_VALUE)) {
+ $valueKey = self::VALUE_KEY;
+ $unmappedObject->$valueKey = $propertyValue;
+ continue;
+ }
+
+ $unmappedObject->$propertyKey = $this->unmapValueByType($property->getType(), $propertyValue);
+ }
+
+ return $unmappedObject;
+ }
+
+ /**
+ * Свойства от самого базового класса к наследнику, внутри класса — в порядке объявления
+ *
+ * @return ModelProperty[]
+ */
+ protected static function orderedProperties(ModelClass $modelClass): array
+ {
+ $properties = $modelClass->getProperties();
+
+ $depth = function (ModelProperty $property): int {
+ $depth = 0;
+ for ($class = $property->getProperty()->getDeclaringClass(); $class = $class->getParentClass(); ) {
+ $depth++;
+ }
+
+ return $depth;
+ };
+
+ // usort стабилен с PHP 8.0: порядок объявления внутри класса сохраняется
+ usort($properties, fn(ModelProperty $a, ModelProperty $b) => $depth($a) <=> $depth($b));
+
+ return $properties;
+ }
+}
diff --git a/src/Objects/BudgetPaymentInfoType.php b/src/Objects/BudgetPaymentInfoType.php
index 17aa308..2c825e9 100644
--- a/src/Objects/BudgetPaymentInfoType.php
+++ b/src/Objects/BudgetPaymentInfoType.php
@@ -81,4 +81,52 @@ public function getPayType(): ?string
{
return $this->PayType;
}
-}
\ No newline at end of file
+
+ public function setDrawerStatus(?string $drawerStatus): static
+ {
+ $this->DrawerStatus = $drawerStatus;
+ return $this;
+ }
+
+ public function setCBC(?string $cbc): static
+ {
+ $this->CBC = $cbc;
+ return $this;
+ }
+
+ public function setOKTMO(?string $oktmo): static
+ {
+ $this->OKTMO = $oktmo;
+ return $this;
+ }
+
+ public function setReason(?string $reason): static
+ {
+ $this->Reason = $reason;
+ return $this;
+ }
+
+ public function setTaxPeriod(?string $taxPeriod): static
+ {
+ $this->TaxPeriod = $taxPeriod;
+ return $this;
+ }
+
+ public function setDocNo(?string $docNo): static
+ {
+ $this->DocNo = $docNo;
+ return $this;
+ }
+
+ public function setDocDate(?string $docDate): static
+ {
+ $this->DocDate = $docDate;
+ return $this;
+ }
+
+ public function setPayType(?string $payType): static
+ {
+ $this->PayType = $payType;
+ return $this;
+ }
+}
diff --git a/src/Objects/CancelationRequest.php b/src/Objects/CancelationRequest.php
new file mode 100644
index 0000000..6eaf5b3
--- /dev/null
+++ b/src/Objects/CancelationRequest.php
@@ -0,0 +1,176 @@
+ Банк, вид ЭД 04)
+ *
+ * @xmlRoot CancelationRequest
+ * @xmlEncoding utf-8
+ */
+class CancelationRequest implements \Stringable
+{
+ use ConvertibleTrait;
+
+ /**
+ * @xmlAttribute
+ */
+ protected string $xmlns = 'http://directbank.1c.ru/XMLSchema';
+
+ /**
+ * @xmlAttribute
+ */
+ protected string $id;
+
+ /**
+ * @xmlAttribute
+ */
+ protected string $formatVersion = DefaultValue::FORMAT_VERSION;
+
+ /**
+ * @xmlAttribute
+ */
+ protected string $creationDate;
+
+ /**
+ * @xmlAttribute
+ */
+ protected ?string $userAgent = null;
+
+ /**
+ * @name Sender
+ */
+ protected CustomerPartyType $sender;
+
+ /**
+ * @name Recipient
+ */
+ protected BankPartyType $recipient;
+
+ /**
+ * Идентификатор отзываемого электронного документа
+ *
+ * @name ExtID
+ */
+ protected string $extID;
+
+ /**
+ * Причина отзыва
+ *
+ * @name Reason
+ */
+ protected ?string $reason = null;
+
+ /**
+ * @name Digest
+ */
+ protected ?DigestType $digest = null;
+
+ public function getId(): string
+ {
+ return $this->id;
+ }
+
+ public function setId(string $id): CancelationRequest
+ {
+ $this->id = $id;
+ return $this;
+ }
+
+ public function getFormatVersion(): string
+ {
+ return $this->formatVersion;
+ }
+
+ public function setFormatVersion(string $formatVersion): CancelationRequest
+ {
+ $this->formatVersion = $formatVersion;
+ return $this;
+ }
+
+ public function getCreationDate(): string
+ {
+ return $this->creationDate;
+ }
+
+ public function setCreationDate(string $creationDate): CancelationRequest
+ {
+ $this->creationDate = $creationDate;
+ return $this;
+ }
+
+ public function getUserAgent(): ?string
+ {
+ return $this->userAgent;
+ }
+
+ public function setUserAgent(?string $userAgent): CancelationRequest
+ {
+ $this->userAgent = $userAgent;
+ return $this;
+ }
+
+ public function getSender(): CustomerPartyType
+ {
+ return $this->sender;
+ }
+
+ public function setSender(CustomerPartyType $sender): CancelationRequest
+ {
+ $this->sender = $sender;
+ return $this;
+ }
+
+ public function getRecipient(): BankPartyType
+ {
+ return $this->recipient;
+ }
+
+ public function setRecipient(BankPartyType $recipient): CancelationRequest
+ {
+ $this->recipient = $recipient;
+ return $this;
+ }
+
+ public function getExtID(): string
+ {
+ return $this->extID;
+ }
+
+ public function setExtID(string $extID): CancelationRequest
+ {
+ $this->extID = $extID;
+ return $this;
+ }
+
+ public function getReason(): ?string
+ {
+ return $this->reason;
+ }
+
+ public function setReason(?string $reason): CancelationRequest
+ {
+ $this->reason = $reason;
+ return $this;
+ }
+
+ public function getDigest(): ?DigestType
+ {
+ return $this->digest;
+ }
+
+ public function setDigest(?DigestType $digest): CancelationRequest
+ {
+ $this->digest = $digest;
+ return $this;
+ }
+
+ public function __toString()
+ {
+ return $this->toXml();
+ }
+}
diff --git a/src/Objects/CustomerDetailsType.php b/src/Objects/CustomerDetailsType.php
index a2caf6c..a7b68a3 100644
--- a/src/Objects/CustomerDetailsType.php
+++ b/src/Objects/CustomerDetailsType.php
@@ -36,4 +36,34 @@ public function getBank(): BankType
{
return $this->Bank;
}
-}
\ No newline at end of file
+
+ public function setName(string $name): static
+ {
+ $this->Name = $name;
+ return $this;
+ }
+
+ public function setINN(?string $inn): static
+ {
+ $this->INN = $inn;
+ return $this;
+ }
+
+ public function setKPP(?string $kpp): static
+ {
+ $this->KPP = $kpp;
+ return $this;
+ }
+
+ public function setAccount(?string $account): static
+ {
+ $this->Account = $account;
+ return $this;
+ }
+
+ public function setBank(BankType $bank): static
+ {
+ $this->Bank = $bank;
+ return $this;
+ }
+}
diff --git a/src/Objects/DigestDataType.php b/src/Objects/DigestDataType.php
new file mode 100644
index 0000000..f5212b6
--- /dev/null
+++ b/src/Objects/DigestDataType.php
@@ -0,0 +1,37 @@
+value = $value;
+ $this->algorithmVersion = $algorithmVersion;
+ }
+
+ public function getValue(): string
+ {
+ return $this->value;
+ }
+
+ public function getAlgorithmVersion(): string
+ {
+ return $this->algorithmVersion;
+ }
+}
diff --git a/src/Objects/DigestType.php b/src/Objects/DigestType.php
new file mode 100644
index 0000000..3e50eb9
--- /dev/null
+++ b/src/Objects/DigestType.php
@@ -0,0 +1,38 @@
+data = new DigestDataType($data, $algorithmVersion);
+ }
+
+ /**
+ * Данные дайджеста в base64
+ */
+ public function getData(): string
+ {
+ return $this->data->getValue();
+ }
+
+ public function getAlgorithmVersion(): string
+ {
+ return $this->data->getAlgorithmVersion();
+ }
+}
diff --git a/src/Objects/Packet.php b/src/Objects/Packet.php
index a0a64c6..9ae8067 100644
--- a/src/Objects/Packet.php
+++ b/src/Objects/Packet.php
@@ -4,7 +4,7 @@
namespace TTBooking\DirectBank\Objects;
-use Traits\ConvertibleTrait;
+use TTBooking\DirectBank\Mapper\ConvertibleTrait;
use Traits\MappableTrait;
use TTBooking\DirectBank\Dictionary\DefaultValue;
diff --git a/src/Objects/PayDocRu.php b/src/Objects/PayDocRu.php
new file mode 100644
index 0000000..7eee5c2
--- /dev/null
+++ b/src/Objects/PayDocRu.php
@@ -0,0 +1,158 @@
+ Банк, вид ЭД 10)
+ *
+ * @see \TTBooking\DirectBank\Dictionary\DocKind::PAY_DOC_RU
+ *
+ * @xmlRoot PayDocRu
+ * @xmlEncoding utf-8
+ */
+class PayDocRu implements \Stringable
+{
+ use ConvertibleTrait;
+
+ /**
+ * @xmlAttribute
+ */
+ protected string $xmlns = 'http://directbank.1c.ru/XMLSchema';
+
+ /**
+ * @xmlAttribute
+ */
+ protected string $id;
+
+ /**
+ * @xmlAttribute
+ */
+ protected string $formatVersion = DefaultValue::FORMAT_VERSION;
+
+ /**
+ * @xmlAttribute
+ */
+ protected string $creationDate;
+
+ /**
+ * @xmlAttribute
+ */
+ protected ?string $userAgent = null;
+
+ /**
+ * @name Sender
+ */
+ protected CustomerPartyType $sender;
+
+ /**
+ * @name Recipient
+ */
+ protected BankPartyType $recipient;
+
+ /**
+ * @name Data
+ */
+ protected PayDocRuApp $data;
+
+ /**
+ * @name Digest
+ */
+ protected ?DigestType $digest = null;
+
+ public function getId(): string
+ {
+ return $this->id;
+ }
+
+ public function setId(string $id): PayDocRu
+ {
+ $this->id = $id;
+ return $this;
+ }
+
+ public function getFormatVersion(): string
+ {
+ return $this->formatVersion;
+ }
+
+ public function setFormatVersion(string $formatVersion): PayDocRu
+ {
+ $this->formatVersion = $formatVersion;
+ return $this;
+ }
+
+ public function getCreationDate(): string
+ {
+ return $this->creationDate;
+ }
+
+ public function setCreationDate(string $creationDate): PayDocRu
+ {
+ $this->creationDate = $creationDate;
+ return $this;
+ }
+
+ public function getUserAgent(): ?string
+ {
+ return $this->userAgent;
+ }
+
+ public function setUserAgent(?string $userAgent): PayDocRu
+ {
+ $this->userAgent = $userAgent;
+ return $this;
+ }
+
+ public function getSender(): CustomerPartyType
+ {
+ return $this->sender;
+ }
+
+ public function setSender(CustomerPartyType $sender): PayDocRu
+ {
+ $this->sender = $sender;
+ return $this;
+ }
+
+ public function getRecipient(): BankPartyType
+ {
+ return $this->recipient;
+ }
+
+ public function setRecipient(BankPartyType $recipient): PayDocRu
+ {
+ $this->recipient = $recipient;
+ return $this;
+ }
+
+ public function getData(): PayDocRuApp
+ {
+ return $this->data;
+ }
+
+ public function setData(PayDocRuApp $data): PayDocRu
+ {
+ $this->data = $data;
+ return $this;
+ }
+
+ public function getDigest(): ?DigestType
+ {
+ return $this->digest;
+ }
+
+ public function setDigest(?DigestType $digest): PayDocRu
+ {
+ $this->digest = $digest;
+ return $this;
+ }
+
+ public function __toString()
+ {
+ return $this->toXml();
+ }
+}
diff --git a/src/Objects/PayDocRuApp.php b/src/Objects/PayDocRuApp.php
index a6284af..ae6bae5 100644
--- a/src/Objects/PayDocRuApp.php
+++ b/src/Objects/PayDocRuApp.php
@@ -15,4 +15,10 @@ public function getBudgetPaymentInfo(): ?BudgetPaymentInfoType
{
return $this->BudgetPaymentInfo;
}
-}
\ No newline at end of file
+
+ public function setBudgetPaymentInfo(?BudgetPaymentInfoType $budgetPaymentInfo): static
+ {
+ $this->BudgetPaymentInfo = $budgetPaymentInfo;
+ return $this;
+ }
+}
diff --git a/src/Objects/PayRequest.php b/src/Objects/PayRequest.php
new file mode 100644
index 0000000..46b1dcc
--- /dev/null
+++ b/src/Objects/PayRequest.php
@@ -0,0 +1,158 @@
+ Банк, вид ЭД 11)
+ *
+ * @see \TTBooking\DirectBank\Dictionary\DocKind::PAY_REQUEST
+ *
+ * @xmlRoot PayRequest
+ * @xmlEncoding utf-8
+ */
+class PayRequest implements \Stringable
+{
+ use ConvertibleTrait;
+
+ /**
+ * @xmlAttribute
+ */
+ protected string $xmlns = 'http://directbank.1c.ru/XMLSchema';
+
+ /**
+ * @xmlAttribute
+ */
+ protected string $id;
+
+ /**
+ * @xmlAttribute
+ */
+ protected string $formatVersion = DefaultValue::FORMAT_VERSION;
+
+ /**
+ * @xmlAttribute
+ */
+ protected string $creationDate;
+
+ /**
+ * @xmlAttribute
+ */
+ protected ?string $userAgent = null;
+
+ /**
+ * @name Sender
+ */
+ protected CustomerPartyType $sender;
+
+ /**
+ * @name Recipient
+ */
+ protected BankPartyType $recipient;
+
+ /**
+ * @name Data
+ */
+ protected PayRequestApp $data;
+
+ /**
+ * @name Digest
+ */
+ protected ?DigestType $digest = null;
+
+ public function getId(): string
+ {
+ return $this->id;
+ }
+
+ public function setId(string $id): PayRequest
+ {
+ $this->id = $id;
+ return $this;
+ }
+
+ public function getFormatVersion(): string
+ {
+ return $this->formatVersion;
+ }
+
+ public function setFormatVersion(string $formatVersion): PayRequest
+ {
+ $this->formatVersion = $formatVersion;
+ return $this;
+ }
+
+ public function getCreationDate(): string
+ {
+ return $this->creationDate;
+ }
+
+ public function setCreationDate(string $creationDate): PayRequest
+ {
+ $this->creationDate = $creationDate;
+ return $this;
+ }
+
+ public function getUserAgent(): ?string
+ {
+ return $this->userAgent;
+ }
+
+ public function setUserAgent(?string $userAgent): PayRequest
+ {
+ $this->userAgent = $userAgent;
+ return $this;
+ }
+
+ public function getSender(): CustomerPartyType
+ {
+ return $this->sender;
+ }
+
+ public function setSender(CustomerPartyType $sender): PayRequest
+ {
+ $this->sender = $sender;
+ return $this;
+ }
+
+ public function getRecipient(): BankPartyType
+ {
+ return $this->recipient;
+ }
+
+ public function setRecipient(BankPartyType $recipient): PayRequest
+ {
+ $this->recipient = $recipient;
+ return $this;
+ }
+
+ public function getData(): PayRequestApp
+ {
+ return $this->data;
+ }
+
+ public function setData(PayRequestApp $data): PayRequest
+ {
+ $this->data = $data;
+ return $this;
+ }
+
+ public function getDigest(): ?DigestType
+ {
+ return $this->digest;
+ }
+
+ public function setDigest(?DigestType $digest): PayRequest
+ {
+ $this->digest = $digest;
+ return $this;
+ }
+
+ public function __toString()
+ {
+ return $this->toXml();
+ }
+}
diff --git a/src/Objects/PayRequestApp.php b/src/Objects/PayRequestApp.php
index ef431e1..b2ee4ef 100644
--- a/src/Objects/PayRequestApp.php
+++ b/src/Objects/PayRequestApp.php
@@ -42,4 +42,21 @@ public function getDocDispatchDate(): ?string
return $this->DocDispatchDate;
}
-}
\ No newline at end of file
+ public function setPaymentCondition(string $paymentCondition): static
+ {
+ $this->PaymentCondition = $paymentCondition;
+ return $this;
+ }
+
+ public function setAcceptTerm(?string $acceptTerm): static
+ {
+ $this->AcceptTerm = $acceptTerm;
+ return $this;
+ }
+
+ public function setDocDispatchDate(?string $docDispatchDate): static
+ {
+ $this->DocDispatchDate = $docDispatchDate;
+ return $this;
+ }
+}
diff --git a/src/Objects/PaymentDataType.php b/src/Objects/PaymentDataType.php
index 6505519..3abfdf0 100644
--- a/src/Objects/PaymentDataType.php
+++ b/src/Objects/PaymentDataType.php
@@ -85,4 +85,70 @@ public function getPurpose(): ?string
{
return $this->Purpose;
}
-}
\ No newline at end of file
+
+ public function setDocNo(string $docNo): static
+ {
+ $this->DocNo = $docNo;
+ return $this;
+ }
+
+ public function setDocDate(string $docDate): static
+ {
+ $this->DocDate = $docDate;
+ return $this;
+ }
+
+ public function setSum(float $sum): static
+ {
+ $this->Sum = $sum;
+ return $this;
+ }
+
+ public function setPayer(CustomerDetailsType $payer): static
+ {
+ $this->Payer = $payer;
+ return $this;
+ }
+
+ public function setPayee(CustomerDetailsType $payee): static
+ {
+ $this->Payee = $payee;
+ return $this;
+ }
+
+ public function setPaymentKind(?string $paymentKind): static
+ {
+ $this->PaymentKind = $paymentKind;
+ return $this;
+ }
+
+ public function setTransitionKind(?string $transitionKind): static
+ {
+ $this->TransitionKind = $transitionKind;
+ return $this;
+ }
+
+ public function setPriority(?string $priority): static
+ {
+ $this->Priority = $priority;
+ return $this;
+ }
+
+ public function setCode(?string $code): static
+ {
+ $this->Code = $code;
+ return $this;
+ }
+
+ public function setIncomeTypeCode(?string $incomeTypeCode): static
+ {
+ $this->IncomeTypeCode = $incomeTypeCode;
+ return $this;
+ }
+
+ public function setPurpose(?string $purpose): static
+ {
+ $this->Purpose = $purpose;
+ return $this;
+ }
+}
diff --git a/src/Objects/Probe.php b/src/Objects/Probe.php
index 09a7c1e..999328e 100644
--- a/src/Objects/Probe.php
+++ b/src/Objects/Probe.php
@@ -3,7 +3,7 @@
namespace TTBooking\DirectBank\Objects;
-use Traits\ConvertibleTrait;
+use TTBooking\DirectBank\Mapper\ConvertibleTrait;
use TTBooking\DirectBank\Dictionary\DefaultValue;
/**
@@ -51,6 +51,11 @@ class Probe implements \Stringable
*/
protected BankPartyType $recipient;
+ /**
+ * @name Digest
+ */
+ protected ?DigestType $digest = null;
+
public function getId(): string
{
return $this->id;
@@ -117,6 +122,17 @@ public function setRecipient(BankPartyType $recipient): Probe
return $this;
}
+ public function getDigest(): ?DigestType
+ {
+ return $this->digest;
+ }
+
+ public function setDigest(?DigestType $digest): Probe
+ {
+ $this->digest = $digest;
+ return $this;
+ }
+
public function __toString()
{
return $this->toXml();
diff --git a/src/Objects/StatementRequest.php b/src/Objects/StatementRequest.php
index d6ee3af..736cd0a 100644
--- a/src/Objects/StatementRequest.php
+++ b/src/Objects/StatementRequest.php
@@ -4,7 +4,7 @@
namespace TTBooking\DirectBank\Objects;
use Mapper\ModelMapper;
-use Traits\ConvertibleTrait;
+use TTBooking\DirectBank\Mapper\ConvertibleTrait;
use TTBooking\DirectBank\Attributes\Name;
use TTBooking\DirectBank\Dictionary\DefaultValue;
@@ -53,6 +53,11 @@ class StatementRequest implements \Stringable
*/
protected StatementRequestData $data;
+ /**
+ * @name Digest
+ */
+ protected ?DigestType $digest = null;
+
/**
* @return string
*/
@@ -165,6 +170,17 @@ public function setData(StatementRequestData $data): StatementRequest
return $this;
}
+ public function getDigest(): ?DigestType
+ {
+ return $this->digest;
+ }
+
+ public function setDigest(?DigestType $digest): StatementRequest
+ {
+ $this->digest = $digest;
+ return $this;
+ }
+
public function __toString()
{
return $this->toXml();
diff --git a/src/Objects/StatusRequest.php b/src/Objects/StatusRequest.php
index 3c07ff4..3699ea1 100644
--- a/src/Objects/StatusRequest.php
+++ b/src/Objects/StatusRequest.php
@@ -3,7 +3,7 @@
namespace TTBooking\DirectBank\Objects;
-use Traits\ConvertibleTrait;
+use TTBooking\DirectBank\Mapper\ConvertibleTrait;
use TTBooking\DirectBank\Dictionary\DefaultValue;
/**
diff --git a/tests/Fixture/xml/1c/CancelationRequest.xml b/tests/Fixture/xml/1c/CancelationRequest.xml
new file mode 100644
index 0000000..ed67fb4
--- /dev/null
+++ b/tests/Fixture/xml/1c/CancelationRequest.xml
@@ -0,0 +1,13 @@
+
+
+
+
+ 05688096-0806-4ef0-af4c-572f75dbaf7c
+ Описание причины отзыва
+
\ No newline at end of file
diff --git a/tests/Fixture/xml/1c/PayDocRu.xml b/tests/Fixture/xml/1c/PayDocRu.xml
new file mode 100644
index 0000000..a33eee4
--- /dev/null
+++ b/tests/Fixture/xml/1c/PayDocRu.xml
@@ -0,0 +1,41 @@
+
+
+
+
+
+ 14
+ 2016-04-22
+ 15
+
+ Торговый дом "Комплексный"
+ 7705260699
+ 770501001
+ 40702810813123123222
+
+ 044525888
+ ДЕМО-БАНК
+ 30101810500000000219
+
+
+
+ ООО "Канцтовары"
+ 7704596181
+ 770401001
+ 40702810401200000035
+
+ 044525999
+ ДЕМО-БАНК2
+ 30101810200000000593
+
+
+ 01
+ 3
+ за товар
+
+
\ No newline at end of file
diff --git a/tests/Fixture/xml/1c/PayRequest.xml b/tests/Fixture/xml/1c/PayRequest.xml
new file mode 100644
index 0000000..5c535a1
--- /dev/null
+++ b/tests/Fixture/xml/1c/PayRequest.xml
@@ -0,0 +1,45 @@
+
+
+
+
+
+ 15
+ 2016-04-22
+ 15
+
+ ООО "Канцтовары"
+ 7704596181
+ 770401001
+ 40702810401200000035
+
+ 044525999
+ ДЕМО-БАНК2
+ Г. МОСКВА
+ 30101810200000000593
+
+
+
+ Торговый дом "Комплексный"
+ 7705260699
+ 770501001
+ 40702810813123123222
+
+ 044525888
+ ДЕМО-БАНК
+ Г. МОСКВА
+ 30101810500000000219
+
+
+ Электронно
+ 02
+ 3
+ за товар
+ 1
+
+
\ No newline at end of file
diff --git a/tests/Fixture/xml/1c/readme.md b/tests/Fixture/xml/1c/readme.md
index 309e891..cbd777b 100644
--- a/tests/Fixture/xml/1c/readme.md
+++ b/tests/Fixture/xml/1c/readme.md
@@ -3,6 +3,8 @@
Официальные примеры из описания стандарта 1С:DirectBank, версия 2.2.2.
Файлы скопированы без изменений.
-- `Statement.xml`, `StatementRequest.xml` —
+- Электронные документы — :
+ `CancelationRequest.xml`, `PayDocRu.xml`, `PayRequest.xml`, `Probe.xml`, `Settings.xml`, `Settings_Logon_Certificate.xml`,
+ `Statement.xml`, `StatementRequest.xml`, `StatusDocNotice.xml`, `StatusRequest.xml`
- `Packet.xml` — пример запроса SendPack из .
Подпись в примере обрезана и не является корректным base64, поэтому XSD файл не проходит.
diff --git a/tests/Objects/PaymentDocumentsTest.php b/tests/Objects/PaymentDocumentsTest.php
new file mode 100644
index 0000000..4393c68
--- /dev/null
+++ b/tests/Objects/PaymentDocumentsTest.php
@@ -0,0 +1,182 @@
+loadXML($xml);
+
+ $this->assertTrue($dom->schemaValidate(__DIR__ . '/../Fixture/xsd/1C-Bank_' . $schema . '.xsd'));
+ }
+
+ protected static function customer(string $name, string $inn, string $account, string $bic): CustomerDetailsType
+ {
+ return (new CustomerDetailsType())
+ ->setName($name)
+ ->setINN($inn)
+ ->setKPP('770501001')
+ ->setAccount($account)
+ ->setBank((new BankType())->setBic($bic)->setName('ДЕМО-БАНК')->setCity('Г. МОСКВА')->setCorrespAcc('30101810500000000219'));
+ }
+
+ protected static function payDocRu(): PayDocRu
+ {
+ return (new PayDocRu())
+ ->setId((string) Uuid::uuid4())
+ ->setCreationDate('2026-09-22T12:00:00')
+ ->setUserAgent('My App')
+ ->setSender((new CustomerPartyType())->setId('2806')->setName('Торговый дом Комплексный')->setInn('7705260699')->setKpp('770501001'))
+ ->setRecipient((new BankPartyType())->setBic('044525888')->setName('ДЕМО-БАНК'))
+ ->setData(
+ (new PayDocRuApp())
+ ->setDocNo('14')
+ ->setDocDate('2026-09-22')
+ ->setSum(1234.56)
+ ->setPayer(self::customer('Торговый дом "Комплексный"', '7705260699', '40702810813123123222', '044525888'))
+ ->setPayee(self::customer('УФК по г. Москве', '7707329152', '03100643000000017300', '004525988'))
+ ->setTransitionKind('01')
+ ->setPriority('5')
+ ->setCode('0')
+ ->setPurpose('Налог на прибыль за август 2026')
+ ->setBudgetPaymentInfo(
+ (new BudgetPaymentInfoType())
+ ->setDrawerStatus('01')
+ ->setCBC('18210101011011000110')
+ ->setOKTMO('45380000')
+ ->setReason('ТП')
+ ->setTaxPeriod('МС.08.2026')
+ ->setDocNo('0')
+ ->setDocDate('0')
+ )
+ );
+ }
+
+ public function testPayDocRu()
+ {
+ $payDocRu = self::payDocRu();
+
+ $this->assertValid((string) $payDocRu, 'PayDocRu');
+
+ $mapped = (new XmlModelMapper())->map((string) $payDocRu, new PayDocRu());
+ $data = $mapped->getData();
+
+ $this->assertSame('14', $data->getDocNo());
+ $this->assertSame(1234.56, $data->getSum());
+ $this->assertSame('7705260699', $data->getPayer()->getINN());
+ $this->assertSame('03100643000000017300', $data->getPayee()->getAccount());
+ $this->assertSame('004525988', $data->getPayee()->getBank()->getBic());
+ $this->assertSame('01', $data->getTransitionKind());
+ $this->assertSame('18210101011011000110', $data->getBudgetPaymentInfo()->getCBC());
+ $this->assertSame('МС.08.2026', $data->getBudgetPaymentInfo()->getTaxPeriod());
+ $this->assertNull($data->getBudgetPaymentInfo()->getPayType());
+ $this->assertNull($mapped->getDigest());
+ }
+
+ public function testPayDocRuWithDigest()
+ {
+ $payDocRu = self::payDocRu()->setDigest(new DigestType(base64_encode('digest'), '1.0'));
+
+ $this->assertValid((string) $payDocRu, 'PayDocRu');
+ $this->assertSame('1.0', (new XmlModelMapper())->map((string) $payDocRu, new PayDocRu())->getDigest()->getAlgorithmVersion());
+ }
+
+ public function testPayDocRuInPacket()
+ {
+ $payDocRu = self::payDocRu();
+
+ $packet = (new Packet())
+ ->setId((string) Uuid::uuid4())
+ ->setCreationDate('2026-09-22T12:00:00')
+ ->setSender((new ParticipantType())->setCustomer($payDocRu->getSender()))
+ ->setRecipient((new ParticipantType())->setBank($payDocRu->getRecipient()))
+ ->setDocument(
+ (new DocumentType())
+ ->setId($payDocRu->getId())
+ ->setDockind(DocKind::PAY_DOC_RU)
+ ->setData(base64_encode((string) $payDocRu))
+ );
+
+ $this->assertValid((string) $packet, 'Packet');
+ $this->assertValid(base64_decode($packet->getDocument()->getData()), 'PayDocRu');
+ }
+
+ public function officialProvider(): array
+ {
+ return [
+ 'PayDocRu' => ['PayDocRu', PayDocRu::class],
+ 'PayRequest' => ['PayRequest', PayRequest::class],
+ ];
+ }
+
+ /**
+ * Официальные примеры 1С разбираются и собираются обратно в XML, проходящий XSD
+ *
+ * @dataProvider officialProvider
+ */
+ public function testOfficialExample(string $name, string $class)
+ {
+ $document = (new XmlModelMapper())->map(self::fixture("1c/$name.xml"), new $class());
+
+ $this->assertSame('7705260699', $document->getSender()->getInn());
+ $this->assertSame('044525888', $document->getRecipient()->getBic());
+ $this->assertSame(15.0, $document->getData()->getSum());
+
+ $this->assertValid($document->toXml(), $name);
+ $this->assertEquals($document, (new XmlModelMapper())->map($document->toXml(), new $class()));
+ }
+
+ public function testOfficialPayRequest()
+ {
+ $data = (new XmlModelMapper())->map(self::fixture('1c/PayRequest.xml'), new PayRequest())->getData();
+
+ $this->assertSame('15', $data->getDocNo());
+ $this->assertSame('7704596181', $data->getPayer()->getINN());
+ $this->assertSame('1', $data->getPaymentCondition());
+ $this->assertNull($data->getAcceptTerm());
+ }
+
+ public function testPayRequest()
+ {
+ $payRequest = (new PayRequest())
+ ->setId((string) Uuid::uuid4())
+ ->setCreationDate('2026-09-22T12:00:00')
+ ->setSender((new CustomerPartyType())->setId('2806'))
+ ->setRecipient((new BankPartyType())->setBic('044525888'))
+ ->setData(
+ (new PayRequestApp())
+ ->setDocNo('15')
+ ->setDocDate('2026-09-22')
+ ->setSum(15)
+ ->setPayer(self::customer('ООО "Канцтовары"', '7704596181', '40702810401200000035', '044525999'))
+ ->setPayee(self::customer('Торговый дом "Комплексный"', '7705260699', '40702810813123123222', '044525888'))
+ ->setPurpose('за товар')
+ ->setPaymentCondition('2')
+ ->setAcceptTerm('5')
+ ->setDocDispatchDate('2026-09-20')
+ );
+
+ $this->assertValid((string) $payRequest, 'PayRequest');
+
+ $data = (new XmlModelMapper())->map((string) $payRequest, new PayRequest())->getData();
+ $this->assertSame('2', $data->getPaymentCondition());
+ $this->assertSame('5', $data->getAcceptTerm());
+ $this->assertSame('2026-09-20', $data->getDocDispatchDate());
+ }
+}
diff --git a/tests/Objects/ServiceDocumentsTest.php b/tests/Objects/ServiceDocumentsTest.php
index 31930a4..ea18e48 100644
--- a/tests/Objects/ServiceDocumentsTest.php
+++ b/tests/Objects/ServiceDocumentsTest.php
@@ -110,6 +110,29 @@ public function testStatusRequest()
$this->assertValid($official->toXml(), 'StatusRequest');
}
+ public function testCancelationRequest()
+ {
+ $request = (new CancelationRequest())
+ ->setId('e8caa148-82a3-446d-a10f-ff82ad514b7e')
+ ->setCreationDate('2016-04-22T10:11:25')
+ ->setSender(self::customer())
+ ->setRecipient(self::bank())
+ ->setExtID('05688096-0806-4ef0-af4c-572f75dbaf7c');
+
+ $this->assertValid((string) $request, 'CancelationRequest');
+ $this->assertNull($request->getReason());
+
+ $request->setReason('Ошибка в реквизитах')->setDigest(new DigestType(base64_encode('digest'), '1.0'));
+ $this->assertValid((string) $request, 'CancelationRequest');
+
+ $official = (new XmlModelMapper())->map(self::fixture('1c/CancelationRequest.xml'), new CancelationRequest());
+
+ $this->assertSame('05688096-0806-4ef0-af4c-572f75dbaf7c', $official->getExtID());
+ $this->assertSame('Описание причины отзыва', $official->getReason());
+ $this->assertNull($official->getDigest());
+ $this->assertValid($official->toXml(), 'CancelationRequest');
+ }
+
public function testProbe()
{
$probe = (new Probe())
@@ -128,6 +151,34 @@ public function testProbe()
$this->assertValid($official->toXml(), 'Probe');
}
+ public function testProbeWithDigest()
+ {
+ $probe = (new Probe())
+ ->setId('eafa28b5-6600-424c-b0d3-3785274d570d')
+ ->setCreationDate('2016-04-22T09:33:57')
+ ->setSender(self::customer())
+ ->setRecipient(self::bank())
+ ->setDigest(new DigestType(base64_encode('digest'), '1.0'));
+
+ $this->assertValid((string) $probe, 'Probe');
+
+ $mapped = (new XmlModelMapper())->map((string) $probe, new Probe());
+
+ $this->assertSame('digest', base64_decode($mapped->getDigest()->getData()));
+ $this->assertSame('1.0', $mapped->getDigest()->getAlgorithmVersion());
+ }
+
+ public function testStatementRequestWithDigest()
+ {
+ $request = (new XmlModelMapper())->map(self::fixture('1c/StatementRequest.xml'), new StatementRequest());
+ $this->assertNull($request->getDigest());
+
+ $request->setDigest(new DigestType(base64_encode('digest'), '2.2.2'));
+
+ $this->assertValid($request->toXml(), 'StatementRequest');
+ $this->assertSame('2.2.2', (new XmlModelMapper())->map($request->toXml(), new StatementRequest())->getDigest()->getAlgorithmVersion());
+ }
+
public function testSettingsLogin()
{
$settings = new Settings();
diff --git a/tests/Objects/StatementTest.php b/tests/Objects/StatementTest.php
index 257e1ca..05b2264 100644
--- a/tests/Objects/StatementTest.php
+++ b/tests/Objects/StatementTest.php
@@ -4,6 +4,7 @@
namespace TTBooking\DirectBank\Objects;
use PHPUnit\Framework\TestCase;
+use TTBooking\DirectBank\Mapper\XmlMapper;
class StatementTest extends TestCase
{
@@ -180,6 +181,35 @@ public function testCheckWithSingleDetails()
}
}
+ public function roundTripProvider(): array
+ {
+ return [
+ 'official example' => ['1c/Statement.xml'],
+ 'all doc kinds' => ['statement_all_doc_kinds.xml'],
+ 'minimal' => ['statement_minimal.xml'],
+ ];
+ }
+
+ /**
+ * Разобранная выписка собирается обратно в XML, проходящий XSD:
+ * поля базовых типов идут раньше полей наследников
+ *
+ * @dataProvider roundTripProvider
+ */
+ public function testRoundTrip(string $fixture)
+ {
+ $statement = new Statement();
+ $statement->mapFromXml(file_get_contents(__DIR__ . '/../Fixture/xml/' . $fixture));
+
+ $xml = (new XmlMapper())->unmap($statement);
+
+ $this->assertValid($xml);
+
+ $again = new Statement();
+ $again->mapFromXml($xml);
+ $this->assertEquals($statement, $again);
+ }
+
protected function assertValid(string $xml): void
{
$dom = new \DOMDocument();