Paykeeper × ERP — сводный gap-анализ
Сопоставление того, что умеет наш ERP, и того, что умеет Paykeeper. Источник: OpenAPI-спеки в _assets/paykeeper/ + наши контракты 03-Services/ и бизнес-спеки 08-Specs/Админка Франшизы/.
Три блока:
- Работает сейчас — функциональность, которая состыковывается без доработок (или с минимальной адаптацией на адаптере).
- Разрывы на нашей стороне — что нужно достроить у нас, чтобы интеграция вообще запустилась.
- Разрывы на стороне Paykeeper — что мы не можем сейчас и что им нужно реализовать в их платформе (это основной список «ТЗ для PK»).
1. Что работает сейчас
| Зона | Их API | Наш сервис | Что стыкуется |
|---|---|---|---|
| Приём платежа (терминал) | paykeeper-app-mpos + webhook POST | Order Service — фиксация оплаты | Кассир проводит платёж на их терминале → прилетает webhook → мы закрываем заказ |
| Фискализация 54-ФЗ | mPOS + ФН + ОФД | — | Полностью у них (ФН, ОФД, чеки). Нам достаточно хранить fop_receipt_key и показывать ссылку на чек |
| Ссылка на чек клиенту | fop_receipt_key в webhook | Order Service.payments | Строим URL https://{tsp}.server.paykeeper.ru/receipt/{id}/{key} из данных webhook |
| Справочник кассиров | POST /change/user/add (LK) | User Service — employees с pos.access | Зеркалим наших сотрудников-кассиров в LK при выдаче pos.access |
| Базовые поля товара | ims-api /products (name, price, tax, item_type, measure, sku, barcode, item_code_is_mandatory, tru_code) | Catalog Service — products | Основной набор полей совпадает (после добавления vat_rate + payment_subject у нас) |
| Маркировка «Честный знак» | item_code_is_mandatory | products.is_marked | Флаг уже есть с обеих сторон |
| Штрихкод / SKU | barcode, sku | barcode, sku | Идентичны |
| Безналичная/наличная оплата | payment_method в deep link (ecom/sbp/cash/mpos) | order_payments.payment_method | Маппинг 1:1 |
| ApplePay / MirPay / SBP | отдельные API + встроенные методы mPOS | — | Полностью у них |
| Deep link инициации оплаты | paykeeper://mobile.app/invoice/ с cart | POS BFF / мобильное приложение | Работает при передаче cart с финальными ценами из нашего прейскуранта |
| Возврат платежа | paykeeper://mobile.app/refund/ (deep link) + флаг refund_allow на пользователе | Order Service — refund (Phase 2+) | Вызов возврата с устройства работает; статус возврата прилетит через последующие обращения к API |
| Выплаты физлицам (курьеры, сборщики) | paykeeper-app-mpos /payout + api-vyplat (OCT) | — (в роадмапе) | Готово к использованию, если понадобится оплата курьерам/самозанятым |
| Поставщики | ims-api /suppliers | Warehouse Service (Phase 2) | Мы можем зеркалить, но необязательно — это вспомогательная зона |
| Тикеты поддержки | support-api | — | Можем встроить раздел «Поддержка» в админку как удобство |
Итого по зелёной зоне: основная платёжная петля (приём оплаты → webhook → закрытие заказа → ссылка на чек) работает «из коробки» после того как мы достроим адаптер и минимальные поля в каталоге.
2. Разрывы на нашей стороне — что достроить нам
Это предусловия для запуска интеграции.
2.1 Catalog Service
| Изменение | Зачем | Усилие |
|---|---|---|
products.vat_rate enum (none/vat0/vat10/vat20/vat110/vat120) | Обязательное поле в PK (tax) — без него фискализация не сформирует чек | XS (миграция + UI) |
products.payment_subject enum (соответствует item_type PK: goods/service/work/excise/...) | Обязательное поле в PK | XS |
products.measure_pk_override (nullable) | Наш unit_of_measure (порция, л, мл) шире, чем их measure (pcs/kg/m/sq_m/kwh/day/Gb/other) — нужна ручная привязка для нестандартных | XS |
products.payment_type (prepay/full/advance/…) | Способ расчёта из 54-ФЗ (тег 1214). Для MVP можно дефолтить в full | XS |
Kafka-события catalog.product.*, catalog.category.*, catalog.price_list.*, catalog.stop_list.* | Сейчас только REST; адаптеру нужно вычитывать изменения | S |
| Outbox-таблица для гарантии доставки в Kafka | Без outbox любое падение = потеря изменения в PK | S |
2.2 User Service
| Изменение | Зачем |
|---|---|
Kafka-события user.employee.created/updated/deleted | Адаптер должен зеркалить кассиров в LK PK при появлении/изменении прав pos.access |
| Outbox-таблица | Тот же аргумент — гарантия доставки |
2.3 Order Service
| Изменение | Зачем |
|---|---|
В order_payments хранить pk_payment_id, pk_fop_receipt_key, pk_user_login, pk_terminal_id | Для сверки, отображения ссылки на чек и логирования |
Consumer payment.received от адаптера | Закрытие заказа по webhook |
2.4 Store Service
| Изменение | Зачем |
|---|---|
Привязка store ↔ pk_terminal_logical_id + pk_merchant_id | В webhook PK присылает только mpos_terminal_id и mpos_merchant_id — без mapping мы не знаем, в какую нашу ТТ записывать продажу |
2.5 Новый сервис — Paykeeper Adapter
| Компонент | Зачем |
|---|---|
| Скелет Spring Boot + БД (mapping / outbox / webhook_log) | Anti-Corruption Layer |
| HTTP клиенты: ims-api, lk-paykeeper, partner-api (при необходимости) | Исходящие вызовы в PK |
Webhook-endpoint POST /webhooks/paykeeper/success с HMAC-SHA1 | Приём платежей |
Endpoint POST /api/v1/pk/invoice-link для генерации deep link | Инициация оплаты из наших клиентов |
| Worker: outbox → PK | Синхронизация каталога и кассиров |
| Reconciliation scheduler | Досверка на случай пропущенных webhook’ов |
2.6 Админка
| Изменение | Зачем |
|---|---|
| Раздел «Интеграции → Paykeeper» (per-franchise / per-legal_entity) | URL ЛК, service-token login, HMAC secret, mapping терминалов на ТТ |
| Блок «Фискальные атрибуты» в карточке товара | Заполнение vat_rate, payment_subject, is_marked |
Permission integrations.edit | Только админ франшизы видит |
3. Разрывы на стороне Paykeeper — что им нужно дореализовать
Сгруппировано по критичности для нашего запуска (P0 → P2).
P0 — блокеры запуска
3.1 Модификаторы товаров
Что у нас: modifier_groups + modifier_options + product_modifiers (BR 1.8, 1.8.1, 1.9.2). Это основа нашего каталога — «Капучино + сироп лесной орех + добавить шот эспрессо». Есть structural (закреплённые — «размер: S/M/L») и free (свободные — добавки).
Что у PK: ничего. В ims-api товар — плоский. В mPOS UI — выбор из плоского списка товаров.
Workaround: мы передаём cart в deep link с финальными позициями вида Капучино M + сироп лесной орех (итого 380 ₽) — одной строкой. Кассир на mPOS видит только итог, выбрать ничего не может.
Проблема workaround’a: кассиру неудобно пробивать заказ с нуля на терминале PK (без нашего приложения). Если mPOS используется как standalone — модификаторы потеряны.
Что нужно от PK:
- В
ims-apiдобавитьmodifier_groups+modifier_options+ привязку к товарам (как минимум: название, мин/макс выбор, цена опции). - В UI mPOS — экран выбора модификаторов после выбора товара.
Если нет планов на их стороне — фиксируем фактическую договорённость: сценарий «кассир берёт терминал и бьёт заказ сам» не поддерживается. Все заказы инициируются из нашего приложения, терминал только принимает оплату по готовому
cart.
3.2 Иерархия категорий
Что у нас: дерево categories с parent_id — «Кофе → Горячие → Эспрессо-напитки». BR 1.7.
Что у PK: плоские tags (без parent_id).
Workaround: схлопываем путь в имя тега — «Кофе / Горячие / Эспрессо-напитки». Уродливо, при изменении иерархии в ERP нужно переименовывать все теги.
Что нужно от PK:
parent_idв схемеtag+ рекурсивное отображение в их UI.
3.3 Per-store прейскуранты
Что у нас: price_lists — прейскурант привязывается к ТТ. В Москве капучино стоит 300 ₽, в Петербурге 260 ₽. BR 1.10.
Что у PK: одна цена price на товар на весь ТСП. Per-terminal цен нет.
Workaround: всегда передаём актуальную цену в cart deep link. Но если кассир откроет каталог PK напрямую — увидит только «глобальную» цену, которая не соответствует ТТ.
Что нужно от PK:
- Справочник прейскурантов в
ims-apiи их привязка к терминалу/ТСП/merchant. - UI в LK для переключения прейскурантов.
3.4 Стоп-листы и доступность товара per-store
Что у нас:
product_stop_list/category_stop_listper-store — товар временно выключен на конкретной ТТ (BR 1.13).available_in_all_stores+product_stores— товар продаётся не во всех ТТ (BR 2.1).
Что у PK: глобальный is_visible. Нет концепта «на терминале #5 товар скрыт, на #6 виден».
Что нужно от PK:
- Availability per-terminal (или per-merchant) — массив терминалов/merchant’ов, где товар доступен.
- Webhook/API для быстрого скрытия товара без передеплоя всего каталога.
3.5 Ролевая модель пользователей LK
Что у нас: permissions-based (BR 1.4.4). Есть пермишены, из них собираются роли. Есть pos.access, pos.refund, stop_list.edit, и т.д.
Что у PK: плоские булевы флаги на пользователе (admin, invoices_only, refund_allow, receipts_allowed, view_only_owned_payments, view_only_owned_receipts, refunds_sum_per_day). Ролей нет. Разделения по терминалам нет — юзер видит либо всё, либо только своё.
Workaround: адаптер транслирует наш набор пермишенов в их флаги (маппинг-таблица). Рабочий, но каждое новое поведение у нас = правка адаптера.
Что нужно от PK:
- Понятие «роль» с конфигурируемым набором прав.
- Привязка пользователя к конкретным терминалам (чтобы кассир одной ТТ не видел платежи другой). Сейчас
view_only_owned_paymentsдаёт только «свои vs чужие», но не «все с моего терминала».
3.6 Webhook возврата и отмены
Что у нас: Order Service (Phase 2) должен реагировать на возвраты и корректировки.
Что у PK: в OpenAPI задокументирован только webhook об успешном платеже (success). Про webhooks о возвратах и корректировках — тишина.
Что нужно от PK:
- Webhook
refund_success/refund_failedс полями: исходныйpayment_id, сумма возврата, причина, ссылка на чек возврата. - Webhook
payment_cancelled— если клиент закрыл экран mPOS, не оплатив. - Webhook
correction(чек коррекции) — если касса пробила чек коррекции по предписанию ФНС.
Без этого мы вынуждены либо полагаться на reconciliation (долго, не real-time), либо тянуть данные самостоятельно через несуществующий GET /payments/.
3.7 API списка платежей за период (для reconciliation)
Что у нас: планируется скедулер который раз в N минут сверяет webhook’и.
Что у PK: в lk-paykeeper такого эндпоинта нет в OpenAPI. В partner-api — есть работа с ТСП, но не с платежами ТСП.
Что нужно от PK:
GET /api/payments/?from={ts}&to={ts}&status=success
Response: список платежей с полным payload как в webhook
Без этого у нас нет способа достоверно сверить что ничего не пропустили.
3.8 Sandbox / тестовый инстанс
Что у PK: в спеках не упомянут.
Что нужно от PK:
- Тестовый ЛК (
sandbox.server.paykeeper.ruили подобный). - Эмулятор терминала — возможность сгенерировать тестовый webhook из админки.
- Тестовые карты / тестовые SBP-операции.
P1 — сильно осложняет, но не блокирует
3.9 Детализация ошибок POST /products/import
Сейчас: возвращает added_count / error_count / skipped_count — счётчики. Если из 500 товаров 3 не прошло, непонятно, какие именно.
Нужно: в ответе массив errors: [{ index: 42, sku: "X", reason: "..." }, ...].
3.10 Информация о чеке в webhook
Сейчас: fop_receipt_key — только ключ для построения URL.
Нужно: добавить в payload webhook: fn_number, fd_number, fp_sign, shift_number, receipt_number_in_shift, offd_sync_status. Это критично для наших внутренних отчётов и реестров (54-ФЗ, налоговый аудит).
3.11 Сквозная информация о смене
Нет:
- Webhook об открытии/закрытии смены (открыл фискальный накопитель).
- Команда принудительного закрытия смены через API.
- Проверка «смена открыта / срок действия ФН не истёк».
Нужно:
GET /api/mpos/shift/{terminal_id}— статус смены.POST /api/mpos/shift/close— программное закрытие.- Webhook
shift_opened/shift_closed/shift_expiring(за час до 24 часов).
Это важно: если ФН не закрыт 24 часа — касса заблокируется, вся ТТ встанет. Нам нужна предварительная видимость.
3.12 Multi-tenancy (гранулярность LK)
Открытый вопрос: один LK покрывает
- один терминал?
- одну ТТ (несколько терминалов)?
- одно юрлицо (несколько ТТ)?
- всю франшизу (несколько юрлиц)?
Что нужно от PK: документированный выбор + обоснование. От ответа зависит гранулярность pk_config в нашем адаптере и бизнес-процесс регистрации новой ТТ.
3.13 Весовой товар / свободная цена на кассе
Что у нас: is_open_price (цена вводится на кассе), is_by_weight (количество с весов).
Что у PK: есть price_can_be_changed — близко к open_price. Про весовой товар с сопряжением с весами — ничего.
Что нужно от PK:
- Флаг
weighed_on_scale+ поддержка протоколов весов (как минимум: ручной ввод веса на mPOS).
3.14 Ограничения по времени / возрасту / алкоголю / табаку
Что у нас: is_alcohol, is_tobacco, is_sugary_drink.
Что у PK: нет.
Нужно (минимум):
- Флаги
is_alcohol/is_tobacco+ предупреждение на mPOS «требуется проверка возраста / продажа разрешена с HH:MM до HH:MM». - Для алкоголя — интеграция с ЕГАИС (УТМ) или указание «ЕГАИС ведётся на стороне ТСП» и как это конфигурируется.
P2 — хотелось бы, но можем жить без
3.15 Скидки, промокоды, лояльность
Мы ведём всё у себя, в cart deep link уходит финальная цена. Их сторона не нужна для этого.
3.16 Техкарты и склад
Полностью наша зона. Их не трогаем.
3.17 Кухонный дисплей / статусы приготовления
Полностью наша зона (POS + KDS — потом в Phase 2).
3.18 CSRF в LK
Мелочь, но неудобно: для каждого POST /change/user/* нужно предварительно получить CSRF-токен отдельным GET. Адаптер кэширует, но это лишний round-trip.
Хотелось бы: альтернативная аутентификация без CSRF для сервисных интеграций (например, API-токен).
3.19 Rate limits и back-off подсказки
Нет документированных лимитов. Нужно:
- Заголовки
X-RateLimit-*в ответах. - HTTP 429 +
Retry-After.
4. Сводная матрица для разговора с PK
| # | Разрыв | Критичность | Их оценка (заполнить после разговора) |
|---|---|---|---|
| 3.1 | Модификаторы в каталоге и mPOS | P0 | |
| 3.2 | Иерархия категорий (parent_id у tags) | P0 | |
| 3.3 | Per-store прейскуранты | P0 | |
| 3.4 | Стоп-листы / availability per-store | P0 | |
| 3.5 | Ролевая модель пользователей + привязка к терминалам | P0 | |
| 3.6 | Webhook возврата, отмены, коррекции | P0 | |
| 3.7 | API списка платежей за период (reconciliation) | P0 | |
| 3.8 | Sandbox-окружение | P0 | |
| 3.9 | Детализация ошибок /products/import | P1 | |
| 3.10 | ФН / ФД / ФП / номер смены в webhook | P1 | |
| 3.11 | API и webhook’и по сменам ФН | P1 | |
| 3.12 | Документированная multi-tenancy модель | P1 | |
| 3.13 | Весовой товар / сопряжение с весами | P1 | |
| 3.14 | Алкоголь / табак / ЕГАИС | P1 | |
| 3.18 | Альтернатива CSRF для LK | P2 | |
| 3.19 | Rate limits | P2 |
5. Что можно запускать уже сейчас (без их доработок)
Даже при всех разрывах выше, минимальный MVP на текущем состоянии обеих сторон собирается:
- Каталог — синк в одну сторону (наш → PK), только плоский справочник товаров с фискальными полями. Без per-store цен, стоп-листов, модификаторов. PK использует это только для того чтобы чек сформировался корректно (название + ставка НДС + единица).
- Приём оплаты — всегда инициируется из нашего POS BFF / мобильного приложения через deep link
/invoice/с готовымcart(где уже посчитана per-store цена + модификаторы). PK-терминал не используется в «standalone» режиме. - Webhook платежа — адаптер принимает, валидирует HMAC, публикует
payment.receivedв Kafka. - Возвраты — через deep link
/refund/, инициируемый из админки / приложения. Без webhook, статус узнаём при следующей сверке. - Сверка — пока у них нет
GET /payments— сверяем через ручной отчёт из LK (выгрузка CSV), раз в день.
Это работает, но:
- Каждая продажа идёт только через наше приложение — терминал PK нельзя использовать отдельно.
- Нет real-time возвратов — узнаём постфактум.
- Нет видимости когда их ФН закончит смену — риск блокировки кассы.
6. Ссылки
- Paykeeper OpenAPI specs (обзор)
- Презентация по адаптеру:
_assets/paykeeper/presentation.html - Наш Catalog Service: Overview, Data Model
- Наш Order Service: Overview
- Ролевая модель: Roles