Перейти к содержанию

Сценарий «Мерчант-инвойс»: двухфазная оплата по QR

Этот документ описывает бизнес-сценарий, в котором магазин выставляет счёт через QR-код, а покупатель оплачивает его позднее. Сценарий двухфазный: «выставление инвойса» (резерв) и «оплата» (подтверждение) разнесены во времени и могут выполняться разными участниками с разных устройств.

Документ адресован продукт-менеджерам, бизнес-аналитикам и интеграторам, которым нужно понимать жизненный цикл инвойса и его альтернативные исходы.


1. Зачем это нужно

Классический перевод «здесь и сейчас» (P2P между двумя известными пользователями) не подходит, если:

  • магазин формирует счёт до того, как покупатель подошёл к кассе;
  • покупатель неизвестен в момент создания инвойса — он определится только после сканирования QR;
  • между моментом «магазин показал QR» и «покупатель приложил палец к Face ID» может пройти от нескольких секунд до нескольких минут.

Чтобы развязать «создание оферты» и «фактический перевод денег», PM поддерживает специальный двухфазный канал — MERCHANT_INVOICE. На фазе «резерв» деньги ещё не движутся, движется только запись в реестре инвойсов. На фазе «оплата» определяется плательщик и происходит фактическое списание.


2. Актёры

Актёр Кто это Что делает
Магазин POS-терминал, касса, mini-app мерчанта Выставляет инвойс, показывает QR, может его отменить
Клиент Покупатель с мобильным приложением OneWallet Сканирует QR, подтверждает оплату
PM (платёжный шлюз) Payment Manager Регистрирует инвойс, проверяет подпись QR, проводит платёж
Внутренняя бухгалтерия Внутренняя учётная система PM Делает движение средств по счетам (детали — в техдоке)

3. Sequence-диаграмма полного flow

sequenceDiagram
    autonumber
    actor M as Магазин
    actor C as Клиент
    participant PM as PM (платёжный шлюз)
    participant Buh as Внутренняя бухгалтерия

    Note over M,PM: Фаза 1 — Резерв (выставление инвойса)
    M->>PM: Выставить инвойс<br/>(сумма, валюта, счёт магазина, TTL)
    PM->>PM: Создать запись инвойса (статус CREATED)<br/>Подписать QR-payload<br/>Зафиксировать срок действия
    PM-->>M: ID инвойса, qr_signature, expiresAt

    Note over M,C: Магазин формирует QR-код и показывает его покупателю.<br/>Между фазами может пройти от секунд до минут.

    Note over C,PM: Фаза 2 — Оплата (подтверждение покупателем)
    C->>C: Сканирует QR<br/>Локально проверяет подпись
    C->>PM: Подтвердить оплату<br/>(ID инвойса, счёт покупателя)
    PM->>PM: Проверить статус и срок действия<br/>Зафиксировать плательщика
    PM->>Buh: Провести платёж<br/>(детали в техдоке)
    Buh-->>PM: Платёж проведён
    PM-->>C: Статус: SETTLED
    PM-->>M: Push-уведомление: инвойс оплачен

4. Шаг за шагом

4.1. Фаза «резерв»: магазин выставляет счёт

  1. Магазин обращается к PM и просит создать инвойс: сумма, валюта, счёт магазина для зачисления, опциональный комментарий, TTL.
  2. PM:
  3. создаёт запись инвойса в статусе CREATED;
  4. не блокирует деньги — плательщик ещё неизвестен, блокировать нечего;
  5. вычисляет дедлайн (expiresAt);
  6. подписывает QR-payload секретным ключом (qr_signature);
  7. возвращает магазину ID инвойса, подпись и срок действия.
  8. Магазин формирует QR-код и показывает его покупателю (на экране кассы, в чеке, на наклейке).

Результат фазы: инвойс существует, ждёт оплаты, деньги ни у кого не списаны.

4.2. Фаза «оплата»: клиент подтверждает

  1. Клиент сканирует QR в приложении OneWallet.
  2. Приложение локально проверяет подпись QR — это защищает от подделанных QR-кодов (см. раздел 5).
  3. Клиент видит сумму, реквизиты магазина и нажимает «Оплатить».
  4. Приложение отправляет в PM запрос на подтверждение: ID инвойса плюс счёт покупателя.
  5. PM атомарно фиксирует плательщика и переводит инвойс в статус SETTLED, поручив внутренней бухгалтерии провести фактическое движение средств между счетами клиента и магазина (детали — в техдоке).
  6. PM уведомляет магазин о факте оплаты в реальном времени.

Результат фазы: деньги списаны со счёта клиента, зачислены на счёт магазина, обе стороны получили подтверждение.


5. Зачем нужен qr_signature

QR-код — это просто картинка с данными. Без защиты любой может «нарисовать» QR со своим счётом, выдать его за чужой и собрать платежи на свой кошелёк.

qr_signature — это цифровая подпись, которой PM скрепляет содержимое QR-кода:

  • При создании инвойса PM подписывает payload (ID инвойса, счёт магазина, сумма, валюта, срок действия) своим секретным ключом.
  • Подпись кладётся в QR-код вместе с данными.
  • Приложение клиента проверяет подпись локально перед тем, как показать пользователю экран оплаты. Если подпись не сходится — QR подделан или испорчен, приложение отказывается платить.
  • На этапе подтверждения PM ещё раз сверяет подпись со стороны сервера.

Без этой подписи злоумышленник мог бы:

  • подменить счёт получателя в чужом QR-коде и угнать платёж;
  • сгенерировать «фальшивый инвойс» с произвольной суммой;
  • продлить срок действия истёкшего QR.

С подписью все эти атаки отсекаются: любое изменение payload ломает подпись, а ключ известен только PM.

Алгоритм подписи и его параметры (формат, длина, шифр) описаны в технической документации — см. блок «Где это в коде».


6. Срок действия инвойса (TTL)

У каждого инвойса есть срок действия — expiresAt. После него инвойс перестаёт быть оплачиваемым.

  • По умолчанию — 10 минут. Этого хватает для типового сценария «покупатель у кассы»: чек распечатан, QR на экране, клиент платит сейчас.
  • Максимум — 1 час. Магазин может явно запросить более длинный TTL (например, для счёта-самообслуживания в зале ожидания), но больше часа PM не выдаст.
  • Конкретное значение INVOICE_DEFAULT_TTL_SECONDS и потолок INVOICE_MAX_TTL_SECONDS берутся из конфигурации платёжного шлюза.

Зачем ограничивать TTL:

  • защита от «вечно висящих» QR, которые могут быть оплачены через неделю по уже неактуальной цене;
  • предсказуемость для магазина: касса знает, что после TTL счёт автоматически закроется и его можно перевыставить;
  • безопасность: украденный или сфотографированный QR не работает бесконечно.

7. Альтернативы основного flow

Помимо «магазин выставил — клиент оплатил», у инвойса есть две альтернативные финальные ветки.

7.1. Альтернатива: магазин отменил инвойс (cancel)

Магазин может явно отозвать инвойс, пока он ещё не оплачен. Типовые причины:

  • товара нет на складе;
  • покупатель передумал и ушёл;
  • кассир ошибся в сумме и хочет перевыставить счёт.
sequenceDiagram
    autonumber
    actor M as Магазин
    participant PM as PM (платёжный шлюз)

    M->>PM: Отменить инвойс<br/>(ID, причина)
    alt Инвойс ещё в статусе CREATED
        PM->>PM: Перевести в CANCELED
        PM-->>M: Подтверждение отмены
    else Инвойс уже оплачен / истёк / отменён
        PM-->>M: Ошибка: отменить нельзя
    end

Ключевые правила:

  • отменить может только тот магазин, который выставлял инвойс (или администратор системы);
  • если инвойс уже оплачен или истёк — отмена невозможна, PM возвращает ошибку;
  • после отмены статус инвойса — CANCELED, оплатить его больше нельзя;
  • деньги нигде не двигались, никаких возвратов не требуется.

7.2. Альтернатива: истёк срок действия (expire)

Если ни клиент не подтвердил оплату, ни магазин не отменил инвойс — по истечении TTL инвойс автоматически переходит в статус EXPIRED.

sequenceDiagram
    autonumber
    participant Sweep as Фоновая задача PM
    participant PM as PM (платёжный шлюз)
    actor M as Магазин

    loop Каждые ~30 секунд
        Sweep->>PM: Найти инвойсы<br/>со статусом CREATED<br/>и истёкшим expiresAt
        PM->>PM: Перевести в EXPIRED
        PM-->>M: Push-уведомление: инвойс истёк
    end

Ключевые правила:

  • истечение происходит автоматически без действий магазина или клиента;
  • если в момент истечения клиент успел нажать «Оплатить» — побеждает тот, кто пришёл первым (PM использует атомарную проверку статуса);
  • истёкший инвойс нельзя ни оплатить, ни отменить — он финален;
  • магазин может выставить новый инвойс на ту же сумму, это будет отдельная запись с новым ID.

8. Финальные статусы

Статус Что произошло Дальнейшие действия
SETTLED Клиент оплатил, деньги зачислены магазину Платёж завершён, возврат — отдельным сценарием
CANCELED Магазин отменил до оплаты Можно выставить новый инвойс
EXPIRED Истёк TTL без оплаты и без отмены Можно выставить новый инвойс

Промежуточные статусы (VALIDATED, AUTHORIZED, SETTLING) существуют внутри фазы «оплата» и видны через детальный аудит — для бизнес-сценария важны только три финальных.


9. Где это в коде

Подробности технической реализации (контракт TwoPhaseChannel, алгоритм подписи QR, атомарные UPDATE с гонками, конкретные API-эндпоинты, curl-примеры):