diff --git a/src/Constants/Constraints.php b/src/Constants/Constraints.php index e3c1350..aa4bc0e 100644 --- a/src/Constants/Constraints.php +++ b/src/Constants/Constraints.php @@ -105,7 +105,7 @@ final class Constraints /** * Минимальная длина кода таможенной декларации (1231) * - * @see https://online.atol.ru/possystem/v4/schema/sell Схема receipt.items.declaration_number + * @see https://online.atol.ru/possystem/v4/schema/sell Схема "#/receipt/items/declaration_number" */ const MIN_LENGTH_DECLARATION_NUMBER = 1; @@ -135,6 +135,13 @@ final class Constraints */ const MAX_LENGTH_CASHIER_NAME = 64; + /** + * Максимальная длина кода товара в байтах (1162) + * + * @see https://online.atol.ru/files/API_atol_online_v4.pdf Документация, стр 21 + */ + const MAX_LENGTH_ITEM_CODE = 32; + /** * Регулярное выражение для валидации строки ИНН * @@ -145,7 +152,6 @@ final class Constraints /** * Регулярное выражение для валидации номера телефона - * * @see https://online.atol.ru/possystem/v4/schema/sell Схема "#/definitions/phone_number" */ const PATTERN_PHONE = /* @lang PhpRegExp */ diff --git a/src/Entities/Item.php b/src/Entities/Item.php index aba875d..9cb0fc1 100644 --- a/src/Entities/Item.php +++ b/src/Entities/Item.php @@ -26,6 +26,7 @@ use AtolOnline\{ Exceptions\TooHighItemQuantityException, Exceptions\TooHighPriceException, Exceptions\TooHighSumException, + Exceptions\TooLongItemCodeException, Exceptions\TooLongItemNameException, Exceptions\TooLongMeasurementUnitException, Exceptions\TooLongUserdataException, @@ -59,6 +60,16 @@ class Item extends Entity */ protected ?string $measurement_unit = null; + /** + * @var string|null Код товара (1162) + */ + protected ?string $code = null; + + /** + * @var string|null Код товара (1162) в форматированной шестнадцатиричной форме + */ + protected ?string $code_hex = null; + /** * @var string|null Признак способа расчёта (1214) */ @@ -229,7 +240,7 @@ class Item extends Entity public function getSum(): float { $sum = $this->getPrice() * $this->getQuantity() + (float)$this->getExcise(); - if ($sum > Constraints::MAX_COUNT_ITEM_PRICE) { + if ($sum > Constraints::MAX_COUNT_ITEM_SUM) { throw new TooHighSumException($this->getName(), $sum); } return $sum; @@ -262,6 +273,49 @@ class Item extends Entity return $this; } + /** + * Возвращает установленный код товара + * + * @return string|null + */ + public function getCode(): ?string + { + return $this->code; + } + + /** + * Возвращает шестнадцатиричное представление кода товара + * + * @return string|null + */ + public function getCodeHex(): ?string + { + return $this->code_hex; + } + + /** + * Устанавливает код товара + * + * @param string|null $code + * @return Item + * @throws TooLongItemCodeException + */ + public function setCode(?string $code): self + { + $hex_string = null; + $code = trim((string)$code); + if (mb_strlen($code) > Constraints::MAX_LENGTH_ITEM_CODE) { + throw new TooLongItemCodeException($this->getName(), $code); + } + if (!empty($code)) { + $hex = bin2hex($code); + $hex_string = trim(preg_replace('/([\dA-Fa-f]{2})/', '$1 ', $hex)); + } + $this->code = $code ?: null; + $this->code_hex = $hex_string ?: null; + return $this; + } + /** * Возвращает признак способа оплаты * @@ -536,6 +590,7 @@ class Item extends Entity 'sum' => $this->getSum(), ]; !is_null($this->getMeasurementUnit()) && $json['measurement_unit'] = $this->getMeasurementUnit(); + !is_null($this->getCodeHex()) && $json['nomenclature_code'] = $this->getCodeHex(); !is_null($this->getPaymentMethod()) && $json['payment_method'] = $this->getPaymentMethod(); !is_null($this->getPaymentObject()) && $json['payment_object'] = $this->getPaymentObject(); !is_null($this->getDeclarationNumber()) && $json['declaration_number'] = $this->getDeclarationNumber(); @@ -545,7 +600,6 @@ class Item extends Entity !is_null($this->getUserData()) && $json['user_data'] = $this->getUserData(); !is_null($this->getExcise()) && $json['excise'] = $this->getExcise(); !is_null($this->getCountryCode()) && $json['country_code'] = $this->getCountryCode(); - //TODO nomenclature_code return $json; } } diff --git a/src/Exceptions/TooLongException.php b/src/Exceptions/TooLongException.php index d26748c..952af10 100644 --- a/src/Exceptions/TooLongException.php +++ b/src/Exceptions/TooLongException.php @@ -31,14 +31,13 @@ class TooLongException extends AtolException * * @param string $value * @param string $message - * @param int $max + * @param float $max */ - public function __construct(string $value, string $message = '', int $max = 0) + public function __construct(string $value, string $message = '', float $max = 0) { - $message = ($message ?: $this->message) . ': '. $value; - if ($max > 0 || $this->max > 0) { - $message .= ' (макс. = ' . ($max ?? $this->max) . ', фактически = ' . mb_strlen($value) . ')'; - } - parent::__construct($message); + parent::__construct( + ($message ?: $this->message) . ': ' . $value . (((float)$max > 0 || (float)$this->max > 0) ? + ' (макс = ' . ($max ?: $this->max) . ', фактически = ' . mb_strlen($value) . ')' : '') + ); } } diff --git a/src/Exceptions/TooLongItemCodeException.php b/src/Exceptions/TooLongItemCodeException.php new file mode 100644 index 0000000..e9dba47 --- /dev/null +++ b/src/Exceptions/TooLongItemCodeException.php @@ -0,0 +1,35 @@ +expectException(NegativeItemExciseException::class); (new Item('test item', 2, 3))->setExcise(-1); } + + /** + * Тестирует установку валидного кода товара + * + * @covers \AtolOnline\Entities\Item::setCode + * @covers \AtolOnline\Entities\Item::getCode + * @covers \AtolOnline\Entities\Item::getCodeHex + * @covers \AtolOnline\Entities\Item::jsonSerialize + * @throws EmptyItemNameException + * @throws NegativeItemPriceException + * @throws NegativeItemQuantityException + * @throws TooHighPriceException + * @throws TooLongItemNameException + * @throws TooManyException + * @throws TooLongItemCodeException + */ + public function testValidNomenclatureCode(): void + { + $code = Helpers::randomStr(Constraints::MAX_LENGTH_ITEM_CODE); + $encoded = trim(preg_replace('/([\dA-Fa-f]{2})/', '$1 ', bin2hex($code))); + + $item = (new Item('test item', 2, 3))->setCode($code); + $this->assertEquals($code, $item->getCode()); + $this->assertEquals($encoded, $item->getCodeHex()); + + $decoded = hex2bin(str_replace(' ', '', $item->getCodeHex())); + $this->assertEquals($decoded, $item->getCode()); + + $this->assertAtolable($item, [ + 'name' => 'test item', + 'price' => 2, + 'quantity' => 3, + 'sum' => 6, + 'nomenclature_code' => $item->getCodeHex(), + ]); + } + + /** + * Тестирует обнуление кода товара + * + * @param mixed $param + * @dataProvider providerNullableStrings + * @covers \AtolOnline\Entities\Item::setCode + * @covers \AtolOnline\Entities\Item::getCode + * @covers \AtolOnline\Entities\Item::getCodeHex + * @throws EmptyItemNameException + * @throws NegativeItemPriceException + * @throws NegativeItemQuantityException + * @throws TooHighPriceException + * @throws TooLongItemCodeException + * @throws TooLongItemNameException + * @throws TooManyException + */ + public function testNullableCode(mixed $param): void + { + $item = (new Item('test item', 2, 3))->setCode($param); + $this->assertNull($item->getCode()); + $this->assertNull($item->getCodeHex()); + } + + /** + * Тестирует выброс исключения при установке слишком отрицательного акциза + * + * @covers \AtolOnline\Entities\Item::setCode + * @covers \AtolOnline\Exceptions\TooLongItemCodeException + * @throws TooLongItemNameException + * @throws TooHighPriceException + * @throws TooManyException + * @throws NegativeItemPriceException + * @throws EmptyItemNameException + * @throws NegativeItemQuantityException + */ + public function testTooLongItemCodeException(): void + { + $this->expectException(TooLongItemCodeException::class); + (new Item('test item', 2, 3))->setCode(Helpers::randomStr(Constraints::MAX_LENGTH_ITEM_CODE + 1)); + } }