Compare commits

...

123 Commits

Author SHA1 Message Date
Anthony Axenov 0a03521a50
Исправлен бейджик build (ci)
https://github.com/badges/shields/issues/8671
2022-12-20 14:43:39 +08:00
Anthony Axenov 268134d47f
Merge pull request #27 from anthonyaxenov/dev
Миграция на php8.1
2022-12-15 11:17:20 +08:00
Anthony Axenov 6f4e756ec9
Убрал TODO из README, вместо этого просто завёл issues #7, #23, #24, #25, #26 2022-12-15 11:15:46 +08:00
Anthony Axenov cda2018aa6
Дополнение о версии в README 2022-12-15 11:02:32 +08:00
Anthony Axenov 2ab45e8c28
Мелочи по докам, TODO в README 2022-12-15 09:34:57 +08:00
Anthony Axenov c7baedae4f
Ссылки на пожертвования в README 2022-12-15 09:15:02 +08:00
Anthony Axenov c3e55f37ce
Ссылки на пожертвования в README 2022-12-15 00:30:40 +08:00
Anthony Axenov 95e3a9cac5
Подчищен composer.json
* обновлены версии основных зависимостей
* удалён myclabs/php-enum
* добавлен psalm с конфигом
* добавлен phpcs с конфигом
* добавлен php-cs-fixer с конфигом
* вероятно, будут ещё настроечные коммиты
2022-12-15 00:24:03 +08:00
Anthony Axenov 4157ab68f5
Миграция на php8.1
* enum-ы теперь enum-ы, а не говно -- теперь всё переведено на них, где это было возможно
* некоторые свойства классов объявлены в конструкторе
* некоторые классы перемещены в корневой неймспейс
* исправлен код-стайл, вычищен некоторый мусор, выправлены тесты... работы над этим продолжаются
2022-12-15 00:19:55 +08:00
Anthony Axenov 692ae43f9f
Ссылки на пожертвования 2022-12-14 23:52:39 +08:00
Anthony Axenov 45542b204a
Удалена поддержка php8.0, корректировки по README 2022-12-14 23:52:08 +08:00
Anthony Axenov b9eae25d2c
Мелочи по README 2022-08-13 21:59:01 +08:00
Anthony Axenov a72963a9ec
Обновлены зависимости 2022-08-13 21:53:03 +08:00
Anthony Axenov 6e31af95c5
Actual homepage url 2022-02-13 23:21:56 +08:00
Anthony Axenov 83a32873b3
Liberapay in funding 2022-01-30 23:01:18 +08:00
Anthony Axenov 9f4b3f2427
Liberapay shields in readme 2022-01-30 23:00:48 +08:00
Anthony Axenov aa390bf453
Merge pull request #19 from anthonyaxenov/dev
Актуализация README
2021-12-20 10:44:14 +08:00
Anthony Axenov d8f13e3054 Merge branch 'master' into dev 2021-12-20 10:43:07 +08:00
Anthony Axenov 080c7e7247
Исправлен README 2021-12-20 10:41:46 +08:00
Anthony Axenov 4bc92ac0ad
Merge pull request #17 from anthonyaxenov/dev
#16 убраны лишние проверки в тестах фискализатора
2021-12-20 00:45:24 +08:00
Anthony Axenov 9fa55e7c5f #16 убраны лишние проверки в тестах фискализатора 2021-12-19 23:07:03 +08:00
Anthony Axenov baf97cad20
Merge pull request #15 from anthonyaxenov/dev
v1.0.0
2021-12-19 23:00:14 +08:00
Anthony Axenov 2e8099e0a4 Актуализирован readme 2021-12-19 22:58:47 +08:00
Anthony Axenov d7f3c81fac Мелкофикс gh-actions 2021-12-19 22:52:21 +08:00
Anthony Axenov e22c1cb091 Финализация оставшихся классов 2021-12-19 22:50:52 +08:00
Anthony Axenov 58bc344a86 Более или менее актуальная документация 2021-12-19 22:30:21 +08:00
Anthony Axenov fdc5ab112a Переименования классов для пущей простоты 2021-12-19 22:29:53 +08:00
Anthony Axenov 71d1f2900c Большие доработки по фискилизации
- у `AtolClient` теперь возможно получить последний отправленный запрос `getLastRequest()`
- у `AtolClient::auth()` удалены аргументы за ненадобностью
- улучшен `Client::jsonSerialize()`
- исправлен `Receipt::jsonSerialize()`
- у `Receipt` и `Correction` появились методы фискализации, вкусный сахарок
- удалён енам `DocumentTypes` за ненадобностью
- исправлены тесты монитора и документов
- рабочий фискализатор с получением результатов и покрытием
2021-12-18 14:45:00 +08:00
Anthony Axenov b4cc0fec53 Мелочи по конфигу composer 2021-12-18 14:09:07 +08:00
Anthony Axenov 573af15bac Фиксы геттеров логина и пароля для `AtolClient` 2021-12-16 18:35:49 +08:00
Anthony Axenov 19653776c5 Фикс оплат и ставок в данных коррекции 2021-12-16 18:33:13 +08:00
Anthony Axenov c7d07a18f1 Мелкофиксы по кодстайлу 2021-12-12 14:50:29 +08:00
Anthony Axenov 464a8f0706 Более корректный порядок тестов 2021-12-12 14:36:04 +08:00
Anthony Axenov 294a3ef2f3 Допокрытие `ArrayAccess`-методов 2021-12-12 11:09:12 +08:00
Anthony Axenov b4af189292 Обновление зависимостей 2021-12-11 15:54:42 +08:00
Anthony Axenov 6787ce3ad7 Класс документа коррекции `Correction` с покрытием и всякая мелочёвка
- финализация Receipt + Payment
- фиксы phpdoc
2021-12-11 15:53:57 +08:00
Anthony Axenov 1d6abfd475 Более симпатичные бейджики в README 2021-12-09 20:14:35 +08:00
Anthony Axenov 058ce5ed3d Доработки коллекций, чека и тестов
- `EntityCollection` сильно упрощён, добавлен выброс исключений при пустом содержимом
- `Receipt::setItems(), setPayments() и setVats()` получили одинаковые проверки входящих данных
- округление в `Vat::setSum()`
- доработаны тесты коллекций
2021-12-09 20:14:35 +08:00
Anthony Axenov 16d1146826 Четвёртая итерация `Receipt`
- 100% покрытие
- элвисы в разных сеттерах
2021-12-08 19:04:14 +08:00
Anthony Axenov 703c5178f5 Минорные апдейты зависимостей 2021-12-08 16:11:39 +08:00
Anthony Axenov fdc64954f9 Третья итерация `Receipt`
- просчёт ставок секи в `setVats()`
- просчёт суммы чека в `setItems()`
- геттеры `getItems()` и `getVats()` возвращают пустую коллекцию, если в чеке они отсутствуют
- фикс `vats => supplier_info` в `jsonSerialize()`
- тесты поставщика, ставок, расчёта ставок и суммы чека
2021-12-08 16:01:25 +08:00
Anthony Axenov 793549aaac `Entity` теперь имплементирует `Arrayable` и `ArrayAccess` для совместимости с иммутабельными методами коллекций 2021-12-08 15:56:07 +08:00
Anthony Axenov b57acf8b05 `EntityCollection` помечен для переделки 2021-12-08 15:50:06 +08:00
Anthony Axenov b39e76f312 Вторая итерация `Receipt`
- фикс nullable-свойств и геттеров
- проверка на пустоту в `setPayments()`, `setItems()` и `setVats()`
- часть тестов с покрытием (конструктор, агент, исключения при пустых коллекциях)
2021-12-07 20:09:12 +08:00
Anthony Axenov a34a6927d1 Небольшой рефакторинг по тестам
- `BasicTestCase::assertAtolable() => assertIsAtolable()`
- генерация тестовых объектов `Vat`, `Payment` и `Item` вынесены в `BasicTestCase`
2021-12-07 20:04:03 +08:00
Anthony Axenov 1f3d5d2f3d Первая итерация `Receipt` 2021-12-06 20:07:17 +08:00
Anthony Axenov 359264db64 Новая сущность `AdditionalUserProps` с покрытием для будущей поддержки в документе 2021-12-06 19:32:50 +08:00
Anthony Axenov 557c76fefa Доработка коллекций и не только
- коллекция `Items` с покрытием
- вынос коллекций из `AtolOnline\Entities` в `AtolOnline\Collections`
- фикс ] в `AtolException`
- финализирован `CorrectionInfo`
- фиксы по тестам коллекций
- прочие мелочи по phpdoc
2021-12-06 16:14:19 +08:00
Anthony Axenov bf09641c8b Класс коллекции оплат `Payments` 2021-12-06 14:17:05 +08:00
Anthony Axenov 6b5a025051 Класс коллекции ставок НДС `Vats` 2021-12-06 14:16:50 +08:00
Anthony Axenov 3d3eba5b4e Базовый класс коллекции объектов `EntityCollection` 2021-12-06 14:16:34 +08:00
Anthony Axenov a5c88cd7d3 Функции проверки наследования классов вернул из `Helpers` в `BasicTestCase` и отрефакторил
- `isSameClass()`
- `checkImplementsInterfaces()`
- `checkUsesTraits()` + переписана под наследование
2021-12-06 14:15:47 +08:00
Anthony Axenov b451c7dc68 Мелочи по phpdoc 2021-12-06 14:13:48 +08:00
Anthony Axenov 7d0c526663 Готов класс оплаты `Payment` для будущей поддержки оплат в документе 2021-12-03 23:53:42 +08:00
Anthony Axenov 65ec639014 Округление цены и количества в сетьерах `Item` до 2 и 3 зн после запятой соответственно
Также переименованы исключения о слишком высоких цене и сумме предмета расчёта, чтобы избежать конфликтов с другими
2021-12-03 23:52:41 +08:00
Anthony Axenov d533164d1b Поддержка `correction_info` 2021-12-03 20:09:14 +08:00
Anthony Axenov 05fd25e810 Фикс `AgentInfoTest` 2021-12-03 19:06:15 +08:00
Anthony Axenov 3e03fdca61 Финализация всех `Entities` 2021-12-03 18:24:21 +08:00
Anthony Axenov c077f98cf9 `Item` - пересчёт НДС при изменении цены, количества и акциза 2021-12-03 18:24:00 +08:00
Anthony Axenov 2260233e3f Общие сеттеры-геттеры сущностей вынесены в трейты `HasEmail`, `HasInn`, `HasPhones`
Кодстайл и микрорефакторинг сущностей
2021-12-03 18:23:00 +08:00
Anthony Axenov c30c7d069f Непереработанные классы переименованы для наглядности 2021-12-03 18:16:28 +08:00
Anthony Axenov 8f235d4730 Удалён ROADMAP, поправлен README 2021-12-03 18:15:16 +08:00
Anthony Axenov 85750cd211 Обновление зависимостей
Также подключен jetbrains/phpstorm-attributes, чтобы всё по красоте
2021-12-03 18:10:06 +08:00
Anthony Axenov 2a66889e46 Поддержка `nomenclature_code` у предмета расчёта + мелкофиксы
- теперь `getSum()` проверяет по `Constraints::MAX_COUNT_ITEM_SUM` вместо `MAX_COUNT_ITEM_PRICE` (как и должен был изначально)
- подправил `TooLongException`
- всякие phpdoc-и
2021-12-03 11:52:40 +08:00
Anthony Axenov 1c0d8ba64d Корректировка readme 2021-12-02 16:14:01 +08:00
Anthony Axenov fc37580078
Merge pull request #10 from anthonyaxenov/gha
Настройка gh-actions для работы с codecov.io
2021-12-02 15:57:20 +08:00
Anthony Axenov 96137d20b2 Настройка gh-actions для работы с codecov.io 2021-12-02 15:56:32 +08:00
Anthony Axenov 267431ec28 Мелочи в readme и phpunit.xml (удалены дефолтные параметры) 2021-12-02 01:10:54 +08:00
Anthony Axenov cb24bb1fb0 Доработки енамов и тегов ФФД 2021-12-02 01:10:16 +08:00
Anthony Axenov 11646113b6 Доработки `Item`
- поддержка `excise`, покрыта тестами
- фикс `setVat()`
- улучшен `jsonSerialize()`
2021-12-02 01:09:25 +08:00
Anthony Axenov 650b46923e Бейджики в readme 2021-12-01 20:12:11 +08:00
Anthony Axenov 3f3eb68ea2 Корректировки в composer.json 2021-12-01 20:11:57 +08:00
Anthony Axenov 5ccb0e9db4 Кучад доработок, главным образом вокруг `Item`
- `Item` почти готов и весь покрыт тестами. Пока остались нереализованными `nomenclature_code` и `excise`
- `Client::setPhone()` теперь выбрасывает InvalidPhoneException
- доработка и создание новых исключений (не буду все перечислять, смотри диффы)
- мелочи по phpdoc и всяким текстовкам
2021-12-01 20:11:08 +08:00
Anthony Axenov bce21f9658 Правки по документации
Актуализирована часть про `Vat`, пересобрал README, мелочи
2021-11-28 12:01:28 +08:00
Anthony Axenov a7205ff754 Переработан `Vat`, покрыт тестами 2021-11-28 00:58:05 +08:00
Anthony Axenov e0ff5a261a Новые теги в `Ffd105Tags` + мелочи 2021-11-28 00:57:19 +08:00
Anthony Axenov 2ebb172f2e Мелочи по конвертации денег 2021-11-28 00:55:34 +08:00
Anthony Axenov e2141551d5 `Enum` стал абстрактным внутри `Enums`, наследникам созданы `getFfdTags()` 2021-11-28 00:55:28 +08:00
Anthony Axenov c9670a1321 Удалены бесполезные `ItemArray`, `VatArray` и `PaymentArray`, будут заменены коллекциями 2021-11-28 00:49:53 +08:00
Anthony Axenov 9d2617858d `AgentInfo`: перенос валидации типа агента из конструктора в сеттер 2021-11-28 00:44:43 +08:00
Anthony Axenov f548032843 Ссылки на телеграм и патреон 2021-11-27 18:53:23 +08:00
Anthony Axenov e0d792d3a4 Реализован и покрыт тестами `AgentInfo` (переименован из `Agent`) 2021-11-27 17:59:50 +08:00
Anthony Axenov 16c8d8a676 Реализован и покрыт тестами `MoneyTransferOperator` 2021-11-26 09:16:46 +08:00
Anthony Axenov 8b79b2be51 Мелкофиксы
- `Ffd105Tags::CLIENT_CONTACTS` => `CLIENT_PHONE_EMAIL`
+ мелочи по `Client`, `ClientTest` и `PayingAgentTest`
2021-11-24 18:55:53 +08:00
Anthony Axenov 95dbc3a5b7 Класс `Supplier` обзавёлся `name` + `inn` и допокрыт тестами 2021-11-24 18:54:48 +08:00
Anthony Axenov 790831e933 `MoneyTransferOperator` => `ReceivePaymentsOperator` 2021-11-24 18:45:01 +08:00
Anthony Axenov 3b75c8b983 Класс `Supplier`, покрытый тестами
Это было слишком легко :(
2021-11-24 17:57:24 +08:00
Anthony Axenov c5b57ec26d Класс `MoneyTransferOperator`, покрытый тестами
Также мелкофиксы по phpdoc `PayingAgent` и его тестам
2021-11-24 17:54:04 +08:00
Anthony Axenov 42d194116f Туча доработок
- класс `PayingAgent`, покрытый тестами
- новые константы для тегов ФФД 1.05 `Ffd105Tags`
- `Entity::jsonSerialize()` object -> array (again)
- `TooManyException::$max` int -> float
- тесты по psr-4, потому что почему бы и нет
- некоторые провайдеры вынесены в `BasicTestCase`
- улучшен тест покупателя
2021-11-24 01:30:54 +08:00
Anthony Axenov b5a01debd2 Новый енам AgentTypes 2021-11-22 14:53:10 +08:00
Anthony Axenov 9d3344a0df Мелкофиксы в конструкторе KktMonitor 2021-11-22 14:52:27 +08:00
Anthony Axenov 00b2643f42 Новые функции в Helpers для проверки классов 2021-11-22 14:52:05 +08:00
Anthony Axenov 3bf8667437 Рефакторинг исключений, новые константы ограничений
Описывать все слишком долго, TLDR:
- упрощены корневые AtolException, {BasicTooLongException => TooLongException}, {BasicTooManyException => TooManyException}
- InvalidSnoException заменён на InvalidEnumValueException
- добавлены новые константы, общий порядок изменён в соответствии с порядком упоминания в документации, ссылки на которую тоже добавлены с указанием страниц

Помимо этого, в enum-ах теперь должен быть предусмотрен метод getFfdTags()
2021-11-22 14:51:10 +08:00
Anthony Axenov e1303f4fdb Правки по документации
- в мониторинг добавлено о приведении к строке
- актуализированы Client и Company
- мелочи в соотв. классах
2021-11-21 19:06:55 +08:00
Anthony Axenov 920c08c610 Правки по документации
- добавлено про мониторинг
- убрано про `base_name` из коррекции
2021-11-21 18:58:03 +08:00
Anthony Axenov d281071970 Удалён метод `AtolOnline\Entities\Kkt::getNetworkError()` за ненадобностью 2021-11-21 18:57:20 +08:00
Anthony Axenov 95499174b0 Warning in readme 2021-11-21 00:33:43 +08:00
Anthony Axenov 6d0cac2cbc Первая итерация рефаторинга фискализатора 2021-11-21 00:19:35 +08:00
Anthony Axenov 6620acf1bf Мелкофикс теста KktMonitor::GetOne() 2021-11-21 00:17:40 +08:00
Anthony Axenov e89369348a Метод KktMonitor::auth() перенесён в AtolClient для совместимости в фискализатором 2021-11-21 00:15:59 +08:00
Anthony Axenov b35b9bfa87 Удалена поддержка base_name в документе коррекции (#5) 2021-11-20 23:46:39 +08:00
Anthony Axenov e1120051c1 Новая сущность Kkt для ответов мониторинга (#8) + мелкий рефакторинг
- enum-константы перенесены в своё пространство Enums
- новые исключения EmptyMonitorDataException + NotEnoughMonitorDataException
- KktMonitor::getAll() теперь возвращает коллекцию объектов Kkt
- KktMonitor::getOne() теперь возвращает объект Kkt
- местами актуализированы return types + phpdoc

Покрытие тестами:
- 61% исключений
- 98% AtolClient (пока хз как покрыть 208-ую строку)
- 100% KktMonitor
- 100% Kkt
- 100% Client
- 100% Company
- 100% Entity
2021-11-20 23:39:08 +08:00
Anthony Axenov 6551366d84 Полное покрытие тестами классов AtolClient + KktMonitor + половины исключений
Часть тестов завязаны на тестовый API мониторинга Атола. Иногда он закашливается и не отвечает, возможно, там рейтлимит. Да и пофиг, моки -- злейшее зло, и мне лень их писать.
2021-11-19 18:42:14 +08:00
Anthony Axenov 2c5144caac KktMonitor::getAll() теперь возвращает коллекцию объектов ККТ
При этом остаётся возможность получить полный ответ через KktMonitor::getResponse()
2021-11-19 18:29:09 +08:00
Anthony Axenov 92a2c6cc48 Переработаны существующие и дописаны новые тест AtolClient
Используется тестовый API АТОЛ Онлайн. 94% покрытие AtolClient, но ещё не закончено
2021-11-19 13:42:51 +08:00
Anthony Axenov 949b31a85a [WIP] Начало работы над тестом KktMonitorTest 2021-11-18 19:07:32 +08:00
Anthony Axenov 03591600dd Поддержка мониторинга (#8) и рефакторинг
- абстрактный класс AtolClient:
    - больше не наследуется от клиента guzzle, но содержит его объект
    - из Kkt вынесены методы, отвечающие за формирование запроса, отправку и получение ответа, в т.ч. авторизацию
- переименованы исключения TooLongKktLoginException, TooLongKktPasswordException, EmptyKktLoginException и EmptyKktPasswordException
- мелочи по AuthFailedException
- заготовки тестов AtolClient и KktMonitor
2021-11-18 12:24:44 +08:00
Anthony Axenov 77481884ad Начало работы по #5 и #6
- строгая типизация
- переработан класс `TestEnvParams`:
    - вынесен на уровень выше из под `AtolOnline\Constants`
    - вместо констант - две функции для получения актуальных параметров подключения по ФФД1.05 и ФФД1.2
- актуализированы `PaymentObjects` согласно #5
- исходники вынесены не уровень выше в `src`
- константы теперь enum через `myclabs/php-enum`
- новые константы `DocumentTypes`
- классы констант финализированы
- все исключения переименованы, а многие так или иначе отрефакторены (не полностью)
- новые исключения `InvalidSnoException`, `InvalidPaymentAddressException`
- `helpers.php` стал полноценным классом `Helpers`
- удалены трейты `HasEmail`, `HasInn`, `RublesKopeksConverter` (конвертация перенесена в `Helpers`)
- удалён хелпер `valid_strlen()`, вместо него теперь везде `mb_strlen()`
- сущности `Client` и `Company` получили свои имплементации для `email` и `inn`
- доработки в `BasicTestCase`
- полное покрытие тестами: `Client`, `Company`, `Helpers`
- поправлен `phpunit.xml`
- везде обновлены копирайты
- актуализированы и исправлены phpdoc, return types
- начато введение `strict_types=1`
- минимальный php теперь 8.0
- обновлены все зависимости
- подключен пакет коллекций laravel для будущего использования
- теперь можно `composer test` и `composer test-cov`
2021-11-18 12:24:30 +08:00
Anthony Axenov cc944a1dbb Бейджи в README 2021-05-25 11:48:37 +08:00
Anthony Axenov bb05f0c752 Merge branch 'master' into dev 2021-05-25 00:58:53 +08:00
Anthony Axenov 1c76608468 Github Actions разграничены для веток master и dev 2021-05-24 23:40:46 +08:00
Anthony Axenov bd6e208216 Обновление composer.json и первичных текстовок 2021-05-24 23:00:34 +08:00
Anthony Axenov 7b4411ec01 Обновление зависимостей 2021-05-24 22:45:03 +08:00
Anthony Axenov 23fa1f7eb9
Update FUNDING.yml 2021-05-24 12:38:39 +08:00
Anthony Axenov 929bf84c97
Merge pull request #3 from komantnick/develop
Исправление багов при отправке сырого JSON для чека коррекции с исправлениями
2021-05-24 10:55:09 +08:00
Nikita Saiapin 5c1c4dba12 Фикс размера НДС 2021-05-23 22:35:45 +08:00
Nikita Saiapin 4c40bebe14 Исправление багов при отправке сырого JSON чека коррекции 2021-05-22 21:48:12 +08:00
Anthony Axenov d321205ac9 Фикс выброса AtolAuthFailedException 2020-10-13 02:06:13 +08:00
Anthony Axenov 387e6e445f Удалён сервер discord и его упоминание в README 2020-06-14 13:52:07 +08:00
Anthony Axenov 3bd043bde7 Kkt::setCallbackUrl() - Фикс проверки callback_url по регулярке 2020-06-14 13:44:35 +08:00
Anthony Axenov 7c8ee85b89 Фикс регулярки callback_url 2020-06-14 13:44:04 +08:00
Anthony Axenov aab68646e6 Фикс сообщений в исключениях 2020-06-14 13:43:32 +08:00
Anthony Axenov e1fb74ac01 Доудалено упоминание о Schemas 2020-06-07 20:28:14 +08:00
192 changed files with 16642 additions and 6610 deletions

7
.github/FUNDING.yml vendored
View File

@ -1,3 +1,4 @@
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # Replace with a single Patreon username
custom: ['https://money.yandex.ru/to/41001685237530']
custom: [
'https://www.buymeacoffee.com/axenov',
'https://yoomoney.ru/to/41001685237530'
]

48
.github/workflows/ci.yml vendored 100644
View File

@ -0,0 +1,48 @@
# https://github.com/shivammathur/setup-php/blob/master/README.md
name: CI
on:
push:
branches: [ master, dev ]
pull_request:
branches: [ dev ]
jobs:
Tests:
runs-on: ubuntu-latest
strategy:
matrix:
php-version:
- 8.1
steps:
- uses: actions/checkout@v2
- name: Cache Composer dependencies
uses: actions/cache@v2
with:
path: ~/.composer/cache
key: php${{ matrix.php-version }}-composer-${{ matrix.dependencies }}-${{ hashFiles('**/composer.json') }}
restore-keys: php${{ matrix.php-version }}-composer-${{ matrix.dependencies }}-
- name: Setup PHP v ${{ matrix.php-version }}
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-version }}
extensions: json, curl, tokenizer, mbstring, xdebug
coverage: xdebug
tools: composer, phpunit
- name: Install dependencies with composer
run: composer install --no-ansi --no-interaction --no-progress
- name: Run tests with phpunit/phpunit
run: vendor/bin/phpunit --coverage-clover=coverage.xml --configuration=phpunit.xml
- name: Send code coverage report to Codecov.io
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
run: bash <(curl -s https://codecov.io/bash) || true

View File

@ -1,26 +0,0 @@
name: Build
on:
push:
branches:
- master
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- name: Validate composer.json and composer.lock
run: composer validate
- name: Install composer dependencies
run: composer install --prefer-dist --no-progress --no-suggest
- name: Run phpunit tests
uses: php-actions/phpunit@v1.0.0
with:
# Configuration file location
config: ./phpunit.xml

1
.gitignore vendored
View File

@ -5,3 +5,4 @@
/config.php
*cache*
/test.php
/.coverage-report

25
.php-cs-fixer.php 100644
View File

@ -0,0 +1,25 @@
<?php
/*
* Copyright (c) 2020-2021 Антон Аксенов (Anthony Axenov)
*
* This code is licensed under MIT.
* Этот код распространяется по лицензии MIT.
* https://github.com/anthonyaxenov/atol-online/blob/master/LICENSE
*/
declare(strict_types=1);
$finder = PhpCsFixer\Finder::create()
->in(__DIR__)
->exclude('docs')
->notPath('test.php');
return (new PhpCsFixer\Config())
->setFinder($finder)
->setIndent(' ')
->setRules([
'@PSR12' => true,
'array_syntax' => ['syntax' => 'short'],
'group_to_single_imports' => false,
]);

138
README.md
View File

@ -1,116 +1,78 @@
# АТОЛ Онлайн
![Build](https://github.com/anthonyaxenov/atol-online/workflows/Build/badge.svg?branch=master)
Библиотека для фискализации чеков по 54-ФЗ через [облачные ККТ АТОЛ](https://online.atol.ru/).
Библиотека для фискализации чеков по 54-ФЗ через [облачную ККТ АТОЛ](https://online.atol.ru/).
Текущая поддерживаемая версия API: **5.1**
[![GitHub Workflow Status (master)](https://img.shields.io/github/actions/workflow/status/anthonyaxenov/atol-online/ci.yml?branch=master&logo=github)](https://github.com/anthonyaxenov/atol-online/actions/workflows/ci.yml)
[![codecov](https://codecov.io/gh/anthonyaxenov/atol-online/branch/master/graph/badge.svg?token=WR2IV7FTF0)](https://codecov.io/gh/anthonyaxenov/atol-online)
[![Stable Version](https://img.shields.io/packagist/v/axenov/atol-online?label=stable)](https://packagist.org/packages/axenov/atol-online)
[![Packagist PHP Version Support](https://img.shields.io/packagist/php-v/axenov/atol-online?color=%23787cb4)](https://packagist.org/packages/axenov/atol-online)
[![License](https://img.shields.io/packagist/l/axenov/atol-online?color=%23369883)](LICENSE)
[![buymeacoffee](https://img.shields.io/badge/-buy_me_a%C2%A0coffee-gray?logo=buy-me-a-coffee)](https://www.buymeacoffee.com/axenov)
**[Документация](/docs/readme.md)**
Текущие поддерживаемые версии АТОЛ Онлайн:
| Протокол | API | ФФД | Статус |
|----------|------|------|----------------|
| v4 | 5.10 | 1.05 | Поддерживается |
| v5 | 3.0 | 1.2 | В планах |
Поддерживаемые возможности:
* Мониторинг ККТ и ФН
* Фискализация документов на облачной ККТ
* Валидация данных до отправки документа на ККТ (насколько это возможно, согласно схеме)
* Расчёты денег в копейках
* PSR-4 автозагрузка, покрытие настоящими тестами, fluent-setters, докблоки
## Системные требования
* PHP 7.2+
* composer
* php-json
* `php v8.1` и выше
* `php-json`
* `php-mbstring`
* [composer](https://getcomposer.org/)
> Для использования на php8.0 используйте версии библиотеки до 1.0.2 включительно.
## Начало работы
### Подключение библиотеки
1. Подключить пакет к вашему проекту:
1. Подключить пакет к проекту:
```bash
composer require axenov/atol-online
```
2. В нужном месте проекта объявить параметры, используя константы, и подключить **composer**, если это не сделано ранее:
2. В нужном месте проекта подключить автозагрузчик composer-зависимостей, если это не сделано ранее:
```php
require($project_root.'/vendor/autoload.php');
require($project_root . '/vendor/autoload.php');
```
где `$project_root` — абсолютный путь к корневой директории вашего проекта.
> При использовании фреймворков это обычно не требуется.
### Использование библиотеки
Вы имеете право использовать и распространять код на условиях **[лицензии MIT](LICENSE)**.
Дополнительная информация может быть найдена здесь:
1. [Документации к библиотеке](/docs)
2. [Документация АТОЛ Онлайн](https://online.atol.ru/lib/)
3. [Исходный код](/src), докблоки
4. [Тесты](/tests/AtolOnline/Tests)
### Тестирование кода библиотеки
Файлы тестов находятся в директории `/tests` корня репозитория.
Для запуска тестов необходимо перейти в корень вашего проекта и выполнить команду:
Для запуска тестов необходимо перейти в корень репозитория и выполнить одну из команд:
```bash
./vendor/bin/phpunit
composer psalm # статический анализ
composer phpcs # синтаксический анализ
composer test # полное тестирование без покрытия
composer coverage # полное тестирование с покрытием
```
## Настройка ККТ
Для работы с облачной ККТ необходимы следующие параметры:
* логин;
* пароль;
* код группы.
Чтоб получить их, нужно:
1. авторизоваться на [online.atol.ru](https://online.atol.ru/lk/Account/Login);
2. на странице [Мои компании](https://online.atol.ru/lk/Company/List) нажать кнопку **Настройки интегратора**.
Скачается XML-файл с нужными настройками.
Также для работы потребуются:
* ИНН продавца;
* URL места расчёта (ссылка на ваш интернет-сервис).
## Использование библиотеки
### Доступные методы и классы
Весь исходный код находится в директории [`/src`](/src).
**Комментарии phpDoc есть буквально к каждому классу, методу или полю.
Прокомментировано вообще всё.**
1. Обращайтесь к [документации](/docs).
2. Обращайтесь к [исходному коду](/src).
3. Обращайтесь к [тестам](/test).
4. **Используйте подсказки вашей IDE.**
Тогда у вас не возникнет затруднений.
Для тех, кто решил подробно разобраться в работе библиотеки, отдельно отмечу нюансы, которые могут ускользнуть от внимания:
1. Класс `AtolOnline\Api\Kkt` унаследован от `GuzzleHttp\Client` со всеми вытекающими;
2. Все классы, унаследованные от `AtolOnline\Entities\AtolEntity` приводятся к JSON-строке.
### Общий алгоритм
1. Задать настройки ККТ
2. Собрать данные о покупателе
3. Собрать данные о продавце
4. Собрать данные о предметах расчёта (товары, услуги и пр.)
5. Создать документ, добавив в него покупателя, продавца и предметы расчёта
6. Отправить документ на регистрацию:
6.1. *Необязательно:* задать `callback_url`, на который АТОЛ отправит HTTP POST о состоянии документа.
7. Запомнить `uuid` из пришедшего ответа, поскольку он пригодится для последующей проверки статуса фискализации.
> Если с документом был передан `callback_url`, то ответ придёт на этот самый URL.
Если с документом **НЕ** был передан `callback_url` **либо** callback от АТОЛа не пришёл в течение 300 секунд (5 минут), нужно запрашивать вручную по `uuid`, пришедшему от АТОЛа в ответ на регистрацию документа.
8. Проверить состояние документа (нет необходимости, если передавался `callback_url`):
8.1. взять `uuid` ответа, полученного на запрос фискализации;
8.2. отправить его в запросе состояния документа.
> Данные о документе можно получить только в течение 32 суток после успешной фискализации.
## Об отправке электронного чека покупателю
После успешной фискализации документа покупатель автоматически получит уведомление **от ОФД**, который используется в связке с вашей ККТ:
* **по email**, если в документе указан email клиента;
* **по смс**:
* если в документе указан номер телефона клиента;
* если на стороне ОФД необходима и подключена услуга СМС-информирования (уточняйте подробности о своего ОФД).
> Если заданы email и телефон, то отдаётся приоритет email.
## Дополнительные ресурсы
**Discord-сервер для обсуждения этой библиотеки: [discord.gg/mFYTQmp](https://discord.gg/mFYTQmp)**
Функционал, находящийся в разработке: [ROADMAP.md](ROADMAP.md)
Официальные ресурсы АТОЛ Онлайн:
* **[Вся документация](https://online.atol.ru/lib/)**
* Telegram-канал: [@atolonline](https://t.me/atolonline)
## Лицензия
Вы имеете право использовать код из этого репозитория только на условиях **[лицензии MIT](LICENSE)**.
После тестирования с покрытием в корне репозитория создаётся отчёт, который сохраняется в директории `.coverage`.
Для тестирования с покрытием необходим `php-xdebug` с параметром `xdebug.mode = coverage,...`.

View File

@ -1,80 +0,0 @@
# Roadmap
Здесь перечислены реализованные функции и находящиеся в разработке.
Порядок их упоминания здесь может не совпадать с порядком реализации.
Эталонная реализация подразумевает полную поддержку всех методов API и обеих схем документов:
* [Документы прихода, возврата прихода, расхода, возврата расхода](https://online.atol.ru/possystem/v4/schema/sell)
* [Документы коррекции прихода, коррекции расхода](https://online.atol.ru/possystem/v4/schema/correction)
## Общий функционал библиотеки
- [x] Переключение настроек доступа к ККТ при переключении тестового режима
- [x] Тесты для класса налоговой ставки (+ массив)
- [ ] Тесты для класса оплаты (+ массив)
- [x] Тесты для класса предмета расчёта (+ массив)
- [x] Тесты для класса клиента
- [x] Тесты для класса компании
- [ ] Тесты для класса данных коррекций
- [ ] Тесты для класса документа
- [ ] Тесты для класса ответа ККТ
- [ ] Тесты для регистрации документа прихода
- [ ] Тесты для регистрации документа возврата прихода
- [ ] Тесты для регистрации документа коррекции прихода
- [ ] Тесты для регистрации документа расхода
- [ ] Тесты для регистрации документа возврата расхода
- [ ] Тесты для регистрации документа коррекции расхода
- [ ] Вообще все расчёты вообще везде должны быть строго в копейках. Рубли (дроби) должны быть только в JSON-представлениях
## Поддержка методов API (регистрация документов)
- [x] приход
- [x] расход
- [x] возврат прихода
- [x] возврат расхода
- [x] коррекция прихода
- [x] коррекция расхода
- [x] проверка статуса документа
## Документы прихода, возврата прихода, расхода, возврата расхода
- [x] Пoддержка `receipt.client` (обязательный)
- [x] Пoддержка `receipt.company` (обязательный)
- [x] Пoддержка `receipt.items` (обязательный)
- [x] Пoддержка `receipt.total` (обязательный)
- [x] Пoддержка `receipt.payments` (обязательный)
- [x] Пoддержка `receipt.vats`
- [ ] Пoддержка `receipt.additional_check_props`
- [x] Пoддержка `receipt.cashier`
- [ ] Пoддержка `receipt.additional_user_props`
- [ ] Пoддержка `receipt.agent_info`
- [ ] Пoддержка `receipt.supplier_info`
- [ ] Пoддержка `receipt.items.agent_info`
- [ ] Пoддержка `receipt.items.supplier_info`
- [ ] Пoддержка `receipt.items.nomenclature_code`
- [ ] Пoддержка `receipt.items.excise`
- [ ] Пoддержка `receipt.items.country_code`
- [ ] Пoддержка `receipt.items.declaration_number`
## Документы коррекции прихода, коррекции расхода
- [x] Пoддержка `correction.company` (обязательный)
- [x] Пoддержка `correction.vats` (обязательный)
- [x] Пoддержка `correction.correction_info` (обязательный)
- [x] Пoддержка `correction.cashier`
## Не будут реализовываться
### Валидация генерируемых документов согласно актуальной схемы API
- Валидатор схемы для документов прихода, возврата прихода, расхода, возврата расхода
- Валидатор схемы для документов коррекции прихода, коррекции расхода
1. Отказ обусловлен скоростью выполнения.
Базовая реализация, которая была начата, подразумевала синглтон, который кешировал однажды полученную схему.
Практика показала, что этот единичный запрос может существенно тормозить работу сервера и в течение долгого времени
не отдавать ответ клиенту.
2. Такая валидация подходит в том случае, если бы при разработке использовалась концепция IoC.
До версии пакета 2.0.0 таких серьёзных имзенений не планируется.

View File

@ -1,54 +1,84 @@
{
"name": "axenov/atol-online",
"description": "Library to use cloud cash register in e-commerce according to Russian Federal Law #54",
"description": "Библиотека для работы с API АТОЛ Онлайн (облачные ККТ для приёма платежей по 54-ФЗ)",
"license": "MIT",
"type": "library",
"keywords": [
"54-fz",
"kkt",
"e-commerce",
"cash",
"cash register",
"payment",
"payment system",
"api",
"чек",
"atol",
"atol-online"
"атол",
"payment",
"e-commerce",
"atol online",
"атол онлайн",
"atol-online",
"атол-онлайн",
"фискализация",
"fiscalization",
"payment system"
],
"homepage": "https://github.com/anthonyaxenov/atol-online",
"readme": "https://github.com/anthonyaxenov/atol-online/blob/master/README.md",
"authors": [
{
"name": "Anthony Axenov",
"homepage": "http://anthonyaxenov.ru",
"homepage": "https://axenov.dev",
"email": "anthonyaxenov@gmail.com"
}
],
"support": {
"rss": "https://github.com/anthonyaxenov/atol-online/discussions/categories/announcements",
"chat": "https://github.com/anthonyaxenov/atol-online/discussions",
"source": "https://github.com/anthonyaxenov/atol-online",
"issues": "https://github.com/anthonyaxenov/atol-online/issues",
"chat": "https://discord.gg/mFYTQmp"
"docs": "https://github.com/anthonyaxenov/atol-online/blob/master/docs/readme.md"
},
"funding": [
{
"type": "Yoomoney",
"url": "https://yoomoney.ru/to/41001685237530"
},
{
"type": "Buy Me a Coffee",
"url": "https://www.buymeacoffee.com/axenov"
}
],
"require": {
"php": ">=7.2",
"php": ">=8.1",
"ext-json": "*",
"guzzlehttp/guzzle": "^6.5",
"psr/log": "^1.1",
"ramsey/uuid": "^3.9"
"ext-mbstring": "*",
"guzzlehttp/guzzle": "^7.5",
"psr/log": "^3",
"ramsey/uuid": "^4.6",
"illuminate/collections": "^v9.43",
"jetbrains/phpstorm-attributes": "^1.0"
},
"require-dev": {
"phpunit/phpunit": "^8.5"
"friendsofphp/php-cs-fixer": "^3.13",
"phpunit/phpunit": "^9.5",
"psalm/plugin-phpunit": "^0.18",
"squizlabs/php_codesniffer": "^3.7",
"vimeo/psalm": "^4.30"
},
"autoload": {
"psr-4": {
"AtolOnline\\": "src/"
}
},
"autoload-dev": {
"classmap": [
"src/AtolOnline/Api/",
"src/AtolOnline/Api/Schemas/",
"src/AtolOnline/Exceptions/",
"src/AtolOnline/Entities/",
"src/AtolOnline/Traits/",
"src/AtolOnline/Constants/",
"tests/"
],
"files": [
"src/helpers.php"
]
},
"scripts": {
"phpcs": "vendor/bin/phpcs",
"psalm": "vendor/bin/psalm",
"test": "vendor/bin/phpunit --colors=always",
"coverage": "php -dxdebug.mode=coverage vendor/bin/phpunit --coverage-html .coverage"
},
"config": {
"optimize-autoloader": true,
"sort-packages": true
}
}

5153
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,83 +0,0 @@
# Работа с клиентами (покупателями)
[Вернуться к содержанию](readme.md)
---
Объект покупателя инициализируется следующим образом:
```php
$customer = new AtolOnline\Entities\Client();
```
У объекта покупателя могут быть указаны любые из следующих атрибутов:
* email (тег ФФД 1008);
* ИНН (тег ФФД 1128);
* наименование (тег ФФД 1127);
* номер телефона (тег ФФД 1008).
> Все эти атрибуты являются **необязательными**.
> Если указаны одновременно и email, и номер телефона, то ОФД отправит чек только на email.
Указать эти атрибуты можно двумя способами:
```php
// 1 способ - через конструктор
$customer = new AtolOnline\Entities\Client(
'John Doe', // наименование
'+1/22/99*73s dsdas654 5s6', // номер телефона +122997365456
'john@example.com', // email
'+fasd3\qe3fs_=nac990139928czc' // номер ИНН 3399013928
);
// 2 способ - через сеттеры
$customer = (new AtolOnline\Entities\Client())
->setEmail('john@example.com')
->setInn('+fasd3\q3fs_=nac9901 3928c-c') // 3399013928
->setName('John Doe')
->setPhone('+1/22/99*73s dsdas654 5s6'); // +122997365456
// либо комбинация этих способов
```
Метод `setEmail()` проверяет входную строку на длину (до 64 символов) и валидность формата email.
Выбрасывает исключения:
* `AtolEmailTooLongException` (если слишком длинный email);
* `AtolEmailValidateException` (если email невалиден).
Метод `setInn()` чистит входную строку от всех символов, кроме цифр, и проверяет длину (либо 10, либо 12 цифр).
Выбрасывает исключение `AtolInnWrongLengthException` (если длина ИНН некорректна).
Метод `setName()` проверяет входную строку на длину (до 256 символов).
Выбрасывает исключение `AtolNameTooLongException` (если слишком длинное имя).
Метод `setPhone()` чистит входную строку от всех символов, кроме цифр и знака `+`, и проверяет длину (до 64 символов).
Выбрасывает исключение `AtolPhoneTooLongException` (если слишком длинный номер телефона).
Конструктор может выбрасывать любое из указанных выше исключений, если в него передаются значения.
Получить установленные значения атрибутов можно через геттеры:
```php
$customer->getInn();
$customer->getEmail();
$customer->getName();
$customer->getPhone();
```
Объект класса приводится к JSON-строке автоматически или принудительным приведением к `string`:
```php
echo $customer;
$json_string = (string)$customer;
```
Чтобы получить те же данные в виде массива, нужно вызвать метод `jsonSerialize()`:
```php
$json_array = $customer->jsonSerialize();
```
---
[Вернуться к содержанию](readme.md)

19
docs/collection.md 100644
View File

@ -0,0 +1,19 @@
# Коллекция сущностей
[Вернуться к содержанию](readme.md#toc)
---
Коллекциями являются объекты, способные хранить в себе [сущности](entity.md).
Они унаследованы от `Illuminate/Support/Collection` и полностью поддерживают все
[стандартные методы](https://laravel.com/docs/master/collections) коллекций Laravel.
Помимо этого, они валидируют количество и вид сущностей, которые могут хранить в себе, согласно схеме API АТОЛ Онлайн.
Коллекции ведут себя аналогично самим сущностям в части приведения к массивам и json-ификации.
---
Читай также: [Сущность](entity.md)
[Вернуться к содержанию](readme.md#toc)

View File

@ -1,80 +0,0 @@
# Работа с компанией (продавцом)
[Вернуться к содержанию](readme.md)
---
Объект компании инициализируется следующим образом:
```php
$customer = new AtolOnline\Entities\Company();
```
У объекта компании должны быть указаны все следующие атрибуты:
* email (тег ФФД 1117);
* ИНН (тег ФФД 1018);
* тип системы налогообложения (тег ФФД 1055) - все типы перечислены в классе `AtolOnline\Constants\SnoTypes`;
* адрес места расчётов (тег ФФД 1187) - для интернет-сервисов указывается URL с протоколом.
> Все эти атрибуты являются **обязательными**.
> Для тестового режима используйте значения ИНН и адреса места расчётов, [указанные здесь](https://online.atol.ru/files/ffd/test_sreda.txt).
Указать эти атрибуты можно двумя способами:
```php
// 1 способ - через конструктор
$company = new AtolOnline\Entities\Company(
AtolOnline\Constants\SnoTypes::OSN, // тип СНО
'5544332219', // номер ИНН
'https://v4.online.atol.ru', // адрес места расчётов
'company@example.com' // email
);
// 2 способ - через сеттеры
$company = (new AtolOnline\Entities\Company())
->setEmail('company@example.com')
->setInn('5544332219')
->setSno(AtolOnline\Constants\SnoTypes::USN_INCOME)
->setPaymentAddress('https://v4.online.atol.ru');
// либо комбинация этих способов
```
Метод `setEmail()` проверяет входную строку на длину (до 64 символов) и валидность формата email.
Выбрасывает исключения:
* `AtolEmailTooLongException` (если слишком длинный email);
* `AtolEmailValidateException` (если email невалиден).
Метод `setInn()` чистит входную строку от всех символов, кроме цифр, и проверяет длину (либо 10, либо 12 цифр).
Выбрасывает исключение `AtolInnWrongLengthException` (если длина ИНН некорректна).
Метод `setPaymentAddress()` проверяет длину (до 256 символов).
Выбрасывает исключение `AtolPaymentAddressTooLongException` (если слишком длинный адрес места расчётов).
Конструктор может выбрасывать любое из указанных выше исключений, если в него передаются параметры.
Получить установленные значения параметров можно через геттеры:
```php
$company->getInn();
$company->getEmail();
$company->getPaymentAddress();
$company->getSno();
```
Объект класса приводится к JSON-строке автоматически или принудительным приведением к `string`:
```php
echo $company;
$json_string = (string)$company;
```
Чтобы получить те же данные в виде массива, нужно вызвать метод `jsonSerialize()`:
```php
$json_array = $company->jsonSerialize();
```
---
[Вернуться к содержанию](readme.md)

View File

@ -1,66 +0,0 @@
# Работа с данными коррекции
[Вернуться к содержанию](readme.md)
---
Объект для данных коррекции инициализируется следующим образом:
```php
$info = new AtolOnline\Entities\CorrectionInfo();
```
У объекта должны быть указаны все следующие обязательные атрибуты:
* тип коррекции (тег ФФД 1173) - все типы перечислены в классе `AtolOnline\Constants\CorrectionTypes`;
* дата документа основания для коррекции в формате `d.m.Y` (тег ФФД 1178);
* номер документа основания для коррекции (тег ФФД 1179);
* описание коррекции (тег ФФД 1177).
Указать эти атрибуты можно двумя способами:
```php
use AtolOnline\{Entities\CorrectionInfo, Constants\CorrectionTypes};
// 1 способ - через конструктор
$info = new CorrectionInfo(
CorrectionTypes::SELF, // тип коррекции
'01.01.2019', // дата документа коррекции
'12345', // номер документа коррекции
'test' // описание коррекции
);
// 2 способ - через сеттеры
$info = (new CorrectionInfo())
->setType(CorrectionTypes::INSTRUCTION)
->setDate('01.01.2019')
->setName('test')
->setNumber('9999');
// либо комбинация этих способов
```
Получить установленные значения атрибутов можно через геттеры:
```php
$info->getType();
$info->getDate();
$info->getName();
$info->getNumber();
```
Объект класса приводится к JSON-строке автоматически или принудительным приведением к `string`:
```php
echo $customer;
$json_string = (string)$customer;
```
Чтобы получить те же данные в виде массива, нужно вызвать метод `jsonSerialize()`:
```php
$json_array = $customer->jsonSerialize();
```
---
[Вернуться к содержанию](readme.md)

View File

@ -1,167 +0,0 @@
# Работа с документами
[Вернуться к содержанию](readme.md)
---
Объект документа инициализируется следующим образом:
```php
$doc = new AtolOnline\Entities\Document();
```
Для документов **прихода, возврата прихода, расхода и возврата расхода** должны быть указаны все следующие обязательные атрибуты:
* [клиент](/docs/client.md);
* [компания](/docs/company.md);
* [предметы расчёта](/docs/items.md);
* [оплаты](/docs/payments.md).
Для документов **коррекции прихода и коррекции расхода** должны быть указаны все следующие обязательные атрибуты:
* [компания](/docs/company.md);
* [оплаты](/docs/payments.md);
* [ставки НДС](/docs/vats.md);
* [данные коррекции](/docs/correction_info.md).
Для любых документов также могут быть указаны следующие необязательные атрибуты:
* ФИО кассира (тег ФФД - 1021).
Установка атрибутов документа происходит через сеттеры.
## Работа с клиентом
Для этого существуют следующие методы:
```php
$doc->setClient($client);
$doc->getClient();
```
> О работе с клиентами более подробно читайте [здесь](/docs/client.md).
## Работа с компанией
Для этого существуют следующие методы:
```php
$doc->setCompany($company);
$doc->getCompany();
```
> О работе с компаниями более подробно читайте [здесь](/docs/company.md).
## Работа с предметами расчёта
Внутри документа существует [массив предметов расчёта](/docs/items.md#array).
По умолчанию он пуст.
Напрямую для манипуляций объект массива недоступен.
Работа с ним происходит через методы документа:
```php
$doc->setItems([$item1, $item2]);
$doc->addItem($item3);
$doc->getItems();
```
Соответственно, эти методы выбрасывают те же исключения, что методы самого массива.
## Работа с оплатами
Внутри документа существует [массив оплат](/docs/payments.md#array).
По умолчанию он пуст.
Напрямую для манипуляций объект массива недоступен.
Работа с ним происходит через методы документа:
```php
$doc->setPayments([$payment1, $payment2]);
$doc->addPayment($payment3);
$doc->getPayments();
```
Соответственно, эти методы выбрасывают те же исключения, что методы самого массива.
Следует отметить, что если при выполнении метода `addPayment()` выполняются следующие условия:
* аргументом передан объект оплаты, у которого не задана сумма,
* ранее документу не задавались оплаты,
то автоматически этому объекту оплаты задаётся полная сумма чека.
## Работа со ставками НДС
Внутри документа существует [массив ставок НДС](/docs/vats.md#array).
По умолчанию он пуст.
Напрямую для манипуляций объект массива недоступен.
Работа с ним происходит через методы документа:
```php
$doc->setVats([$vat1, $vat2]);
$doc->addVat($vat3);
$doc->getVats();
```
Соответственно, эти методы выбрасывают те же исключения, что методы самого массива.
Также существует метод `clearVats()`, который удаляет все вложенные объекты ставок НДС - из предметов расчёта и самого документа.
Следует отметить, что если при выполнении метода `addVat()` выполняются следующие условия:
* аргументом передан объект ставки, у которого не задана сумма,
* ранее документу не задавались ставки,
то автоматически этому объекту налога задаётся полная сумма чека, от которой расчитывается итоговый размер налога.
## Общая сумма документа
Расчёт происходит автоматически в следующих случаях:
* изменение предметов расчёта (`setItems()`, `addItem()`);
* добавление оплат (`addPayment()` в случае, когда оплата передана без суммы);
* изменение ставок НДС (`setVats()`, `clearVats()`, `addVat()` в случае, когда ставка передана без суммы);
* приведение объекта документа к строке.
Также можно вызвать вручную метод `calcTotal()`.
Он расчитывает полную сумму чека по предметам расчёта и пересчитывает **все** налоговые ставки.
Получить итог можно с помощью метода `getTotal()`.
Всё в рублях.
<a name='correction'></a>
## Работа с данными коррекции
Если документ создаётся с целью коррекции прихода или расхода, то он обязательно должен содержать [данные коррекции](/docs/correction_info.md).
Задать и получить эти данные очень просто:
```php
$doc->setCorrectionInfo(new AtolOnline\Entities\CorrectionInfo(
AtolOnline\Constants\CorrectionTypes::SELF, // тип коррекции
'01.01.2019', // дата документа коррекции
'12345', // номер документа коррекции
'test' // описание коррекции
));
$doc->getCorrectionInfo();
```
## Работа с кассиром
Для этого существуют следующие методы:
```php
$doc->setCashier('Иванова Лариса Васильевна');
$doc->getCashier();
```
## Прочее
Объект класса приводится к JSON-строке автоматически или принудительным приведением к `string`:
```php
echo $doc;
$json_string = (string)$doc;
```
Чтобы получить те же данные в виде массива, нужно вызвать метод `jsonSerialize()`:
```php
$json_array = $doc->jsonSerialize();
```
---
[Вернуться к содержанию](readme.md)

52
docs/entity.md 100644
View File

@ -0,0 +1,52 @@
# Сущность
[Вернуться к содержанию](readme.md#toc)
---
Сущностями являются все классы, которые необходимы для взаимодействия с API.
Они находятся в директори `src/Entities` и расширяют абстрактный класс `AtolOnline\Entities\Entity`.
Каждая сущность содержит в себе только те данные, которые необходимы согласно схемы API АТОЛ Онлайн.
Ниже перечислены возможности сущностей.
## Приведение к строке JSON
```php
echo $entity;
$json_string = (string)$entity;
```
## Приведение к массиву
```php
// результат идентичен
$json_array1 = $entity->jsonSerialize();
$json_array2 = $entity->toArray();
```
## Чтение из массива
```php
$var = new \AtolOnline\Entities\Client('Иванов Иван');
echo $var['name']; // 'Иванов Иван'
$var['name'] = 'Петров Пётр'; // BadMethodCallException
```
## Fluent-сеттеры
Реализованы на уровне конкретных классов сущностей, но у некоторых могут полностью отсуствовать.
```php
$entity->setFoo($value)->setBar('bar')->...
```
Сеттеры валидируют и фильтруют данные согласно схеме АТОЛ Онлайн API, а в случае ошибочных значений -- выбрасывают
исключения.
---
Читай также: [Коллекция сущностей](collection.md)
[Вернуться к содержанию](readme.md#toc)

237
docs/fiscalizing.md 100644
View File

@ -0,0 +1,237 @@
# Фискализация документов
[Вернуться к содержанию](readme.md#toc)
---
## Доступ к ККТ
Для работы с облачной ККТ необходимы следующие параметры:
* логин;
* пароль;
* код группы.
Чтоы получить их, нужно:
1. авторизоваться в личном кабинете [online.atol.ru](https://online.atol.ru/lk/Account/Login);
2. на странице [Мои компании](https://online.atol.ru/lk/Company/List) нажать кнопку **Настройки интегратора**.
Скачается XML-файл с нужными настройками.
Также для работы потребуются:
* ИНН продавца;
* URL места расчёта (ссылка на ваш интернет-сервис).
## Использование
Объект ККТ инициализируется следующим образом:
```php
$kkt = new AtolOnline\Api\Fiscalizer();
```
Установить параметры подключения можно двумя путями:
```php
use AtolOnline\Api\Fiscalizer;
// 1 способ - через конструктор
$kkt = new Fiscalizer(group: 'mygroup', login: 'mylogin', password: 'mypassword');
// 2 способ - через сеттеры
$kkt = (new Fiscalizer())
->setLogin($login)
->setGroup($group)
->setPassword($password);
```
<a name="testmode"></a>
## Тестовый режим
По умолчанию фискализатор создаётся для работы в тестовом режиме. Это означает, что работа с АТОЛ Онлайн API будет
происходить [в тестовой среде](https://online.atol.ru/files/ffd/test_sreda.txt).
> Под тестовым режимом работы подразумевается использование тестовых ККТ, которые принадлежат компании АТОЛ.
Управление тестовым режимом происходит следующим образом:
```php
$kkt = new Fiscalizer(); // включен по умолчанию
$kkt = new Fiscalizer(false); // выключен явно
$kkt->setTestMode(); // включен явно
$kkt->setTestMode(true); // включен явно
$kkt->setTestMode(false); // выключен явно
```
**При включенном тестовом режиме используются тестовые ККТ**, т.к. перед отправкой запроса подменяются:
* логин;
* пароль;
* группа ККТ;
* ИНН клиента (покупателя);
* ИНН и адрес места расчётов компании (продавца).
Таким образом:
* использовать тестовый режим -- безопасно;
* при переключении тестового режима устанавливать заново свои параметры подключения не требуется.
**При выключенном тестовом режиме используются ваши ККТ.**
Если по каким-то причинам у вас не получится использовать тестовый режим, вы можете проводить свои тесты в боевом
режиме (на собственной ККТ). В этом случае важно понимать следующее:
1. сразу после оформления документа **прихода** необходимо оформлять точно такой же документ **возврата прихода**;
2. [вы обязательно забудете о пункте 1](http://murphy-law.net.ru/basics.html);
3. пп. 1 и 2 в любом случае скажутся на ваших финансовых отчётах;
4. вся ответственность за пп. 1-3 и последствия ложится только на вас.
## Авторизация на ККТ
Перед первым запросом на ККТ происходит аутентификация на сервере по логину и паролю. В ответ приходит авторизационный
токен, срок жизни коего равен **24 часам**. После первой успешной операции возможно получить этот токен следующим
образом:
```php
$kkt->getToken(); // вернёт строку длиной 128 символа
```
Этот токен можно сохранить и переиспользовать в течение всего срока его жизни, но далее следует получить новый токен.
Ранее полученный токен следует указывать до отправки запросов следующим образом:
```php
$kkt->setToken($token_string);
```
Если токен был установлен перед выполнением операции, то при выполнении операции будет использоваться именно он. Если
операция завершится ошибочно из-за истёкшего токена, следует повторить операцию без использования метода `setToken()`,
либо обнулив его следующим образом:
```php
$kkt->setToken(null);
```
Тогда будет получен новый токен.
## Регистрация документа
Для регистрации документа **прихода** необходимо вызвать метод `sell()`:
```php
$result = $kkt->sell($document);
$result2 = $receipt->sell($kkt);
```
Для регистрации документа **возврата прихода** необходимо вызвать метод `sellRefund()`:
```php
$result = $kkt->sellRefund($document);
$result2 = $receipt->sellRefund($kkt);
```
Для регистрации документа **расхода** необходимо вызвать метод `buy()`:
```php
$result = $kkt->buy($document);
$result2 = $receipt->buy($kkt);
```
Для регистрации документа **возврата расхода** необходимо вызвать метод `buyRefund()`:
```php
$result = $kkt->buyRefund($document);
$result2 = $receipt->buyRefund($kkt);
```
Для регистрации документа **коррекции прихода** необходимо вызвать метод `sellCorrection()`:
```php
$result = $kkt->sellCorrect($document);
$result2 = $correction->sellCorrect($kkt);
```
Для регистрации документа **коррекции расхода** необходимо вызвать метод `buyCorrection()`:
```php
$result = $kkt->buyCorrect($document);
$result2 = $correction->buyCorrect($kkt);
```
### Собственный идентификатор документа (`external_id`)
Каждый документ, переданный на ККТ для регистрации, всегда имеет свой идентификатор, абсолютно уникальный среди всех
документов когда-либо регистрировавшихся на ККТ, даже если при регистрации были ошибки. По умолчанию это UUID версии 4.
Чтобы использовать собственный идентификатор, следует передать нужное строковое значение вторым параметром в любой из
шести описанных выше методов, например:
```php
$result = $kkt->sellRefund($document, 'order_' . $order->id);
```
Если `external_id` не указан явно или имеет пустое значение, то будет сгенерирован новый UUID. Узнать его можно будет
только в ответе от ККТ после регистрации документа в очереди на фискализацию.
### Передача `callback_url`
Перед регистрацией документа можно указать `callback_url`. АТОЛ отправит на указанный URL результат регистрации. По
этому адресу должен располагаться ваш собственный обработчик статуса фискализации.
```php
$kkt->setCallbackUrl('http://example.com/process-kkt-result');
$kkt->getCallbackUrl();
```
## Проверка статуса документа
Если перед отправкой документа на регистрацию был задан `callback_url` через метод `setCallbackUrl()`, то ответ придёт
на указанный адрес автоматически, как только документ обработается на стороне ККТ. Ответ может быть как об успешной
регистрации, так и ошибочной.
В любом случае, вам доступны два метода, с помощью которых вы можете проверять статус документа самостоятельно:
```php
$kkt->getDocumentStatus(); // делает единичный запрос
$kkt->pollDocumentStatus(); // делает запросы до получения конечного статуса (не-wait)
```
Эти методы принимают на вход `uuid` кода регистрации. Этот UUID нужно взять из ответа, полученного при отправке
документа на регистрацию:
```php
$sell_result = $kkt->sell($document);
$status = $kkt->pollDocumentStatus($sell_result->uuid);
```
Метод `pollDocumentStatus()` многократно опрашивает ККТ на предмет состояния документа. Метод может принимать до трёх
параметров:
* uuid;
* количество попыток (по умолчанию — 5);
* время между попытками в секундах (по умолчанию — 1).
```php
// Проверять статус 10 раз на протяжении 20 секунд — каждые две секунды
$kkt->pollDocumentStatus($sell_result->uuid, 10, 20);
```
Учитывайте, что метод вернёт результат как только сменится статус регистрации на успешный `done` или ошибочный `error`.
Использовать его лучше сразу после отправки документа на регистрацию (как в примере выше).
> Как правило, фискализация одного документа занимает 4-6 секунд с учётом регистрации.
Метод `getDocumentStatus()` принимает на вход только `uuid` и запрашивает состояние документа лишь единожды.
Использовать его целесообразнее в те моменты, когда нет необходимости знать успех регистрации сразу после отправки
документа.
> Обратите внимание, что АТОЛ позволяет получать статус документа в течение 32 суток с момента его регистрации.
---
Читай также: [Обработка ответа API](response.md)
[Вернуться к содержанию](readme.md#toc)

View File

@ -1,202 +0,0 @@
# Работа с предметами расчёта
[Вернуться к содержанию](readme.md)
---
## Один объект
Объект предмета расчёта инициализируется следующим образом:
```php
$vat = new AtolOnline\Entities\Item();
```
У объекта предмета расчёта должны быть указаны все следующие обязательные атрибуты:
* наименование (тег ФФД - 1030);
* цена (тег ФФД - 1079);
* количество, вес (тег ФФД - 1023).
У объекта предмета расчёта также могут быть указаны следующие необязательные атрибуты:
* единица измерения количества (тег ФФД - 1197);
* признак способа оплаты (тег ФФД - 1214) - перечислены в классе `AtolOnline\Constants\PaymentMethods`;
* признак предмета расчёта (тег ФФД - 1212) - перечислены в классе `AtolOnline\Constants\PaymentObjects`;
* [ставка НДС](/docs/vats.md);
* дополнительный реквизит (тег ФФД - 1191).
Установить многие (но не все) атрибуты можно следующими способами:
```php
use AtolOnline\{
Constants\PaymentMethods,
Constants\PaymentObjects,
Constants\VatTypes,
Entities\Item
};
// 1 способ - через конструктор
$item = new Item(
'Банан', // наименование
100, // цена
1, // количество, вес
'кг', // единица измерения
VatTypes::VAT20, // ставка НДС
PaymentObjects::SERVICE, // признак предмета расчёта
PaymentMethods::FULL_PAYMENT // признак способа расчёта
);
// 2 способ - через сеттеры
$item = new Item();
$item->setName('Банан');
$item->setPrice(100);
$item->setQuantity(2.41);
//$item->setQuantity(2.41, 'кг');
$item->setMeasurementUnit('кг');
$item->setVatType(VatTypes::VAT20);
$item->setPaymentObject(PaymentObjects::COMMODITY);
$item->setPaymentMethod(PaymentMethods::FULL_PAYMENT);
```
Метод `setName()` проверяет входную строку на длину (до 128 символов).
Выбрасывает исключение `AtolNameTooLongException` (если слишком длинное наименование).
Метод `setPrice()` проверяет аргумент на величину (до 42949672.95) и пересчитывает общую стоимость.
Выбрасывает исключение `AtolPriceTooHighException` (если цена слишком высока).
Метод `setMeasurementUnit()` проверяет входную строку на длину (до 16 символов).
Выбрасывает исключение `AtolUnitTooLongException` (если слишком длинная строка единицы измерения).
Метод `setQuantity()` проверяет первый аргумент на величину (до 99999.999) и пересчитывает общую стоимость.
Выбрасывает исключения:
* `AtolQuantityTooHighException` (если количество слишком велико);
* `AtolPriceTooHighException` (если общая стоимость слишком велика).
Также вторым аргументом может принимать единицу измерения количества.
В этом случае дополнительно работает сеттер `setMeasurementUnit()`.
Метод `setVatType()` задаёт тип ставки НДС, пересчитывает размер налога и общую стоимость.
Выбрасывает исключение `AtolPriceTooHighException` (если цена слишком высока).
Может принимать `null` для удаления налога.
Дополнительный реквизит устанавливается отдельным методом `setUserData()`:
```php
$item->setUserData('some data');
```
Он проверяет строку на длину (до 64 символов).
Выбрасывает исключение `AtolUserdataTooLongException` (если слишком длинный дополнительный реквизит).
Для установки признака предмета расчёта существует метод `setPaymentObject()`.
На вход следует передавать одной из значений, перечисленных в классе `AtolOnline\Constants\PaymentObjects`.
```php
$item->setPaymentObject(AtolOnline\Constants\PaymentObjects::JOB);
```
Для установки признака способа оплаты существует метод `setPaymentMethod()`.
На вход следует передавать одной из значений, перечисленных в классе `AtolOnline\Constants\PaymentMethods`.
```php
$item->setPaymentMethod(AtolOnline\Constants\PaymentMethods::FULL_PAYMENT);
```
Для получения заданных значений атрибутов реализованы соответствующие геттеры:
```php
$item->getName();
$item->getPrice();
$item->getQuantity();
$item->getMeasurementUnit();
$item->getPaymentMethod();
$item->getPaymentObject();
$item->getVat(); // возвращает объект ставки НДС либо null
$item->getUserData();
```
Для пересчёта общей стоимости и размера налога существует метод `calcSum()`.
```php
$item->calcSum();
```
Этот метод отрабатывает при вызове `setPrice()`, `setQuantity()` и `setVatType()`.
Выбрасывает исключение `AtolPriceTooHighException` (если общая сумма слишком высока).
Получить уже расчитанную общую сумму можно простым геттером:
```php
$item->getSum();
```
Объект класса приводится к JSON-строке автоматически или принудительным приведением к `string`:
```php
echo $item;
$json_string = (string)$item;
```
Чтобы получить те же данные в виде массива, нужно вызвать метод `jsonSerialize()`:
```php
$json_array = $item->jsonSerialize();
```
<a name="array"></a>
## Массив объектов предметов расчёта
> Максимальное количество объектов в массиве - 100.
Массив инициализируется следующим образом:
```php
$item_array = new AtolOnline\Entities\ItemArray();
```
Чтобы задать содержимое массива, используйте метод `set()`:
```php
$item_array->set([
$item_object1,
$item_object2
]);
```
Очистить его можно передачей в сеттер пустого массива:
```php
$item_array->set([]);
```
Чтобы добавить объект к существующим элементам массива, используйте метод `add()`:
```php
$item = new AtolOnline\Entities\Item('Банан', 100, 1);
$item_array->add($item);
```
Методы `set()` и `add()` проверяют количество элементов в массиве перед его обновлением.
Выбрасывают исключение `AtolTooManyItemsException` (если в массиве уже максимальное количество объектов).
Чтобы получить содержимое массива, используйте метод `get()`:
```php
$item_array->get();
```
Объект класса приводится к JSON-строке автоматически или принудительным приведением к `string`:
```php
echo $item_array;
$json_string = (string)$item_array;
```
Чтобы получить те же данные в виде массива, нужно вызвать метод `jsonSerialize()`:
```php
$json_array = $item_array->jsonSerialize();
```
---
[Вернуться к содержанию](readme.md)

View File

@ -1,296 +0,0 @@
# Работа с ККТ
[Вернуться к содержанию](readme.md)
---
Объект ККТ инициализируется следующим образом:
```php
$kkt = new AtolOnline\Api\Kkt();
```
## Настройка ККТ
Для работы с облачной ККТ необходимы следующие параметры:
* логин кассы;
* пароль кассы;
* код группы кассы;
Чтоб получить их, нужно:
1. авторизоваться на [online.atol.ru](https://online.atol.ru/lk/Account/Login);
2. на странице [Мои компании](https://online.atol.ru/lk/Company/List) нажать кнопку **Настройки интегратора**.
Скачается XML-файл с нужными настройками.
Установить эти параметры можно двумя путями:
```php
// 1 способ - через конструктор
$kkt = new AtolOnline\Api\Kkt($group, $login, $password);
// 2 способ - через сеттеры
$kkt = (new AtolOnline\Api\Kkt())
->setLogin($login)
->setGroup($group)
->setPassword($password);
```
Получить заданные параметры можно через соответствующие геттеры:
```php
$kkt->getLogin();
$kkt->getPassword();
$kkt->getGroup();
```
Также для работы потребуются:
* ИНН продавца;
* URL места расчёта (ссылка на ваш интернет-сервис).
Эти параметры нужно задать [объекту компании](/docs/company.md), который будет передаваться в документах через эту ККТ.
<a name='testmode'></a>
## Тестовый режим
На самом деле, в АТОЛ Онлайн нет понятия *тестовая операция* или чего-то в этом духе.
АТОЛ предоставляет нам отдельную тестовую среду (ККТ).
[Её настройки](https://online.atol.ru/files/ffd/test_sreda.txt) уже указаны в коде библиотеки.
*Под тестовым режимом работы подразумевается использование другой (тестовой) ККТ.*
При включенном тестовом режиме:
* меняется логин, пароль и группа (для обращения на тестовую ККТ)
* между авторизацией и операцией над документом, в `Company` документа переопределяется ИНН, СНО и адрес места
расчётов на те, что указаны в [параметрах тестовой среды](https://online.atol.ru/files/ffd/test_sreda.txt).
В библиотеке есть переключатель настроек ККТ.
С его помощью можете поменять вашу боевую ККТ на тестовую и наоборот.
Это можно сделать одним из следующих способов:
```php
// включить в любом месте кода:
$kkt->setTestMode();
$kkt->setTestMode(true);
$kkt->setTestMode(false); // выключить
```
> Если вы включили тестовый режим (как показано выше), то используются именно эта ККТ, а не ваша.
> После выключения тестового режима настройки доступа к ККТ меняются на ваши (используется уже ваша ККТ).
Для включения тестового режима необязательно задавать параметры боевой ККТ.
Если по каким-то причинам у вас не получится использовать тестовый режим, вы можете проводить свои тесты в боевом
режиме (на собственной ККТ).
В этом случае важно понимать следующее:
1. сразу после оформления документа **прихода** необходимо оформлять точно такой же документ **возврата прихода**;
2. [вы обязательно забудете о пункте 1](http://murphy-law.net.ru/basics.html);
3. пп. 1 и 2 в любом случае скажутся на ваших финансовых отчётах;
4. вся ответственность за пп. 1-3 и последствия ложится только на вас.
## Авторизация на ККТ
Перед первым запросом на ККТ происходит аутентификация на сервере по логину и паролю.
В ответ приходит авторизационный токен, срок жизни коего равен **24 часам**.
После первой успешной операции возможно получить этот токен следующим образом:
```php
$kkt->getAuthToken(); // вернёт строку длиной 128 символа
```
Этот токен можно сохранить и переиспользовать в течение всего срока его жизни.
Спустя это время следует получить новый токен.
Для дальнейшего использования однажды полученный токен следует указывать следующим образом:
```php
$kkt->setAuthToken($token_string);
```
Если токен был установлен перед выполнением операции, то при выполнении операции будет использоваться именно он, а новый
запрашиваться не будет. Если операция завершится ошибочно из-за истёкшего токена, следует повторить операцию без
использования метода `setAuthToken()`, либо обнулив его следующим образом:
```php
$kkt->setAuthToken(null);
```
## Регистрация документа
Для регистрации документа **прихода** необходимо вызвать метод `sell()`:
```php
$result = $kkt->sell($document);
```
Для регистрации документа **возврата прихода** необходимо вызвать метод `sellRefund()`:
```php
$result = $kkt->sellRefund($document);
```
Для регистрации документа **расхода** необходимо вызвать метод `buy()`:
```php
$result = $kkt->buy($document);
```
Для регистрации документа **возврата расхода** необходимо вызвать метод `buyRefund()`:
```php
$result = $kkt->buyRefund($document);
```
Для операций, перечисленных выше, документы не должны содержать [данных коррекции](/docs/documents.md#correction).
Тогда как для операций коррекции, которые описаны ниже, эти данные должны присутствовать.
Для регистрации документа **коррекции прихода** необходимо вызвать метод `sellCorrection()`:
```php
$result = $kkt->sellCorrection($document);
```
Для регистрации документа **коррекции расхода** необходимо вызвать метод `buyCorrection()`:
```php
$result = $kkt->buyCorrection($document);
```
Любой из перечисленных выше шести методов может выбросить исключение `AtolAuthFailedException` при ошибке
аутентификации или авторизации.
### Собственный идентификатор документа
Каждый документ, переданный на ККТ для регистрации, всегда имеет свой идентификатор, абсолютно уникальный среди всех
документов когда-либо регистрировавшихся на ККТ, даже если при регистрации были ошибки.
По умолчанию это UUID версии 4.
Чтобы использовать собственный идентификатор, следует передать нужное строковое значение вторым параметром в любой из
шести описанных выше методов, например:
```php
$kkt->sell($document, $my_unique_id);
$kkt->sellRefund($document, $my_unique_id);
$kkt->buy($document, $my_unique_id);
$kkt->buyRefund($document, $my_unique_id);
$kkt->sellCorrection($document, $my_unique_id);
$kkt->buyCorrection($document, $my_unique_id);
```
Если `$my_unique_id` равен `null` или пустой строке, то будет сгенерирован новый UUID.
Узнать его можно будет только в ответе от ККТ.
### Передача callback_url
Перед регистрацией документа можно указать `callback_url`.
АТОЛ отправит на указанный URL результат регистрации.
По этому адресу должен располагаться код, который будет обрабатывать этот результат.
```php
$kkt->setCallbackUrl('http://example.com/process-kkt-result');
$kkt->getCallbackUrl();
```
Метод `setCallbackUrl()` проверяет входную строку на длину (до 256 символов) и валидность формата url по
регулярному выражению:
```
^http(s?)\:\/\/[0-9a-zA-Zа-яА-Я]([-.\w]*[0-9a-zA-Zа-яА-Я])*(:(0-9)*)*(\/?)([a-zAZ0-9а-яА-Я\-\.\?\,\'\/\\\+&=%$#_]*)?$
```
Выбрасывает исключения:
* `AtolCallbackUrlTooLongException` (если слишком длинный url);
* `AtolInvalidCallbackUrlException` (если url невалиден).
## Обработка результата регистрации
Методы `sell()`, `sellRefund()`, `sellCorrection()`, `buy()`, `buyRefund()` и `buyCorrection()` возвращают объект `AtolOnline\Api\KktResponse`.
Этот же объект можно получить через геттер `getLastResponse()`.
Этот объект содержит в себе HTTP-код ответа, массив заголовков и JSON-декодированные данные тела ответа.
```php
$result = $kkt->getLastResponse(); // вернёт последний ответ от API
$headers = $result->getHeaders(); // вернёт заголовки
$code = $result->getCode(); // вернёт код ответа
$body = $result->getContent(); // вернёт JSON-декодированное тело ответа
```
Обращаться к полям JSON-декодированного объекта можно опуская вызов метода `getContent()` таким образом:
```php
// вернёт значение поля uuid
$uuid = $result->getContent()->uuid;
$uuid = $result->uuid;
// вернёт текст ошибки
$err_text = $result->getContent()->error->text;
$err_text = $result->error->text;
```
Проверка корректности ответа (отсутствия ошибок) работает через метод `isValid()`:
```php
$kkt->getLastResponse()->isValid(); // вернёт true, если ошибок нет
```
## Проверка статуса документа
Если перед отправкой документа на регистрацию был задан callback_url через метод `setCallbackUrl()`, то ответ придёт на указанный адрес автоматически, как только документ обработается на стороне ККТ.
Ответ может быть как об успешной регистрации, так и ошибочной.
В любом случае, вам доступны два метода, с помощью которых вы можете проверять статус документа самостоятельно:
```php
$kkt->getDocumentStatus();
$kkt->pollDocumentStatus();
```
Эти методы принимают на вход `uuid` кода регистрации.
Этот UUID можно получить из ответа, полученного при отправке документа на регистрацию:
```php
$sell_result = $kkt->sell($document);
$kkt->pollDocumentStatus($sell_result->uuid);
$kkt->getDocumentStatus($sell_result->uuid);
```
Метод `pollDocumentStatus()` многократно опрашивает ККТ на предмет состояния документа.
Метод может принимать до трёх параметров:
* uuid;
* количество попыток (по умолчанию — 5);
* время между попытками в секундах (по умолчанию — 1).
```php
// Проверять статус 10 раз на протяжении 20 секунд — каждые две секунды
$kkt->pollDocumentStatus($sell_result->uuid, 10, 20);
```
Учитывайте, что метод вернёт результат как только сменится статус регистрации на успешный или ошибочный.
Использовать его лучше сразу после отправки документа на регистрацию (как в примере выше).
> Как правило, полная регистрация одного документа занимает 4-5 секунд.
Метод `getDocumentStatus()` принимает на вход только `uuid` и запрашивает состояние документа лишь единожды.
Использовать его целесообразнее в те моменты, когда нет необходимости знать успех регистрации сразу после отправки документа.
> Обратите внимание, что АТОЛ позволяет получать статус документа в течение 32 суток с момента его регистрации.
Методы `pollDocumentStatus()` и `getDocumentStatus()` возвращают объект `AtolOnline\Api\KktResponse`.
Оба выбрасывают исключение `AtolUuidValidateException` (если переданная строка UUID невалидна).
Объект класса приводится к JSON-строке автоматически или принудительным приведением к `string`:
```php
echo $item;
$json_string = (string)$item;
```
Чтобы получить те же данные в виде массива, нужно вызвать метод `jsonSerialize()`:
```php
$json_array = $item->jsonSerialize();
```
---
[Вернуться к содержанию](readme.md)

119
docs/monitoring.md 100644
View File

@ -0,0 +1,119 @@
# Мониторинг ККТ
[Вернуться к содержанию](readme.md#toc)
---
Библиотека предоставляет возможность следить за состоянием ваших облачных ККТ через API.
## Инициализация
Для этого следует использовать класс `KktMonitor`:
```php
// можно передать параметры подключения в конструктор
$monitor = new AtolOnline\Api\Monitor(
login: 'mylogin',
password: 'qwerty'
);
// можно - отдельными сеттерами
$monitor = new AtolOnline\Api\Monitor();
->setLogin($credentials['login'])
->setPassword($credentials['password']);
```
Логин и пароль для мониторинга те же, что для регистрации документов.
**По умолчанию монитор работает в тестовом режиме.**
Перевести его в боевой режим можно:
```php
// передачей в конструктор `false` первым параметром:
$monitor = new AtolOnline\Api\Monitor(false, /*...*/);
// или отдельным сеттером
$monitor->setTestMode(false);
```
**Тестовый режим** нужен для проверки работоспособности библиотеки и API АТОЛ.
**В боевом режиме** можно получать данные по своим ККТ.
## Получение данных обо всех своих ККТ
Для получения данных обо всех своих ККТ следует вызвать метод `AtolOnline\Api\KktMonitor::getAll()`:
```php
$kkts = $monitor->getAll();
```
В ответе будет итерируемая коллекция объектов `AtolOnline\Entities\Kkt`.
Каждый из этих объектов содержит атрибуты:
```php
// для примера получим первую ККТ из всех
$kkt = $kkts->first();
// посмотрим на её атрибуты:
$kkt->serialNumber; // Заводской номер ККТ
$kkt->registrationNumber; // Регистрационный номер машины (РНМ)
$kkt->deviceNumber; // Номер автоматического устройства (внутренний идентификатор устройства)
$kkt->fiscalizationDate; // Дата активации (фискализации) ФН с указанием таймзоны
$kkt->fiscalStorageExpiration; // Дата замены ФН (Срок действия ФН), с указанием таймзоны
$kkt->signedDocuments; // Количество подписанных документов в ФН
$kkt->fiscalStoragePercentageUse; // Наполненость ФН в %
$kkt->fiscalStorageINN; // ИНН компании (указанный в ФН)
$kkt->fiscalStorageSerialNumber; // Заводской (серийный) номер ФН
$kkt->fiscalStoragePaymentAddress; // Адрес расчёта, указанный в ФН
$kkt->groupCode; // Код группы кассы
$kkt->timestamp; // Время и дата формирования данных, UTC
$kkt->isShiftOpened; // Признак открыта смена (true) или закрыта (false)
$kkt->shiftNumber; // Номер смены (или "Номер закрытой смены", когда смена закрыта)
$kkt->shiftReceipt; // Номер документа за смену (или "Кол-во чеков закрытой смены", когда смена закрыта)
$kkt->unsentDocs; // Количество неотправленных документов. Указывается, если значение отлично от 0.
$kkt->firstUnsetDocTimestamp; // Дата первого неотправленного документа. Указывается, если есть неотправленные документы.
$kkt->networkErrorCode; // Код ошибки сети
```
Эти поля описаны в документации мониторинга на [стр. 11](https://online.atol.ru/files/API_service_information.pdf)
Сопоставления кодов ошибок и их описаний доступны в массиве `AtolOnline\Entities\Kkt::ERROR_CODES`.
Объект класса приводится к JSON-строке автоматически или принудительно:
```php
echo $kkt;
$json_string = (string)$kkt;
```
Чтобы получить те же данные в виде массива, нужно вызвать метод `jsonSerialize()`:
```php
$json_array = $kkt->jsonSerialize();
```
## Получение данных об одной из своих ККТ
Для этого следует вызвать метод `AtolOnline\Api\KktMonitor::getOne()`, передав на вход серийный номер (`serialNumber`)
нужной ККТ:
```php
$kkt = $monitor->getOne($kkts->first()->serialNumber);
```
Метод вернёт единственный объект `AtolOnline\Entities\Kkt` с атрибутами, описанными выше.
## Получение последнего ответа от сервера
Класс `AtolOnline\Api\KktMonitor` расширяет абстрактный класс `AtolOnline\Api\AtolClient`.
Это значит, что последний ответ от API АТОЛ всегда сохраняется объектом класса `AtolOnline\Api\AtolReponse`. К нему
можно обратиться через метод `AtolOnline\Api\KktMonitor::getResponse()`, независимо от того, что возвращают другие
методы монитора.
---
Читай также: [Обработка ответа API](response.md)
[Вернуться к содержанию](readme.md#toc)

View File

@ -1,134 +0,0 @@
# Работа с оплатами
[Вернуться к содержанию](readme.md)
---
## Один объект
Объект оплаты инициализируется следующим образом:
```php
$payment = new AtolOnline\Entities\Payment();
```
У объекта оплаты должны быть указаны все следующие атрибуты:
* тип оплаты (теги ФФД: 1031, 1081, 1215, 1216, 1217) - все типы перечислены в классе `AtolOnline\Constants\PaymentTypes` (по умолчанию `ELECTRON`)
* сумма оплаты (теги ФФД: 1031, 1081, 1215, 1216, 1217; по умолчанию 0)
> Все эти атрибуты являются **обязательными**.
Установить атрибуты можно следующими способами:
```php
// 1 способ - через конструктор
$payment = new AtolOnline\Entities\Payment(
AtolOnline\Constants\PaymentTypes::OTHER, // тип оплаты
123.45 // сумма оплаты
);
// 2 способ - через сеттер
$payment = (new AtolOnline\Entities\Payment())
->setType(AtolOnline\Constants\PaymentTypes::OTHER) // тип оплаты
->setSum(123.45); // сумма оплаты
```
Размер налога высчитывается автоматически из общей суммы.
Сумму, от которой нужно расчитать размер налога, можно передать следующими способами:
```php
// 1 способ - через конструктор
$payment = new AtolOnline\Entities\Payment(
AtolOnline\Constants\PaymentTypes::CASH, // тип оплаты
1234.56 // сумма оплаты в рублях
);
// 2 способ - через сеттер
$payment = (new AtolOnline\Entities\Payment())
->setType(AtolOnline\Constants\PaymentTypes::CASH) // тип оплаты
->setSum(1234.56); // сумма оплаты в рублях
```
Получить установленную сумму оплаты в рублях можно через геттер `getSum()`:
```php
var_dump($payment->getSum());
```
Объект класса приводится к JSON-строке автоматически или принудительным приведением к `string`:
```php
echo $payment;
$json_string = (string)$payment;
```
Чтобы получить те же данные в виде массива, нужно вызвать метод `jsonSerialize()`:
```php
$json_array = $payment->jsonSerialize();
```
<a name="array"></a>
## Массив объектов оплат
> Максимальное количество объектов в массиве - 10.
Массив инициализируется следующим образом:
```php
$payment_array = new AtolOnline\Entities\PaymentArray();
```
Чтобы задать содержимое массива, используйте метод `set()`:
```php
use AtolOnline\{Constants\PaymentTypes, Entities\Payment};
$payment_array->set([
new Payment(PaymentTypes::ELECTRON, 123),
new Payment(PaymentTypes::ELECTRON, 53.2),
new Payment(PaymentTypes::ELECTRON, 23.99),
new Payment(PaymentTypes::ELECTRON, 11.43)
]);
```
Очистить его можно передачей в сеттер пустого массива:
```php
$payment_array->set([]);
```
Чтобы добавить объект к существующим элементам массива, используйте метод `add()`:
```php
use AtolOnline\{Constants\PaymentTypes, Entities\Payment};
$payment = new Payment(PaymentTypes::PRE_PAID, 20);
$payment_array->add($payment);
```
Методы `set()` и `add()` проверяют количество элементов в массиве перед его обновлением.
Выбрасывают исключение `AtolTooManyPaymentsException` (если в массиве уже максимальное количество объектов).
Чтобы получить содержимое массива, используйте метод `get()`:
```php
$payment_array->get();
```
Объект класса приводится к JSON-строке автоматически или принудительным приведением к `string`:
```php
echo $payment_array;
$json_string = (string)$payment_array;
```
Чтобы получить те же данные в виде массива, нужно вызвать метод `jsonSerialize()`:
```php
$json_array = $payment_array->jsonSerialize();
```
---
[Вернуться к содержанию](readme.md)

View File

@ -1,15 +1,47 @@
# Документация к библиотеке atol-online
# Документация к библиотеке
Содержание:
1. [Работа с клиентами (покупателями)](client.md)
2. [Работа с компанией (продавцом)](company.md)
3. [Работа с оплатами](payments.md)
4. [Работа со ставками НДС](vats.md)
5. [Работа с предметами расчёта](items.md)
6. [Работа с данными коррекции](correction_info.md)
7. [Работа с документами](documents.md)
8. [Работа с ККТ](kkt.md)
<a id="toc"></a>
## Содержание
---
* [Общий алгоритм](#getstarted)
* [Сущность](entity.md)
* [Коллекция сущностей](collection.md)
* [Мониторинг ККТ](monitoring.md)
* [Фискализация документа](fiscalizing.md)
* [Обработка ответа API](response.md)
Если вы нашли опечатку или какое-то несоответствие — делайте pull-request.
Если вы нашли опечатку или какое-то несоответствие — делайте pull-request.
<a id="getstarted"></a>
## Общий алгоритм
1. Создать документ `AtolOnline\Entities\Receipt` или `AtolOnline\Entities\Correction`,
добавив в него все необходимые данные
2. Отправить документ на регистрацию:
2.1. *Необязательно:* при отправке задать `callback_url`, на который АТОЛ отправит HTTP POST о состоянии документа;
2.2. *Необязательно:* при отправке задать `external_id`, чтобы присвоить свой уникальный идентификатор документа;
3. Запомнить `uuid` из пришедшего ответа, поскольку он пригодится для последующей проверки статуса фискализации.
> Если с документом был передан `callback_url`, то ответ придёт на этот самый URL.
> Он должен быть обработан вашим сервисом в соответствии с бизнес-процессом.
> Если с документом **не был** передан `callback_url` **либо** callback от АТОЛа не был получен/обработан в течение
> 300 секунд (5 минут), нужно запрашивать вручную по `uuid`, пришедшему от АТОЛа в ответ на регистрацию документа.
4. Проверить состояние документа:
4.1. взять `uuid` ответа, полученного на запрос фискализации;
4.2. отправить его в запросе состояния документа.
> Данные о документе можно получить только в течение 32 суток после успешной фискализации.
В зависимости от специфики бизнеса, в документах можно/нужно передавать разную информацию. Подробности в документации
АТОЛ Онлайн и исходниках библиотеки.
## Об отправке электронного чека покупателю
После успешной фискализации документа покупатель автоматически получит уведомление **от ОФД**, который используется в
связке с вашей ККТ:
* **по email**, если в документе указан email клиента;
* **по смс**:
* если в документе указан номер телефона клиента;
* если на стороне ОФД необходима и подключена услуга СМС-информирования (уточняйте подробности о своего ОФД).
> Если заданы email и телефон, то ОФД отдаёт приоритет email.

57
docs/response.md 100644
View File

@ -0,0 +1,57 @@
# Обработка ответа API
[Вернуться к содержанию](readme.md#toc)
---
Объект класса `AtolOnline\Api\AtolResponse` возвращается всеми методами, которые обращаются к АТОЛ Онлайн API.
Поскольку классы `AtolOnline\Api\Fiscalizer` и `AtolOnline\Api\KktMonitor` наследуются от
абстрактного `AtolOnline\Api\AtolClient`, они оба предоставляют метод `getLastReponse()` для возврата последнего
полученного ответа от API.
Таким образом, а общем случае необязательно сразу сохранять ответ в переменную:
```php
$response = $kkt->sell($receipt);
```
Достаточно обратиться к нему позднее:
```php
$kkt->sell($receipt);
// ...
$response = $kkt->getLastResponse();
```
Однако, при сложной логике и многократных запросах следует пользоваться этим с умом и осторожностью.
Объект `AtolResponse` содержит в себе HTTP-код ответа, массив заголовков и JSON-декодированные данные тела ответа.
```php
$headers = $response->getHeaders(); // вернёт заголовки
$code = $response->getCode(); // вернёт код ответа
$body = $response->getContent(); // вернёт JSON-декодированный объект тела ответа
```
Обращаться к полям тела ответа можно опуская вызов метода `getContent()`:
```php
// вернёт значение поля uuid
$uuid = $response->getContent()->uuid;
$uuid = $response->uuid;
// вернёт текст ошибки
$err_text = $response->getContent()->error->text;
$err_text = $response->error->text;
```
Проверка успешности операции доступна через метод `isSuccessful()`:
```php
$response->isSuccessful(); // вернёт true, если ошибок нет
```
---
[Вернуться к содержанию](readme.md#toc)

View File

@ -1,194 +0,0 @@
# Работа со ставками НДС
[Вернуться к содержанию](readme.md)
---
## Один объект
Объект ставки НДС инициализируется следующим образом:
```php
$vat = new AtolOnline\Entities\Vat();
```
У объекта ставки должны быть указаны все следующие атрибуты:
* тип ставки (теги ФФД: 1199 для предмета расчёта; 1105, 1104, 1103, 1102, 1107, 1106 для чека) - все типы перечислены в классе `AtolOnline\Constants\VatTypes` (по умолчанию `NONE`)
* размер налога (теги ФФД: 1200 для предмета расчёта; 1105, 1104, 1103, 1102, 1107, 1106 для чека)
> Все эти атрибуты являются **обязательными**.
Установить тип ставки НДС можно следующими способами:
```php
// 1 способ - через конструктор
$vat = new AtolOnline\Entities\Vat(
AtolOnline\Constants\VatTypes::VAT20 // тип ставки
);
// 2 способ - через сеттер
$vat = (new AtolOnline\Entities\Vat())
->setType(AtolOnline\Constants\VatTypes::VAT20); // тип ставки
```
Размер налога высчитывается автоматически из общей суммы.
Сумму, от которой нужно расчитать размер налога, можно передать следующими способами:
```php
// 1 способ - через конструктор
$vat = new AtolOnline\Entities\Vat(
AtolOnline\Constants\VatTypes::VAT10, // тип ставки
1234.56 // общая сумма в рублях
);
// 2 способ - через сеттер
$vat = (new AtolOnline\Entities\Vat())
->setType(AtolOnline\Constants\VatTypes::VAT10) // тип ставки
->setSum(150); // общая сумма в рублях
```
Сумму можно установить и до установки типа ставки.
Объект её запомнит и пересчитает итоговый размер налога при смене типа ставки:
```php
$vat = (new AtolOnline\Entities\Vat())
->setSum(150) // общая сумма в рублях
->setType(AtolOnline\Constants\VatTypes::VAT10); // тип ставки
```
Получить установленную расчётную сумму в рублях можно через геттер `getSum()`:
```php
var_dump($vat->getSum());
// double(150)
```
Получить расчитанный размер налога в рублях можно через геттер `getFinalSum()`:
```php
var_dump($vat->getFinalSum());
// double(15): для примера выше это 10% от 150р = 15р
```
Общую сумму, из которой расчитывается размер налога, можно увеличить, используя метод `addSum()`.
Указанная в рублях сумма увеличится и итоговый размер налога пересчитается.
Для уменьшения суммы следует передать отрицательное число.
Разберём комплексный пример изменения типа ставки и расчётной суммы:
```php
use AtolOnline\{Entities\Vat, Constants\VatTypes};
$vat = new Vat(VatTypes::VAT20, 120);
echo "НДС20 от 120р: ";
var_dump($vat->getFinalSum());
echo "НДС10 от 120р: ";
$vat->setType(VatTypes::VAT10);
var_dump($vat->getFinalSum());
$vat->addSum(40);
echo "НДС10 от {$vat->getSum()}р: ";
var_dump($vat->getFinalSum());
$vat->setType(VatTypes::VAT20)->addSum(-20);
echo "НДС20 от {$vat->getSum()}р: ";
var_dump($vat->getFinalSum());
$vat->setType(VatTypes::VAT120);
echo "НДС20/120 от {$vat->getSum()}р: ";
var_dump($vat->getFinalSum());
```
Результат будет следующим:
```
НДС20 от 120р:
double(24)
НДС10 от 120р:
double(12)
НДС10 от 160р:
double(16)
НДС20 от 140р:
double(28)
НДС20/120 от 140р:
double(23.33)
```
Объект класса приводится к JSON-строке автоматически или принудительным приведением к `string`:
```php
echo $vat;
$json_string = (string)$vat;
```
Чтобы получить те же данные в виде массива, нужно вызвать метод `jsonSerialize()`:
```php
$json_array = $vat->jsonSerialize();
```
<a name="array"></a>
## Массив объектов ставок НДС
> Максимальное количество в массиве - 6.
Массив инициализируется следующим образом:
```php
$vat_array = new AtolOnline\Entities\VatArray();
```
Чтобы задать содержимое массива, используйте метод `set()`:
```php
use AtolOnline\{Constants\VatTypes, Entities\Vat};
$vat_array->set([
new Vat(VatTypes::VAT10, 123),
new Vat(VatTypes::VAT110, 53.2),
new Vat(VatTypes::VAT20, 23.99),
new Vat(VatTypes::VAT120, 11.43)
]);
```
Очистить его можно передачей в сеттер пустого массива:
```php
$vat_array->set([]);
```
Чтобы добавить объект к существующим элементам массива, используйте метод `add()`:
```php
use AtolOnline\{Constants\VatTypes, Entities\Vat};
$vat = new Vat(VatTypes::VAT20, 20);
$vat_array->add($vat);
```
Методы `set()` и `add()` проверяют количество элементов в массиве перед его обновлением.
Выбрасывают исключение `AtolTooManyVatsException` (если в массиве уже максимальное количество объектов).
Чтобы получить содержимое массива, используйте метод `get()`:
```php
$vat_array->get();
```
Объект класса приводится к JSON-строке автоматически или принудительным приведением к `string`:
```php
echo $vat_array;
$json_string = (string)$vat_array;
```
Чтобы получить те же данные в виде массива, нужно вызвать метод `jsonSerialize()`:
```php
$json_array = $vat_array->jsonSerialize();
```
---
[Вернуться к содержанию](readme.md)

26
phpcs.xml 100644
View File

@ -0,0 +1,26 @@
<?xml version="1.0"?>
<!--
~ Copyright (c) 2020-2021 Антон Аксенов (Anthony Axenov)
~
~ This code is licensed under MIT.
~ Этот код распространяется по лицензии MIT.
~ https://github.com/anthonyaxenov/atol-online/blob/master/LICENSE
-->
<ruleset name="atol-online standards">
<description>atol-online coding standards</description>
<file>src</file>
<file>tests</file>
<exclude-pattern>*/docs/*</exclude-pattern>
<exclude-pattern>*/vendor/*</exclude-pattern>
<arg name="extensions" value="php"/>
<arg name="colors"/>
<arg value="p"/>
<rule ref="PSR12">
<exclude name="PSR12.Files.FileHeader.SpacingInsideBlock"/>
</rule>
</ruleset>

View File

@ -1,25 +1,34 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ Copyright (c) 2020-2021 Антон Аксенов (Anthony Axenov)
~
~ This code is licensed under MIT.
~ Этот код распространяется по лицензии MIT.
~ https://github.com/anthonyaxenov/atol-online/blob/master/LICENSE
-->
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="./vendor/phpunit/phpunit/phpunit.xsd"
backupGlobals="false"
backupStaticAttributes="false"
bootstrap="vendor/autoload.php"
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
processIsolation="false"
stopOnFailure="false">
colors="true">
<testsuites>
<testsuite name="Unit">
<file>ClientTest.php</file>
<file>CompanyTest.php</file>
<file>VatTest.php</file>
<directory suffix="Test.php">./tests/Unit</directory>
<testsuite name="Helpers">
<file>tests/AtolOnline/Tests/HelpersTest.php</file>
</testsuite>
<testsuite name="Feature">
<file>ItemTest.php</file>
<directory suffix="Test.php">./tests/Feature</directory>
<testsuite name="Entities">
<directory>tests/AtolOnline/Tests/Entities</directory>
</testsuite>
<testsuite name="Collections">
<directory>tests/AtolOnline/Tests/Collections</directory>
</testsuite>
<testsuite name="Api">
<directory>tests/AtolOnline/Tests/Api</directory>
</testsuite>
</testsuites>
</phpunit>
<coverage>
<include>
<directory suffix=".php">src</directory>
</include>
</coverage>
</phpunit>

62
psalm.xml 100644
View File

@ -0,0 +1,62 @@
<?xml version="1.0"?>
<!--
~ Copyright (c) 2020-2021 Антон Аксенов (Anthony Axenov)
~
~ This code is licensed under MIT.
~ Этот код распространяется по лицензии MIT.
~ https://github.com/anthonyaxenov/atol-online/blob/master/LICENSE
-->
<psalm
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="https://getpsalm.org/schema/config"
xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd"
errorLevel="7"
addParamDefaultToDocblockType="true"
allowStringToStandInForClass="true"
disableSuppressAll="true"
ensureArrayIntOffsetsExist="true"
ensureArrayStringOffsetsExist="true"
findUnusedCode="true"
findUnusedPsalmSuppress="true"
findUnusedVariablesAndParams="true"
runTaintAnalysis="true"
sealAllMethods="true"
sealAllProperties="true"
strictBinaryOperands="true"
>
<!-- Default values:
allowNamedArgumentCalls="true"
checkForThrowsDocblock="false"
checkForThrowsInGlobalScope="false"
hideExternalErrors="false"
hoistConstants="false"
ignoreInternalFunctionFalseReturn="true"
ignoreInternalFunctionNullReturn="true"
inferPropertyTypesFromConstructor="true"
memoizeMethodCallResults="false"
rememberPropertyAssignmentsAfterCall="true"
reportInfo="true"
reportMixedIssues="true" - when errorLevel>=3
resolveFromConfigFile="true"
skipChecksOnUnresolvableIncludes="false"
throwExceptionOnError="false"
useDocblockPropertyTypes="false"
useDocblockTypes="true"
usePhpDocMethodsWithoutMagicCall="false"
usePhpDocPropertiesWithoutMagicCall="false"
-->
<projectFiles>
<directory name="src"/>
<directory name="tests"/>
<ignoreFiles>
<directory name="vendor"/>
</ignoreFiles>
</projectFiles>
<plugins>
<pluginClass class="Psalm\PhpUnitPlugin\Plugin"/>
</plugins>
</psalm>

View File

@ -0,0 +1,332 @@
<?php
/*
* Copyright (c) 2020-2021 Антон Аксенов (Anthony Axenov)
*
* This code is licensed under MIT.
* Этот код распространяется по лицензии MIT.
* https://github.com/anthonyaxenov/atol-online/blob/master/LICENSE
*/
declare(strict_types=1);
namespace AtolOnline\Api;
use AtolOnline\Constraints;
use AtolOnline\Exceptions\{
AuthFailedException,
EmptyLoginException,
EmptyPasswordException,
TooLongLoginException,
TooLongPasswordException};
use AtolOnline\TestEnvParams;
use GuzzleHttp\{
Client,
Exception\GuzzleException};
use JetBrains\PhpStorm\Pure;
/**
* Класс для подключения АТОЛ Онлайн API
*/
abstract class AtolClient
{
/**
* @var array Последний запрос к серверу АТОЛ
*/
protected array $request;
/**
* @var AtolResponse|null Последний ответ сервера АТОЛ
*/
protected ?AtolResponse $response;
/**
* @var bool Флаг тестового режима
*/
protected bool $test_mode;
/**
* @var Client HTTP-клиент для работы с API
*/
protected Client $http;
/**
* @var string|null Логин доступа к API
*/
private ?string $login = null;
/**
* @var string|null Пароль доступа к API (readonly)
*/
private ?string $password = null;
/**
* @var string|null Токен авторизации
*/
private ?string $token = null;
/**
* Конструктор
*
* @param bool $test_mode
* @param string|null $login
* @param string|null $password
* @param array $config
* @throws EmptyLoginException
* @throws EmptyPasswordException
* @throws TooLongLoginException
* @throws TooLongPasswordException
* @see https://guzzle.readthedocs.io/en/latest/request-options.html Допустимые параметры для $config
*/
public function __construct(
bool $test_mode = true,
?string $login = null,
?string $password = null,
array $config = []
) {
$this->http = new Client(
array_merge($config, [
'http_errors' => $config['http_errors'] ?? false,
])
);
$this->setTestMode($test_mode);
!is_null($login) && $this->setLogin($login);
!is_null($password) && $this->setPassword($password);
}
/**
* Возвращает последний запрос к серверу
*
* @return array
*/
public function getLastRequest(): array
{
return $this->request;
}
/**
* Возвращает последний ответ сервера
*
* @return AtolResponse|null
*/
public function getLastResponse(): ?AtolResponse
{
return $this->response;
}
/**
* Возвращает установленный флаг тестового режима
*
* @return bool
*/
public function isTestMode(): bool
{
return $this->test_mode;
}
/**
* Устанавливает флаг тестового режима
*
* @param bool $test_mode
* @return $this
*/
public function setTestMode(bool $test_mode = true): self
{
$this->test_mode = $test_mode;
return $this;
}
/**
* Возвращает текущий токен авторизации
*
* @return string|null
*/
public function getToken(): ?string
{
return $this->token;
}
/**
* Устанавливает токен авторизации
*
* @param string|null $token
* @return $this
*/
public function setToken(?string $token): self
{
$this->token = $token;
return $this;
}
/**
* Возвращает логин доступа к API
*
* @return string|null
*/
#[Pure]
public function getLogin(): ?string
{
return $this->isTestMode()
? TestEnvParams::FFD105()['login']
: $this->login;
}
/**
* Устанавливает логин доступа к API
*
* @param string $login
* @return $this
* @throws EmptyLoginException
* @throws TooLongLoginException
*/
public function setLogin(string $login): self
{
$login = trim($login);
if (empty($login)) {
throw new EmptyLoginException();
} elseif (mb_strlen($login) > Constraints::MAX_LENGTH_LOGIN) {
throw new TooLongLoginException($login);
}
$this->login = $login;
return $this;
}
/**
* Возвращает пароль доступа к API
*
* @return string|null
*/
#[Pure]
public function getPassword(): ?string
{
return $this->isTestMode()
? TestEnvParams::FFD105()['password']
: $this->password;
}
/**
* Устанавливает пароль доступа к API
*
* @param string $password
* @return $this
* @throws EmptyPasswordException Пароль ККТ не может быть пустым
* @throws TooLongPasswordException Слишком длинный пароль ККТ
*/
public function setPassword(string $password): self
{
if (empty($password)) {
throw new EmptyPasswordException();
} elseif (mb_strlen($password) > Constraints::MAX_LENGTH_PASSWORD) {
throw new TooLongPasswordException($password);
}
$this->password = $password;
return $this;
}
/**
* Возвращает набор заголовков для HTTP-запроса
*
* @return array
*/
#[Pure]
private function getHeaders(): array
{
$headers['Content-type'] = 'application/json; charset=utf-8';
if ($this->getToken()) {
$headers['Token'] = $this->getToken();
}
return $headers;
}
/**
* Возвращает полный URL для запроса
*
* @param string $method
* @return string
*/
protected function getUrlToMethod(string $method): string
{
return $this->getMainEndpoint() . '/' . trim($method);
}
/**
* Отправляет авторизационный запрос на сервер АТОЛ и возвращает авторизационный токен
*
* @return string|null
* @throws AuthFailedException
* @throws EmptyPasswordException
* @throws EmptyLoginException
* @throws GuzzleException
*/
protected function doAuth(): ?string
{
$result = $this->sendRequest('POST', $this->getAuthEndpoint(), [
'login' => $this->getLogin() ?? throw new EmptyLoginException(),
'pass' => $this->getPassword() ?? throw new EmptyPasswordException(),
]);
if (!$result->isSuccessful() || !$result->getContent()->token) {
throw new AuthFailedException($result);
}
return $result->getContent()?->token;
}
/**
* Отправляет запрос и возвращает декодированный ответ
*
* @param string $http_method Метод HTTP
* @param string $url URL
* @param array|null $data Данные для передачи
* @param array|null $options Параметры Guzzle
* @return AtolResponse
* @throws GuzzleException
* @see https://guzzle.readthedocs.io/en/latest/request-options.html
*/
protected function sendRequest(
string $http_method,
string $url,
?array $data = null,
?array $options = null
): AtolResponse {
$http_method = strtoupper(trim($http_method));
$options['headers'] = array_merge($this->getHeaders(), $options['headers'] ?? []);
$http_method != 'GET' && $options['json'] = $data;
$this->request = array_merge([
'method' => $http_method,
'url' => $url,
], $options);
$response = $this->http->request($http_method, $url, $options);
return $this->response = new AtolResponse($response);
}
/**
* Выполняет авторизацию на сервере АТОЛ
* Авторизация выполнится только если неизвестен токен
*
* @return bool
* @throws AuthFailedException
* @throws EmptyLoginException
* @throws EmptyPasswordException
* @throws GuzzleException
*/
public function auth(): bool
{
if (empty($this->getToken()) && $token = $this->doAuth()) {
$this->setToken($token);
}
return !empty($this->getToken());
}
/**
* Возвращает URL для запроса авторизации
*
* @return string
*/
abstract protected function getAuthEndpoint(): string;
/**
* Возвращает URL для запросов
*
* @return string
*/
abstract protected function getMainEndpoint(): string;
}

View File

@ -1,52 +1,61 @@
<?php
/**
* Copyright (c) Антон Аксенов (aka Anthony Axenov)
/*
* Copyright (c) 2020-2021 Антон Аксенов (Anthony Axenov)
*
* This code is licensed under MIT.
* Этот код распространяется по лицензии MIT.
* https://github.com/anthonyaxenov/atol-online/blob/master/LICENSE
*/
/** @noinspection PhpMultipleClassDeclarationsInspection */
declare(strict_types=1);
namespace AtolOnline\Api;
use JetBrains\PhpStorm\{
ArrayShape,
Pure};
use JsonSerializable;
use Psr\Http\Message\ResponseInterface;
use stdClass;
use Stringable;
/**
* Класс AtolResponse, описывающий ответ от ККТ
*
* @property mixed $error
* @package AtolOnline\Api
*/
class KktResponse implements JsonSerializable
final class AtolResponse implements JsonSerializable, Stringable
{
/**
* @var int Код ответа сервера
*/
protected $code;
protected int $code;
/**
* @var \stdClass Содержимое ответа сервера
* @var object|array|null Содержимое ответа сервера
*/
protected $content;
protected object | array | null $content;
/**
* @var array Заголовки ответа
*/
protected $headers;
protected array $headers;
/**
* AtolResponse constructor.
*
* @param \Psr\Http\Message\ResponseInterface $response
* @param ResponseInterface $response
*/
public function __construct(ResponseInterface $response)
{
$this->code = $response->getStatusCode();
$this->headers = $response->getHeaders();
$this->content = json_decode($response->getBody());
$this->content = json_decode((string)$response->getBody());
}
/**
* Возвращает заголовки ответа
*
@ -56,18 +65,19 @@ class KktResponse implements JsonSerializable
{
return $this->headers;
}
/**
* Возвращает запрошенный параметр из декодированного объекта результата
*
* @param $name
* @return mixed
*/
public function __get($name)
#[Pure]
public function __get($name): mixed
{
return $this->getContent()->$name;
return $this->getContent()?->$name;
}
/**
* Возвращает код ответа
*
@ -77,42 +87,50 @@ class KktResponse implements JsonSerializable
{
return $this->code;
}
/**
* Возвращает объект результата запроса
*
* @return stdClass|null
* @return mixed
*/
public function getContent(): ?stdClass
public function getContent(): mixed
{
return $this->content;
}
/**
* Проверяет успешность запроса по соержимому результата
*
* @return bool
*/
public function isValid()
#[Pure]
public function isSuccessful(): bool
{
return !empty($this->getCode())
&& !empty($this->getContent())
&& empty($this->getContent()->error)
&& (int)$this->getCode() < 400;
&& $this->getCode() < 400;
}
/**
* Возвращает текстовое представление
*/
public function __toString()
public function __toString(): string
{
return json_encode($this->jsonSerialize(), JSON_UNESCAPED_UNICODE);
}
/**
* @inheritDoc
*/
public function jsonSerialize()
#[ArrayShape(
[
'code' => 'int',
'headers' => 'array|\string[][]',
'body' => 'mixed',
]
)]
public function jsonSerialize(): array
{
return [
'code' => $this->code,
@ -120,4 +138,4 @@ class KktResponse implements JsonSerializable
'body' => $this->content,
];
}
}
}

View File

@ -0,0 +1,379 @@
<?php
/*
* Copyright (c) 2020-2021 Антон Аксенов (Anthony Axenov)
*
* This code is licensed under MIT.
* Этот код распространяется по лицензии MIT.
* https://github.com/anthonyaxenov/atol-online/blob/master/LICENSE
*/
declare(strict_types=1);
namespace AtolOnline\Api;
use AtolOnline\{
Constraints,
TestEnvParams};
use AtolOnline\Entities\{
Correction,
Receipt};
use AtolOnline\Exceptions\{
AuthFailedException,
EmptyGroupException,
EmptyLoginException,
EmptyPasswordException,
InvalidCallbackUrlException,
InvalidEntityInCollectionException,
InvalidInnLengthException,
InvalidPaymentAddressException,
InvalidUuidException,
TooLongCallbackUrlException,
TooLongLoginException,
TooLongPasswordException,
TooLongPaymentAddressException};
use GuzzleHttp\Exception\GuzzleException;
use JetBrains\PhpStorm\Pure;
use Ramsey\Uuid\Uuid;
/**
* Класс фискализатора для регистрации документов на ККТ
*/
final class Fiscalizer extends AtolClient
{
/**
* @var string|null Группа ККТ
*/
private ?string $group = null;
/**
* @var string|null URL для приёма POST-запроса от API АТОЛ с результатом регистрации документа
*/
private ?string $callback_url = null;
/**
* Конструктор
*
* @param bool $test_mode
* @param string|null $login
* @param string|null $password
* @param string|null $group
* @param array $config
* @throws EmptyLoginException
* @throws EmptyPasswordException
* @throws TooLongLoginException
* @throws TooLongPasswordException
* @throws EmptyGroupException
* @see https://guzzle.readthedocs.io/en/latest/request-options.html
*/
public function __construct(
bool $test_mode = true,
?string $login = null,
?string $password = null,
?string $group = null,
array $config = []
) {
parent::__construct($test_mode, $login, $password, $config);
!is_null($group) && $this->setGroup($group);
}
/**
* Возвращает группу доступа к ККТ в соответствии с флагом тестового режима
*
* @return string|null
*/
#[Pure]
public function getGroup(): ?string
{
return $this->isTestMode()
? TestEnvParams::FFD105()['group']
: $this->group;
}
/**
* Устанавливает группу доступа к ККТ
*
* @param string $group
* @return $this
* @throws EmptyGroupException
*/
public function setGroup(string $group): self
{
// критерии к длине строки не описаны ни в схеме, ни в документации
empty($group = trim($group)) && throw new EmptyGroupException();
$this->group = $group;
return $this;
}
/**
* Возвращает URL для приёма колбеков
*
* @return string|null
*/
public function getCallbackUrl(): ?string
{
return $this->callback_url;
}
/**
* Устанавливает URL для приёма колбеков
*
* @param string|null $url
* @return $this
* @throws TooLongCallbackUrlException
* @throws InvalidCallbackUrlException
*/
public function setCallbackUrl(?string $url = null): self
{
$url = trim((string)$url);
if (mb_strlen($url) > Constraints::MAX_LENGTH_CALLBACK_URL) {
throw new TooLongCallbackUrlException($url);
} elseif (!empty($url) && !preg_match(Constraints::PATTERN_CALLBACK_URL, $url)) {
throw new InvalidCallbackUrlException();
}
$this->callback_url = $url ?: null;
return $this;
}
/**
* Регистрирует документ прихода
*
* @param Receipt $receipt Объект документа
* @param string|null $external_id Уникальный код документа (если не указан, то будет создан новый UUID)
* @return AtolResponse|null
* @throws AuthFailedException
* @throws EmptyLoginException
* @throws EmptyPasswordException
* @throws GuzzleException
* @throws InvalidEntityInCollectionException
* @throws InvalidInnLengthException
* @throws InvalidPaymentAddressException
* @throws TooLongPaymentAddressException
*/
public function sell(Receipt $receipt, ?string $external_id = null): ?AtolResponse
{
return $this->registerDocument('sell', $receipt, $external_id);
}
/**
* Регистрирует документ возврата прихода
*
* @param Receipt $receipt Объект документа
* @param string|null $external_id Уникальный код документа (если не указан, то будет создан новый UUID)
* @return AtolResponse|null
* @throws AuthFailedException
* @throws EmptyLoginException
* @throws EmptyPasswordException
* @throws GuzzleException
* @throws InvalidEntityInCollectionException
* @throws InvalidInnLengthException
* @throws InvalidPaymentAddressException
* @throws TooLongPaymentAddressException
*/
public function sellRefund(Receipt $receipt, ?string $external_id = null): ?AtolResponse
{
return $this->registerDocument('sell_refund', $receipt, $external_id);
}
/**
* Регистрирует документ коррекции прихода
*
* @param Correction $correction Объект документа
* @param string|null $external_id Уникальный код документа (если не указан, то будет создан новый UUID)
* @return AtolResponse|null
* @throws AuthFailedException
* @throws EmptyLoginException
* @throws EmptyPasswordException
* @throws GuzzleException
* @throws InvalidEntityInCollectionException
* @throws InvalidInnLengthException
* @throws InvalidPaymentAddressException
* @throws TooLongPaymentAddressException
*/
public function sellCorrect(Correction $correction, ?string $external_id = null): ?AtolResponse
{
return $this->registerDocument('sell_correction', $correction, $external_id);
}
/**
* Регистрирует документ расхода
*
* @param Receipt $receipt Объект документа
* @param string|null $external_id Уникальный код документа (если не указан, то будет создан новый UUID)
* @return AtolResponse|null
* @throws AuthFailedException
* @throws EmptyLoginException
* @throws EmptyPasswordException
* @throws GuzzleException
* @throws InvalidEntityInCollectionException
* @throws InvalidInnLengthException
* @throws InvalidPaymentAddressException
* @throws TooLongPaymentAddressException
*/
public function buy(Receipt $receipt, ?string $external_id = null): ?AtolResponse
{
return $this->registerDocument('buy', $receipt, $external_id);
}
/**
* Регистрирует документ возврата расхода
*
* @param Receipt $receipt Объект документа
* @param string|null $external_id Уникальный код документа (если не указан, то будет создан UUID)
* @return AtolResponse|null
* @throws AuthFailedException
* @throws EmptyLoginException
* @throws EmptyPasswordException
* @throws GuzzleException
* @throws InvalidEntityInCollectionException
* @throws InvalidInnLengthException
* @throws InvalidPaymentAddressException
* @throws TooLongPaymentAddressException
*/
public function buyRefund(Receipt $receipt, ?string $external_id = null): ?AtolResponse
{
return $this->registerDocument('buy_refund', $receipt, $external_id);
}
/**
* Регистрирует документ коррекции расхода
*
* @param Correction $correction Объект документа
* @param string|null $external_id Уникальный код документа (если не указан, то будет создан новый UUID)
* @return AtolResponse|null
* @throws AuthFailedException
* @throws EmptyLoginException
* @throws EmptyPasswordException
* @throws GuzzleException
* @throws InvalidEntityInCollectionException
* @throws InvalidInnLengthException
* @throws InvalidPaymentAddressException
* @throws TooLongPaymentAddressException
*/
public function buyCorrect(Correction $correction, ?string $external_id = null): ?AtolResponse
{
return $this->registerDocument('buy_correction', $correction, $external_id);
}
/**
* Проверяет статус чека на ККТ один раз
*
* @param string $uuid UUID регистрации
* @return AtolResponse|null
* @throws AuthFailedException
* @throws EmptyLoginException
* @throws EmptyPasswordException
* @throws GuzzleException
* @throws InvalidUuidException
*/
public function getDocumentStatus(string $uuid): ?AtolResponse
{
!Uuid::isValid($uuid = trim($uuid)) && throw new InvalidUuidException($uuid);
return $this->auth()
? $this->sendRequest('GET', $this->getFullUrl('report/' . $uuid))
: null;
}
/**
* Проверяет статус чека на ККТ нужное количество раз с указанным интервалом.
* Вернёт результат как только при очередной проверке сменится статус регистрации документа.
*
* @param string $uuid UUID регистрации
* @param int $retry_count Количество попыток
* @param int $timeout Таймаут в секундах между попытками
* @return AtolResponse|null
* @throws AuthFailedException
* @throws EmptyLoginException
* @throws EmptyPasswordException
* @throws GuzzleException
* @throws InvalidUuidException
*/
public function pollDocumentStatus(string $uuid, int $retry_count = 5, int $timeout = 1): ?AtolResponse
{
$try = 0;
do {
$response = $this->getDocumentStatus($uuid);
if ($response->isSuccessful() && $response->getContent()->status == 'done') {
break;
} else {
sleep($timeout);
}
++$try;
} while ($try < $retry_count);
return $response;
}
/**
* Отправляет документ на регистрацию
*
* @param string $api_method Метод API
* @param Receipt|Correction $document Документ
* @param string|null $external_id Уникальный код документа (если не указан, то будет создан новый UUID)
* @return AtolResponse|null
* @throws AuthFailedException
* @throws EmptyLoginException
* @throws EmptyPasswordException
* @throws GuzzleException
* @throws InvalidEntityInCollectionException
* @throws InvalidInnLengthException
* @throws InvalidPaymentAddressException
* @throws TooLongPaymentAddressException
*/
protected function registerDocument(
string $api_method,
Receipt | Correction $document,
?string $external_id = null
): ?AtolResponse {
$this->isTestMode() && $document->getCompany()
->setInn(TestEnvParams::FFD105()['inn'])
->setPaymentAddress(TestEnvParams::FFD105()['payment_address']);
$this->isTestMode() && $document instanceof Receipt
&& $document->getClient()->setInn(TestEnvParams::FFD105()['inn']);
$this->getCallbackUrl() && $data['service'] = ['callback_url' => $this->getCallbackUrl()];
return $this->auth()
? $this->sendRequest(
'POST',
$this->getFullUrl($api_method),
array_merge($data ?? [], [
'timestamp' => date('d.m.Y H:i:s'),
'external_id' => $external_id ?: Uuid::uuid4()->toString(),
$document::DOC_TYPE => $document->jsonSerialize(),
])
)
: null;
}
/**
* @inheritDoc
*/
#[Pure]
protected function getAuthEndpoint(): string
{
return $this->isTestMode()
? 'https://testonline.atol.ru/possystem/v4/getToken'
: 'https://online.atol.ru/possystem/v4/getToken';
}
/**
* @inheritDoc
*/
#[Pure]
protected function getMainEndpoint(): string
{
return $this->isTestMode()
? 'https://testonline.atol.ru/possystem/v4/'
: 'https://online.atol.ru/possystem/v4/';
}
/**
* Возвращает полный URL метода API
*
* @param string $api_method
* @return string
*/
#[Pure]
protected function getFullUrl(string $api_method): string
{
return $this->getMainEndpoint() . $this->getGroup() . '/' . trim($api_method);
}
}

130
src/Api/Monitor.php 100644
View File

@ -0,0 +1,130 @@
<?php
/*
* Copyright (c) 2020-2021 Антон Аксенов (Anthony Axenov)
*
* This code is licensed under MIT.
* Этот код распространяется по лицензии MIT.
* https://github.com/anthonyaxenov/atol-online/blob/master/LICENSE
*/
declare(strict_types=1);
namespace AtolOnline\Api;
use AtolOnline\Entities\Kkt;
use AtolOnline\Exceptions\{
AuthFailedException,
EmptyLoginException,
EmptyMonitorDataException,
EmptyPasswordException,
NotEnoughMonitorDataException};
use GuzzleHttp\Exception\GuzzleException;
use Illuminate\Support\Collection;
use JetBrains\PhpStorm\Pure;
/**
* Класс для мониторинга ККТ
*
* @see https://online.atol.ru/files/API_service_information.pdf Документация
*/
final class Monitor extends AtolClient
{
/**
* @inheritDoc
*/
#[Pure]
protected function getAuthEndpoint(): string
{
return $this->isTestMode()
? 'https://testonline.atol.ru/api/auth/v1/gettoken'
: 'https://online.atol.ru/api/auth/v1/gettoken';
}
/**
* @inheritDoc
*/
#[Pure]
protected function getMainEndpoint(): string
{
return $this->isTestMode()
? 'https://testonline.atol.ru/api/kkt/v1'
: 'https://online.atol.ru/api/kkt/v1';
}
/**
* Получает от API информацию обо всех ККТ и ФН в рамках группы
*
* @param int|null $limit
* @param int|null $offset
* @return AtolResponse|null
* @throws GuzzleException
* @throws AuthFailedException
* @throws EmptyLoginException
* @throws EmptyPasswordException
* @see https://online.atol.ru/files/API_service_information.pdf Документация, стр 9
*/
protected function fetchAll(?int $limit = null, ?int $offset = null): ?AtolResponse
{
$params = [];
!is_null($limit) && $params['limit'] = $limit;
!is_null($offset) && $params['offset'] = $offset;
return $this->auth()
? $this->sendRequest('GET', self::getUrlToMethod('cash-registers'), $params)
: null;
}
/**
* Возвращает информацию обо всех ККТ и ФН в рамках группы
*
* @param int|null $limit
* @param int|null $offset
* @return Collection
* @throws AuthFailedException
* @throws EmptyLoginException
* @throws EmptyPasswordException
* @throws GuzzleException
* @see https://online.atol.ru/files/API_service_information.pdf Документация, стр 9
*/
public function getAll(?int $limit = null, ?int $offset = null): Collection
{
$collection = collect($this->fetchAll($limit, $offset)->getContent());
return $collection->map(fn ($data) => new Kkt($data));
}
/**
* Получает от API информацию о конкретной ККТ по её серийному номеру
*
* @param string $serial_number
* @return AtolResponse
* @throws GuzzleException
* @see https://online.atol.ru/files/API_service_information.pdf Документация, стр 11
*/
protected function fetchOne(string $serial_number): AtolResponse
{
return $this->sendRequest(
'GET',
self::getUrlToMethod('cash-registers') . '/' . trim($serial_number),
options: [
'headers' => [
'Accept' => 'application/hal+json',
],
]
);
}
/**
* Возвращает информацию о конкретной ККТ по её серийному номеру
*
* @param string $serial_number
* @return Kkt
* @throws GuzzleException
* @throws EmptyMonitorDataException
* @throws NotEnoughMonitorDataException
* @see https://online.atol.ru/files/API_service_information.pdf Документация, стр 11
*/
public function getOne(string $serial_number): Kkt
{
return new Kkt($this->fetchOne($serial_number)->getContent()->data);
}
}

View File

@ -1,576 +0,0 @@
<?php
/**
* Copyright (c) Антон Аксенов (aka Anthony Axenov)
*
* This code is licensed under MIT.
* Этот код распространяется по лицензии MIT.
* https://github.com/anthonyaxenov/atol-online/blob/master/LICENSE
*/
namespace AtolOnline\Api;
use AtolOnline\{Constants\Constraints,
Constants\TestEnvParams,
Entities\Company,
Entities\Document,
Exceptions\AtolAuthFailedException,
Exceptions\AtolCallbackUrlTooLongException,
Exceptions\AtolCorrectionInfoException,
Exceptions\AtolInvalidCallbackUrlException,
Exceptions\AtolInvalidUuidException,
Exceptions\AtolKktLoginEmptyException,
Exceptions\AtolKktLoginTooLongException,
Exceptions\AtolKktPasswordEmptyException,
Exceptions\AtolKktPasswordTooLongException,
Exceptions\AtolWrongDocumentTypeException
};
use GuzzleHttp\Client;
use Ramsey\Uuid\Uuid;
/**
* Класс для отправки запросов на ККТ
*
* @package AtolOnline\Api
*/
class Kkt extends Client
{
/**
* @var bool Флаг тестового режима работы
*/
protected $is_test_mode = false;
/**
* @var array Настройки доступа к ККТ
*/
protected $kkt_config = [];
/**
* @var \AtolOnline\Api\KktResponse|null Последний ответ сервера АТОЛ
*/
protected $last_response;
/**
* @var string|null Токен авторизации
*/
private $auth_token;
/**
* Kkt constructor.
*
* @param string|null $group
* @param string|null $login
* @param string|null $pass
* @param bool $test_mode Флаг тестового режима
* @param array $guzzle_config Конфигурация GuzzleHttp
* @throws \AtolOnline\Exceptions\AtolKktLoginEmptyException Логин ККТ не может быть пустым
* @throws \AtolOnline\Exceptions\AtolKktLoginTooLongException Слишком длинный логин ККТ
* @throws \AtolOnline\Exceptions\AtolKktPasswordEmptyException Пароль ККТ не может быть пустым
* @throws \AtolOnline\Exceptions\AtolKktPasswordTooLongException Слишком длинный пароль ККТ
* @see https://guzzle.readthedocs.io/en/latest/request-options.html
*/
public function __construct(
?string $group = null,
?string $login = null,
?string $pass = null,
bool $test_mode = false,
array $guzzle_config = []
) {
$this->resetKktConfig();
if ($group) {
$this->setGroup($group);
}
if ($login) {
$this->setLogin($login);
}
if ($login) {
$this->setPassword($pass);
}
$this->setTestMode($test_mode);
$guzzle_config['base_uri'] = $this->getEndpoint();
$guzzle_config['http_errors'] = $guzzle_config['http_errors'] ?? false;
parent::__construct($guzzle_config);
}
/**
* Устанавливает группу доступа к ККТ
*
* @param string $group
* @return $this
*/
public function setGroup(string $group)
{
$this->kkt_config['prod']['group'] = $group;
return $this;
}
/**
* Возвращает группу доступа к ККТ в соответствии с флагом тестового режима
*
* @return string
*/
public function getGroup(): string
{
return $this->kkt_config[$this->isTestMode() ? 'test' : 'prod']['group'];
}
/**
* Устанавливает логин доступа к ККТ
*
* @param string $login
* @return $this
* @throws \AtolOnline\Exceptions\AtolKktLoginEmptyException Логин ККТ не может быть пустым
* @throws \AtolOnline\Exceptions\AtolKktLoginTooLongException Слишком длинный логин ККТ
*/
public function setLogin(string $login)
{
if (empty($login)) {
throw new AtolKktLoginEmptyException();
} elseif (valid_strlen($login) > Constraints::MAX_LENGTH_LOGIN) {
throw new AtolKktLoginTooLongException($login, Constraints::MAX_LENGTH_LOGIN);
}
$this->kkt_config['prod']['login'] = $login;
return $this;
}
/**
* Возвращает логин доступа к ККТ в соответствии с флагом тестового режима
*
* @return string
*/
public function getLogin(): string
{
return $this->kkt_config[$this->isTestMode() ? 'test' : 'prod']['login'];
}
/**
* Устанавливает пароль доступа к ККТ
*
* @param string $password
* @return $this
* @throws \AtolOnline\Exceptions\AtolKktPasswordEmptyException Пароль ККТ не может быть пустым
* @throws \AtolOnline\Exceptions\AtolKktPasswordTooLongException Слишком длинный пароль ККТ
*/
public function setPassword(string $password)
{
if (empty($password)) {
throw new AtolKktPasswordEmptyException();
} elseif (valid_strlen($password) > Constraints::MAX_LENGTH_PASSWORD) {
throw new AtolKktPasswordTooLongException($password, Constraints::MAX_LENGTH_PASSWORD);
}
$this->kkt_config['prod']['pass'] = $password;
return $this;
}
/**
* Возвращает логин ККТ в соответствии с флагом тестового режима
*
* @return string
*/
public function getPassword(): string
{
return $this->kkt_config[$this->isTestMode() ? 'test' : 'prod']['pass'];
}
/**
* Устанавливает URL для приёма колбеков
*
* @param string $url
* @return $this
* @throws \AtolOnline\Exceptions\AtolCallbackUrlTooLongException Слишком длинный Callback URL
* @throws \AtolOnline\Exceptions\AtolInvalidCallbackUrlException Невалидный Callback URL
*/
public function setCallbackUrl(string $url)
{
if (valid_strlen($url) > Constraints::MAX_LENGTH_CALLBACK_URL) {
throw new AtolCallbackUrlTooLongException($url, Constraints::MAX_LENGTH_CALLBACK_URL);
} elseif (preg_match(Constraints::PATTERN_CALLBACK_URL, $url)) {
throw new AtolInvalidCallbackUrlException();
}
$this->kkt_config[$this->isTestMode() ? 'test' : 'prod']['callback_url'] = $url;
return $this;
}
/**
* Возвращает URL для приёма колбеков
*
* @return string
*/
public function getCallbackUrl(): string
{
return $this->kkt_config[$this->isTestMode() ? 'test' : 'prod']['callback_url'];
}
/**
* Возвращает последний ответ сервера
*
* @return mixed
*/
public function getLastResponse()
{
return $this->last_response;
}
/**
* Возвращает флаг тестового режима
*
* @return bool
*/
public function isTestMode(): bool
{
return $this->is_test_mode;
}
/**
* Устанавливает флаг тестового режима
*
* @param bool $test_mode
* @return $this
*/
public function setTestMode(bool $test_mode = true)
{
$this->is_test_mode = $test_mode;
return $this;
}
/**
* Регистрирует документ прихода
*
* @param \AtolOnline\Entities\Document $document Объект документа
* @param string|null $external_id Уникальный код документа (если не указан, то будет создан UUID)
* @return \AtolOnline\Api\KktResponse
* @throws \AtolOnline\Exceptions\AtolAuthFailedException Ошибка авторизации
* @throws \AtolOnline\Exceptions\AtolCorrectionInfoException В документе есть данные коррекции
* @throws \AtolOnline\Exceptions\AtolInnWrongLengthException Некорректная длина ИНН
* @throws \AtolOnline\Exceptions\AtolPaymentAddressTooLongException Слишком длинный адрес места расчётов
* @throws \AtolOnline\Exceptions\AtolWrongDocumentTypeException Некорректный тип документа
* @throws \GuzzleHttp\Exception\GuzzleException
*/
public function sell(Document $document, ?string $external_id = null)
{
if ($document->getCorrectionInfo()) {
throw new AtolCorrectionInfoException('Некорректная операция над документом коррекции');
}
return $this->registerDocument('sell', 'receipt', $document, $external_id);
}
/**
* Регистрирует документ возврата прихода
*
* @param \AtolOnline\Entities\Document $document Объект документа
* @param string|null $external_id Уникальный код документа (если не указан, то будет создан UUID)
* @return \AtolOnline\Api\KktResponse
* @throws \AtolOnline\Exceptions\AtolAuthFailedException Ошибка авторизации
* @throws \AtolOnline\Exceptions\AtolCorrectionInfoException В документе есть данные коррекции
* @throws \AtolOnline\Exceptions\AtolInnWrongLengthException Некорректная длина ИНН
* @throws \AtolOnline\Exceptions\AtolPaymentAddressTooLongException Слишком длинный адрес места расчётов
* @throws \AtolOnline\Exceptions\AtolPriceTooHighException Слишком большая сумма
* @throws \AtolOnline\Exceptions\AtolTooManyVatsException Слишком много ставок НДС
* @throws \AtolOnline\Exceptions\AtolWrongDocumentTypeException Некорректный тип документа
* @throws \GuzzleHttp\Exception\GuzzleException
*/
public function sellRefund(Document $document, ?string $external_id = null)
{
if ($document->getCorrectionInfo()) {
throw new AtolCorrectionInfoException('Invalid operation on correction document');
}
return $this->registerDocument('sell_refund', 'receipt', $document->clearVats(), $external_id);
}
/**
* Регистрирует документ коррекции прихода
*
* @param \AtolOnline\Entities\Document $document Объект документа
* @param string|null $external_id Уникальный код документа (если не указан, то будет создан UUID)
* @return \AtolOnline\Api\KktResponse
* @throws \AtolOnline\Exceptions\AtolAuthFailedException Ошибка авторизации
* @throws \AtolOnline\Exceptions\AtolCorrectionInfoException В документе отсутствуют данные коррекции
* @throws \AtolOnline\Exceptions\AtolInnWrongLengthException Некорректная длина ИНН
* @throws \AtolOnline\Exceptions\AtolPaymentAddressTooLongException Слишком длинный адрес места расчётов
* @throws \AtolOnline\Exceptions\AtolTooManyItemsException Слишком много предметов расчёта
* @throws \AtolOnline\Exceptions\AtolWrongDocumentTypeException Некорректный тип документа
* @throws \GuzzleHttp\Exception\GuzzleException
*/
public function sellCorrection(Document $document, ?string $external_id = null)
{
if (!$document->getCorrectionInfo()) {
throw new AtolCorrectionInfoException();
}
$document->setClient(null)->setItems([]);
return $this->registerDocument('sell_correction', 'correction', $document, $external_id);
}
/**
* Регистрирует документ расхода
*
* @param \AtolOnline\Entities\Document $document
* @param string|null $external_id Уникальный код документа (если не указан, то будет создан UUID)
* @return \AtolOnline\Api\KktResponse
* @throws \AtolOnline\Exceptions\AtolAuthFailedException Ошибка авторизации
* @throws \AtolOnline\Exceptions\AtolCorrectionInfoException В документе есть данные коррекции
* @throws \AtolOnline\Exceptions\AtolInnWrongLengthException Некорректная длина ИНН
* @throws \AtolOnline\Exceptions\AtolPaymentAddressTooLongException Слишком длинный адрес места расчётов
* @throws \AtolOnline\Exceptions\AtolWrongDocumentTypeException Некорректный тип документа
* @throws \GuzzleHttp\Exception\GuzzleException
*/
public function buy(Document $document, ?string $external_id = null)
{
if ($document->getCorrectionInfo()) {
throw new AtolCorrectionInfoException('Invalid operation on correction document');
}
return $this->registerDocument('buy', 'receipt', $document, $external_id);
}
/**
* Регистрирует документ возврата расхода
*
* @param \AtolOnline\Entities\Document $document
* @param string|null $external_id Уникальный код документа (если не указан, то будет создан UUID)
* @return \AtolOnline\Api\KktResponse
* @throws \AtolOnline\Exceptions\AtolAuthFailedException Ошибка авторизации
* @throws \AtolOnline\Exceptions\AtolCorrectionInfoException В документе есть данные коррекции
* @throws \AtolOnline\Exceptions\AtolInnWrongLengthException Некорректная длина ИНН
* @throws \AtolOnline\Exceptions\AtolPaymentAddressTooLongException Слишком длинный адрес места расчётов
* @throws \AtolOnline\Exceptions\AtolPriceTooHighException Слишком большая сумма
* @throws \AtolOnline\Exceptions\AtolTooManyVatsException Слишком много ставок НДС
* @throws \AtolOnline\Exceptions\AtolWrongDocumentTypeException Некорректный тип документа
* @throws \GuzzleHttp\Exception\GuzzleException
*/
public function buyRefund(Document $document, ?string $external_id = null)
{
if ($document->getCorrectionInfo()) {
throw new AtolCorrectionInfoException('Invalid operation on correction document');
}
return $this->registerDocument('buy_refund', 'receipt', $document->clearVats(), $external_id);
}
/**
* Регистрирует документ коррекции расхода
*
* @param \AtolOnline\Entities\Document $document
* @param string|null $external_id Уникальный код документа (если не указан, то будет создан UUID)
* @return \AtolOnline\Api\KktResponse
* @throws \AtolOnline\Exceptions\AtolAuthFailedException Ошибка авторизации
* @throws \AtolOnline\Exceptions\AtolCorrectionInfoException В документе отсутствуют данные коррекции
* @throws \AtolOnline\Exceptions\AtolInnWrongLengthException Некорректная длтина ИНН
* @throws \AtolOnline\Exceptions\AtolPaymentAddressTooLongException Слишком длинный адрес места расчётов
* @throws \AtolOnline\Exceptions\AtolTooManyItemsException Слишком много предметов расчёта
* @throws \AtolOnline\Exceptions\AtolWrongDocumentTypeException Некорректный тип документа
* @throws \GuzzleHttp\Exception\GuzzleException
*/
public function buyCorrection(Document $document, ?string $external_id = null)
{
if (!$document->getCorrectionInfo()) {
throw new AtolCorrectionInfoException();
}
$document->setClient(null)->setItems([]);
return $this->registerDocument('buy_correction', 'correction', $document, $external_id);
}
/**
* Проверяет статус чека на ККТ один раз
*
* @param string $uuid UUID регистрации
* @return \AtolOnline\Api\KktResponse
* @throws \AtolOnline\Exceptions\AtolAuthFailedException Ошибка авторизации
* @throws \AtolOnline\Exceptions\AtolInvalidUuidException Некорректный UUID документа
* @throws \GuzzleHttp\Exception\GuzzleException
*/
public function getDocumentStatus(string $uuid)
{
$uuid = trim($uuid);
if (!Uuid::isValid($uuid)) {
throw new AtolInvalidUuidException($uuid);
}
$this->auth();
return $this->sendAtolRequest('GET', 'report/'.$uuid);
}
/**
* Проверяет статус чека на ККТ нужное количество раз с указанным интервалом.
* Вернёт результат как только при очередной проверке сменится статус регистрации документа.
*
* @param string $uuid UUID регистрации
* @param int $retry_count Количество попыток
* @param int $timeout Таймаут в секундах между попытками
* @return \AtolOnline\Api\KktResponse
* @throws \AtolOnline\Exceptions\AtolAuthFailedException Ошибка авторизации
* @throws \AtolOnline\Exceptions\AtolInvalidUuidException Некорректный UUID документа
* @throws \GuzzleHttp\Exception\GuzzleException
*/
public function pollDocumentStatus(string $uuid, int $retry_count = 5, int $timeout = 1)
{
$try = 0;
do {
$response = $this->getDocumentStatus($uuid);
if ($response->isValid() && $response->getContent()->status == 'done') {
break;
} else {
sleep($timeout);
}
++$try;
} while ($try < $retry_count);
return $response;
}
/**
* Возвращает текущий токен авторизации
*
* @return string
*/
public function getAuthToken(): ?string
{
return $this->auth_token;
}
/**
* Устанавливает заранее известный токен авторизации
*
* @param string|null $auth_token
* @return $this
*/
public function setAuthToken(?string $auth_token)
{
$this->auth_token = $auth_token;
return $this;
}
/**
* Сбрасывает настройки ККТ по умолчанию
*/
protected function resetKktConfig(): void
{
$this->kkt_config['prod']['group'] = '';
$this->kkt_config['prod']['login'] = '';
$this->kkt_config['prod']['pass'] = '';
$this->kkt_config['prod']['url'] = 'https://online.atol.ru/possystem/v4';
$this->kkt_config['prod']['callback_url'] = '';
$this->kkt_config['test']['group'] = TestEnvParams::GROUP;
$this->kkt_config['test']['login'] = TestEnvParams::LOGIN;
$this->kkt_config['test']['pass'] = TestEnvParams::PASSWORD;
$this->kkt_config['test']['url'] = 'https://testonline.atol.ru/possystem/v4';
$this->kkt_config['test']['callback_url'] = '';
}
/**
* Возвращает набор заголовков для HTTP-запроса
*
* @return array
*/
protected function getHeaders()
{
$headers['Content-type'] = 'application/json; charset=utf-8';
if ($this->getAuthToken()) {
$headers['Token'] = $this->getAuthToken();
}
return $headers;
}
/**
* Возвращает адрес сервера в соответствии с флагом тестового режима
*
* @return string
*/
protected function getEndpoint(): string
{
return $this->kkt_config[$this->isTestMode() ? 'test' : 'prod']['url'];
}
/**
* Возвращает полный URL до метода API
*
* @param string $to_method
* @param array|null $get_parameters
* @return string
*/
protected function makeUrl(string $to_method, array $get_parameters = null)
{
$url = $this->getEndpoint().($this->getAuthToken() ? '/'.$this->getGroup() : '').'/'.$to_method;
if ($get_parameters && is_array($get_parameters)) {
$url .= '?'.http_build_query($get_parameters);
}
return $url;
}
/**
* Делает запрос, возвращает декодированный ответ
*
* @param string $http_method Метод HTTP (GET, POST и пр)
* @param string $api_method Метод API
* @param mixed $data Данные для передачи
* @param array|null $options Параметры Guzzle
* @return \AtolOnline\Api\KktResponse
* @throws \GuzzleHttp\Exception\GuzzleException
* @see https://guzzle.readthedocs.io/en/latest/request-options.html
*/
protected function sendAtolRequest(string $http_method, string $api_method, $data = null, array $options = null)
{
$http_method = strtoupper($http_method);
$options['headers'] = $this->getHeaders();
$url = $http_method == 'GET'
? $this->makeUrl($api_method, $data)
: $this->makeUrl($api_method, ['token' => $this->getAuthToken()]);
if ($http_method != 'GET') {
$options['json'] = $data;
}
$response = $this->request($http_method, $url, $options);
return $this->last_response = new KktResponse($response);
}
/**
* Производит авторизацию на ККТ и получает токен доступа для дальнейших HTTP-запросов
*
* @return bool
* @throws \AtolOnline\Exceptions\AtolAuthFailedException Ошибка авторизации
* @throws \GuzzleHttp\Exception\GuzzleException
*/
protected function auth()
{
if (!$this->getAuthToken()) {
$result = $this->sendAtolRequest('GET', 'getToken', [
'login' => $this->getLogin(),
'pass' => $this->getPassword(),
]);
if (!$result->isValid() || !$result->getContent()->token) {
throw new AtolAuthFailedException($result);
}
$this->auth_token = $result->getContent()->token;
}
return true;
}
/**
* Отправляет документ на регистрацию
*
* @param string $api_method Метод API
* @param string $type Тип документа: receipt, correction
* @param \AtolOnline\Entities\Document $document Объект документа
* @param string|null $external_id Уникальный код документа (если не указан, то будет создан UUID)
* @return \AtolOnline\Api\KktResponse
* @throws \AtolOnline\Exceptions\AtolAuthFailedException Ошибка авторизации
* @throws \AtolOnline\Exceptions\AtolWrongDocumentTypeException Некорректный тип документа
* @throws \AtolOnline\Exceptions\AtolInnWrongLengthException Некорректная длина ИНН
* @throws \AtolOnline\Exceptions\AtolPaymentAddressTooLongException Слишком длинный адрес места расчётов
* @throws \GuzzleHttp\Exception\GuzzleException
*/
protected function registerDocument(string $api_method, string $type, Document $document, ?string $external_id = null)
{
$type = trim($type);
if (!in_array($type, ['receipt', 'correction'])) {
throw new AtolWrongDocumentTypeException($type);
}
$this->auth();
if ($this->isTestMode()) {
$document->setCompany(($document->getCompany() ?: new Company())
->setInn(TestEnvParams::INN)
->setSno(TestEnvParams::SNO)
->setPaymentAddress(TestEnvParams::PAYMENT_ADDRESS));
}
$data['timestamp'] = date('d.m.y H:i:s');
$data['external_id'] = $external_id ?: Uuid::uuid4()->toString();
$data[$type] = $document;
if ($this->getCallbackUrl()) {
$data['service'] = ['callback_url' => $this->getCallbackUrl()];
}
return $this->sendAtolRequest('POST', trim($api_method), $data);
}
}

View File

@ -1,76 +0,0 @@
<?php
namespace AtolOnline\Constants;
/**
* Класс с константами ограничений: максимальные длины, правила валидации значений
*
* @package AtolOnline\Constants
*/
class Constraints
{
/**
* Максимальная длина Callback URL
*/
const MAX_LENGTH_CALLBACK_URL = 256;
/**
* Максимальная длина email
*/
const MAX_LENGTH_EMAIL = 64;
/**
* Максимальная длина логина ККТ
*/
const MAX_LENGTH_LOGIN = 100;
/**
* Максимальная длина пароля ККТ
*/
const MAX_LENGTH_PASSWORD = 100;
/**
* Максимальная длина имени покупателя
*/
const MAX_LENGTH_CLIENT_NAME = 256;
/**
* Максимальная длина телефона покупателя
*/
const MAX_LENGTH_CLIENT_PHONE = 64;
/**
* Максимальная длина адреса места расчётов
*/
const MAX_LENGTH_PAYMENT_ADDRESS = 256;
/**
* Максимальная длина имени кассира
*/
const MAX_LENGTH_CASHIER_NAME = 64;
/**
* Максимальная длина наименования предмета расчётов
*/
const MAX_LENGTH_ITEM_NAME = 128;
/**
* Максимальная длина единицы измерения предмета расчётов
*/
const MAX_LENGTH_MEASUREMENT_UNIT = 16;
/**
* Максимальная длина пользовательских данных для предмета расчётов
*/
const MAX_LENGTH_USER_DATA = 64;
/**
* Регулярное выражание для валидации строки ИНН
*/
const PATTERN_INN = "/(^[0-9]{10}$)|(^[0-9]{12}$)/";
/**
* Регулярное выражание для валидации строки Callback URL
*/
const PATTERN_CALLBACK_URL = "^http(s?)\:\/\/[0-9a-zA-Zа-яА-Я]([-.\w]*[0-9a-zA-Zа-яА-Я])*(:(0-9)*)*(\/?)([a-zAZ0-9а-яА-Я\-\.\?\,\'\/\\\+&=%\$#_]*)?$";
}

View File

@ -1,108 +0,0 @@
<?php
/**
* Copyright (c) Антон Аксенов (aka Anthony Axenov)
*
* This code is licensed under MIT.
* Этот код распространяется по лицензии MIT.
* https://github.com/anthonyaxenov/atol-online/blob/master/LICENSE
*/
namespace AtolOnline\Constants;
/**
* Константы, определяющие признаки предметов расчёта. Тег ФФД - 1212.
*
* @package AtolOnline\Constants
*/
class PaymentObjects
{
/**
* Товар, кроме подакцизного
*/
const COMMODITY = 'commodity';
/**
* Товар подакцизный
*/
const EXCISE = 'excise';
/**
* Работа
*/
const JOB = 'job';
/**
* Услуга
*/
const SERVICE = 'service';
/**
* Ставка азартной игры
*/
const GAMBLING_BET = 'gambling_bet';
/**
* Выигрыш азартной игры
*/
const GAMBLING_PRIZE = 'gambling_prize';
/**
* Лотерея
*/
const LOTTERY = 'lottery';
/**
* Выигрыш лотереи
*/
const LOTTERY_PRIZE = 'lottery_prize';
/**
* Предоставление результатов интеллектуальной деятельности
*/
const INTELLECTUAL_ACTIVITY = 'intellectual_activity';
/**
* Платёж (задаток, кредит, аванс, предоплата, пеня, штраф, бонус и пр.)
*/
const PAYMENT = 'payment';
/**
* Агентское вознаграждение
*/
const AGENT_COMMISSION = 'agent_commission';
/**
* Составной предмет расчёта
*/
const COMPOSITE = 'composite';
/**
* Другой предмет расчёта
*/
const ANOTHER = 'another';
/**
* Имущественное право
*/
const PROPERTY_RIGHT = 'property_right';
/**
* Внереализационный доход
*/
const NON_OPERATING_GAIN = 'non-operating_gain';
/**
* Страховые взносы
*/
const INSURANCE_PREMIUM = 'insurance_premium';
/**
* Торговый сбор
*/
const SALES_TAX = 'sales_tax';
/**
* Курортный сбор
*/
const RESORT_FEE = 'resort_fee';
}

View File

@ -1,53 +0,0 @@
<?php
/**
* Copyright (c) Антон Аксенов (aka Anthony Axenov)
*
* This code is licensed under MIT.
* Этот код распространяется по лицензии MIT.
* https://github.com/anthonyaxenov/atol-online/blob/master/LICENSE
*/
namespace AtolOnline\Constants;
/**
* Константы, определяющие виды оплат. Тег ФФД - 1031, 1081, 1215, 1216, 1217.
*
* @package AtolOnline\Constants
*/
class PaymentTypes
{
/**
* Расчёт наличными. Тег ФФД - 1031.
*/
const CASH = 0;
/**
* Расчёт безналичными. Тег ФФД - 1081.
*/
const ELECTRON = 1;
/**
* Предварительная оплата (зачет аванса). Тег ФФД - 1215.
*/
const PRE_PAID = 2;
/**
* Предварительная оплата (кредит). Тег ФФД - 1216.
*/
const CREDIT = 3;
/**
* Иная форма оплаты (встречное предоставление). Тег ФФД - 1217.
*/
const OTHER = 4;
/**
* Расширенный типы оплаты
* Для каждого фискального типа оплаты можно указать расширенный тип оплаты
*/
const ADD_5 = 5;
const ADD_6 = 6;
const ADD_7 = 7;
const ADD_8 = 8;
const ADD_9 = 9;
}

View File

@ -1,49 +0,0 @@
<?php
/**
* Copyright (c) Антон Аксенов (aka Anthony Axenov)
*
* This code is licensed under MIT.
* Этот код распространяется по лицензии MIT.
* https://github.com/anthonyaxenov/atol-online/blob/master/LICENSE
*/
namespace AtolOnline\Constants;
/**
* Константы, определяющие параметры тестовой среды
*
* @see https://online.atol.ru/files/ffd/test_sreda.txt
* @package AtolOnline\Constants
*/
class TestEnvParams
{
/**
* Логин
*/
const LOGIN = 'v4-online-atol-ru';
/**
* Пароль
*/
const PASSWORD = 'iGFFuihss';
/**
* Группа
*/
const GROUP = 'v4-online-atol-ru_4179';
/**
* Система налогообложения
*/
const SNO = SnoTypes::OSN;
/**
* ИНН
*/
const INN = '5544332219';
/**
* Адрес места расчётов
*/
const PAYMENT_ADDRESS = 'https://v4.online.atol.ru';
}

View File

@ -1,149 +0,0 @@
<?php
/**
* Copyright (c) Антон Аксенов (aka Anthony Axenov)
*
* This code is licensed under MIT.
* Этот код распространяется по лицензии MIT.
* https://github.com/anthonyaxenov/atol-online/blob/master/LICENSE
*/
namespace AtolOnline\Entities;
use AtolOnline\{Constants\Constraints, Exceptions\AtolNameTooLongException, Exceptions\AtolPhoneTooLongException, Traits\HasEmail, Traits\HasInn};
/**
* Класс Client, описывающий сущность покупателя
*
* @package AtolOnline\Entities
*/
class Client extends Entity
{
use
/**
* Покупатель может иметь почту. Тег ФФД - 1008.
*/
HasEmail,
/**
* Покупатель может иметь ИНН. Тег ФФД - 1228.
*/
HasInn;
/**
* @var string Телефон покупателя. Тег ФФД - 1008.
*/
protected $phone;
/**
* @var string Имя покупателя. Тег ФФД - 1227.
*/
protected $name;
/**
* Client constructor.
*
* @param string|null $name Наименование
* @param string|null $phone Телефон
* @param string|null $email Email
* @param string|null $inn ИНН
* @throws \AtolOnline\Exceptions\AtolEmailTooLongException Слишком длинный email
* @throws \AtolOnline\Exceptions\AtolEmailValidateException Невалидный email
* @throws \AtolOnline\Exceptions\AtolInnWrongLengthException Некорректная длина ИНН
* @throws \AtolOnline\Exceptions\AtolNameTooLongException Слишком длинное имя
* @throws \AtolOnline\Exceptions\AtolPhoneTooLongException СЛишком длинный номер телефона
*/
public function __construct(?string $name = null, ?string $phone = null, ?string $email = null, ?string $inn = null)
{
if ($name) {
$this->setName($name);
}
if ($email) {
$this->setEmail($email);
}
if ($phone) {
$this->setPhone($phone);
}
if ($inn) {
$this->setInn($inn);
}
}
/**
* Возвращает имя покупателя. Тег ФФД - 1227.
*
* @return string
*/
public function getName()
{
return $this->name;
}
/**
* Устанавливает имя покупателя
* Тег ФФД - 1227.
*
* @param string $name
* @return $this
* @throws AtolNameTooLongException
*/
public function setName(string $name)
{
$name = trim($name);
if (valid_strlen($name) > Constraints::MAX_LENGTH_CLIENT_NAME) {
throw new AtolNameTooLongException($name, Constraints::MAX_LENGTH_CLIENT_NAME);
}
$this->name = $name;
return $this;
}
/**
* Возвращает телефон покупателя.
* Тег ФФД - 1008.
*
* @return string
*/
public function getPhone()
{
return $this->phone ?? '';
}
/**
* Устанавливает телефон покупателя.
* Тег ФФД - 1008.
* Входная строка лишается всех знаков, кроме цифр и знака '+'.
*
* @param string $phone
* @return $this
* @throws AtolPhoneTooLongException
*/
public function setPhone(string $phone)
{
$phone = preg_replace("/[^0-9+]/", '', $phone);
if (valid_strlen($phone) > Constraints::MAX_LENGTH_CLIENT_PHONE) {
throw new AtolPhoneTooLongException($phone, Constraints::MAX_LENGTH_CLIENT_PHONE);
}
$this->phone = $phone;
return $this;
}
/**
* @inheritDoc
*/
public function jsonSerialize()
{
$json = [];
if ($this->getName()) {
$json['name'] = $this->getName() ?? '';
}
if ($this->getEmail()) {
$json['email'] = $this->getEmail() ?? '';
}
if ($this->getPhone()) {
$json['phone'] = $this->getPhone() ?? '';
}
if ($this->getInn()) {
$json['inn'] = $this->getInn() ?? '';
}
return $json;
}
}

View File

@ -1,138 +0,0 @@
<?php
/**
* Copyright (c) Антон Аксенов (aka Anthony Axenov)
*
* This code is licensed under MIT.
* Этот код распространяется по лицензии MIT.
* https://github.com/anthonyaxenov/atol-online/blob/master/LICENSE
*/
namespace AtolOnline\Entities;
use AtolOnline\{Constants\Constraints,
Exceptions\AtolEmailTooLongException,
Exceptions\AtolEmailValidateException,
Exceptions\AtolInnWrongLengthException,
Exceptions\AtolPaymentAddressTooLongException,
Traits\HasEmail,
Traits\HasInn
};
/**
* Класс, описывающий сущность компании-продавца
*
* @package AtolOnline\Entities
*/
class Company extends Entity
{
use
/**
* Продавец должен иметь почту. Тег ФФД - 1117.
*/
HasEmail,
/**
* Продавец должен иметь ИНН. Тег ФФД - 1018.
*/
HasInn;
/**
* @var string Система налогообложения продавца. Тег ФФД - 1055.
*/
protected $sno;
/**
* @var string Место расчётов (адрес интернет-магазина). Тег ФФД - 1187.
*/
protected $payment_address;
/**
* Company constructor.
*
* @param string|null $sno
* @param string|null $inn
* @param string|null $paymentAddress
* @param string|null $email
* @throws AtolEmailTooLongException Слишком длинный email
* @throws AtolEmailValidateException Невалидный email
* @throws AtolInnWrongLengthException Некорректная длина ИНН
* @throws AtolPaymentAddressTooLongException Слишком длинный адрес места расчётов
*/
public function __construct(string $sno = null, string $inn = null, string $paymentAddress = null, string $email = null)
{
if ($sno) {
$this->setSno($sno);
}
if ($inn) {
$this->setInn($inn);
}
if ($paymentAddress) {
$this->setPaymentAddress($paymentAddress);
}
if ($email) {
$this->setEmail($email);
}
}
/**
* Возвращает установленный тип налогообложения. Тег ФФД - 1055.
*
* @return string
*/
public function getSno()
{
return $this->sno;
}
/**
* Устанавливает тип налогообложения. Тег ФФД - 1055.
*
* @param string $sno
* @return $this
*/
public function setSno(string $sno)
{
$this->sno = trim($sno);
return $this;
}
/**
* Возвращает установленный адрес места расчётов. Тег ФФД - 1187.
*
* @return string
*/
public function getPaymentAddress()
{
return $this->payment_address;
}
/**
* Устанавливает адрес места расчётов. Тег ФФД - 1187.
*
* @param string $payment_address
* @return $this
* @throws AtolPaymentAddressTooLongException Слишком длинный адрес места расчётов
*/
public function setPaymentAddress(string $payment_address)
{
$payment_address = trim($payment_address);
if (valid_strlen($payment_address) > Constraints::MAX_LENGTH_PAYMENT_ADDRESS) {
throw new AtolPaymentAddressTooLongException($payment_address, Constraints::MAX_LENGTH_PAYMENT_ADDRESS);
}
$this->payment_address = $payment_address;
return $this;
}
/**
* @inheritDoc
*/
public function jsonSerialize()
{
return [
'email' => $this->getEmail(),
'sno' => $this->getSno(),
'inn' => $this->getInn(),
'payment_address' => $this->getPaymentAddress(),
];
}
}

View File

@ -1,171 +0,0 @@
<?php
/**
* Copyright (c) Антон Аксенов (aka Anthony Axenov)
*
* This code is licensed under MIT.
* Этот код распространяется по лицензии MIT.
* https://github.com/anthonyaxenov/atol-online/blob/master/LICENSE
*/
namespace AtolOnline\Entities;
/**
* Класс CorrectionInfo, описывающий данные коррекции
*
* @package AtolOnline\Entities
*/
class CorrectionInfo extends Entity
{
/**
* @var int Тип коррекции. Тег ФФД - 1173.
*/
protected $type;
/**
* @var string Дата документа основания для коррекции. Тег ФФД - 1178.
*/
protected $base_date;
/**
* @var string Номер документа основания для коррекции. Тег ФФД - 1179.
*/
protected $base_number;
/**
* @var string Описание коррекции. Тег ФФД - 1177.
*/
protected $base_name;
/**
* CorrectionInfo constructor.
*
* @param string|null $type Тип коррекции
* @param string|null $base_date Дата документа
* @param string|null $base_number Номер документа
* @param string|null $base_name Описание коррекции
*/
public function __construct(?string $type = null, ?string $base_date = null, ?string $base_number = null, ?string $base_name = null)
{
if ($type) {
$this->setType($type);
}
if ($base_date) {
$this->setDate($base_date);
}
if ($base_number) {
$this->setNumber($base_number);
}
if ($base_name) {
$this->setName($base_name);
}
}
/**
* Возвращает номер документа основания для коррекции.
* Тег ФФД - 1179.
*
* @return string|null
*/
public function getNumber(): ?string
{
return $this->base_name;
}
/**
* Устанавливает номер документа основания для коррекции.
* Тег ФФД - 1179.
*
* @param string $number
* @return $this
*/
public function setNumber(string $number)
{
$this->base_number = trim($number);
return $this;
}
/**
* Возвращает описание коррекции.
* Тег ФФД - 1177.
*
* @return string|null
*/
public function getName(): ?string
{
return $this->base_name;
}
/**
* Устанавливает описание коррекции.
* Тег ФФД - 1177.
*
* @param string $name
* @return $this
*/
public function setName(string $name)
{
$this->base_name = trim($name);
return $this;
}
/**
* Возвращает дату документа основания для коррекции.
* Тег ФФД - 1178.
*
* @return string|null
*/
public function getDate(): ?string
{
return $this->base_date;
}
/**
* Устанавливает дату документа основания для коррекции.
* Тег ФФД - 1178.
*
* @param string $date Строка в формате d.m.Y
* @return $this
*/
public function setDate(string $date)
{
$this->base_date = $date;
return $this;
}
/**
* Возвращает тип коррекции.
* Тег ФФД - 1173.
*
* @return string|null
*/
public function getType(): ?string
{
return $this->type;
}
/**
* Устанавливает тип коррекции.
* Тег ФФД - 1173.
*
* @param string $type
* @return $this
*/
public function setType(string $type)
{
$this->type = $type;
return $this;
}
/**
* @inheritDoc
*/
public function jsonSerialize()
{
return [
'type' => $this->getType() ?? '', // обязателен
'base_date' => $this->getDate() ?? '', // обязателен
'base_number' => $this->getNumber() ?? '', // обязателен
'base_name' => $this->getName() ?? '' // обязателен
];
}
}

View File

@ -1,432 +0,0 @@
<?php
/**
* Copyright (c) Антон Аксенов (aka Anthony Axenov)
*
* This code is licensed under MIT.
* Этот код распространяется по лицензии MIT.
* https://github.com/anthonyaxenov/atol-online/blob/master/LICENSE
*/
namespace AtolOnline\Entities;
use AtolOnline\Constants\Constraints;
use AtolOnline\Exceptions\AtolCashierTooLongException;
use AtolOnline\Exceptions\AtolException;
use AtolOnline\Exceptions\AtolInvalidJsonException;
/**
* Класс, описывающий документ
*
* @package AtolOnline\Entities
*/
class Document extends Entity
{
/**
* @var \AtolOnline\Entities\ItemArray Массив предметов расчёта
*/
protected $items;
/**
* @var \AtolOnline\Entities\VatArray Массив ставок НДС
*/
protected $vats;
/**
* @var \AtolOnline\Entities\PaymentArray Массив оплат
*/
protected $payments;
/**
* @var \AtolOnline\Entities\Company Объект компании (продавца)
*/
protected $company;
/**
* @var \AtolOnline\Entities\Client Объект клиента (покупателя)
*/
protected $client;
/**
* @var int Итоговая сумма чека. Тег ФФД - 1020.
*/
protected $total = 0;
/**
* @var string ФИО кассира. Тег ФФД - 1021.
*/
protected $cashier;
/**
* @var \AtolOnline\Entities\CorrectionInfo Данные коррекции
*/
protected $correction_info;
/**
* Document constructor.
*/
public function __construct()
{
$this->vats = new VatArray();
$this->payments = new PaymentArray();
$this->items = new ItemArray();
}
/**
* Удаляет все налоги из документа и предметов расчёта
*
* @return $this
* @throws \AtolOnline\Exceptions\AtolPriceTooHighException Слишком большая сумма
* @throws \AtolOnline\Exceptions\AtolTooManyVatsException Слишком много ставок НДС
*/
public function clearVats()
{
$this->setVats([]);
return $this;
}
/**
* Добавляет новую ставку НДС в массив ставок НДС
*
* @param \AtolOnline\Entities\Vat $vat Объект ставки НДС
* @return $this
* @throws \AtolOnline\Exceptions\AtolTooManyVatsException Слишком много ставок НДС
*/
public function addVat(Vat $vat)
{
$this->vats->add($vat);
return $this;
}
/**
* Возвращает массив ставок НДС
*
* @return \AtolOnline\Entities\Vat[]
*/
public function getVats(): array
{
return $this->vats->get();
}
/**
* Устанавливает массив ставок НДС
*
* @param \AtolOnline\Entities\Vat[] $vats Массив ставок НДС
* @return $this
* @throws \AtolOnline\Exceptions\AtolTooManyVatsException Слишком много ставок НДС
* @throws \Exception
*/
public function setVats(array $vats)
{
$this->vats->set($vats);
return $this;
}
/**
* Добавляет новую оплату в массив оплат
*
* @param \AtolOnline\Entities\Payment $payment Объект оплаты
* @return $this
* @throws \Exception
* @throws \AtolOnline\Exceptions\AtolTooManyPaymentsException Слишком много оплат
*/
public function addPayment(Payment $payment)
{
if (count($this->getPayments()) == 0 && !$payment->getSum()) {
$payment->setSum($this->calcTotal());
}
$this->payments->add($payment);
return $this;
}
/**
* Возвращает массив оплат
*
* @return \AtolOnline\Entities\Payment[]
*/
public function getPayments(): array
{
return $this->payments->get();
}
/**
* Устанавливает массив оплат
*
* @param \AtolOnline\Entities\Payment[] $payments Массив оплат
* @return $this
* @throws \AtolOnline\Exceptions\AtolTooManyPaymentsException Слишком много оплат
*/
public function setPayments(array $payments)
{
$this->payments->set($payments);
return $this;
}
/**
* Добавляет новый предмет расчёта в массив предметов расчёта
*
* @param \AtolOnline\Entities\Item $item Объект предмета расчёта
* @return $this
* @throws \AtolOnline\Exceptions\AtolTooManyItemsException Слишком много предметов расчёта
*/
public function addItem(Item $item)
{
$this->items->add($item);
return $this;
}
/**
* Возвращает массив предметов расчёта
*
* @return \AtolOnline\Entities\Item[]
*/
public function getItems(): array
{
return $this->items->get();
}
/**
* Устанавливает массив предметов расчёта
*
* @param \AtolOnline\Entities\Item[] $items Массив предметов расчёта
* @return $this
* @throws \AtolOnline\Exceptions\AtolTooManyItemsException Слишком много предметов расчёта
*/
public function setItems(array $items)
{
$this->items->set($items);
return $this;
}
/**
* Возвращает заданного клиента (покупателя)
*
* @return Client|null
*/
public function getClient(): ?Client
{
return $this->client;
}
/**
* Устанавливает клиента (покупателя)
*
* @param Client|null $client
* @return $this
*/
public function setClient(?Client $client)
{
$this->client = $client;
return $this;
}
/**
* Возвращает заданную компанию (продавца)
*
* @return Company|null
*/
public function getCompany(): ?Company
{
return $this->company;
}
/**
* Устанавливает компанию (продавца)
*
* @param Company|null $company
* @return $this
*/
public function setCompany(?Company $company)
{
$this->company = $company;
return $this;
}
/**
* Возвращает ФИО кассира. Тег ФФД - 1021.
*
* @return string|null
*/
public function getCashier(): ?string
{
return $this->cashier;
}
/**
* Устанавливает ФИО кассира. Тег ФФД - 1021.
*
* @param string|null $cashier
* @return $this
* @throws \AtolOnline\Exceptions\AtolCashierTooLongException
*/
public function setCashier(?string $cashier)
{
if ($cashier !== null) {
$cashier = trim($cashier);
if (valid_strlen($cashier) > Constraints::MAX_LENGTH_CASHIER_NAME) {
throw new AtolCashierTooLongException($cashier, Constraints::MAX_LENGTH_CASHIER_NAME);
}
}
$this->cashier = $cashier;
return $this;
}
/**
* Возвращает данные коррекции
*
* @return \AtolOnline\Entities\CorrectionInfo|null
*/
public function getCorrectionInfo(): ?CorrectionInfo
{
return $this->correction_info;
}
/**
* Устанавливает данные коррекции
*
* @param \AtolOnline\Entities\CorrectionInfo|null $correction_info
* @return $this
*/
public function setCorrectionInfo(?CorrectionInfo $correction_info)
{
$this->correction_info = $correction_info;
return $this;
}
/**
* Пересчитывает, сохраняет и возвращает итоговую сумму чека по всем позициям (включая НДС). Тег ФФД - 1020.
*
* @return float
* @throws \Exception
*/
public function calcTotal()
{
$sum = 0;
$this->clearVats();
foreach ($this->items->get() as $item) {
$sum += $item->calcSum();
$this->addVat(new Vat($item->getVat()->getType(), $item->getSum()));
}
return $this->total = round($sum, 2);
}
/**
* Возвращает итоговую сумму чека. Тег ФФД - 1020.
*
* @return float
*/
public function getTotal(): float
{
return $this->total;
}
/**
* Собирает объект документа из сырой json-строки
*
* @param string $json
* @return \AtolOnline\Entities\Document
* @throws \AtolOnline\Exceptions\AtolEmailTooLongException
* @throws \AtolOnline\Exceptions\AtolEmailValidateException
* @throws \AtolOnline\Exceptions\AtolException
* @throws \AtolOnline\Exceptions\AtolInnWrongLengthException
* @throws \AtolOnline\Exceptions\AtolInvalidJsonException
* @throws \AtolOnline\Exceptions\AtolNameTooLongException
* @throws \AtolOnline\Exceptions\AtolPaymentAddressTooLongException
* @throws \AtolOnline\Exceptions\AtolPhoneTooLongException
* @throws \AtolOnline\Exceptions\AtolPriceTooHighException
* @throws \AtolOnline\Exceptions\AtolTooManyException
* @throws \AtolOnline\Exceptions\AtolTooManyItemsException
* @throws \AtolOnline\Exceptions\AtolTooManyPaymentsException
* @throws \AtolOnline\Exceptions\AtolUnitTooLongException
* @throws \AtolOnline\Exceptions\AtolUserdataTooLongException
*/
public static function fromRaw(string $json)
{
$array = json_decode($json, true);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new AtolInvalidJsonException();
}
$doc = new self();
if (isset($array['company'])) {
$doc->setCompany(new Company(
$array['company']['sno'] ?? null,
$array['company']['inn'] ?? null,
$array['company']['payment_address'] ?? null,
$array['company']['email'] ?? null
));
}
if (isset($array['client'])) {
$doc->setClient(new Client(
$array['client']['name'] ?? null,
$array['client']['phone'] ?? null,
$array['client']['email'] ?? null,
$array['client']['inn'] ?? null
));
}
if (isset($array['items'])) {
foreach ($array['items'] as $ar_item) {
$item = new Item(
$ar_item['name'] ?? null,
$ar_item['price'] ?? null,
$ar_item['quantity'] ?? null,
$ar_item['measurement_unit'] ?? null,
$ar_item['vat']['type'] ?? null,
$ar_item['payment_object'] ?? null,
$ar_item['payment_method'] ?? null
);
if (!empty($ar_item['user_data'])) {
$item->setUserData($ar_item['user_data'] ?? null);
}
$doc->addItem($item);
}
}
if (isset($array['payments'])) {
foreach ($array['payments'] as $ar_payment) {
$payment = new Payment();
if (isset($ar_payment['type'])) {
$payment->setType($ar_payment['type']);
}
if (isset($ar_payment['sum'])) {
$payment->setSum($ar_payment['sum']);
}
$doc->payments->add($payment);
}
}
if (isset($array['total']) && $array['total'] != $doc->calcTotal()) {
throw new AtolException('Real total sum not equals to provided in JSON one');
}
return $doc;
}
/**
* Возвращает массив для кодирования в json
*
* @throws \Exception
*/
public function jsonSerialize()
{
if ($this->getCompany()) {
$json['company'] = $this->getCompany()->jsonSerialize(); // обязательно
}
if ($this->getPayments()) {
$json['payments'] = $this->payments->jsonSerialize(); // обязательно
}
if ($this->getCashier()) {
$json['cashier'] = $this->getCashier();
}
if ($this->getCorrectionInfo()) {
$json['correction_info'] = $this->getCorrectionInfo()->jsonSerialize(); // обязательно для коррекционных
} else {
if ($this->getClient()) {
$json['client'] = $this->getClient()->jsonSerialize(); // обязательно для некоррекционных
}
if ($this->getItems()) {
$json['items'] = $this->items->jsonSerialize(); // обязательно для некоррекционных
}
$json['total'] = $this->calcTotal(); // обязательно для некоррекционных
}
if ($this->getVats()) {
$json['vats'] = $this->vats->jsonSerialize();
}
return $json;
}
}

View File

@ -1,28 +0,0 @@
<?php
/**
* Copyright (c) Антон Аксенов (aka Anthony Axenov)
*
* This code is licensed under MIT.
* Этот код распространяется по лицензии MIT.
* https://github.com/anthonyaxenov/atol-online/blob/master/LICENSE
*/
namespace AtolOnline\Entities;
use JsonSerializable;
/**
* Абстрактное описание любой сущности, представляемой как JSON
*
* @package AtolOnline\Entities
*/
abstract class Entity implements JsonSerializable
{
/**
* @inheritDoc
*/
public function __toString()
{
return json_encode($this->jsonSerialize(), JSON_UNESCAPED_UNICODE);
}
}

View File

@ -1,397 +0,0 @@
<?php
/**
* Copyright (c) Антон Аксенов (aka Anthony Axenov)
*
* This code is licensed under MIT.
* Этот код распространяется по лицензии MIT.
* https://github.com/anthonyaxenov/atol-online/blob/master/LICENSE
*/
namespace AtolOnline\Entities;
use AtolOnline\{Constants\Constraints,
Exceptions\AtolNameTooLongException,
Exceptions\AtolPriceTooHighException,
Exceptions\AtolTooManyException,
Exceptions\AtolUnitTooLongException,
Exceptions\AtolUserdataTooLongException,
Traits\RublesKopeksConverter
};
/**
* Предмет расчёта (товар, услуга)
*
* @package AtolOnline\Entities
*/
class Item extends Entity
{
use RublesKopeksConverter;
/**
* @var string Наименование. Тег ФФД - 1030.
*/
protected $name;
/**
* @var int Цена в копейках (с учётом скидок и наценок). Тег ФФД - 1079.
*/
protected $price = 0;
/**
* @var float Количество, вес. Тег ФФД - 1023.
*/
protected $quantity = 0.0;
/**
* @var float Сумма в копейках. Тег ФФД - 1043.
*/
protected $sum = 0;
/**
* @var string Единица измерения количества. Тег ФФД - 1197.
*/
protected $measurement_unit;
/**
* @var Vat Ставка НДС
*/
protected $vat;
/**
* @var string Признак способа расчёта. Тег ФФД - 1214.
*/
protected $payment_method;
/**
* @var string Признак объекта расчёта. Тег ФФД - 1212.
*/
protected $payment_object;
/**
* @var string Дополнительный реквизит. Тег ФФД - 1191.
*/
protected $user_data;
/**
* Item constructor.
*
* @param string|null $name Наименование
* @param float|null $price Цена за одну единицу
* @param float|null $quantity Количество
* @param string|null $measurement_unit Единица измерения
* @param string|null $vat_type Ставка НДС
* @param string|null $payment_object Признак
* @param string|null $payment_method Способ расчёта
* @throws AtolNameTooLongException Слишком длинное наименование
* @throws AtolPriceTooHighException Слишком высокая цена за одну единицу
* @throws AtolTooManyException Слишком большое количество
* @throws AtolUnitTooLongException Слишком длинное название единицы измерения
*/
public function __construct(
?string $name = null,
?float $price = null,
?float $quantity = null,
?string $measurement_unit = null,
$vat_type = null,
?string $payment_object = null,
?string $payment_method = null
) {
if ($name) {
$this->setName($name);
}
if ($price) {
$this->setPrice($price);
}
if ($quantity) {
$this->setQuantity($quantity);
}
if ($measurement_unit) {
$this->setMeasurementUnit($measurement_unit);
}
if ($vat_type) {
$this->setVatType($vat_type);
}
if ($payment_object) {
$this->setPaymentObject($payment_object);
}
if ($payment_method) {
$this->setPaymentMethod($payment_method);
}
}
/**
* Возвращает наименование. Тег ФФД - 1030.
*
* @return string
*/
public function getName()
{
return $this->name;
}
/**
* Устаналивает наименование. Тег ФФД - 1030.
*
* @param string $name Наименование
* @return $this
* @throws AtolNameTooLongException Слишком длинное имя/наименование
*/
public function setName(string $name)
{
$name = trim($name);
if (valid_strlen($name) > Constraints::MAX_LENGTH_ITEM_NAME) {
throw new AtolNameTooLongException($name, Constraints::MAX_LENGTH_ITEM_NAME);
}
$this->name = $name;
return $this;
}
/**
* Возвращает цену в рублях. Тег ФФД - 1079.
*
* @return float
*/
public function getPrice()
{
return self::toRub($this->price);
}
/**
* Устанавливает цену в рублях. Тег ФФД - 1079.
*
* @param float $rubles Цена за одну единицу в рублях
* @return $this
* @throws AtolPriceTooHighException Слишком высокая цена за одну единицу
*/
public function setPrice(float $rubles)
{
if ($rubles > 42949672.95) {
throw new AtolPriceTooHighException($rubles, 42949672.95);
}
$this->price = self::toKop($rubles);
$this->calcSum();
return $this;
}
/**
* Возвращает количество. Тег ФФД - 1023.
*
* @return float
*/
public function getQuantity(): float
{
return $this->quantity;
}
/**
* Устанавливает количество. Тег ФФД - 1023.
*
* @param float $quantity Количество
* @param string|null $measurement_unit Единица измерения количества
* @return $this
* @throws AtolTooManyException Слишком большое количество
* @throws AtolPriceTooHighException Слишком высокая общая стоимость
* @throws AtolUnitTooLongException Слишком длинное название единицы измерения
*/
public function setQuantity(float $quantity, string $measurement_unit = null)
{
$quantity = round($quantity, 3);
if ($quantity > 99999.999) {
throw new AtolTooManyException($quantity, 99999.999);
}
$this->quantity = $quantity;
$this->calcSum();
if ($measurement_unit) {
$this->setMeasurementUnit($measurement_unit);
}
return $this;
}
/**
* Возвращает заданную единицу измерения количества. Тег ФФД - 1197.
*
* @return string
*/
public function getMeasurementUnit(): string
{
return $this->measurement_unit;
}
/**
* Устанавливает единицу измерения количества. Тег ФФД - 1197.
*
* @param string $measurement_unit Единица измерения количества
* @return $this
* @throws AtolUnitTooLongException Слишком длинное название единицы измерения
*/
public function setMeasurementUnit(string $measurement_unit)
{
$measurement_unit = trim($measurement_unit);
if (valid_strlen($measurement_unit) > Constraints::MAX_LENGTH_MEASUREMENT_UNIT) {
throw new AtolUnitTooLongException($measurement_unit, Constraints::MAX_LENGTH_MEASUREMENT_UNIT);
}
$this->measurement_unit = $measurement_unit;
return $this;
}
/**
* Возвращает признак способа оплаты. Тег ФФД - 1214.
*
* @return string
*/
public function getPaymentMethod(): string
{
return $this->payment_method;
}
/**
* Устанавливает признак способа оплаты. Тег ФФД - 1214.
*
* @param string $payment_method Признак способа оплаты
* @return $this
* @todo Проверка допустимых значений
*/
public function setPaymentMethod(string $payment_method)
{
$this->payment_method = trim($payment_method);
return $this;
}
/**
* Возвращает признак предмета расчёта. Тег ФФД - 1212.
*
* @return string
*/
public function getPaymentObject(): string
{
return $this->payment_object;
}
/**
* Устанавливает признак предмета расчёта. Тег ФФД - 1212.
*
* @param string $payment_object Признак предмета расчёта
* @return $this
* @todo Проверка допустимых значений
*/
public function setPaymentObject(string $payment_object)
{
$this->payment_object = trim($payment_object);
return $this;
}
/**
* Возвращает ставку НДС
*
* @return \AtolOnline\Entities\Vat|null
*/
public function getVat(): ?Vat
{
return $this->vat;
}
/**
* Устанавливает ставку НДС
*
* @param string|null $vat_type Тип ставки НДС. Передать null, чтобы удалить ставку.
* @return $this
* @throws \AtolOnline\Exceptions\AtolPriceTooHighException
*/
public function setVatType(?string $vat_type)
{
if ($vat_type) {
$this->vat
? $this->vat->setType($vat_type)
: $this->vat = new Vat($vat_type);
} else {
$this->vat = null;
}
$this->calcSum();
return $this;
}
/**
* Возвращает дополнительный реквизит. Тег ФФД - 1191.
*
* @return string|null
*/
public function getUserData(): ?string
{
return $this->user_data;
}
/**
* Устанавливает дополнительный реквизит. Тег ФФД - 1191.
*
* @param string $user_data Дополнительный реквизит. Тег ФФД - 1191.
* @return $this
* @throws AtolUserdataTooLongException Слишком длинный дополнительный реквизит
*/
public function setUserData(string $user_data)
{
$user_data = trim($user_data);
if (valid_strlen($user_data) > Constraints::MAX_LENGTH_USER_DATA) {
throw new AtolUserdataTooLongException($user_data, Constraints::MAX_LENGTH_USER_DATA);
}
$this->user_data = $user_data;
return $this;
}
/**
* Возвращает стоимость. Тег ФФД - 1043.
*
* @return float
*/
public function getSum(): float
{
return self::toRub($this->sum);
}
/**
* Расчитывает стоимость и размер НДС на неё
*
* @return float
* @throws AtolPriceTooHighException Слишком большая сумма
*/
public function calcSum()
{
$sum = $this->quantity * $this->price;
if (self::toRub($sum) > 42949672.95) {
throw new AtolPriceTooHighException($sum, 42949672.95);
}
$this->sum = $sum;
if ($this->vat) {
$this->vat->setSum(self::toRub($sum));
}
return $this->getSum();
}
/**
* @inheritDoc
*/
public function jsonSerialize()
{
$json = [
'name' => $this->getName(), // обязательно
'price' => $this->getPrice(), // обязательно
'quantity' => $this->getQuantity(), // обязательно
'sum' => $this->getSum(), // обязательно
'measurement_unit' => $this->getMeasurementUnit(),
'payment_method' => $this->getPaymentMethod(),
'payment_object' => $this->getPaymentObject()
//TODO nomenclature_code
//TODO agent_info
//TODO supplier_info
//TODO excise
//TODO country_code
//TODO declaration_number
];
if ($this->getVat()) {
$json['vat'] = $this->getVat()->jsonSerialize();
}
if ($this->getUserData()) {
$json['user_data'] = $this->getUserData();
}
return $json;
}
}

View File

@ -1,113 +0,0 @@
<?php
/**
* Copyright (c) Антон Аксенов (aka Anthony Axenov)
*
* This code is licensed under MIT.
* Этот код распространяется по лицензии MIT.
* https://github.com/anthonyaxenov/atol-online/blob/master/LICENSE
*/
namespace AtolOnline\Entities;
use AtolOnline\Exceptions\AtolTooManyItemsException;
/**
* Класс, описывающий массив предметов расчёта
*
* @package AtolOnline\Entities
*/
class ItemArray extends Entity
{
/**
* Максимальное количество элементов в массиве
* По документации ограничение по количеству предметов расчёта = от 1 до 100,
* однако в схеме sell не указан receipt.properties.items.maxItems
*/
public const MAX_COUNT = 100;
/**
* @var Item[] Массив предметов расчёта
*/
private $items = [];
/**
* ItemArray constructor.
*
* @param Item[]|null $items Массив предметов расчёта
* @throws AtolTooManyItemsException Слишком много предметов расчёта
*/
public function __construct(?array $items = null)
{
if ($items) {
$this->set($items);
}
}
/**
* Устанавливает массив предметов расчёта
*
* @param Item[] $items Массив предметов расчёта
* @return $this
* @throws AtolTooManyItemsException Слишком много предметов расчёта
*/
public function set(array $items)
{
if ($this->validateCount($items)) {
$this->items = $items;
}
return $this;
}
/**
* Добавляет предмет расчёта в массив
*
* @param Item $item Объект предмета расчёта
* @return $this
* @throws AtolTooManyItemsException Слишком много предметов расчёта
*/
public function add(Item $item)
{
if ($this->validateCount()) {
$this->items[] = $item;
}
return $this;
}
/**
* Возвращает массив предметов расчёта
*
* @return Item[]
*/
public function get()
{
return $this->items;
}
/**
* @inheritDoc
*/
public function jsonSerialize()
{
$result = [];
foreach ($this->get() as $item) {
$result[] = $item->jsonSerialize();
}
return $result;
}
/**
* Проверяет количество предметов расчёта
*
* @param Item[]|null $items Если передать массив, то проверит количество его элементов.
* Иначе проверит количество уже присвоенных элементов.
* @return bool true если всё хорошо, иначе выбрасывает исключение
* @throws AtolTooManyItemsException Слишком много предметов расчёта
*/
protected function validateCount(?array $items = null): bool
{
if ((!empty($items) && count($items) >= self::MAX_COUNT) || count($this->items) >= self::MAX_COUNT) {
throw new AtolTooManyItemsException(count($items), self::MAX_COUNT);
}
return true;
}
}

View File

@ -1,97 +0,0 @@
<?php
/**
* Copyright (c) Антон Аксенов (aka Anthony Axenov)
*
* This code is licensed under MIT.
* Этот код распространяется по лицензии MIT.
* https://github.com/anthonyaxenov/atol-online/blob/master/LICENSE
*/
namespace AtolOnline\Entities;
use AtolOnline\Constants\PaymentTypes;
/**
* Класс, описывающий оплату. Тег ФФД - 1031, 1081, 1215, 1216, 1217.
*
* @package AtolOnline\Entities
*/
class Payment extends Entity
{
/**
* @var int Тип оплаты
*/
protected $type;
/**
* @var float Сумма оплаты
*/
protected $sum;
/**
* Payment constructor.
*
* @param int $payment_type Тип оплаты
* @param float $sum Сумма оплаты
*/
public function __construct(int $payment_type = PaymentTypes::ELECTRON, float $sum = 0.0)
{
$this->setType($payment_type);
$this->setSum($sum);
}
/**
* Возвращает тип оплаты. Тег ФФД - 1031, 1081, 1215, 1216, 1217.
*
* @return int
*/
public function getType(): int
{
return $this->type;
}
/**
* Устанавливает тип оплаты. Тег ФФД - 1031, 1081, 1215, 1216, 1217.
*
* @param int $type
* @return $this
*/
public function setType(int $type)
{
$this->type = $type;
return $this;
}
/**
* Возвращает сумму оплаты
*
* @return float
*/
public function getSum(): float
{
return $this->sum;
}
/**
* Устанавливает сумму оплаты
*
* @param float $sum
* @return $this
*/
public function setSum(float $sum)
{
$this->sum = $sum;
return $this;
}
/**
* @inheritDoc
*/
public function jsonSerialize()
{
return [
'type' => $this->getType(),
'sum' => $this->getSum(),
];
}
}

View File

@ -1,111 +0,0 @@
<?php
/**
* Copyright (c) Антон Аксенов (aka Anthony Axenov)
*
* This code is licensed under MIT.
* Этот код распространяется по лицензии MIT.
* https://github.com/anthonyaxenov/atol-online/blob/master/LICENSE
*/
namespace AtolOnline\Entities;
use AtolOnline\Exceptions\AtolTooManyPaymentsException;
/**
* Класс, описывающий массив оплат
*
* @package AtolOnline\Entities
*/
class PaymentArray extends Entity
{
/**
* Максимальное количество элементов массива
*/
public const MAX_COUNT = 10;
/**
* @var Payment[] Массив оплат
*/
private $payments = [];
/**
* ItemArray constructor.
*
* @param Payment[]|null $payments Массив оплат
* @throws AtolTooManyPaymentsException Слишком много оплат
*/
public function __construct(?array $payments = null)
{
if ($payments) {
$this->set($payments);
}
}
/**
* Устанавливает массив оплат
*
* @param Payment[] $payments
* @return $this
* @throws AtolTooManyPaymentsException Слишком много оплат
*/
public function set(array $payments)
{
if ($this->validateCount($payments)) {
$this->payments = $payments;
}
return $this;
}
/**
* Добавляет новую оплату к заданным
*
* @param Payment $payment Объект оплаты
* @return $this
* @throws AtolTooManyPaymentsException Слишком много оплат
*/
public function add(Payment $payment)
{
if ($this->validateCount()) {
$this->payments[] = $payment;
}
return $this;
}
/**
* Возвращает массив оплат
*
* @return Payment[]
*/
public function get()
{
return $this->payments;
}
/**
* @inheritDoc
*/
public function jsonSerialize()
{
$result = [];
foreach ($this->get() as $payment) {
$result[] = $payment->jsonSerialize();
}
return $result;
}
/**
* Проверяет количество налоговых ставок
*
* @param Payment[]|null $payments Если передать массив, то проверит количество его элементов.
* Иначе проверит количество уже присвоенных элементов.
* @return bool true если всё хорошо, иначе выбрасывает исключение
* @throws AtolTooManyPaymentsException Слишком много оплат
*/
protected function validateCount(?array $payments = null): bool
{
if ((!empty($payments) && count($payments) >= self::MAX_COUNT) || count($this->payments) >= self::MAX_COUNT) {
throw new AtolTooManyPaymentsException(count($payments), self::MAX_COUNT);
}
return true;
}
}

View File

@ -1,187 +0,0 @@
<?php
/**
* Copyright (c) Антон Аксенов (aka Anthony Axenov)
*
* This code is licensed under MIT.
* Этот код распространяется по лицензии MIT.
* https://github.com/anthonyaxenov/atol-online/blob/master/LICENSE
*/
namespace AtolOnline\Entities;
use AtolOnline\{Constants\VatTypes, Traits\RublesKopeksConverter};
/**
* Класс, описывающий ставку НДС
*
* @package AtolOnline\Entities
*/
class Vat extends Entity
{
use RublesKopeksConverter;
/**
* @var string Выбранный тип ставки НДС. Тег ФФД - 1199, 1105, 1104, 1103, 1102, 1107, 1106.
*/
private $type;
/**
* @var int Сумма в копейках, от которой пересчитывается размер НДС
*/
private $sum_original = 0;
/**
* @var int Сумма НДС в копейках
*/
private $sum_final = 0;
/**
* Vat constructor.
*
* @param string $type Тип ставки НДС
* @param float|null $rubles Исходная сумма в рублях, от которой нужно расчитать размер НДС
*/
public function __construct(string $type = VatTypes::NONE, float $rubles = null)
{
$this->type = $type;
if ($rubles) {
$this->setSum($rubles);
}
}
/**
* Устанавливает размер НДС от суммы в копейках
*
* @param string $type Тип ставки НДС
* @param int $kopeks Копейки
* @return float|int
* @see https://nalog-nalog.ru/nds/nalogovaya_baza_nds/kak-schitat-nds-pravilno-vychislyaem-20-ot-summy-primer-algoritm/
* @see https://glavkniga.ru/situations/k500734
* @see https://www.b-kontur.ru/nds-kalkuljator-online
*/
protected static function calculator(string $type, int $kopeks)
{
switch ($type) {
case VatTypes::NONE:
case VatTypes::VAT0:
return 0;
case VatTypes::VAT10:
//return $kopeks * 10 / 100;
case VatTypes::VAT110:
return $kopeks * 10 / 110;
case VatTypes::VAT18:
//return $kopeks * 18 / 100;
case VatTypes::VAT118:
return $kopeks * 18 / 118;
case VatTypes::VAT20:
//return $kopeks * 20 / 100;
case VatTypes::VAT120:
return $kopeks * 20 / 120;
}
return 0;
}
/**
* Возвращает тип ставки НДС. Тег ФФД - 1199, 1105, 1104, 1103, 1102, 1107, 1106.
*
* @return string
*/
public function getType(): string
{
return $this->type;
}
/**
* Устанавливает тип ставки НДС. Тег ФФД - 1199, 1105, 1104, 1103, 1102, 1107, 1106.
* Автоматически пересчитывает итоговый размер НДС от исходной суммы.
*
* @param string $type Тип ставки НДС
* @return $this
*/
public function setType(string $type)
{
$this->type = $type;
$this->setFinal();
return $this;
}
/**
* Возвращает расчитанный итоговый размер ставки НДС в рублях. Тег ФФД - 1200.
*
* @return float
*/
public function getFinalSum()
{
return self::toRub($this->sum_final);
}
/**
* Устанавливает исходную сумму, от которой будет расчитываться итоговый размер НДС.
* Автоматически пересчитывает итоговый размер НДС от исходной суммы.
*
* @param float $rubles Сумма в рублях за предмет расчёта, из которой высчитывается размер НДС
* @return $this
*/
public function setSum(float $rubles)
{
$this->sum_original = self::toKop($rubles);
$this->setFinal();
return $this;
}
/**
* Возвращает исходную сумму, от которой расчитывается размер налога
*
* @return float
*/
public function getSum(): float
{
return self::toRub($this->sum_original);
}
/**
* Прибавляет указанную сумму к общей исходной сумме.
* Автоматически пересчитывает итоговый размер НДС от новой исходной суммы.
*
* @param float $rubles
* @return $this
*/
public function addSum(float $rubles)
{
$this->sum_original += self::toKop($rubles);
$this->setFinal();
return $this;
}
/**
* Расчитывает и возвращает размер НДС от указанной суммы в рублях.
* Не изменяет итоговый размер НДС.
*
* @param float|null $rubles
* @return float
*/
public function calc(float $rubles): float
{
return self::toRub(self::calculator($this->type, self::toKop($rubles)));
}
/**
* @inheritDoc
*/
public function jsonSerialize()
{
return [
'type' => $this->getType(),
'sum' => $this->getFinalSum(),
];
}
/**
* Расчитывает и устанавливает итоговый размер ставки от исходной суммы в копейках
*/
protected function setFinal()
{
$this->sum_final = self::calculator($this->type, $this->sum_original);
return $this;
}
}

View File

@ -1,115 +0,0 @@
<?php
/**
* Copyright (c) Антон Аксенов (aka Anthony Axenov)
*
* This code is licensed under MIT.
* Этот код распространяется по лицензии MIT.
* https://github.com/anthonyaxenov/atol-online/blob/master/LICENSE
*/
namespace AtolOnline\Entities;
use AtolOnline\Exceptions\AtolTooManyVatsException;
/**
* Класс, описывающий массив ставок НДС
*
* @package AtolOnline\Entities
*/
class VatArray extends Entity
{
/**
* Максимальное количество элементов массива
*/
public const MAX_COUNT = 6;
/**
* @var Vat[] Массив ставок НДС
*/
private $vats = [];
/**
* VatArray constructor.
*
* @param Vat[]|null $vats Массив ставок НДС
* @throws AtolTooManyVatsException Слишком много ставок НДС
*/
public function __construct(?array $vats = null)
{
if ($vats) {
$this->set($vats);
}
}
/**
* Устанавливает массив ставок НДС
*
* @param Vat[] $vats Массив ставок НДС
* @return $this
* @throws AtolTooManyVatsException Слишком много ставок НДС
*/
public function set(array $vats)
{
if ($this->validateCount($vats)) {
$this->vats = $vats;
}
return $this;
}
/**
* Добавляет новую ставку НДС в массив
*
* @param Vat $vat Объект ставки НДС
* @return $this
* @throws AtolTooManyVatsException Слишком много ставок НДС
*/
public function add(Vat $vat)
{
if ($this->validateCount()) {
if (isset($this->vats[$vat->getType()])) {
$this->vats[$vat->getType()]->addSum($vat->getSum());
} else {
$this->vats[$vat->getType()] = $vat;
}
}
return $this;
}
/**
* Возвращает массив ставок НДС
*
* @return Vat[]
*/
public function get()
{
return $this->vats;
}
/**
* @inheritDoc
*/
public function jsonSerialize()
{
$result = [];
foreach ($this->get() as $vat) {
$result[] = $vat->jsonSerialize();
}
return $result;
}
/**
* Проверяет количество налоговых ставок
*
* @param Vat[]|null $vats Если передать массив, то проверит количество его элементов.
* Иначе проверит количество уже присвоенных элементов.
* @return bool true если всё хорошо, иначе выбрасывает исключение
* @throws AtolTooManyVatsException Слишком много ставок НДС
*/
protected function validateCount(?array $vats = null): bool
{
if ((!empty($vats) && count($vats) >= self::MAX_COUNT) || count($this->vats) >= self::MAX_COUNT) {
throw new AtolTooManyVatsException(count($vats), self::MAX_COUNT);
}
return true;
}
}

View File

@ -1,41 +0,0 @@
<?php
/**
* Copyright (c) Антон Аксенов (aka Anthony Axenov)
*
* This code is licensed under MIT.
* Этот код распространяется по лицензии MIT.
* https://github.com/anthonyaxenov/atol-online/blob/master/LICENSE
*/
namespace AtolOnline\Exceptions;
use AtolOnline\Api\KktResponse;
use Exception;
use Throwable;
/**
* Исключение, возникающее при работе с АТОЛ Онлайн
*
* @package AtolOnline\Exceptions
*/
class AtolAuthFailedException extends Exception
{
/**
* AtolAuthFailedException constructor.
*
* @param \AtolOnline\Api\KktResponse $last_response
* @param string $message
* @param int $code
* @param \Throwable|null $previous
*/
public function __construct(KktResponse $last_response, $message = "", $code = 0, Throwable $previous = null)
{
$message = $last_response->isValid()
? $message
: '['.$last_response->error->code.'] '.$last_response->error->text.
'. ERROR_ID: '.$last_response->error->error_ID.
'. TYPE: '.$last_response->error->type;
$code = $last_response->isValid() ? $code : $last_response->error->code;
parent::__construct($message, $code, $previous);
}
}

View File

@ -1,30 +0,0 @@
<?php
/**
* Copyright (c) Антон Аксенов (aka Anthony Axenov)
*
* This code is licensed under MIT.
* Этот код распространяется по лицензии MIT.
* https://github.com/anthonyaxenov/atol-online/blob/master/LICENSE
*/
namespace AtolOnline\Exceptions;
/**
* Исключение, возникающее при попытке указать слишком длинное имя кассира
*
* @package AtolOnline\Exceptions
*/
class AtolCashierTooLongException extends AtolTooLongException
{
/**
* @inheritDoc
*/
protected $ffd_tags = [
1021,
];
/**
* @var string Сообщение об ошибке
*/
protected $message = 'Cashier name is too long';
}

View File

@ -1,23 +0,0 @@
<?php
/**
* Copyright (c) Антон Аксенов (aka Anthony Axenov)
*
* This code is licensed under MIT.
* Этот код распространяется по лицензии MIT.
* https://github.com/anthonyaxenov/atol-online/blob/master/LICENSE
*/
namespace AtolOnline\Exceptions;
/**
* Исключение, возникающее при попытке зарегистрировать документ без данных коррекции
*
* @package AtolOnline\Exceptions
*/
class AtolCorrectionInfoException extends AtolException
{
/**
* @var string Сообщение об ошибке
*/
protected $message = 'Document must have correction info';
}

View File

@ -1,31 +0,0 @@
<?php
/**
* Copyright (c) Антон Аксенов (aka Anthony Axenov)
*
* This code is licensed under MIT.
* Этот код распространяется по лицензии MIT.
* https://github.com/anthonyaxenov/atol-online/blob/master/LICENSE
*/
namespace AtolOnline\Exceptions;
/**
* Исключение, возникающее при попытке указать пустой email
*
* @package AtolOnline\Exceptions
*/
class AtolEmailEmptyException extends AtolException
{
/**
* @inheritDoc
*/
protected $ffd_tags = [
1008,
1117,
];
/**
* @var string Сообщение об ошибке
*/
protected $message = 'Email cannot be empty';
}

View File

@ -1,31 +0,0 @@
<?php
/**
* Copyright (c) Антон Аксенов (aka Anthony Axenov)
*
* This code is licensed under MIT.
* Этот код распространяется по лицензии MIT.
* https://github.com/anthonyaxenov/atol-online/blob/master/LICENSE
*/
namespace AtolOnline\Exceptions;
/**
* Исключение, возникающее при попытке указать слишком длинный email
*
* @package AtolOnline\Exceptions
*/
class AtolEmailTooLongException extends AtolTooLongException
{
/**
* @inheritDoc
*/
protected $ffd_tags = [
1008,
1117,
];
/**
* @var string Сообщение об ошибке
*/
protected $message = 'Email is too long';
}

View File

@ -1,41 +0,0 @@
<?php
/**
* Copyright (c) Антон Аксенов (aka Anthony Axenov)
*
* This code is licensed under MIT.
* Этот код распространяется по лицензии MIT.
* https://github.com/anthonyaxenov/atol-online/blob/master/LICENSE
*/
namespace AtolOnline\Exceptions;
use Throwable;
/**
* Исключение, возникающее при ошибке валидации email
*
* @package AtolOnline\Exceptions
*/
class AtolEmailValidateException extends AtolException
{
/**
* @inheritDoc
*/
protected $ffd_tags = [
1008,
1117,
];
/**
* AtolEmailValidateException constructor.
*
* @param $email
* @param string $message
* @param int $code
* @param \Throwable|null $previous
*/
public function __construct($email, $message = "", $code = 0, Throwable $previous = null)
{
parent::__construct($message ?: 'Invalid email: '.$email, $code, $previous);
}
}

View File

@ -1,51 +0,0 @@
<?php
/**
* Copyright (c) Антон Аксенов (aka Anthony Axenov)
*
* This code is licensed under MIT.
* Этот код распространяется по лицензии MIT.
* https://github.com/anthonyaxenov/atol-online/blob/master/LICENSE
*/
namespace AtolOnline\Exceptions;
use Exception;
use Throwable;
/**
* Исключение, возникающее при работе с АТОЛ Онлайн
*
* @package AtolOnline\Exceptions
*/
class AtolException extends Exception
{
/**
* @var int[] Теги ФФД
*/
protected $ffd_tags = null;
/**
* AtolException constructor.
*
* @param string $message
* @param int $code
* @param \Throwable|null $previous
*/
public function __construct($message = "", $code = 0, Throwable $previous = null)
{
if ($this->getFfdTags()) {
$message .= ' [FFD tags: '.implode(', ', $this->getFfdTags()).']';
}
parent::__construct($message, $code, $previous);
}
/**
* Возвращает теги ФФД, с которыми связано исключение
*
* @return array|null
*/
protected function getFfdTags(): ?array
{
return $this->ffd_tags;
}
}

View File

@ -1,44 +0,0 @@
<?php
/**
* Copyright (c) Антон Аксенов (aka Anthony Axenov)
*
* This code is licensed under MIT.
* Этот код распространяется по лицензии MIT.
* https://github.com/anthonyaxenov/atol-online/blob/master/LICENSE
*/
namespace AtolOnline\Exceptions;
use Throwable;
/**
* Исключение, возникающее при попытке указать ИНН некорректной длины
*
* @package AtolOnline\Exceptions
*/
class AtolInnWrongLengthException extends AtolException
{
/**
* @inheritDoc
*/
protected $ffd_tags = [
1016,
1018,
1226,
1228,
];
/**
* AtolInnWrongLengthException constructor.
*
* @param $inn
* @param string $message
* @param int $code
* @param Throwable|null $previous
*/
public function __construct($inn, $message = "", $code = 0, Throwable $previous = null)
{
parent::__construct($message ?: 'INN length must be 10 or 12 digits only, but actual is '.
valid_strlen($inn), $code, $previous);
}
}

View File

@ -1,32 +0,0 @@
<?php
/**
* Copyright (c) Антон Аксенов (aka Anthony Axenov)
*
* This code is licensed under MIT.
* Этот код распространяется по лицензии MIT.
* https://github.com/anthonyaxenov/atol-online/blob/master/LICENSE
*/
namespace AtolOnline\Exceptions;
use Throwable;
/**
* Исключение, возникающее при работе с невалидным JSON
*
* @package AtolOnline\Exceptions
*/
class AtolInvalidJsonException extends AtolException
{
/**
* AtolInnWrongLengthException constructor.
*
* @param string $message
* @param int $code
* @param Throwable|null $previous
*/
public function __construct($message = "", $code = 0, Throwable $previous = null)
{
parent::__construct($message ?: 'Invalid JSON: ['.json_last_error().'] '.json_last_error_msg(), $code, $previous);
}
}

View File

@ -1,33 +0,0 @@
<?php
/**
* Copyright (c) Антон Аксенов (aka Anthony Axenov)
*
* This code is licensed under MIT.
* Этот код распространяется по лицензии MIT.
* https://github.com/anthonyaxenov/atol-online/blob/master/LICENSE
*/
namespace AtolOnline\Exceptions;
use Throwable;
/**
* Исключение, возникающее при ошибке валидации UUID
*
* @package AtolOnline\Exceptions
*/
class AtolInvalidUuidException extends AtolException
{
/**
* AtolInvalidUuidException constructor.
*
* @param $uuid
* @param string $message
* @param int $code
* @param \Throwable|null $previous
*/
public function __construct($uuid, $message = "", $code = 0, Throwable $previous = null)
{
parent::__construct($message ?: 'Invalid UUID: '.$uuid, $code, $previous);
}
}

View File

@ -1,23 +0,0 @@
<?php
/**
* Copyright (c) Антон Аксенов (aka Anthony Axenov)
*
* This code is licensed under MIT.
* Этот код распространяется по лицензии MIT.
* https://github.com/anthonyaxenov/atol-online/blob/master/LICENSE
*/
namespace AtolOnline\Exceptions;
/**
* Исключение, возникающее при попытке указать пустой логин ККТ
*
* @package AtolOnline\Exceptions
*/
class AtolKktLoginEmptyException extends AtolException
{
/**
* @var string Сообщение об ошибке
*/
protected $message = 'KKT login cannot be empty';
}

View File

@ -1,23 +0,0 @@
<?php
/**
* Copyright (c) Антон Аксенов (aka Anthony Axenov)
*
* This code is licensed under MIT.
* Этот код распространяется по лицензии MIT.
* https://github.com/anthonyaxenov/atol-online/blob/master/LICENSE
*/
namespace AtolOnline\Exceptions;
/**
* Исключение, возникающее при попытке указать пустой пароль ККТ
*
* @package AtolOnline\Exceptions
*/
class AtolKktPasswordEmptyException extends AtolException
{
/**
* @var string Сообщение об ошибке
*/
protected $message = 'KKT password cannot be empty';
}

View File

@ -1,34 +0,0 @@
<?php
/**
* Copyright (c) Антон Аксенов (aka Anthony Axenov)
*
* This code is licensed under MIT.
* Этот код распространяется по лицензии MIT.
* https://github.com/anthonyaxenov/atol-online/blob/master/LICENSE
*/
namespace AtolOnline\Exceptions;
/**
* Исключение, возникающее при попытке указать слишком длинное имя
*
* @package AtolOnline\Exceptions
*/
class AtolNameTooLongException extends AtolTooLongException
{
/**
* @inheritDoc
*/
protected $ffd_tags = [
1026,
1030,
1085,
1225,
1227,
];
/**
* @var string Сообщение об ошибке
*/
protected $message = 'Name is too long';
}

View File

@ -1,30 +0,0 @@
<?php
/**
* Copyright (c) Антон Аксенов (aka Anthony Axenov)
*
* This code is licensed under MIT.
* Этот код распространяется по лицензии MIT.
* https://github.com/anthonyaxenov/atol-online/blob/master/LICENSE
*/
namespace AtolOnline\Exceptions;
/**
* Исключение, возникающее при попытке указать слишком длинный платёжный адрес
*
* @package AtolOnline\Exceptions
*/
class AtolPaymentAddressTooLongException extends AtolException
{
/**
* @inheritDoc
*/
protected $ffd_tags = [
1187,
];
/**
* @var string Сообщение об ошибке
*/
protected $message = 'Payment address is too long';
}

View File

@ -1,34 +0,0 @@
<?php
/**
* Copyright (c) Антон Аксенов (aka Anthony Axenov)
*
* This code is licensed under MIT.
* Этот код распространяется по лицензии MIT.
* https://github.com/anthonyaxenov/atol-online/blob/master/LICENSE
*/
namespace AtolOnline\Exceptions;
/**
* Исключение, возникающее при попытке указать слишком длинный телефон
*
* @package AtolOnline\Exceptions
*/
class AtolPhoneTooLongException extends AtolTooLongException
{
/**
* @inheritDoc
*/
protected $ffd_tags = [
1008,
1073,
1074,
1075,
1171,
];
/**
* @var string Сообщение об ошибке
*/
protected $message = 'Phone is too long';
}

View File

@ -1,30 +0,0 @@
<?php
/**
* Copyright (c) Антон Аксенов (aka Anthony Axenov)
*
* This code is licensed under MIT.
* Этот код распространяется по лицензии MIT.
* https://github.com/anthonyaxenov/atol-online/blob/master/LICENSE
*/
namespace AtolOnline\Exceptions;
/**
* Исключение, возникающее при попытке указать слишком высокую цену (сумму)
*
* @package AtolOnline\Exceptions
*/
class AtolPriceTooHighException extends AtolTooManyException
{
/**
* @inheritDoc
*/
protected $ffd_tags = [
1079,
];
/**
* @var string Сообщение об ошибке
*/
protected $message = 'Price is too high';
}

View File

@ -1,40 +0,0 @@
<?php
/**
* Copyright (c) Антон Аксенов (aka Anthony Axenov)
*
* This code is licensed under MIT.
* Этот код распространяется по лицензии MIT.
* https://github.com/anthonyaxenov/atol-online/blob/master/LICENSE
*/
namespace AtolOnline\Exceptions;
use Throwable;
/**
* Исключение, возникающее при попытке указать слишком длинное что-либо
*
* @package AtolOnline\Exceptions
*/
class AtolTooLongException extends AtolException
{
/**
* @var string Сообщение об ошибке
*/
protected $message = 'Parameter is too long';
/**
* AtolTooLongException constructor.
*
* @param $string
* @param $max
* @param string $message
* @param int $code
* @param Throwable|null $previous
*/
public function __construct($string, $max, $message = "", $code = 0, Throwable $previous = null)
{
parent::__construct($message ?: $this->message.' (max length - '.$max.', actual length - '.
valid_strlen($string), $code, $previous);
}
}

View File

@ -1,40 +0,0 @@
<?php
/**
* Copyright (c) Антон Аксенов (aka Anthony Axenov)
*
* This code is licensed under MIT.
* Этот код распространяется по лицензии MIT.
* https://github.com/anthonyaxenov/atol-online/blob/master/LICENSE
*/
namespace AtolOnline\Exceptions;
use Throwable;
/**
* Исключение, возникающее при попытке указать слишком большое количество чего-либо
*
* @package AtolOnline\Exceptions
*/
class AtolTooManyException extends AtolException
{
/**
* @var string Сообщение об ошибке
*/
protected $message = 'Quantity is too high';
/**
* AtolTooManyException constructor.
*
* @param $quantity
* @param $max
* @param string $message
* @param int $code
* @param Throwable|null $previous
*/
public function __construct($quantity, $max, $message = "", $code = 0, Throwable $previous = null)
{
$message = $message ?: $this->message.' (max - '.$max.', actual - '.$quantity.')';
parent::__construct($message, $code, $previous);
}
}

View File

@ -1,33 +0,0 @@
<?php
/**
* Copyright (c) Антон Аксенов (aka Anthony Axenov)
*
* This code is licensed under MIT.
* Этот код распространяется по лицензии MIT.
* https://github.com/anthonyaxenov/atol-online/blob/master/LICENSE
*/
namespace AtolOnline\Exceptions;
/**
* Исключение, возникающее при попытке добавить слишком много платежей в массив
*
* @package AtolOnline\Exceptions
*/
class AtolTooManyPaymentsException extends AtolTooManyException
{
/**
* @inheritDoc
*/
protected $ffd_tags = [
1031,
1081,
1215,
1217,
];
/**
* @var string Сообщение об ошибке
*/
protected $message = 'Too many payments';
}

View File

@ -1,35 +0,0 @@
<?php
/**
* Copyright (c) Антон Аксенов (aka Anthony Axenov)
*
* This code is licensed under MIT.
* Этот код распространяется по лицензии MIT.
* https://github.com/anthonyaxenov/atol-online/blob/master/LICENSE
*/
namespace AtolOnline\Exceptions;
/**
* Исключение, возникающее при попытке добавить слишком много ставок НДС в массив
*
* @package AtolOnline\Exceptions
*/
class AtolTooManyVatsException extends AtolTooManyException
{
/**
* @inheritDoc
*/
protected $ffd_tags = [
1102,
1103,
1104,
1105,
1106,
1107,
];
/**
* @var string Сообщение об ошибке
*/
protected $message = 'Too many vats';
}

View File

@ -1,30 +0,0 @@
<?php
/**
* Copyright (c) Антон Аксенов (aka Anthony Axenov)
*
* This code is licensed under MIT.
* Этот код распространяется по лицензии MIT.
* https://github.com/anthonyaxenov/atol-online/blob/master/LICENSE
*/
namespace AtolOnline\Exceptions;
/**
* Исключение, возникающее при попытке указать слишком длинный телефон
*
* @package AtolOnline\Exceptions
*/
class AtolUnitTooLongException extends AtolTooLongException
{
/**
* @inheritDoc
*/
protected $ffd_tags = [
1197,
];
/**
* @var string Сообщение об ошибке
*/
protected $message = 'Measurement unit is too long';
}

View File

@ -1,30 +0,0 @@
<?php
/**
* Copyright (c) Антон Аксенов (aka Anthony Axenov)
*
* This code is licensed under MIT.
* Этот код распространяется по лицензии MIT.
* https://github.com/anthonyaxenov/atol-online/blob/master/LICENSE
*/
namespace AtolOnline\Exceptions;
/**
* Исключение, возникающее при попытке указать слишком длинный дополнительный реквизит
*
* @package AtolOnline\Exceptions
*/
class AtolUserdataTooLongException extends AtolTooLongException
{
/**
* @inheritDoc
*/
protected $ffd_tags = [
1191,
];
/**
* @var string Сообщение об ошибке
*/
protected $message = 'User data is too long';
}

View File

@ -1,33 +0,0 @@
<?php
/**
* Copyright (c) Антон Аксенов (aka Anthony Axenov)
*
* This code is licensed under MIT.
* Этот код распространяется по лицензии MIT.
* https://github.com/anthonyaxenov/atol-online/blob/master/LICENSE
*/
namespace AtolOnline\Exceptions;
use Throwable;
/**
* Исключение, возникающее при попытке указать некорректный тип документа
*
* @package AtolOnline\Exceptions
*/
class AtolWrongDocumentTypeException extends AtolException
{
/**
* AtolWrongDocumentTypeException constructor.
*
* @param $type
* @param string $message
* @param int $code
* @param Throwable|null $previous
*/
public function __construct($type, $message = "", $code = 0, Throwable $previous = null)
{
parent::__construct($message ?: "Wrong document type: 'receipt' or 'correction' expected, but '$type' provided", $code, $previous);
}
}

View File

@ -1,55 +0,0 @@
<?php
/**
* Copyright (c) Антон Аксенов (aka Anthony Axenov)
*
* This code is licensed under MIT.
* Этот код распространяется по лицензии MIT.
* https://github.com/anthonyaxenov/atol-online/blob/master/LICENSE
*/
namespace AtolOnline\Traits;
use AtolOnline\{Constants\Constraints, Exceptions\AtolEmailTooLongException, Exceptions\AtolEmailValidateException};
/**
* Добавляет объекту функционал для работы с email
*
* @package AtolOnline\Traits
*/
trait HasEmail
{
/**
* @var string Почта
*/
protected $email;
/**
* Возвращает установленную почту. Тег ФФД: 1008, 1117.
*
* @return string
*/
public function getEmail()
{
return $this->email;
}
/**
* Устанавливает почту. Тег ФФД: 1008, 1117.
*
* @param string $email
* @return $this
* @throws \AtolOnline\Exceptions\AtolEmailTooLongException Слишком длинный email
* @throws \AtolOnline\Exceptions\AtolEmailValidateException Невалидный email
*/
public function setEmail(string $email)
{
$email = trim($email);
if (valid_strlen($email) > Constraints::MAX_LENGTH_EMAIL) {
throw new AtolEmailTooLongException($email, Constraints::MAX_LENGTH_EMAIL);
} elseif (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
throw new AtolEmailValidateException($email);
}
$this->email = $email;
return $this;
}
}

View File

@ -1,54 +0,0 @@
<?php
/**
* Copyright (c) Антон Аксенов (aka Anthony Axenov)
*
* This code is licensed under MIT.
* Этот код распространяется по лицензии MIT.
* https://github.com/anthonyaxenov/atol-online/blob/master/LICENSE
*/
namespace AtolOnline\Traits;
use AtolOnline\Constants\Constraints;
use AtolOnline\Exceptions\AtolInnWrongLengthException;
/**
* Добавляет объекту функционал для работы с ИНН
*
* @package AtolOnline\Traits
*/
trait HasInn
{
/**
* @var string ИНН
*/
protected $inn;
/**
* Возвращает установленный ИНН. Тег ФФД: 1228, 1018.
*
* @return string
*/
public function getInn()
{
return $this->inn ?? '';
}
/**
* Устанавливает ИНН. Тег ФФД: 1228, 1018.
* Входная строка лишается всех знаков, кроме цифр.
*
* @param string $inn
* @return $this
* @throws AtolInnWrongLengthException Некорректная длина ИНН
*/
public function setInn(string $inn)
{
$inn = preg_replace("/[^0-9]/", '', $inn);
if (preg_match_all(Constraints::PATTERN_INN, $inn) == 0) {
throw new AtolInnWrongLengthException($inn);
}
$this->inn = $inn;
return $this;
}
}

View File

@ -1,40 +0,0 @@
<?php
/**
* Copyright (c) Антон Аксенов (aka Anthony Axenov)
*
* This code is licensed under MIT.
* Этот код распространяется по лицензии MIT.
* https://github.com/anthonyaxenov/atol-online/blob/master/LICENSE
*/
namespace AtolOnline\Traits;
/**
* Свойство класса, позволяющее конвертировать рубли <-> копейки
*
* @package AtolOnline\Traits
*/
trait RublesKopeksConverter
{
/**
* Конвертирует рубли в копейки, учитывая только 2 знака после запятой
*
* @param float|null $rubles Рубли
* @return int Копейки
*/
protected static function toKop(?float $rubles = null)
{
return $rubles === null ? null : (int)round($rubles * 100, 2);
}
/**
* Конвертирует копейки в рубли, оставляя только 2 знака после запятой
*
* @param int|null $kopeks Копейки
* @return float Рубли
*/
protected static function toRub(?int $kopeks = null)
{
return $kopeks === null ? null : round($kopeks / 100, 2);
}
}

View File

@ -0,0 +1,71 @@
<?php
/*
* Copyright (c) 2020-2021 Антон Аксенов (Anthony Axenov)
*
* This code is licensed under MIT.
* Этот код распространяется по лицензии MIT.
* https://github.com/anthonyaxenov/atol-online/blob/master/LICENSE
*/
declare(strict_types=1);
namespace AtolOnline\Collections;
use AtolOnline\Exceptions\InvalidEntityInCollectionException;
use Illuminate\Support\Collection;
/**
* Абстрактное описание коллекции любых сущностей
*/
abstract class EntityCollection extends Collection
{
/**
* @inheritDoc
* @throws InvalidEntityInCollectionException
*/
public function jsonSerialize(): array
{
$this->checkCount();
$this->checkItemsClasses();
return parent::jsonSerialize();
}
/**
* Проверяет количество элементов коллекции
*
* @return static
*/
public function checkCount(): static
{
$this->isEmpty() && throw new (static::EMPTY_EXCEPTION_CLASS)();
$this->count() > static::MAX_COUNT && throw new (static::TOO_MANY_EXCEPTION_CLASS)(static::MAX_COUNT);
return $this;
}
/**
* Проверяет корректность класса элемента коллекции
*
* @param mixed $item
* @return static
* @throws InvalidEntityInCollectionException
*/
public function checkItemClass(mixed $item): static
{
if (!is_object($item) || $item::class !== static::ENTITY_CLASS) {
throw new InvalidEntityInCollectionException(static::class, static::ENTITY_CLASS, $item);
}
return $this;
}
/**
* Проверяет корректность классов элементов коллекции
*
* @return static
* @throws InvalidEntityInCollectionException
*/
public function checkItemsClasses(): static
{
return $this->each(fn ($item) => $this->checkItemClass($item));
}
}

View File

@ -0,0 +1,42 @@
<?php
/*
* Copyright (c) 2020-2021 Антон Аксенов (Anthony Axenov)
*
* This code is licensed under MIT.
* Этот код распространяется по лицензии MIT.
* https://github.com/anthonyaxenov/atol-online/blob/master/LICENSE
*/
namespace AtolOnline\Collections;
use AtolOnline\Constraints;
use AtolOnline\Entities\Item;
use AtolOnline\Exceptions\EmptyItemsException;
use AtolOnline\Exceptions\TooManyItemsException;
/**
* Класс, описывающий коллекцию предметов расчёта для документа
*/
final class Items extends EntityCollection
{
/**
* Класс объектов, находящихся в коллекции
*/
protected const ENTITY_CLASS = Item::class;
/**
* Максмальное количество объектов в коллекции
*/
protected const MAX_COUNT = Constraints::MAX_COUNT_DOC_ITEMS;
/**
* Класс исключения для выброса при пустой коллекции
*/
protected const EMPTY_EXCEPTION_CLASS = EmptyItemsException::class;
/**
* Класс-наследник TooManyException для выброса при превышении количества
*/
protected const TOO_MANY_EXCEPTION_CLASS = TooManyItemsException::class;
}

View File

@ -0,0 +1,42 @@
<?php
/*
* Copyright (c) 2020-2021 Антон Аксенов (Anthony Axenov)
*
* This code is licensed under MIT.
* Этот код распространяется по лицензии MIT.
* https://github.com/anthonyaxenov/atol-online/blob/master/LICENSE
*/
namespace AtolOnline\Collections;
use AtolOnline\Constraints;
use AtolOnline\Entities\Payment;
use AtolOnline\Exceptions\EmptyPaymentsException;
use AtolOnline\Exceptions\TooManyPaymentsException;
/**
* Класс, описывающий коллекцию оплат для документа
*/
final class Payments extends EntityCollection
{
/**
* Класс объектов, находящихся в коллекции
*/
protected const ENTITY_CLASS = Payment::class;
/**
* Максмальное количество объектов в коллекции
*/
protected const MAX_COUNT = Constraints::MAX_COUNT_DOC_PAYMENTS;
/**
* Класс исключения для выброса при пустой коллекции
*/
protected const EMPTY_EXCEPTION_CLASS = EmptyPaymentsException::class;
/**
* Класс-наследник TooManyException для выброса при превышении количества
*/
protected const TOO_MANY_EXCEPTION_CLASS = TooManyPaymentsException::class;
}

View File

@ -0,0 +1,42 @@
<?php
/*
* Copyright (c) 2020-2021 Антон Аксенов (Anthony Axenov)
*
* This code is licensed under MIT.
* Этот код распространяется по лицензии MIT.
* https://github.com/anthonyaxenov/atol-online/blob/master/LICENSE
*/
namespace AtolOnline\Collections;
use AtolOnline\Constraints;
use AtolOnline\Entities\Vat;
use AtolOnline\Exceptions\EmptyVatsException;
use AtolOnline\Exceptions\TooManyVatsException;
/**
* Класс, описывающий коллекцию ставок НДС для документа
*/
final class Vats extends EntityCollection
{
/**
* Класс объектов, находящихся в коллекции
*/
protected const ENTITY_CLASS = Vat::class;
/**
* Максмальное количество объектов в коллекции
*/
protected const MAX_COUNT = Constraints::MAX_COUNT_DOC_VATS;
/**
* Класс исключения для выброса при пустой коллекции
*/
protected const EMPTY_EXCEPTION_CLASS = EmptyVatsException::class;
/**
* Класс-наследник TooManyException для выброса при превышении количества
*/
protected const TOO_MANY_EXCEPTION_CLASS = TooManyVatsException::class;
}

221
src/Constraints.php 100644
View File

@ -0,0 +1,221 @@
<?php
/*
* Copyright (c) 2020-2021 Антон Аксенов (Anthony Axenov)
*
* This code is licensed under MIT.
* Этот код распространяется по лицензии MIT.
* https://github.com/anthonyaxenov/atol-online/blob/master/LICENSE
*/
declare(strict_types=1);
namespace AtolOnline;
/**
* Класс с константами ограничений
*/
final class Constraints
{
/**
* Максимальная длина Callback URL
*/
public const MAX_LENGTH_CALLBACK_URL = 256;
/**
* Максимальная длина email
*/
public const MAX_LENGTH_EMAIL = 64;
/**
* Максимальная длина логина ККТ
*/
public const MAX_LENGTH_LOGIN = 100;
/**
* Максимальная длина пароля ККТ
*/
public const MAX_LENGTH_PASSWORD = 100;
/**
* Максимальная длина адреса места расчётов
*/
public const MAX_LENGTH_PAYMENT_ADDRESS = 256;
/**
* Максимальная длина наименования покупателя (1227)
*
* @see https://online.atol.ru/files/API_atol_online_v4.pdf Документация, стр 17
*/
public const MAX_LENGTH_CLIENT_NAME = 256;
/**
* Максимальная длина наименования предмета расчёта (1030)
*
* @see https://online.atol.ru/files/API_atol_online_v4.pdf Документация, стр 21
*/
public const MAX_LENGTH_ITEM_NAME = 128;
/**
* Максимальная цена за единицу предмета расчёта (1079)
*
* @see https://online.atol.ru/files/API_atol_online_v4.pdf Документация, стр 21
*/
public const MAX_COUNT_ITEM_PRICE = 42949672.95;
/**
* Максимальное количество (вес) единицы предмета расчёта (1023)
*
* @see https://online.atol.ru/files/API_atol_online_v4.pdf Документация, стр 21
*/
public const MAX_COUNT_ITEM_QUANTITY = 99999.999;
/**
* Максимальная стоимость всех предметов расчёта в документе прихода, расхода,
* возврата прихода, возврата расхода (1043)
*
* @see https://online.atol.ru/files/API_atol_online_v4.pdf Документация, стр 21
*/
public const MAX_COUNT_ITEM_SUM = 42949672.95;
/**
* Максимальная длина телефона или email покупателя (1008)
*
* @see https://online.atol.ru/files/API_atol_online_v4.pdf Документация, стр 17
*/
public const MAX_LENGTH_CLIENT_CONTACT = 64;
/**
* Длина операции для платёжного агента (1044)
*
* @see https://online.atol.ru/files/API_atol_online_v4.pdf Документация, стр 19
*/
public const MAX_LENGTH_PAYING_AGENT_OPERATION = 24;
/**
* Максимальное количество предметов расчёта в документе прихода, расхода, возврата прихода, возврата расхода
*
* @see https://online.atol.ru/files/API_atol_online_v4.pdf Документация, стр 21
*/
public const MAX_COUNT_DOC_ITEMS = 100;
/**
* Максимальная длина единицы измерения предмета расчётов
*
* @see https://online.atol.ru/files/API_atol_online_v4.pdf Документация, стр 21
*/
public const MAX_LENGTH_MEASUREMENT_UNIT = 16;
/**
* Максимальная длина пользовательских данных для предмета расчётов (1191)
*
* @see https://online.atol.ru/files/API_atol_online_v4.pdf Документация, стр 29
*/
public const MAX_LENGTH_USER_DATA = 64;
/**
* Минимальная длина кода таможенной декларации (1231)
*
* @see https://online.atol.ru/possystem/v4/schema/sell Схема "#/receipt/items/declaration_number"
*/
public const MIN_LENGTH_DECLARATION_NUMBER = 1;
/**
* Максимальная длина кода таможенной декларации (1231)
*
* @see https://online.atol.ru/files/API_atol_online_v4.pdf Документация, стр 30
*/
public const MAX_LENGTH_DECLARATION_NUMBER = 32;
/**
* Максимальное количество платежей в любом документе
*
* @see https://online.atol.ru/files/API_atol_online_v4.pdf Документация, стр 30 и 35
*/
public const MAX_COUNT_DOC_PAYMENTS = 10;
/**
* Максимальное количество ставок НДС в любом документе
*
* @see https://online.atol.ru/files/API_atol_online_v4.pdf Документация, стр 31 и 36
*/
public const MAX_COUNT_DOC_VATS = 6;
/**
* Максимальная сумма одной оплаты
*/
public const MAX_COUNT_PAYMENT_SUM = 99999.999;
/**
* Максимальная длина имени кассира (1021)
*
* @see https://online.atol.ru/files/API_atol_online_v4.pdf Документация, стр 32
*/
public const MAX_LENGTH_CASHIER_NAME = 64;
/**
* Максимальная длина кода товара в байтах (1162)
*
* @see https://online.atol.ru/files/API_atol_online_v4.pdf Документация, стр 21
*/
public const MAX_LENGTH_ITEM_CODE = 32;
/**
* Максимальная длина значения дополнительного реквизита чека (1192)
*
* @see https://online.atol.ru/files/API_atol_online_v4.pdf Документация, стр 32
*/
public const MAX_LENGTH_ADD_CHECK_PROP = 16;
/**
* Максимальная длина наименования дополнительного реквизита пользователя (1085)
*
* @see https://online.atol.ru/files/API_atol_online_v4.pdf Документация, стр 32
*/
public const MAX_LENGTH_ADD_USER_PROP_NAME = 64;
/**
* Максимальная длина значения дополнительного реквизита пользователя (1086)
*
* @see https://online.atol.ru/files/API_atol_online_v4.pdf Документация, стр 32
*/
public const MAX_LENGTH_ADD_USER_PROP_VALUE = 256;
/**
* Формат даты документа коррекции
*/
public const CORRECTION_DATE_FORMAT = 'd.m.Y';
/**
* Регулярное выражение для валидации строки ИНН
*
* @see https://online.atol.ru/possystem/v4/schema/sell Схема "#/receipt/client/inn"
*/
public const PATTERN_INN
= /* @lang PhpRegExp */
'/(^[\d]{10}$)|(^[\d]{12}$)/';
/**
* Регулярное выражение для валидации номера телефона
*
* @see https://online.atol.ru/possystem/v4/schema/sell Схема "#/definitions/phone_number"
*/
public const PATTERN_PHONE
= /* @lang PhpRegExp */
'/^([^\s\\\]{0,17}|\+[^\s\\\]{1,18})$/';
/**
* Регулярное выражение для валидации строки Callback URL
*/
public const PATTERN_CALLBACK_URL
= /* @lang PhpRegExp */
'/^http(s?):\/\/[0-9a-zA-Zа-яА-Я]' .
'([-.\w]*[0-9a-zA-Zа-яА-Я])*(:(0-9)*)*(\/?)([a-zAZ0-9а-яА-Я\-.?,\'\/\\\+&=%\$#_]*)?$/';
/**
* Регулярное выражение для валидации кода страны происхождения товара
*/
public const PATTERN_OKSM_CODE
= /* @lang PhpRegExp */
'/^[\d]{3}$/';
}

View File

@ -0,0 +1,123 @@
<?php
/*
* Copyright (c) 2020-2021 Антон Аксенов (Anthony Axenov)
*
* This code is licensed under MIT.
* Этот код распространяется по лицензии MIT.
* https://github.com/anthonyaxenov/atol-online/blob/master/LICENSE
*/
declare(strict_types=1);
namespace AtolOnline\Entities;
use AtolOnline\Constraints;
use AtolOnline\Exceptions\{
EmptyAddUserPropNameException,
EmptyAddUserPropValueException,
TooLongAddUserPropNameException,
TooLongAddUserPropValueException};
use JetBrains\PhpStorm\{
ArrayShape,
Pure};
/**
* Класс, описывающий дополнительный реквизит пользователя
*
* @see https://online.atol.ru/files/API_atol_online_v4.pdf Документация, стр 32
*/
final class AdditionalUserProps extends Entity
{
/**
* Конструктор объекта покупателя
*
* @param string $name Наименование (1227)
* @param string $value Значение (1008)
* @throws EmptyAddUserPropNameException
* @throws EmptyAddUserPropValueException
* @throws TooLongAddUserPropNameException
* @throws TooLongAddUserPropValueException
*/
public function __construct(
protected string $name,
protected string $value,
) {
$this->setName($name)->setValue($value);
}
/**
* Возвращает наименование реквизита
*
* @return string
*/
public function getName(): string
{
return $this->name;
}
/**
* Устанавливает наименование реквизита
*
* @param string $name
* @return $this
* @throws TooLongAddUserPropNameException
* @throws EmptyAddUserPropNameException
*/
public function setName(string $name): self
{
$name = trim($name);
if (mb_strlen($name) > Constraints::MAX_LENGTH_ADD_USER_PROP_NAME) {
throw new TooLongAddUserPropNameException($name);
}
if (empty($name)) {
throw new EmptyAddUserPropNameException($name);
}
$this->name = $name;
return $this;
}
/**
* Возвращает установленный телефон
*
* @return string|null
*/
public function getValue(): ?string
{
return $this->value;
}
/**
* Устанавливает значение реквизита
*
* @param string $value
* @return $this
* @throws TooLongAddUserPropValueException
* @throws EmptyAddUserPropValueException
*/
public function setValue(string $value): self
{
$value = trim($value);
if (mb_strlen($value) > Constraints::MAX_LENGTH_CLIENT_NAME) {
throw new TooLongAddUserPropValueException($value);
}
if (empty($value)) {
throw new EmptyAddUserPropValueException($value);
}
$this->value = $value;
return $this;
}
/**
* @inheritDoc
*/
#[Pure]
#[ArrayShape(['name' => 'string', 'value' => 'null|string'])]
public function jsonSerialize(): array
{
return [
'name' => $this->getName(),
'value' => $this->getValue(),
];
}
}

View File

@ -0,0 +1,149 @@
<?php
/*
* Copyright (c) 2020-2021 Антон Аксенов (Anthony Axenov)
*
* This code is licensed under MIT.
* Этот код распространяется по лицензии MIT.
* https://github.com/anthonyaxenov/atol-online/blob/master/LICENSE
*/
declare(strict_types=1);
namespace AtolOnline\Entities;
use AtolOnline\Enums\AgentType;
/**
* Класс, описывающий данные агента
*
* @see https://online.atol.ru/files/API_atol_online_v4.pdf Документация, стр 26-28
*/
final class AgentInfo extends Entity
{
/**
* Конструктор
*
* @param AgentType|null $type Признак агента (1057)
* @param PayingAgent|null $payingAgent Платёжный агент
* @param ReceivePaymentsOperator|null $receivePaymentsOperator Оператор по приёму платежей
* @param MoneyTransferOperator|null $moneyTransferOperator Оператор перевода
*/
public function __construct(
protected ?AgentType $type = null,
protected ?PayingAgent $payingAgent = null,
protected ?ReceivePaymentsOperator $receivePaymentsOperator = null,
protected ?MoneyTransferOperator $moneyTransferOperator = null,
) {
$this->setType($type);
}
/**
* Возвращает установленный признак оператора
*
* @return AgentType|null
*/
public function getType(): ?AgentType
{
return $this->type;
}
/**
* Устанавливает признак оператора
*
* @param AgentType|null $type
* @return AgentInfo
*/
public function setType(?AgentType $type): self
{
$this->type = $type;
return $this;
}
/**
* Возвращает установленного платёжного агента
*
* @return PayingAgent|null
*/
public function getPayingAgent(): ?PayingAgent
{
return $this->payingAgent;
}
/**
* Устанавливает платёжного агента
*
* @param PayingAgent|null $agent
* @return AgentInfo
*/
public function setPayingAgent(?PayingAgent $agent): self
{
$this->payingAgent = $agent;
return $this;
}
/**
* Возвращает установленного оператора по приёму платежей
*
* @return ReceivePaymentsOperator|null
*/
public function getReceivePaymentsOperator(): ?ReceivePaymentsOperator
{
return $this->receivePaymentsOperator;
}
/**
* Устанавливает оператора по приёму платежей
*
* @param ReceivePaymentsOperator|null $operator
* @return AgentInfo
*/
public function setReceivePaymentsOperator(?ReceivePaymentsOperator $operator): self
{
$this->receivePaymentsOperator = $operator;
return $this;
}
/**
* Возвращает установленного оператора перевода
*
* @return MoneyTransferOperator|null
*/
public function getMoneyTransferOperator(): ?MoneyTransferOperator
{
return $this->moneyTransferOperator;
}
/**
* Устанавливает оператора перевода
*
* @param MoneyTransferOperator|null $operator
* @return AgentInfo
*/
public function setMoneyTransferOperator(?MoneyTransferOperator $operator): self
{
$this->moneyTransferOperator = $operator;
return $this;
}
/**
* @inheritDoc
*/
public function jsonSerialize(): array
{
$json = [];
if ($this?->type) {
$json['type'] = $this->getType();
}
if ($this->payingAgent?->jsonSerialize()) {
$json['paying_agent'] = $this->payingAgent->jsonSerialize();
}
if ($this->receivePaymentsOperator?->jsonSerialize()) {
$json['receive_payments_operator'] = $this->receivePaymentsOperator->jsonSerialize();
}
if ($this->moneyTransferOperator?->jsonSerialize()) {
$json['money_transfer_operator'] = $this->moneyTransferOperator->jsonSerialize();
}
return $json;
}
}

View File

@ -0,0 +1,133 @@
<?php
/*
* Copyright (c) 2020-2021 Антон Аксенов (Anthony Axenov)
*
* This code is licensed under MIT.
* Этот код распространяется по лицензии MIT.
* https://github.com/anthonyaxenov/atol-online/blob/master/LICENSE
*/
declare(strict_types=1);
namespace AtolOnline\Entities;
use AtolOnline\Constraints;
use AtolOnline\Exceptions\{
InvalidEmailException,
InvalidInnLengthException,
InvalidPhoneException,
TooLongClientNameException,
TooLongEmailException};
use AtolOnline\Traits\{
HasEmail,
HasInn};
use JetBrains\PhpStorm\Pure;
/**
* Класс, описывающий покупателя
*
* @see https://online.atol.ru/files/API_atol_online_v4.pdf Документация, стр 17
*/
final class Client extends Entity
{
use HasEmail;
use HasInn;
/**
* Конструктор объекта покупателя
*
* @param string|null $name Наименование (1227)
* @param string|null $email Телефон (1008)
* @param string|null $phone Email (1008)
* @param string|null $inn ИНН (1228)
* @throws InvalidEmailException
* @throws InvalidInnLengthException
* @throws InvalidPhoneException
* @throws TooLongClientNameException
* @throws TooLongEmailException
*/
public function __construct(
protected ?string $name = null,
protected ?string $phone = null,
?string $email = null,
?string $inn = null
) {
!is_null($name) && $this->setName($name);
!is_null($email) && $this->setEmail($email);
!is_null($phone) && $this->setPhone($phone);
!is_null($inn) && $this->setInn($inn);
}
/**
* Возвращает наименование покупателя
*
* @return string|null
*/
public function getName(): ?string
{
return $this->name;
}
/**
* Устанавливает наименование покупателя
*
* @param string|null $name
* @return $this
* @throws TooLongClientNameException
*/
public function setName(?string $name): self
{
if (is_string($name)) {
$name = preg_replace('/[\n\r\t]/', '', trim($name));
if (mb_strlen($name) > Constraints::MAX_LENGTH_CLIENT_NAME) {
throw new TooLongClientNameException($name);
}
}
$this->name = $name ?: null;
return $this;
}
/**
* Возвращает установленный телефон
*
* @return string|null
*/
public function getPhone(): ?string
{
return $this->phone;
}
/**
* Устанавливает телефон
*
* @param string|null $phone Номер телефона
* @return $this
* @throws InvalidPhoneException
*/
public function setPhone(?string $phone): self
{
if (is_string($phone)) {
$phone = preg_replace('/\D/', '', trim($phone));
if (preg_match(Constraints::PATTERN_PHONE, $phone) !== 1) {
throw new InvalidPhoneException($phone);
}
}
$this->phone = empty($phone) ? null : "+$phone";
return $this;
}
/**
* @inheritDoc
*/
#[Pure]
public function jsonSerialize(): array
{
$json = [];
!is_null($this->getName()) && $json['name'] = $this->getName();
!is_null($this->getPhone()) && $json['phone'] = $this->getPhone();
!is_null($this->getEmail()) && $json['email'] = $this->getEmail();
!is_null($this->getInn()) && $json['inn'] = $this->getInn();
return $json;
}
}

View File

@ -0,0 +1,132 @@
<?php
/*
* Copyright (c) 2020-2021 Антон Аксенов (Anthony Axenov)
*
* This code is licensed under MIT.
* Этот код распространяется по лицензии MIT.
* https://github.com/anthonyaxenov/atol-online/blob/master/LICENSE
*/
declare(strict_types=1);
namespace AtolOnline\Entities;
use AtolOnline\{
Constraints,
Enums\SnoType,
Traits\HasEmail,
Traits\HasInn};
use AtolOnline\Exceptions\{
InvalidEmailException,
InvalidInnLengthException,
InvalidPaymentAddressException,
TooLongEmailException,
TooLongPaymentAddressException};
use JetBrains\PhpStorm\ArrayShape;
/**
* Класс, описывающий сущность компании-продавца
*
* @see https://online.atol.ru/files/API_atol_online_v4.pdf Документация, стр 17
*/
final class Company extends Entity
{
use HasEmail;
use HasInn;
/**
* Конструктор
*
* @param string $inn ИНН (1018)
* @param SnoType $sno Система налогообложения продавца (1055)
* @param string $paymentAddress Место расчётов (адрес интернет-магазина) (1187)
* @param string $email Почта (1117)
* @throws InvalidEmailException
* @throws InvalidInnLengthException
* @throws InvalidPaymentAddressException
* @throws TooLongEmailException
* @throws TooLongPaymentAddressException
*/
public function __construct(
string $inn,
protected SnoType $sno,
protected string $paymentAddress,
string $email,
) {
$this->setInn($inn)
->setPaymentAddress($paymentAddress)
->setEmail($email);
}
/**
* Возвращает установленный тип налогообложения
*
* @return SnoType
*/
public function getSno(): SnoType
{
return $this->sno;
}
/**
* Устанавливает тип налогообложения
*
* @param SnoType $sno
* @return $this
*/
public function setSno(SnoType $sno): self
{
$this->sno = $sno;
return $this;
}
/**
* Возвращает установленный адрес места расчётов
*
* @return string
*/
public function getPaymentAddress(): string
{
return $this->paymentAddress;
}
/**
* Устанавливает адрес места расчётов
*
* @param string $paymentAddress
* @return $this
* @throws TooLongPaymentAddressException
* @throws InvalidPaymentAddressException
*/
public function setPaymentAddress(string $paymentAddress): self
{
$paymentAddress = trim($paymentAddress);
if (empty($paymentAddress)) {
throw new InvalidPaymentAddressException();
} elseif (mb_strlen($paymentAddress) > Constraints::MAX_LENGTH_PAYMENT_ADDRESS) {
throw new TooLongPaymentAddressException($paymentAddress);
}
$this->paymentAddress = $paymentAddress;
return $this;
}
/**
* @inheritDoc
*/
#[ArrayShape([
'sno' => 'string',
'email' => 'string',
'inn' => 'string',
'payment_address' => 'string',
])]
public function jsonSerialize(): array
{
return [
'inn' => $this->getInn(),
'sno' => $this->getSno(),
'payment_address' => $this->getPaymentAddress(),
'email' => $this->getEmail(),
];
}
}

View File

@ -0,0 +1,252 @@
<?php
/*
* Copyright (c) 2020-2021 Антон Аксенов (Anthony Axenov)
*
* This code is licensed under MIT.
* Этот код распространяется по лицензии MIT.
* https://github.com/anthonyaxenov/atol-online/blob/master/LICENSE
*/
namespace AtolOnline\Entities;
use AtolOnline\{
Api\AtolResponse,
Api\Fiscalizer,
Collections\Payments,
Collections\Vats,
Constraints};
use AtolOnline\Exceptions\{
AuthFailedException,
EmptyLoginException,
EmptyPasswordException,
InvalidEntityInCollectionException,
InvalidInnLengthException,
InvalidPaymentAddressException,
TooLongCashierException,
TooLongPaymentAddressException};
use Exception;
use GuzzleHttp\Exception\GuzzleException;
use JetBrains\PhpStorm\ArrayShape;
/**
* Класс, описывающий документ коррекции
*
* @see https://online.atol.ru/files/API_atol_online_v4.pdf Документация, стр 35
*/
final class Correction extends Entity
{
/**
* Тип документа
*/
public const DOC_TYPE = 'correction';
/**
* @todo вынести в трейт?
* @var string|null ФИО кассира
*/
protected ?string $cashier = null;
/**
* Конструктор
*
* @param Company $company Продавец
* @param CorrectionInfo $correctionInfo Данные коррекции
* @param Payments $payments Коллекция оплат
* @param Vats $vats Коллекция ставок НДС
* @throws InvalidEntityInCollectionException
* @throws Exception
*/
public function __construct(
protected Company $company,
protected CorrectionInfo $correctionInfo,
protected Payments $payments,
protected Vats $vats,
) {
$this->setCompany($company)->setCorrectionInfo($correctionInfo)->setPayments($payments)->setVats($vats);
}
/**
* Возвращает установленного продавца
*
* @return Company
*/
public function getCompany(): Company
{
return $this->company;
}
/**
* Устанаваливает продавца
*
* @param Company $company
* @return $this
*/
public function setCompany(Company $company): self
{
$this->company = $company;
return $this;
}
/**
* Возвращает установленного кассира
*
* @return string|null
*/
public function getCashier(): ?string
{
return $this->cashier;
}
/**
* Устанаваливает кассира
*
* @param string|null $cashier
* @return $this
* @throws TooLongCashierException
*/
public function setCashier(?string $cashier): self
{
if (is_string($cashier)) {
$cashier = trim($cashier);
if (mb_strlen($cashier) > Constraints::MAX_LENGTH_CASHIER_NAME) {
throw new TooLongCashierException($cashier);
}
}
$this->cashier = $cashier ?: null;
return $this;
}
/**
* Возвращает установленные данные коррекции
*
* @return CorrectionInfo
*/
public function getCorrectionInfo(): CorrectionInfo
{
return $this->correctionInfo;
}
/**
* Устанавливает данные коррекции
*
* @param CorrectionInfo $correctionInfo
* @return Correction
*/
public function setCorrectionInfo(CorrectionInfo $correctionInfo): Correction
{
$this->correctionInfo = $correctionInfo;
return $this;
}
/**
* Возвращает установленную коллекцию оплат
*
* @return Payments
*/
public function getPayments(): Payments
{
return $this->payments;
}
/**
* Устанаваливает коллекцию оплат
*
* @param Payments $payments
* @return $this
* @throws InvalidEntityInCollectionException
*/
public function setPayments(Payments $payments): self
{
$payments->checkCount()->checkItemsClasses();
$this->payments = $payments;
return $this;
}
/**
* Возвращает установленную коллекцию ставок НДС
*
* @return Vats
*/
public function getVats(): Vats
{
return $this->vats ?? new Vats();
}
/**
* Устанаваливает коллекцию ставок НДС
*
* @param Vats $vats
* @return $this
* @throws Exception
*/
public function setVats(Vats $vats): self
{
$vats->checkCount()->checkItemsClasses();
$this->vats = $vats;
return $this;
}
/**
* Регистрирует коррекцию прихода по текущему документу
*
* @param Fiscalizer $fiscalizer Объект фискализатора
* @param string|null $external_id Уникальный код документа (если не указан, то будет создан новый UUID)
* @return AtolResponse|null
* @throws AuthFailedException
* @throws EmptyLoginException
* @throws EmptyPasswordException
* @throws GuzzleException
* @throws InvalidEntityInCollectionException
* @throws InvalidInnLengthException
* @throws InvalidPaymentAddressException
* @throws TooLongPaymentAddressException
*/
public function sellCorrect(Fiscalizer $fiscalizer, ?string $external_id = null): ?AtolResponse
{
return $fiscalizer->sellCorrect($this, $external_id);
}
/**
* Регистрирует коррекцию расхода по текущему документу
*
* @param Fiscalizer $fiscalizer Объект фискализатора
* @param string|null $external_id Уникальный код документа (если не указан, то будет создан новый UUID)
* @return AtolResponse|null
* @throws AuthFailedException
* @throws EmptyLoginException
* @throws EmptyPasswordException
* @throws GuzzleException
* @throws InvalidEntityInCollectionException
* @throws InvalidInnLengthException
* @throws InvalidPaymentAddressException
* @throws TooLongPaymentAddressException
*/
public function buyCorrect(Fiscalizer $fiscalizer, ?string $external_id = null): ?AtolResponse
{
return $fiscalizer->buyCorrect($this, $external_id);
}
/**
* @inheritDoc
* @throws InvalidEntityInCollectionException
*/
#[ArrayShape([
'company' => '\AtolOnline\Entities\Company',
'correction_info' => '\AtolOnline\Entities\CorrectionInfo',
'payments' => 'array',
'vats' => '\AtolOnline\Collections\Vats|null',
'cashier' => 'null|string',
])]
public function jsonSerialize(): array
{
$json = [
'company' => $this->getCompany(),
'correction_info' => $this->getCorrectionInfo(),
'payments' => $this->getPayments()->jsonSerialize(),
'vats' => $this->getVats(),
];
!is_null($this->getCashier()) && $json['cashier'] = $this->getCashier();
return $json;
}
}

View File

@ -0,0 +1,154 @@
<?php
/*
* Copyright (c) 2020-2021 Антон Аксенов (Anthony Axenov)
*
* This code is licensed under MIT.
* Этот код распространяется по лицензии MIT.
* https://github.com/anthonyaxenov/atol-online/blob/master/LICENSE
*/
declare(strict_types=1);
namespace AtolOnline\Entities;
use AtolOnline\Constraints;
use AtolOnline\Enums\CorrectionType;
use AtolOnline\Exceptions\{
EmptyCorrectionNumberException,
InvalidCorrectionDateException,};
use DateTime;
use DateTimeImmutable;
use DateTimeInterface;
use Exception;
use JetBrains\PhpStorm\{
ArrayShape};
/**
* Класс, описывающий данные коррекции
*
* @see https://online.atol.ru/files/API_atol_online_v4.pdf Документация, стр 35
*/
final class CorrectionInfo extends Entity
{
/**
* @var DateTimeImmutable Дата документа основания для коррекции (1178)
*/
protected DateTimeImmutable $date;
/**
* Конструктор
*
* @param CorrectionType $type Тип коррекции (1173)
* @param DateTimeInterface|string $date Дата документа основания для коррекции (1178)
* @param string $number Номер документа основания для коррекции (1179)
* @throws InvalidCorrectionDateException
* @throws EmptyCorrectionNumberException
*/
public function __construct(
protected CorrectionType $type,
DateTimeInterface | string $date,
protected string $number,
) {
$this->setType($type)
->setDate($date)
->setNumber($number);
}
/**
* Возвращает тип коррекции
*
* @return CorrectionType|null
*/
public function getType(): ?CorrectionType
{
return $this->type;
}
/**
* Устанавливает тип коррекции
*
* @param CorrectionType $type
* @return $this
*/
public function setType(CorrectionType $type): self
{
$this->type = $type;
return $this;
}
/**
* Возвращает дату документа основания для коррекции
*
* @return DateTimeImmutable
*/
public function getDate(): DateTimeImmutable
{
return $this->date;
}
/**
* Устанавливает дату документа основания для коррекции
*
* @param DateTimeInterface|string $date Строковая дата в формате d.m.Y либо объект DateTime с датой
* @return $this
* @throws InvalidCorrectionDateException
*/
public function setDate(DateTimeInterface | string $date): self
{
try {
if (is_string($date)) {
$this->date = new DateTimeImmutable(trim($date));
} elseif ($date instanceof DateTime) {
$this->date = DateTimeImmutable::createFromMutable($date);
} elseif ($date instanceof DateTimeImmutable) {
$this->date = $date;
}
} catch (Exception $e) {
throw new InvalidCorrectionDateException($date, $e->getMessage());
}
return $this;
}
/**
* Возвращает установленный номер документа основания для коррекции
*
* @return string|null
*/
public function getNumber(): ?string
{
return $this->number;
}
/**
* Устанавливает номер документа основания для коррекции
*
* @param string $number
* @return $this
* @throws EmptyCorrectionNumberException
*/
public function setNumber(string $number): self
{
$number = trim($number);
empty($number) && throw new EmptyCorrectionNumberException();
$this->number = $number;
return $this;
}
/**
* @inheritDoc
*/
#[ArrayShape([
'type' => 'string',
'base_date' => 'string',
'base_number' => 'string',
])]
public function jsonSerialize(): array
{
return [
'type' => $this->getType(),
'base_date' => $this->getDate()->format(Constraints::CORRECTION_DATE_FORMAT),
'base_number' => $this->getNumber(),
];
}
}

View File

@ -0,0 +1,94 @@
<?php
/*
* Copyright (c) 2020-2021 Антон Аксенов (Anthony Axenov)
*
* This code is licensed under MIT.
* Этот код распространяется по лицензии MIT.
* https://github.com/anthonyaxenov/atol-online/blob/master/LICENSE
*/
/** @noinspection PhpMultipleClassDeclarationsInspection */
declare(strict_types=1);
namespace AtolOnline\Entities;
use ArrayAccess;
use BadMethodCallException;
use Illuminate\Contracts\Support\Arrayable;
use JetBrains\PhpStorm\ArrayShape;
use JsonSerializable;
use Stringable;
/**
* Абстрактное описание любой сущности, представляемой как json
*/
abstract class Entity implements JsonSerializable, Stringable, Arrayable, ArrayAccess
{
/**
* @inheritDoc
*/
abstract public function jsonSerialize(): array;
/**
* @inheritDoc
*/
#[ArrayShape([
'company' => "\AtolOnline\Entities\Company",
'correction_info' => "\AtolOnline\Entities\CorrectionInfo",
'payments' => "array",
'vats' => "\AtolOnline\Collections\Vats|null",
'cashier' => "null|string",
])]
public function toArray()
{
return $this->jsonSerialize();
}
/**
* Возвращает строковое представление json-структуры объекта
*
* @return false|string
*/
public function __toString()
{
return json_encode($this->toArray(), JSON_UNESCAPED_UNICODE);
}
/**
* @inheritDoc
*/
public function offsetExists(mixed $offset): bool
{
return isset($this->toArray()[$offset]);
}
/**
* @inheritDoc
*/
public function offsetGet(mixed $offset): mixed
{
return $this->toArray()[$offset];
}
/**
* @inheritDoc
*/
public function offsetSet(mixed $offset, mixed $value): void
{
throw new BadMethodCallException(
'Объект ' . static::class . ' нельзя изменять как массив. Следует использовать сеттеры.'
);
}
/**
* @inheritDoc
*/
public function offsetUnset(mixed $offset): void
{
throw new BadMethodCallException(
'Объект ' . static::class . ' нельзя изменять как массив. Следует использовать сеттеры.'
);
}
}

View File

@ -0,0 +1,559 @@
<?php
/*
* Copyright (c) 2020-2021 Антон Аксенов (Anthony Axenov)
*
* This code is licensed under MIT.
* Этот код распространяется по лицензии MIT.
* https://github.com/anthonyaxenov/atol-online/blob/master/LICENSE
*/
declare(strict_types=1);
namespace AtolOnline\Entities;
use AtolOnline\Constraints;
use AtolOnline\Enums\{
PaymentMethod,
PaymentObject,
VatType};
use AtolOnline\Exceptions\{
EmptyItemNameException,
InvalidDeclarationNumberException,
InvalidOKSMCodeException,
NegativeItemExciseException,
NegativeItemPriceException,
NegativeItemQuantityException,
TooHighItemPriceException,
TooHighItemQuantityException,
TooHighItemSumException,
TooLongItemCodeException,
TooLongItemNameException,
TooLongMeasurementUnitException,
TooLongUserdataException,
TooManyException};
/**
* Предмет расчёта (товар, услуга)
*
* @see https://online.atol.ru/files/API_atol_online_v4.pdf Документация, стр 21-30
*/
final class Item extends Entity
{
/**
* @var string|null Единица измерения (1197)
*/
protected ?string $measurement_unit = null;
/**
* @var string|null Код товара (1162)
*/
protected ?string $code = null;
/**
* @var string|null Код товара (1162) в форматированной шестнадцатиричной форме
*/
protected ?string $codeHex = null;
/**
* @var PaymentMethod|null Признак способа расчёта (1214)
*/
protected ?PaymentMethod $paymentMethod = null;
/**
* @var PaymentObject|null Признак предмета расчёта (1212)
*/
protected ?PaymentObject $paymentObject = null;
/**
* @var string|null Номер таможенной декларации (1321)
*/
protected ?string $declarationNumber = null;
/**
* @var Vat|null Ставка НДС
*/
protected ?Vat $vat = null;
/**
* @var AgentInfo|null Атрибуты агента
*/
protected ?AgentInfo $agentInfo = null;
/**
* @var Supplier|null Атрибуты поставшика
*/
protected ?Supplier $supplier = null;
/**
* @var string|null Дополнительный реквизит (1191)
*/
protected ?string $userData = null;
/**
* @var float|null Сумма акциза, включенная в стоимость (1229)
*/
protected ?float $excise = null;
/**
* @var string|null Цифровой код страны происхождения товара (1230)
*/
protected ?string $countryCode = null;
/**
* Конструктор
*
* @param string|null $name Наименование (1030)
* @param float|null $price Цена в рублях (с учётом скидок и наценок) (1079)
* @param float|null $quantity Количество/вес (1023)
* @throws TooLongItemNameException
* @throws TooHighItemPriceException
* @throws TooManyException
* @throws NegativeItemPriceException
* @throws EmptyItemNameException
* @throws NegativeItemQuantityException
*/
public function __construct(
protected ?string $name = null,
protected ?float $price = null,
protected ?float $quantity = null,
) {
!is_null($name) && $this->setName($name);
!is_null($price) && $this->setPrice($price);
!is_null($quantity) && $this->setQuantity($quantity);
}
/**
* Возвращает наименование
*
* @return string
*/
public function getName(): string
{
return $this->name;
}
/**
* Устаналивает наименование
*
* @param string $name Наименование
* @return $this
* @throws TooLongItemNameException
* @throws EmptyItemNameException
*/
public function setName(string $name): self
{
if (mb_strlen($name = trim($name)) > Constraints::MAX_LENGTH_ITEM_NAME) {
throw new TooLongItemNameException($name);
}
empty($name) && throw new EmptyItemNameException();
$this->name = $name;
return $this;
}
/**
* Возвращает цену в рублях
*
* @return float
*/
public function getPrice(): float
{
return $this->price;
}
/**
* Устанавливает цену в рублях
*
* @param float $price
* @return $this
* @throws NegativeItemPriceException
* @throws TooHighItemPriceException
* @throws TooHighItemSumException
*/
public function setPrice(float $price): self
{
$price = round($price, 2);
$price > Constraints::MAX_COUNT_ITEM_PRICE && throw new TooHighItemPriceException($this->getName(), $price);
$price < 0 && throw new NegativeItemPriceException($this->getName(), $price);
$this->price = $price;
$this->getVat()?->setSum($this->getSum());
return $this;
}
/**
* Возвращает количество
*
* @return float
*/
public function getQuantity(): float
{
return $this->quantity;
}
/**
* Устанавливает количество
*
* @param float $quantity Количество
* @return $this
* @throws TooHighItemQuantityException
* @throws NegativeItemQuantityException
* @throws TooHighItemSumException
*/
public function setQuantity(float $quantity): self
{
$quantity = round($quantity, 3);
if ($quantity > Constraints::MAX_COUNT_ITEM_QUANTITY) {
throw new TooHighItemQuantityException($this->getName(), $quantity);
}
$quantity < 0 && throw new NegativeItemQuantityException($this->getName(), $quantity);
$this->quantity = $quantity;
$this->getVat()?->setSum($this->getSum());
return $this;
}
/**
* Возвращает стоимость (цена * количество + акциз)
*
* @return float
* @throws TooHighItemSumException
*/
public function getSum(): float
{
$sum = $this->getPrice() * $this->getQuantity() + (float)$this->getExcise();
if ($sum > Constraints::MAX_COUNT_ITEM_SUM) {
throw new TooHighItemSumException($this->getName(), $sum);
}
return $sum;
}
/**
* Возвращает заданную единицу измерения количества
*
* @return string|null
*/
public function getMeasurementUnit(): ?string
{
return $this->measurement_unit;
}
/**
* Устанавливает единицу измерения количества
*
* @param string|null $measurement_unit
* @return $this
* @throws TooLongMeasurementUnitException
*/
public function setMeasurementUnit(?string $measurement_unit): self
{
$measurement_unit = trim((string)$measurement_unit);
if (mb_strlen($measurement_unit) > Constraints::MAX_LENGTH_MEASUREMENT_UNIT) {
throw new TooLongMeasurementUnitException($measurement_unit);
}
$this->measurement_unit = $measurement_unit ?: null;
return $this;
}
/**
* Возвращает установленный код товара
*
* @return string|null
*/
public function getCode(): ?string
{
return $this->code;
}
/**
* Возвращает шестнадцатиричное представление кода товара
*
* @return string|null
*/
public function getCodeHex(): ?string
{
return $this->codeHex;
}
/**
* Устанавливает код товара
*
* @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->codeHex = $hex_string ?: null;
return $this;
}
/**
* Возвращает признак способа оплаты
*
* @return PaymentMethod|null
*/
public function getPaymentMethod(): ?PaymentMethod
{
return $this->paymentMethod;
}
/**
* Устанавливает признак способа оплаты
*
* @param PaymentMethod|null $paymentMethod Признак способа оплаты
* @return $this
*/
public function setPaymentMethod(?PaymentMethod $paymentMethod): self
{
$this->paymentMethod = $paymentMethod;
return $this;
}
/**
* Возвращает признак предмета расчёта
*
* @return PaymentObject|null
*/
public function getPaymentObject(): ?PaymentObject
{
return $this->paymentObject;
}
/**
* Устанавливает признак предмета расчёта
*
* @param PaymentObject|null $paymentObject Признак предмета расчёта
* @return $this
*/
public function setPaymentObject(?PaymentObject $paymentObject): self
{
$this->paymentObject = $paymentObject;
return $this;
}
/**
* Возвращает ставку НДС
*
* @return Vat|null
*/
public function getVat(): ?Vat
{
return $this->vat;
}
/**
* Устанавливает ставку НДС
*
* @param Vat | VatType | null $vat Объект ставки, одно из значений VatTypes или null для удаления ставки
* @return $this
* @throws TooHighItemSumException
*/
public function setVat(Vat | VatType | null $vat): self
{
if (is_null($vat)) {
$this->vat = null;
} elseif ($vat instanceof Vat) {
$vat->setSum($this->getSum());
$this->vat = $vat;
} else {
$this->vat = new Vat($vat, $this->getSum());
}
return $this;
}
/**
* Возвращает установленный объект атрибутов агента
*
* @return AgentInfo|null
*/
public function getAgentInfo(): ?AgentInfo
{
return $this->agentInfo;
}
/**
* Устанавливает атрибуты агента
*
* @param AgentInfo|null $agentInfo
* @return Item
*/
public function setAgentInfo(?AgentInfo $agentInfo): self
{
$this->agentInfo = $agentInfo;
return $this;
}
/**
* Возвращает установленного поставщика
*
* @return Supplier|null
*/
public function getSupplier(): ?Supplier
{
return $this->supplier;
}
/**
* Устанавливает поставщика
*
* @param Supplier|null $supplier
* @return Item
*/
public function setSupplier(?Supplier $supplier): self
{
$this->supplier = $supplier;
return $this;
}
/**
* Возвращает дополнительный реквизит
*
* @return string|null
*/
public function getUserData(): ?string
{
return $this->userData;
}
/**
* Устанавливает дополнительный реквизит
*
* @param string|null $userData Дополнительный реквизит
* @return $this
* @throws TooLongUserdataException
*/
public function setUserData(?string $userData): self
{
$userData = trim((string)$userData);
if (mb_strlen($userData) > Constraints::MAX_LENGTH_USER_DATA) {
throw new TooLongUserdataException($userData);
}
$this->userData = $userData ?: null;
return $this;
}
/**
* Возвращает установленную сумму акциза
*
* @return float|null
*/
public function getExcise(): ?float
{
return $this->excise;
}
/**
* Устанавливает сумму акциза
*
* @param float|null $excise
* @return Item
* @throws NegativeItemExciseException
* @throws TooHighItemSumException
*/
public function setExcise(?float $excise): self
{
if ($excise < 0) {
throw new NegativeItemExciseException($this->getName(), $excise);
}
$this->excise = $excise;
$this->getVat()?->setSum($this->getSum());
return $this;
}
/**
* Возвращает установленный код страны происхождения товара
*
* @return string|null
* @see https://ru.wikipedia.org/wiki/Общероссийский_классификатор_стран_мира
* @see https://classifikators.ru/oksm
*/
public function getCountryCode(): ?string
{
return $this->countryCode;
}
/**
* Устанавливает код страны происхождения товара
*
* @param string|null $countryCode
* @return Item
* @throws InvalidOKSMCodeException
* @see https://classifikators.ru/oksm
* @see https://ru.wikipedia.org/wiki/Общероссийский_классификатор_стран_мира
*/
public function setCountryCode(?string $countryCode): self
{
$countryCode = trim((string)$countryCode);
if (preg_match(Constraints::PATTERN_OKSM_CODE, $countryCode) != 1) {
throw new InvalidOKSMCodeException($countryCode);
}
$this->countryCode = $countryCode ?: null;
return $this;
}
/**
* Возвращает установленный код таможенной декларации
*
* @return string|null
*/
public function getDeclarationNumber(): ?string
{
return $this->declarationNumber;
}
/**
* Устанавливает код таможенной декларации
*
* @param string|null $declarationNumber
* @return Item
* @throws InvalidDeclarationNumberException
*/
public function setDeclarationNumber(?string $declarationNumber): self
{
if (is_string($declarationNumber)) {
$declarationNumber = trim($declarationNumber);
$is_short = mb_strlen($declarationNumber) < Constraints::MIN_LENGTH_DECLARATION_NUMBER;
$is_long = mb_strlen($declarationNumber) > Constraints::MAX_LENGTH_DECLARATION_NUMBER;
if ($is_short || $is_long) {
throw new InvalidDeclarationNumberException($declarationNumber);
}
}
$this->declarationNumber = $declarationNumber;
return $this;
}
/**
* @inheritDoc
* @throws TooHighItemSumException
*/
public function jsonSerialize(): array
{
$json = [
'name' => $this->getName(),
'price' => $this->getPrice(),
'quantity' => $this->getQuantity(),
'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();
$this->getVat()?->jsonSerialize() && $json['vat'] = $this->getVat()->jsonSerialize();
$this->getAgentInfo()?->jsonSerialize() && $json['agent_info'] = $this->getAgentInfo()->jsonSerialize();
$this->getSupplier()?->jsonSerialize() && $json['supplier_info'] = $this->getSupplier()->jsonSerialize();
!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();
return $json;
}
}

View File

@ -0,0 +1,139 @@
<?php
/*
* Copyright (c) 2020-2021 Антон Аксенов (Anthony Axenov)
*
* This code is licensed under MIT.
* Этот код распространяется по лицензии MIT.
* https://github.com/anthonyaxenov/atol-online/blob/master/LICENSE
*/
declare(strict_types=1);
namespace AtolOnline\Entities;
use AtolOnline\Exceptions\{
EmptyMonitorDataException,
NotEnoughMonitorDataException};
use DateTime;
use Exception;
/**
* Класс сущности ККТ, получаемой от монитора
*
* @see https://online.atol.ru/files/API_service_information.pdf Документация, стр 11
* @property-read string|null serialNumber Заводской номер ККТ
* @property-read string|null registrationNumber Регистрационный номер машины (РНМ)
* @property-read string|null deviceNumber Номер автоматического устройства (внутренний идентификатор устройства)
* @property-read DateTime|string|null fiscalizationDate Дата активации (фискализации) ФН с указанием таймзоны
* @property-read DateTime|string|null fiscalStorageExpiration Дата замены ФН (Срок действия ФН), с указанием таймзоны
* @property-read int|null signedDocuments Количество подписанных документов в ФН
* @property-read float|null fiscalStoragePercentageUse Наполненость ФН в %
* @property-read string|null fiscalStorageINN ИНН компании (указанный в ФН)
* @property-read string|null fiscalStorageSerialNumber Заводской (серийный) номер ФН
* @property-read string|null fiscalStoragePaymentAddress Адрес расчёта, указанный в ФН
* @property-read string|null groupCode Код группы кассы
* @property-read DateTime|string|null timestamp Время и дата формирования данных, UTC
* @property-read bool|null isShiftOpened Признак открыта смена (true) или закрыта (false)
* @property-read int|null shiftNumber Номер смены (или "Номер закрытой смены", когда смена закрыта)
* @property-read int|null shiftReceipt Номер документа за смену (или "Кол-во чеков закрытой смены", когда смена
* закрыта)
* @property-read int|null unsentDocs Количество неотправленных документов. Указывается, если значение отлично от 0.
* @property-read DateTime|string|null firstUnsetDocTimestamp Дата первого неотправленного документа. Указывается, если
* есть неотправленные документы.
* @property-read int|null networkErrorCode Код ошибки сети
*/
final class Kkt extends Entity
{
/**
* Сопоставление кодов сетевых ошибок ККТ с их описаниями
*/
public const ERROR_CODES = [
0 => 'Нет ошибок',
1 => 'Отсутствует физический канал связи',
2 => 'Ошибка сетевых настроек или нет соединения с сервером ОФД',
3 => 'Разрыв соединения при передаче документа на сервер',
4 => 'Некорректный заголовок сессионного пакета',
5 => 'Превышен таймаут ожидания квитанции',
6 => 'Разрыв соединения при приёме квитанции',
7 => 'Превышен таймаут передачи документа на сервер',
8 => 'ОФД-процесс не иницилизирован',
];
/**
* @var string[] Список обязательных атрибутов
*/
private array $properties = [
'serialNumber',
'registrationNumber',
'deviceNumber',
'fiscalizationDate',
'fiscalStorageExpiration',
'signedDocuments',
'fiscalStoragePercentageUse',
'fiscalStorageINN',
'fiscalStorageSerialNumber',
'fiscalStoragePaymentAddress',
'groupCode',
'timestamp',
'isShiftOpened',
'shiftNumber',
'shiftReceipt',
//'unsentDocs',
//'firstUnsetDocTimestamp',
'networkErrorCode',
];
/**
* @var string[] Массив атрибутов, которые кастуются к DateTime
*/
private array $timestamps = [
'fiscalizationDate',
'fiscalStorageExpiration',
'firstUnsetDocTimestamp',
'timestamp',
];
/**
* Конструктор
*
* @throws EmptyMonitorDataException
* @throws NotEnoughMonitorDataException
*/
public function __construct(protected object $data)
{
if (empty((array)$data)) {
throw new EmptyMonitorDataException();
}
$diff = array_diff($this->properties, array_keys((array)$data));
if (count($diff) !== 0) {
throw new NotEnoughMonitorDataException($diff);
}
}
/**
* Эмулирует обращение к атрибутам
*
* @param string $name
* @return null
* @throws Exception
*/
public function __get(string $name)
{
if (empty($this->data?->$name)) {
return null;
}
if (in_array($name, $this->timestamps)) {
return new DateTime($this->data->$name);
}
return $this->data->$name;
}
/**
* @inheritDoc
*/
public function jsonSerialize(): array
{
return (array)$this->data;
}
}

View File

@ -0,0 +1,133 @@
<?php
/*
* Copyright (c) 2020-2021 Антон Аксенов (Anthony Axenov)
*
* This code is licensed under MIT.
* Этот код распространяется по лицензии MIT.
* https://github.com/anthonyaxenov/atol-online/blob/master/LICENSE
*/
declare(strict_types=1);
namespace AtolOnline\Entities;
use AtolOnline\Exceptions\{
InvalidInnLengthException,
InvalidPhoneException};
use AtolOnline\Traits\{
HasInn,
HasPhones};
use Illuminate\Support\Collection;
/**
* Класс, описывающий оператора перевода
*
* @see https://online.atol.ru/files/API_atol_online_v4.pdf Документация, стр 28
*/
final class MoneyTransferOperator extends Entity
{
use HasInn;
use HasPhones;
/**
* @var string|null Наименование (1026)
*/
protected ?string $name = null;
/**
* @var string|null ИНН (1016)
*/
protected ?string $inn = null;
/**
* @var string|null Адрес (1005)
*/
protected ?string $address = null;
/**
* @var Collection Телефоны (1075)
*/
protected Collection $phones;
/**
* Конструктор
*
* @param string|null $name Наименование поставщика (1225)
* @param string|null $inn ИНН (1226)
* @param string|null $address Адрес (1005)
* @param array|Collection|null $phones Телефоны поставщика (1171)
* @throws InvalidInnLengthException
* @throws InvalidPhoneException
*/
public function __construct(
?string $name = null,
?string $inn = null,
?string $address = null,
array | Collection | null $phones = null,
) {
$this->setName($name);
$this->setInn($inn);
$this->setAddress($address);
$this->setPhones($phones);
}
/**
* Возвращает установленное наименование поставщика
*
* @return string|null
*/
public function getName(): ?string
{
return $this->name;
}
/**
* Устанавливает наименование поставщика
*
* @param string|null $name
* @return $this
*/
public function setName(?string $name): self
{
// критерии валидной строки не описаны ни в схеме, ни в документации
$this->name = trim((string)$name) ?: null;
return $this;
}
/**
* Возвращает установленный адрес места расчётов
*
* @return string|null
*/
public function getAddress(): ?string
{
return $this->address;
}
/**
* Устанавливает адрес места расчётов
*
* @param string|null $address
* @return $this
*/
public function setAddress(?string $address): self
{
// критерии валидной строки не описаны ни в схеме, ни в документации
$this->address = trim((string)$address) ?: null;
return $this;
}
/**
* @inheritDoc
*/
public function jsonSerialize(): array
{
$json = [];
$this->getName() && $json['name'] = $this->getName();
$this->getInn() && $json['inn'] = $this->getInn();
$this->getAddress() && $json['address'] = $this->getAddress();
!$this->getPhones()->isEmpty() && $json['phones'] = $this->getPhones()->toArray();
return $json;
}
}

View File

@ -0,0 +1,91 @@
<?php
/*
* Copyright (c) 2020-2021 Антон Аксенов (Anthony Axenov)
*
* This code is licensed under MIT.
* Этот код распространяется по лицензии MIT.
* https://github.com/anthonyaxenov/atol-online/blob/master/LICENSE
*/
declare(strict_types=1);
namespace AtolOnline\Entities;
use AtolOnline\Constraints;
use AtolOnline\Exceptions\{
InvalidPhoneException,
TooLongPayingAgentOperationException};
use AtolOnline\Traits\HasPhones;
use Illuminate\Support\Collection;
/**
* Класс, описывающий платёжного агента
*
* @see https://online.atol.ru/files/API_atol_online_v4.pdf Документация, стр 19
*/
final class PayingAgent extends Entity
{
use HasPhones;
/**
* @var string|null Наименование операции (1044)
*/
protected ?string $operation = null;
/**
* Конструктор
*
* @param string|null $operation Наименование операции (1044)
* @param array|Collection|null $phones Телефоны платёжного агента (1073)
* @throws TooLongPayingAgentOperationException
* @throws InvalidPhoneException
*/
public function __construct(
?string $operation = null,
array | Collection | null $phones = null,
) {
!is_null($operation) && $this->setOperation($operation);
$this->setPhones($phones);
}
/**
* Устанавливает операцию
*
* @param string|null $operation
* @return $this
* @throws TooLongPayingAgentOperationException
*/
public function setOperation(?string $operation): self
{
if (!is_null($operation)) {
$operation = trim($operation);
if (mb_strlen($operation) > Constraints::MAX_LENGTH_PAYING_AGENT_OPERATION) {
throw new TooLongPayingAgentOperationException($operation);
}
}
$this->operation = $operation ?: null;
return $this;
}
/**
* Вoзвращает установленную операцию
*
* @return string|null
*/
public function getOperation(): ?string
{
return $this->operation;
}
/**
* @inheritDoc
*/
public function jsonSerialize(): array
{
$json = [];
$this->getOperation() && $json['operation'] = $this->getOperation();
!$this->getPhones()->isEmpty() && $json['phones'] = $this->getPhones()->toArray();
return $json;
}
}

View File

@ -0,0 +1,108 @@
<?php
/*
* Copyright (c) 2020-2021 Антон Аксенов (Anthony Axenov)
*
* This code is licensed under MIT.
* Этот код распространяется по лицензии MIT.
* https://github.com/anthonyaxenov/atol-online/blob/master/LICENSE
*/
declare(strict_types=1);
namespace AtolOnline\Entities;
use AtolOnline\{
Constraints,
Enums\PaymentType,};
use AtolOnline\Exceptions\{
NegativePaymentSumException,
TooHighPaymentSumException,};
use JetBrains\PhpStorm\{
ArrayShape,
Pure};
/**
* Класс, описывающий оплату
*
* @see https://online.atol.ru/files/API_atol_online_v4.pdf Документация, стр 30
*/
final class Payment extends Entity
{
/**
* Конструктор
*
* @param PaymentType $type Тип оплаты
* @param float $sum Сумма оплаты (1031, 1081, 1215, 1216, 1217)
* @throws NegativePaymentSumException
* @throws TooHighPaymentSumException
*/
public function __construct(
protected PaymentType $type,
protected float $sum,
) {
$this->setType($type)->setSum($sum);
}
/**
* Возвращает установленный тип оплаты
*
* @return PaymentType
*/
public function getType(): PaymentType
{
return $this->type;
}
/**
* Устанавливает тип оплаты
*
* @param PaymentType $type
* @return $this
*/
public function setType(PaymentType $type): self
{
$this->type = $type;
return $this;
}
/**
* Возвращает установленную сумму оплаты
*
* @return float
*/
public function getSum(): float
{
return $this->sum;
}
/**
* Устанавливает сумму оплаты
*
* @param float $sum
* @return $this
* @throws TooHighPaymentSumException
* @throws NegativePaymentSumException
*/
public function setSum(float $sum): self
{
$sum = round($sum, 2);
$sum > Constraints::MAX_COUNT_PAYMENT_SUM && throw new TooHighPaymentSumException($sum);
$sum < 0 && throw new NegativePaymentSumException($sum);
$this->sum = $sum;
return $this;
}
/**
* @inheritDoc
*/
#[Pure]
#[ArrayShape(['type' => 'int', 'sum' => 'float'])]
public function jsonSerialize(): array
{
return [
'type' => $this->getType(),
'sum' => $this->getSum(),
];
}
}

View File

@ -0,0 +1,481 @@
<?php
/*
* Copyright (c) 2020-2021 Антон Аксенов (Anthony Axenov)
*
* This code is licensed under MIT.
* Этот код распространяется по лицензии MIT.
* https://github.com/anthonyaxenov/atol-online/blob/master/LICENSE
*/
declare(strict_types=1);
namespace AtolOnline\Entities;
use AtolOnline\Api\AtolResponse;
use AtolOnline\Api\Fiscalizer;
use AtolOnline\Collections\Items;
use AtolOnline\Collections\Payments;
use AtolOnline\Collections\Vats;
use AtolOnline\Constraints;
use AtolOnline\Exceptions\AuthFailedException;
use AtolOnline\Exceptions\EmptyItemsException;
use AtolOnline\Exceptions\EmptyLoginException;
use AtolOnline\Exceptions\EmptyPasswordException;
use AtolOnline\Exceptions\InvalidEntityInCollectionException;
use AtolOnline\Exceptions\InvalidInnLengthException;
use AtolOnline\Exceptions\InvalidPaymentAddressException;
use AtolOnline\Exceptions\TooLongAddCheckPropException;
use AtolOnline\Exceptions\TooLongCashierException;
use AtolOnline\Exceptions\TooLongPaymentAddressException;
use Exception;
use GuzzleHttp\Exception\GuzzleException;
/**
* Класс, описывающий документ прихода, расхода, возврата прихода, возврата расхода
*
* @see https://online.atol.ru/files/API_atol_online_v4.pdf Документация, стр 17
*/
final class Receipt extends Entity
{
/**
* Тип документа
*/
public const DOC_TYPE = 'receipt';
/**
* @var Client Покупатель
*/
protected Client $client;
/**
* @todo вынести в трейт?
* @var Company Продавец
*/
protected Company $company;
/**
* @var AgentInfo|null Агент
*/
protected ?AgentInfo $agent_info = null;
/**
* @var Supplier|null Поставщик
*/
protected ?Supplier $supplier = null;
/**
* @var Items Коллекция предметов расчёта
*/
protected Items $items;
/**
* @todo вынести в трейт?
* @var Payments Коллекция оплат
*/
protected Payments $payments;
/**
* @var Vats|null Коллекция ставок НДС
*/
protected ?Vats $vats = null;
/**
* @var float Итоговая сумма чека
*/
protected float $total = 0;
/**
* @todo вынести в трейт?
* @var string|null ФИО кассира
*/
protected ?string $cashier = null;
/**
* @var string|null Дополнительный реквизит
*/
protected ?string $add_check_props = null;
/**
* @var AdditionalUserProps|null Дополнительный реквизит пользователя
*/
protected ?AdditionalUserProps $add_user_props = null;
/**
* Конструктор
*
* @param Client $client
* @param Company $company
* @param Items $items
* @param Payments $payments
* @throws EmptyItemsException
* @throws InvalidEntityInCollectionException
*/
public function __construct(Client $client, Company $company, Items $items, Payments $payments)
{
$this->setClient($client)->setCompany($company)->setItems($items)->setPayments($payments);
}
/**
* Возвращает установленного покупателя
*
* @return Client
*/
public function getClient(): Client
{
return $this->client;
}
/**
* Устанаваливает покупателя
*
* @param Client $client
* @return $this
*/
public function setClient(Client $client): self
{
$this->client = $client;
return $this;
}
/**
* Возвращает установленного продавца
*
* @return Company
*/
public function getCompany(): Company
{
return $this->company;
}
/**
* Устанаваливает продавца
*
* @param Company $company
* @return $this
*/
public function setCompany(Company $company): self
{
$this->company = $company;
return $this;
}
/**
* Возвращает установленного агента
*
* @return AgentInfo|null
*/
public function getAgentInfo(): ?AgentInfo
{
return $this->agent_info;
}
/**
* Устанаваливает агента
*
* @param AgentInfo|null $agent_info
* @return $this
*/
public function setAgentInfo(?AgentInfo $agent_info): self
{
$this->agent_info = $agent_info;
return $this;
}
/**
* Поставщика
*
* @return Supplier|null
*/
public function getSupplier(): ?Supplier
{
return $this->supplier;
}
/**
* Поставщика
*
* @param Supplier|null $supplier
* @return $this
*/
public function setSupplier(?Supplier $supplier): self
{
$this->supplier = $supplier;
return $this;
}
/**
* Возвращает установленную коллекцию предметов расчёта
*
* @return Items
*/
public function getItems(): Items
{
return $this->items ?? new Items();
}
/**
* Устанаваливает коллекцию предметов расчёта
*
* @param Items $items
* @return $this
* @throws InvalidEntityInCollectionException
* @throws Exception
* @throws EmptyItemsException
*/
public function setItems(Items $items): self
{
$items->checkCount();
$items->checkItemsClasses();
$this->items = $items;
$this->getItems()->each(fn ($item) => $this->total += $item->getSum());
$this->total = round($this->total, 2);
return $this;
}
/**
* Возвращает установленную коллекцию оплат
*
* @return Payments
*/
public function getPayments(): Payments
{
return $this->payments;
}
/**
* Устанаваливает коллекцию оплат
*
* @param Payments $payments
* @return $this
* @throws InvalidEntityInCollectionException
*/
public function setPayments(Payments $payments): self
{
$payments->checkCount();
$payments->checkItemsClasses();
$this->payments = $payments;
return $this;
}
/**
* Возвращает установленную коллекцию ставок НДС
*
* @return Vats|null
*/
public function getVats(): ?Vats
{
return $this->vats ?? new Vats();
}
/**
* Устанаваливает коллекцию ставок НДС
*
* @param Vats|null $vats
* @return $this
* @throws Exception
*/
public function setVats(?Vats $vats): self
{
$vats->checkCount();
$vats->checkItemsClasses();
$this->vats = $vats;
/** @var Vat $vat */
$this->getVats()->each(fn ($vat) => $vat->setSum($this->getTotal()));
return $this;
}
/**
* Возвращает полную сумму чека
*
* @return float
*/
public function getTotal(): float
{
return $this->total;
}
/**
* Возвращает установленного кассира
*
* @return string|null
*/
public function getCashier(): ?string
{
return $this->cashier;
}
/**
* Устанаваливает кассира
*
* @param string|null $cashier
* @return $this
* @throws TooLongCashierException
*/
public function setCashier(?string $cashier): self
{
if (is_string($cashier)) {
$cashier = trim($cashier);
if (mb_strlen($cashier) > Constraints::MAX_LENGTH_CASHIER_NAME) {
throw new TooLongCashierException($cashier);
}
}
$this->cashier = $cashier ?: null;
return $this;
}
/**
* Возвращает установленный дополнительный реквизит чека
*
* @return string|null
*/
public function getAddCheckProps(): ?string
{
return $this->add_check_props;
}
/**
* Устанаваливает дополнительный реквизит чека
*
* @param string|null $add_check_props
* @return $this
* @throws TooLongAddCheckPropException
*/
public function setAddCheckProps(?string $add_check_props): self
{
if (is_string($add_check_props)) {
$add_check_props = trim($add_check_props);
if (mb_strlen($add_check_props) > Constraints::MAX_LENGTH_ADD_CHECK_PROP) {
throw new TooLongAddCheckPropException($add_check_props);
}
}
$this->add_check_props = $add_check_props ?: null;
return $this;
}
/**
* Возвращает установленный дополнительный реквизит пользователя
*
* @return AdditionalUserProps|null
*/
public function getAddUserProps(): ?AdditionalUserProps
{
return $this->add_user_props;
}
/**
* Устанаваливает дополнительный реквизит пользователя
*
* @param AdditionalUserProps|null $add_user_props
* @return $this
*/
public function setAddUserProps(?AdditionalUserProps $add_user_props): self
{
$this->add_user_props = $add_user_props;
return $this;
}
/**
* Регистрирует приход по текущему документу
*
* @param Fiscalizer $fiscalizer Объект фискализатора
* @param string|null $external_id Уникальный код документа (если не указан, то будет создан новый UUID)
* @return AtolResponse|null
* @throws AuthFailedException
* @throws EmptyLoginException
* @throws EmptyPasswordException
* @throws GuzzleException
* @throws InvalidEntityInCollectionException
* @throws InvalidInnLengthException
* @throws InvalidPaymentAddressException
* @throws TooLongPaymentAddressException
*/
public function sell(Fiscalizer $fiscalizer, ?string $external_id = null): ?AtolResponse
{
return $fiscalizer->sell($this, $external_id);
}
/**
* Регистрирует возврат прихода по текущему документу
*
* @param Fiscalizer $fiscalizer Объект фискализатора
* @param string|null $external_id Уникальный код документа (если не указан, то будет создан новый UUID)
* @return AtolResponse|null
* @throws AuthFailedException
* @throws EmptyLoginException
* @throws EmptyPasswordException
* @throws GuzzleException
* @throws InvalidEntityInCollectionException
* @throws InvalidInnLengthException
* @throws InvalidPaymentAddressException
* @throws TooLongPaymentAddressException
*/
public function sellRefund(Fiscalizer $fiscalizer, ?string $external_id = null): ?AtolResponse
{
return $fiscalizer->sellRefund($this, $external_id);
}
/**
* Регистрирует расход по текущему документу
*
* @param Fiscalizer $fiscalizer Объект фискализатора
* @param string|null $external_id Уникальный код документа (если не указан, то будет создан новый UUID)
* @return AtolResponse|null
* @throws AuthFailedException
* @throws EmptyLoginException
* @throws EmptyPasswordException
* @throws GuzzleException
* @throws InvalidEntityInCollectionException
* @throws InvalidInnLengthException
* @throws InvalidPaymentAddressException
* @throws TooLongPaymentAddressException
*/
public function buy(Fiscalizer $fiscalizer, ?string $external_id = null): ?AtolResponse
{
return $fiscalizer->buy($this, $external_id);
}
/**
* Регистрирует возврат расхода по текущему документу
*
* @param Fiscalizer $fiscalizer Объект фискализатора
* @param string|null $external_id Уникальный код документа (если не указан, то будет создан новый UUID)
* @return AtolResponse|null
* @throws AuthFailedException
* @throws EmptyLoginException
* @throws EmptyPasswordException
* @throws GuzzleException
* @throws InvalidEntityInCollectionException
* @throws InvalidInnLengthException
* @throws InvalidPaymentAddressException
* @throws TooLongPaymentAddressException
*/
public function buyRefund(Fiscalizer $fiscalizer, ?string $external_id = null): ?AtolResponse
{
return $fiscalizer->buyRefund($this, $external_id);
}
/**
* Возвращает массив для кодирования в json
*
* @throws Exception
*/
public function jsonSerialize(): array
{
$json = [
'client' => $this->getClient()->jsonSerialize(),
'company' => $this->getCompany()->jsonSerialize(),
'items' => $this->getItems()->jsonSerialize(),
'total' => $this->getTotal(),
'payments' => $this->getPayments()->jsonSerialize(),
];
$this->getAgentInfo()?->jsonSerialize() && $json['agent_info'] = $this->getAgentInfo()->jsonSerialize();
$this->getSupplier()?->jsonSerialize() && $json['supplier_info'] = $this->getSupplier()->jsonSerialize();
$this->getVats()?->isNotEmpty() && $json['vats'] = $this->getVats();
!is_null($this->getAddCheckProps()) && $json['additional_check_props'] = $this->getAddCheckProps();
!is_null($this->getCashier()) && $json['cashier'] = $this->getCashier();
$this->getAddUserProps()?->jsonSerialize() &&
$json['additional_user_props'] = $this->getAddUserProps()->jsonSerialize();
return $json;
}
}

View File

@ -0,0 +1,48 @@
<?php
/*
* Copyright (c) 2020-2021 Антон Аксенов (Anthony Axenov)
*
* This code is licensed under MIT.
* Этот код распространяется по лицензии MIT.
* https://github.com/anthonyaxenov/atol-online/blob/master/LICENSE
*/
declare(strict_types=1);
namespace AtolOnline\Entities;
use AtolOnline\Exceptions\InvalidPhoneException;
use AtolOnline\Traits\HasPhones;
use Illuminate\Support\Collection;
/**
* Класс, описывающий оператора по приёму платежей
*
* @see https://online.atol.ru/files/API_atol_online_v4.pdf Документация, стр 19-20
*/
final class ReceivePaymentsOperator extends Entity
{
use HasPhones;
/**
* Конструктор
*
* @param array|Collection|null $phones Телефоны оператора по приёму платежей (1074)
* @throws InvalidPhoneException
*/
public function __construct(array | Collection | null $phones = null)
{
$this->setPhones($phones);
}
/**
* @inheritDoc
*/
public function jsonSerialize(): array
{
$json = [];
!$this->getPhones()->isEmpty() && $json['phones'] = $this->getPhones()->toArray();
return $json;
}
}

View File

@ -0,0 +1,91 @@
<?php
/*
* Copyright (c) 2020-2021 Антон Аксенов (Anthony Axenov)
*
* This code is licensed under MIT.
* Этот код распространяется по лицензии MIT.
* https://github.com/anthonyaxenov/atol-online/blob/master/LICENSE
*/
declare(strict_types=1);
namespace AtolOnline\Entities;
use AtolOnline\Exceptions\{
InvalidInnLengthException,
InvalidPhoneException};
use AtolOnline\Traits\{
HasInn,
HasPhones};
use Illuminate\Support\Collection;
/**
* Класс, описывающий поставшика
*
* @see https://online.atol.ru/files/API_atol_online_v4.pdf Документация, стр 29
*/
final class Supplier extends Entity
{
use HasPhones;
use HasInn;
/**
* @var string|null Наименование (1225)
*/
protected ?string $name = null;
/**
* Конструктор
*
* @param string|null $name Наименование поставщика (1225)
* @param string|null $inn ИНН (1226)
* @param array|Collection|null $phones Телефоны поставщика (1171)
* @throws InvalidInnLengthException
* @throws InvalidPhoneException
*/
public function __construct(
?string $name = null,
?string $inn = null,
array | Collection | null $phones = null,
) {
!is_null($name) && $this->setName($name);
!is_null($inn) && $this->setInn($inn);
$this->setPhones($phones);
}
/**
* Возвращает установленное наименование поставщика
*
* @return string|null
*/
public function getName(): ?string
{
return $this->name;
}
/**
* Устанавливает наименование поставщика
*
* @param string|null $name
* @return Supplier
*/
public function setName(?string $name): self
{
// критерии к длине строки не описаны ни в схеме, ни в документации
$this->name = trim($name) ?: null;
return $this;
}
/**
* @inheritDoc
*/
public function jsonSerialize(): array
{
$json = [];
$this->getName() && $json['name'] = $this->getName();
$this->getInn() && $json['inn'] = $this->getInn();
!$this->getPhones()->isEmpty() && $json['phones'] = $this->getPhones()->toArray();
return $json;
}
}

View File

@ -0,0 +1,135 @@
<?php
/*
* Copyright (c) 2020-2021 Антон Аксенов (Anthony Axenov)
*
* This code is licensed under MIT.
* Этот код распространяется по лицензии MIT.
* https://github.com/anthonyaxenov/atol-online/blob/master/LICENSE
*/
declare(strict_types=1);
namespace AtolOnline\Entities;
use AtolOnline\Enums\VatType;
use AtolOnline\Helpers;
use JetBrains\PhpStorm\{
ArrayShape,
Pure};
/**
* Класс, описывающий ставку НДС
*
* @see https://online.atol.ru/files/API_atol_online_v4.pdf Документация, стр 25, 31
*/
final class Vat extends Entity
{
/**
* Конструктор
*
* @param VatType $type Тип ставки НДС (1199, 1105, 1104, 1103, 1102, 1107, 1106)
* @param float $sum Исходная сумма в рублях, от которой нужно расчитать размер НДС
*/
public function __construct(
protected VatType $type,
protected float $sum,
) {
$this->setType($type)->setSum($sum);
}
/**
* Устанавливает тип ставки НДС
* Автоматически пересчитывает итоговый размер НДС от исходной суммы.
*
* @param VatType $type Тип ставки НДС
* @return $this
*/
public function setType(VatType $type): self
{
$this->type = $type;
return $this;
}
/**
* Возвращает тип ставки НДС
*
* @return VatType
*/
public function getType(): VatType
{
return $this->type;
}
/**
* Возвращает исходную сумму, от которой расчитывается размер налога
*
* @return float
*/
public function getSum(): float
{
return $this->sum;
}
/**
* Устанавливает исходную сумму, от которой будет расчитываться итоговый размер НДС.
* Автоматически пересчитывает итоговый размер НДС от исходной суммы.
*
* @param float $rubles Сумма в рублях за предмет расчёта, из которой высчитывается размер НДС
* @return $this
*/
public function setSum(float $rubles): self
{
$this->sum = round($rubles, 2);
return $this;
}
/**
* Возвращает расчитанный итоговый размер ставки НДС в рублях
*
* @return float
* @see https://nalog-nalog.ru/nds/nalogovaya_baza_nds/kak-schitat-nds-pravilno-vychislyaem-20-ot-summy-primer-algoritm/
* @see https://glavkniga.ru/situations/k500734
* @see https://www.b-kontur.ru/nds-kalkuljator-online
*/
#[Pure]
public function getCalculated(): float
{
return Helpers::toRub(
match ($this->getType()) {
VatType::VAT10 => Helpers::toKop($this->sum) * 10 / 100,
VatType::VAT18 => Helpers::toKop($this->sum) * 18 / 100,
VatType::VAT20 => Helpers::toKop($this->sum) * 20 / 100,
VatType::VAT110 => Helpers::toKop($this->sum) * 10 / 110,
VatType::VAT118 => Helpers::toKop($this->sum) * 18 / 118,
VatType::VAT120 => Helpers::toKop($this->sum) * 20 / 120,
default => 0,
}
);
}
/**
* Прибавляет указанную сумму к исходной
*
* @param float $rubles
* @return $this
*/
public function addSum(float $rubles): self
{
$this->sum += $rubles;
return $this;
}
/**
* @inheritDoc
*/
#[Pure]
#[ArrayShape(['type' => 'string', 'sum' => 'float'])]
public function jsonSerialize(): array
{
return [
'type' => $this->getType(),
'sum' => $this->getCalculated(),
];
}
}

View File

@ -0,0 +1,56 @@
<?php
/*
* Copyright (c) 2020-2021 Антон Аксенов (Anthony Axenov)
*
* This code is licensed under MIT.
* Этот код распространяется по лицензии MIT.
* https://github.com/anthonyaxenov/atol-online/blob/master/LICENSE
*/
declare(strict_types=1);
namespace AtolOnline\Enums;
/**
* Константы, определяющие типы агента
*
* @see https://online.atol.ru/files/API_atol_online_v4.pdf Документация, стр 18, 26
*/
enum AgentType: string
{
/**
* Банковский платёжный агент
*/
case BANK_PAYING_AGENT = 'bank_paying_agent';
/**
* Банковский платёжный субагент
*/
case BANK_PAYING_SUBAGENT = 'bank_paying_subagent';
/**
* Платёжный агент
*/
case PAYING_AGENT = 'payingAgent';
/**
* Платёжный субагент
*/
case PAYING_SUBAGENT = 'paying_subagent';
/**
* Поверенный
*/
case ATTRONEY = 'attorney';
/**
* Комиссионер
*/
case COMMISSION_AGENT = 'commission_agent';
/**
* Другой тип агента
*/
case ANOTHER = 'another';
}

View File

@ -1,28 +1,31 @@
<?php
/**
* Copyright (c) Антон Аксенов (aka Anthony Axenov)
/*
* Copyright (c) 2020-2021 Антон Аксенов (Anthony Axenov)
*
* This code is licensed under MIT.
* Этот код распространяется по лицензии MIT.
* https://github.com/anthonyaxenov/atol-online/blob/master/LICENSE
*/
namespace AtolOnline\Constants;
declare(strict_types=1);
namespace AtolOnline\Enums;
/**
* Константы, определяющие типы документов коррекции
*
* @package AtolOnline\Constants
* @see https://online.atol.ru/files/API_atol_online_v4.pdf Документация, стр 35
*/
class CorrectionTypes
enum CorrectionType: string
{
/**
* Самостоятельно
*/
const SELF = 'self';
case SELF = 'self';
/**
* По предписанию
*/
const INSTRUCTION = 'instruction';
}
case INSTRUCTION = 'instruction';
}

View File

@ -1,54 +1,56 @@
<?php
/**
* Copyright (c) Антон Аксенов (aka Anthony Axenov)
/*
* Copyright (c) 2020-2021 Антон Аксенов (Anthony Axenov)
*
* This code is licensed under MIT.
* Этот код распространяется по лицензии MIT.
* https://github.com/anthonyaxenov/atol-online/blob/master/LICENSE
*/
namespace AtolOnline\Constants;
declare(strict_types=1);
namespace AtolOnline\Enums;
/**
* Константы, определяющие признаки способов расчёта. Тег ФФД - 1214.
* Константы, определяющие признаки способов расчёта
*
* @package AtolOnline\Constants
* @see https://online.atol.ru/files/API_atol_online_v4.pdf Документация, стр 22
*/
class PaymentMethods
enum PaymentMethod: string
{
/**
* Предоплата 100% до передачи предмета расчёта
*/
const FULL_PREPAYMENT = 'full_prepayment';
case FULL_PREPAYMENT = 'full_prepayment';
/**
* Частичная предоплата до передачи предмета расчёта
*/
const PREPAYMENT = 'prepayment';
case PREPAYMENT = 'prepayment';
/**
* Аванс
*/
const ADVANCE = 'advance';
case ADVANCE = 'advance';
/**
* Полная оплата с учётом аванса/предоплаты в момент передачи предмета расчёта
*/
const FULL_PAYMENT = 'full_payment';
case FULL_PAYMENT = 'full_payment';
/**
* Частичный расчёт в момент передачи предмета расчёта (дальнейшая оплата в кредит)
*/
const PARTIAL_PAYMENT = 'partial_payment';
case PARTIAL_PAYMENT = 'partial_payment';
/**
* Передача предмета расчёта в кредит
*/
const CREDIT = 'credit';
case CREDIT = 'credit';
/**
* Оплата кредита
*/
const CREDIT_PAYMENT = 'credit_payment';
}
case CREDIT_PAYMENT = 'credit_payment';
}

View File

@ -0,0 +1,159 @@
<?php
/*
* Copyright (c) 2020-2021 Антон Аксенов (Anthony Axenov)
*
* This code is licensed under MIT.
* Этот код распространяется по лицензии MIT.
* https://github.com/anthonyaxenov/atol-online/blob/master/LICENSE
*/
declare(strict_types=1);
namespace AtolOnline\Enums;
/**
* Константы, определяющие признаки предметов расчёта
*
* @see https://online.atol.ru/files/API_atol_online_v4.pdf Документация, стр 23
*/
enum PaymentObject: string
{
/**
* Товар, кроме подакцизного
*/
case COMMODITY = 'commodity';
/**
* Товар подакцизный
*/
case EXCISE = 'excise';
/**
* Работа
*/
case JOB = 'job';
/**
* Услуга
*/
case SERVICE = 'service';
/**
* Ставка азартной игры
*/
case GAMBLING_BET = 'gambling_bet';
/**
* Выигрыш азартной игры
*/
case GAMBLING_PRIZE = 'gambling_prize';
/**
* Лотерея
*/
case LOTTERY = 'lottery';
/**
* Выигрыш лотереи
*/
case LOTTERY_PRIZE = 'lottery_prize';
/**
* Предоставление результатов интеллектуальной деятельности
*/
case INTELLECTUAL_ACTIVITY = 'intellectual_activity';
/**
* Платёж (задаток, кредит, аванс, предоплата, пеня, штраф, бонус и пр.)
*/
case PAYMENT = 'payment';
/**
* Агентское вознаграждение
*/
case AGENT_COMMISSION = 'agent_commission';
/**
* Составной предмет расчёта
*
* @deprecated Более не используется согласно ФФД 1.05
* @see https://online.atol.ru/files/API_atol_online_v4.pdf Документация, стр 25 (payment_object)
*/
case COMPOSITE = 'composite';
/**
* Другой предмет расчёта
*/
case ANOTHER = 'another';
/**
* Имущественное право
*/
case PROPERTY_RIGHT = 'property_right';
/**
* Внереализационный доход
*/
case NON_OPERATING_GAIN = 'non-operating_gain';
/**
* Страховые взносы
*/
case INSURANCE_PREMIUM = 'insurance_premium';
/**
* Торговый сбор
*/
case SALES_TAX = 'sales_tax';
/**
* Курортный сбор
*/
case RESORT_FEE = 'resort_fee';
/**
* Взнос в счёт оплаты пени, штрафе, вознаграждении, бонусе и ином аналогичном предмете расчёта
*/
case AWARD = 'award';
/**
* Залог
*/
case DEPOSIT = 'deposit';
/**
* Расход, уменьшающий доход (в соответствии со статьей 346.16 НК РФ)
*/
case EXPENSE = 'expense';
/**
* Взнос на ОПС ИП
*/
case PEN_INSURANCE_IP = 'pension_insurance_ip';
/**
* Взнос на ОПС
*/
case PEN_INSURANCE = 'pension_insurance';
/**
* Взнос на ОМС ИП
*/
case MED_INSURANCE_IP = 'medical_insurance_ip';
/**
* Взнос на ОМС
*/
case MED_INSURANCE = 'medical_insurance';
/**
* Взнос на ОСС
*/
case SOC_INSURANCE = 'social_insurance';
/**
* Платёж казино
*/
case CASINO_PAYMENT = 'casino_payment';
}

View File

@ -0,0 +1,76 @@
<?php
/*
* Copyright (c) 2020-2021 Антон Аксенов (Anthony Axenov)
*
* This code is licensed under MIT.
* Этот код распространяется по лицензии MIT.
* https://github.com/anthonyaxenov/atol-online/blob/master/LICENSE
*/
declare(strict_types=1);
namespace AtolOnline\Enums;
/**
* Константы, определяющие виды оплат
*
* @see https://online.atol.ru/files/API_atol_online_v4.pdf Документация, стр 35
*/
enum PaymentType: int
{
/**
* Расчёт наличными
*/
case CASH = 0;
/**
* Расчёт безналичными
*/
case ELECTRON = 1;
/**
* Предварительная оплата (зачёт аванса)
*/
case PREPAID = 2;
/**
* Предварительная оплата (кредит)
*/
case CREDIT = 3;
/**
* Иная форма оплаты (встречное предоставление)
*/
case OTHER = 4;
/**
* Расширенный типы оплаты (5)
* Для каждого фискального типа оплаты можно указать расширенный тип оплаты
*/
case ADD_5 = 5;
/**
* Расширенный типы оплаты (6)
* Для каждого фискального типа оплаты можно указать расширенный тип оплаты
*/
case ADD_6 = 6;
/**
* Расширенный типы оплаты (7)
* Для каждого фискального типа оплаты можно указать расширенный тип оплаты
*/
case ADD_7 = 7;
/**
* Расширенный типы оплаты (8)
* Для каждого фискального типа оплаты можно указать расширенный тип оплаты
*/
case ADD_8 = 8;
/**
* Расширенный типы оплаты (9)
* Для каждого фискального типа оплаты можно указать расширенный тип оплаты
*/
case ADD_9 = 9;
}

View File

@ -1,48 +1,49 @@
<?php
/**
* Copyright (c) Антон Аксенов (aka Anthony Axenov)
/*
* Copyright (c) 2020-2021 Антон Аксенов (Anthony Axenov)
*
* This code is licensed under MIT.
* Этот код распространяется по лицензии MIT.
* https://github.com/anthonyaxenov/atol-online/blob/master/LICENSE
*/
namespace AtolOnline\Constants;
declare(strict_types=1);
namespace AtolOnline\Enums;
/**
* Константы, определяющие типы операций (чеков)
*
* @package AtolOnline\Constants
*/
class ReceiptOperationTypes
enum ReceiptOperationType: string
{
/**
* Приход (мы продали)
*/
const SELL = 'sell';
case SELL = 'sell';
/**
* Возврат прихода (нам вернули предмет расчёта, мы вернули деньги)
* Возврат прихода (нам вернули предмет расчёта, мы вернули средства)
*/
const SELL_REFUND = 'sell_refund';
case SELL_REFUND = 'sell_refund';
/**
* Коррекция прихода
*/
const SELL_CORRECTION = 'sell_correction';
case SELL_CORRECTION = 'sell_correction';
/**
* Расход (мы купили)
*/
const BUY = 'buy';
case BUY = 'buy';
/**
* Возврат расхода (мы вернули предмет расчёта, нам вернули деньги)
* Возврат расхода (мы вернули предмет расчёта, нам вернули средства)
*/
const BUY_REFUND = 'buy_refund';
case BUY_REFUND = 'buy_refund';
/**
* Коррекция прихода
* Коррекция прихода (догоняем неучтённые средства)
*/
const BUY_CORRECTION = 'buy_correction';
}
case BUY_CORRECTION = 'buy_correction';
}

View File

@ -1,48 +1,51 @@
<?php
/**
* Copyright (c) Антон Аксенов (aka Anthony Axenov)
/*
* Copyright (c) 2020-2021 Антон Аксенов (Anthony Axenov)
*
* This code is licensed under MIT.
* Этот код распространяется по лицензии MIT.
* https://github.com/anthonyaxenov/atol-online/blob/master/LICENSE
*/
namespace AtolOnline\Constants;
declare(strict_types=1);
namespace AtolOnline\Enums;
/**
* Константы, определяющие типы налогообложения
*
* @package AtolOnline\Constants
* @see https://online.atol.ru/files/API_atol_online_v4.pdf Документация, стр 35
*/
class SnoTypes
enum SnoType: string
{
/**
* Общая СН
*/
const OSN = 'osn';
case OSN = 'osn';
/**
* Упрощенная СН (доходы)
*/
const USN_INCOME = 'usn_income';
case USN_INCOME = 'usn_income';
/**
* Упрощенная СН (доходы минус расходы)
*/
const USN_INCOME_OUTCOME = 'usn_income_outcome';
case USN_INCOME_OUTCOME = 'usn_income_outcome';
/**
* Единый налог на вмененный доход
*/
const ENDV = 'envd';
case ENDV = 'envd';
/**
* Единый сельскохозяйственный налог
*/
const ESN = 'esn';
case ESN = 'esn';
/**
* Патентная СН
*/
const PATENT = 'patent';
}
case PATENT = 'patent';
}

Some files were not shown because too many files have changed in this diff Show More