Paykeeper × ERP — сводный gap-анализ

Сопоставление того, что умеет наш ERP, и того, что умеет Paykeeper. Источник: OpenAPI-спеки в _assets/paykeeper/ + наши контракты 03-Services/ и бизнес-спеки 08-Specs/Админка Франшизы/.

Три блока:

  1. Работает сейчас — функциональность, которая состыковывается без доработок (или с минимальной адаптацией на адаптере).
  2. Разрывы на нашей стороне — что нужно достроить у нас, чтобы интеграция вообще запустилась.
  3. Разрывы на стороне Paykeeper — что мы не можем сейчас и что им нужно реализовать в их платформе (это основной список «ТЗ для PK»).

1. Что работает сейчас

ЗонаИх APIНаш сервисЧто стыкуется
Приём платежа (терминал)paykeeper-app-mpos + webhook POSTOrder Service — фиксация оплатыКассир проводит платёж на их терминале → прилетает webhook → мы закрываем заказ
Фискализация 54-ФЗmPOS + ФН + ОФДПолностью у них (ФН, ОФД, чеки). Нам достаточно хранить fop_receipt_key и показывать ссылку на чек
Ссылка на чек клиентуfop_receipt_key в webhookOrder 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_mandatoryproducts.is_markedФлаг уже есть с обеих сторон
Штрихкод / SKUbarcode, skubarcode, 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/ с cartPOS 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 /suppliersWarehouse 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/...)Обязательное поле в PKXS
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 можно дефолтить в fullXS
Kafka-события catalog.product.*, catalog.category.*, catalog.price_list.*, catalog.stop_list.*Сейчас только REST; адаптеру нужно вычитывать измененияS
Outbox-таблица для гарантии доставки в KafkaБез outbox любое падение = потеря изменения в PKS

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_list per-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Модификаторы в каталоге и mPOSP0
3.2Иерархия категорий (parent_id у tags)P0
3.3Per-store прейскурантыP0
3.4Стоп-листы / availability per-storeP0
3.5Ролевая модель пользователей + привязка к терминаламP0
3.6Webhook возврата, отмены, коррекцииP0
3.7API списка платежей за период (reconciliation)P0
3.8Sandbox-окружениеP0
3.9Детализация ошибок /products/importP1
3.10ФН / ФД / ФП / номер смены в webhookP1
3.11API и webhook’и по сменам ФНP1
3.12Документированная multi-tenancy модельP1
3.13Весовой товар / сопряжение с весамиP1
3.14Алкоголь / табак / ЕГАИСP1
3.18Альтернатива CSRF для LKP2
3.19Rate limitsP2

5. Что можно запускать уже сейчас (без их доработок)

Даже при всех разрывах выше, минимальный MVP на текущем состоянии обеих сторон собирается:

  1. Каталог — синк в одну сторону (наш → PK), только плоский справочник товаров с фискальными полями. Без per-store цен, стоп-листов, модификаторов. PK использует это только для того чтобы чек сформировался корректно (название + ставка НДС + единица).
  2. Приём оплаты — всегда инициируется из нашего POS BFF / мобильного приложения через deep link /invoice/ с готовым cart (где уже посчитана per-store цена + модификаторы). PK-терминал не используется в «standalone» режиме.
  3. Webhook платежа — адаптер принимает, валидирует HMAC, публикует payment.received в Kafka.
  4. Возвраты — через deep link /refund/, инициируемый из админки / приложения. Без webhook, статус узнаём при следующей сверке.
  5. Сверка — пока у них нет GET /payments — сверяем через ручной отчёт из LK (выгрузка CSV), раз в день.

Это работает, но:

  • Каждая продажа идёт только через наше приложение — терминал PK нельзя использовать отдельно.
  • Нет real-time возвратов — узнаём постфактум.
  • Нет видимости когда их ФН закончит смену — риск блокировки кассы.

6. Ссылки