From 86ae83b24fc6ac5e756b34b036a786f61c534d9a Mon Sep 17 00:00:00 2001 From: Egor Gruzdev Date: Tue, 22 Sep 2026 23:33:28 +0200 Subject: [PATCH 1/5] =?UTF-8?q?=D0=9F=D0=BE=D1=80=D1=8F=D0=B4=D0=BE=D0=BA?= =?UTF-8?q?=20=D1=8D=D0=BB=D0=B5=D0=BC=D0=B5=D0=BD=D1=82=D0=BE=D0=B2=20?= =?UTF-8?q?=D0=BF=D1=80=D0=B8=20=D1=81=D0=B1=D0=BE=D1=80=D0=BA=D0=B5=20XML?= =?UTF-8?q?:=20=D0=BF=D0=BE=D0=BB=D1=8F=20=D0=B1=D0=B0=D0=B7=D0=BE=D0=B2?= =?UTF-8?q?=D0=BE=D0=B3=D0=BE=20=D1=82=D0=B8=D0=BF=D0=B0=20=D1=80=D0=B0?= =?UTF-8?q?=D0=BD=D1=8C=D1=88=D0=B5=20=D0=BF=D0=BE=D0=BB=D0=B5=D0=B9=20?= =?UTF-8?q?=D0=BD=D0=B0=D1=81=D0=BB=D0=B5=D0=B4=D0=BD=D0=B8=D0=BA=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit XmlMapper выводит свойства от базового класса к наследнику, как в xsd:extension. Исходящие документы собираются через него (TTBooking\DirectBank\Mapper\ConvertibleTrait). Разобранная выписка со всеми видами PayDoc собирается обратно в XML, проходящий XSD. Closes #26 --- src/Mapper/ConvertibleTrait.php | 18 ++++++++ src/Mapper/XmlMapper.php | 79 ++++++++++++++++++++++++++++++++ src/Objects/Packet.php | 2 +- src/Objects/Probe.php | 2 +- src/Objects/StatementRequest.php | 2 +- src/Objects/StatusRequest.php | 2 +- tests/Objects/StatementTest.php | 30 ++++++++++++ 7 files changed, 131 insertions(+), 4 deletions(-) create mode 100644 src/Mapper/ConvertibleTrait.php create mode 100644 src/Mapper/XmlMapper.php 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/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/Probe.php b/src/Objects/Probe.php index 09a7c1e..40a03d2 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; /** diff --git a/src/Objects/StatementRequest.php b/src/Objects/StatementRequest.php index d6ee3af..e202571 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; 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/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(); From 16375a369bfcd66f22f3fec1f8c834cfbcfdf7e6 Mon Sep 17 00:00:00 2001 From: Egor Gruzdev Date: Tue, 22 Sep 2026 23:34:22 +0200 Subject: [PATCH 2/5] =?UTF-8?q?DigestType:=20=D0=B4=D0=B0=D0=B9=D0=B4?= =?UTF-8?q?=D0=B6=D0=B5=D1=81=D1=82=20=D0=B2=20StatementRequest=20=D0=B8?= =?UTF-8?q?=20Probe?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refs #27 --- src/Objects/DigestDataType.php | 37 +++++++++++++++++++++++++ src/Objects/DigestType.php | 38 ++++++++++++++++++++++++++ src/Objects/Probe.php | 16 +++++++++++ src/Objects/StatementRequest.php | 16 +++++++++++ tests/Objects/ServiceDocumentsTest.php | 28 +++++++++++++++++++ 5 files changed, 135 insertions(+) create mode 100644 src/Objects/DigestDataType.php create mode 100644 src/Objects/DigestType.php 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/Probe.php b/src/Objects/Probe.php index 40a03d2..999328e 100644 --- a/src/Objects/Probe.php +++ b/src/Objects/Probe.php @@ -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 e202571..736cd0a 100644 --- a/src/Objects/StatementRequest.php +++ b/src/Objects/StatementRequest.php @@ -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/tests/Objects/ServiceDocumentsTest.php b/tests/Objects/ServiceDocumentsTest.php index 31930a4..2ee061d 100644 --- a/tests/Objects/ServiceDocumentsTest.php +++ b/tests/Objects/ServiceDocumentsTest.php @@ -128,6 +128,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(); From e4c188f5bb2cef62c4f49e80f315e5e061a5a7ca Mon Sep 17 00:00:00 2001 From: Egor Gruzdev Date: Tue, 22 Sep 2026 23:36:16 +0200 Subject: [PATCH 3/5] =?UTF-8?q?=D0=98=D1=81=D1=85=D0=BE=D0=B4=D1=8F=D1=89?= =?UTF-8?q?=D0=B8=D0=B5=20=D0=BF=D0=BB=D0=B0=D1=82=D1=91=D0=B6=D0=BD=D1=8B?= =?UTF-8?q?=D0=B5=20=D0=B4=D0=BE=D0=BA=D1=83=D0=BC=D0=B5=D0=BD=D1=82=D1=8B?= =?UTF-8?q?:=20PayDocRu=20=D0=B8=20PayRequest?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - корневые документы PayDocRu (10) и PayRequest (11) с Digest - сеттеры для PaymentDataType, CustomerDetailsType, BudgetPaymentInfoType, PayDocRuApp, PayRequestApp - собранные документы и официальные примеры 1С после обратной сборки проходят XSD Closes #17 --- src/Objects/BudgetPaymentInfoType.php | 50 ++++++- src/Objects/CustomerDetailsType.php | 32 ++++- src/Objects/PayDocRu.php | 158 +++++++++++++++++++++ src/Objects/PayDocRuApp.php | 8 +- src/Objects/PayRequest.php | 158 +++++++++++++++++++++ src/Objects/PayRequestApp.php | 19 ++- src/Objects/PaymentDataType.php | 68 ++++++++- tests/Fixture/xml/1c/PayDocRu.xml | 41 ++++++ tests/Fixture/xml/1c/PayRequest.xml | 45 ++++++ tests/Fixture/xml/1c/readme.md | 4 +- tests/Objects/PaymentDocumentsTest.php | 182 +++++++++++++++++++++++++ 11 files changed, 759 insertions(+), 6 deletions(-) create mode 100644 src/Objects/PayDocRu.php create mode 100644 src/Objects/PayRequest.php create mode 100644 tests/Fixture/xml/1c/PayDocRu.xml create mode 100644 tests/Fixture/xml/1c/PayRequest.xml create mode 100644 tests/Objects/PaymentDocumentsTest.php 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/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/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/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..1fb1710 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` — +- Электронные документы — : + `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()); + } +} From bef0305a4180c0dd4b524149f710eb36492eb1c8 Mon Sep 17 00:00:00 2001 From: Egor Gruzdev Date: Tue, 22 Sep 2026 23:37:04 +0200 Subject: [PATCH 4/5] =?UTF-8?q?=D0=97=D0=B0=D0=BF=D1=80=D0=BE=D1=81=20?= =?UTF-8?q?=D0=BE=D0=B1=20=D0=BE=D1=82=D0=B7=D1=8B=D0=B2=D0=B5=20=D1=8D?= =?UTF-8?q?=D0=BB=D0=B5=D0=BA=D1=82=D1=80=D0=BE=D0=BD=D0=BD=D0=BE=D0=B3?= =?UTF-8?q?=D0=BE=20=D0=B4=D0=BE=D0=BA=D1=83=D0=BC=D0=B5=D0=BD=D1=82=D0=B0?= =?UTF-8?q?=20(04):=20CancelationRequest?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #20 Closes #27 --- src/Objects/CancelationRequest.php | 176 ++++++++++++++++++++ tests/Fixture/xml/1c/CancelationRequest.xml | 13 ++ tests/Fixture/xml/1c/readme.md | 2 +- tests/Objects/ServiceDocumentsTest.php | 23 +++ 4 files changed, 213 insertions(+), 1 deletion(-) create mode 100644 src/Objects/CancelationRequest.php create mode 100644 tests/Fixture/xml/1c/CancelationRequest.xml 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/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/readme.md b/tests/Fixture/xml/1c/readme.md index 1fb1710..cbd777b 100644 --- a/tests/Fixture/xml/1c/readme.md +++ b/tests/Fixture/xml/1c/readme.md @@ -4,7 +4,7 @@ Файлы скопированы без изменений. - Электронные документы — : - `PayDocRu.xml`, `PayRequest.xml`, `Probe.xml`, `Settings.xml`, `Settings_Logon_Certificate.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/ServiceDocumentsTest.php b/tests/Objects/ServiceDocumentsTest.php index 2ee061d..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()) From a410f0709a5f5e7ae9d3bd6b267630fb1aca3d44 Mon Sep 17 00:00:00 2001 From: Egor Gruzdev Date: Tue, 22 Sep 2026 23:37:43 +0200 Subject: [PATCH 5/5] =?UTF-8?q?README:=20=D0=BF=D0=BB=D0=B0=D1=82=D1=91?= =?UTF-8?q?=D0=B6=D0=BD=D0=BE=D0=B5=20=D0=BF=D0=BE=D1=80=D1=83=D1=87=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D0=B5,=20=D0=BE=D1=82=D0=B7=D1=8B=D0=B2=20=D0=B4?= =?UTF-8?q?=D0=BE=D0=BA=D1=83=D0=BC=D0=B5=D0=BD=D1=82=D0=B0,=20=D0=B4?= =?UTF-8?q?=D0=B0=D0=B9=D0=B4=D0=B6=D0=B5=D1=81=D1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 65 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) 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`). ## Справочники