# Оплата заказов

Деньги идут **напрямую продавцу**: teeu их не держит и платёжным агентом не является. Отсюда всё
остальное — эквайринг подключает сам продавец своими ключами, счёт выставляется от его юрлица.

## Оплата — отдельная ось, а не шаг в цепочке

У заказа два независимых состояния: `status` (цепочка доставки) и `payment_status`. Так сделано
сознательно: заказ бывает отправлен и не оплачен, по счёту деньги приходят неделями, а часть заказов
оплаты через сайт не предполагает вовсе. Вставь мы «ожидание оплаты» в общую цепочку — через неё
пришлось бы тащить всех.

| Статус оплаты | Когда |
|---|---|
| `not_required` | способ «напрямую продавцу» — оплаты через сайт нет |
| `pending` | покупатель выбрал оплату, продавец ещё не выставил сумму |
| `issued` | сумма зафиксирована, ждём денег |
| `paid` | оплачен |
| `refunded` | возврат |

Смена статуса оплаты живёт **только** в `Services\Commerce\PaymentService` — так же, как смена
статуса доставки только в `OrderTransitionService`.

## Способ выбирает покупатель, сумму назначает продавец

Способ (`payment_method`) выбирается один раз в корзине: картой, по счёту для организаций или
напрямую. Дальше не меняется.

Момент выставления выбирает продавец, а не система. Причина простая: **стоимость доставки ставит
продавец уже после оформления**, а она входит в сумму. До этого платить не за что.

## Сумма фиксируется снимком

`payable_total` записывается в момент выставления и дальше не пересчитывается. Иначе продавец
поменяет доставку, пока покупатель открывает оплату, и заплачено будет не то. Нужно пересчитать —
сначала «Снять выставление», это явное действие.

Оплату можно отметить и без выставления (продавец получил деньги сам) — тогда сумма фиксируется в
этот момент.

## Счёт для юрлиц

**Кто где заводит реквизиты.** Продавец — банковские, в кабинете («Реквизиты»); их может быть
несколько, расчётные счета в разных банках дело обычное, и при выставлении он выбирает нужные.
Покупатель — свои организации, в кабинете («Мои организации»), и выбирает одну при оформлении.
Первая запись у обоих становится основной сама: иначе человек добавит одну и будет гадать, почему
ничего не подставилось.

**Всё в счёте — снимок.** Реквизиты обеих сторон, позиции и сумма пишутся в `invoice_snapshot` при
выставлении. Организация может переехать, продавец — закрыть счёт в банке, а выставленный счёт
остаётся документом с теми данными, что были в тот момент. Удаление реквизитов или организации
выставленные счета не меняет — так и написано в подтверждении удаления.

**Номер сквозной по магазину и году**, а не идентификатор заказа: бухгалтерия ждёт возрастающей
нумерации.

**QR по ГОСТ Р 56042** — тот, что сканируют банковские приложения и подставляют реквизиты сами.
Сумма в копейках. Пустые поля не печатаются: часть банков спотыкается о `KPP=` без значения, а у ИП
КПП нет вовсе.

**PDF** собирается dompdf на встроенном DejaVu Sans — единственном шрифте с кириллицей в окружении.
Вёрстка только таблицами: flex и grid dompdf не понимает. Примечание из реквизитов печатается
красным под итогом — туда пишут «оплата физическими лицами недопустима» и подобное.

**Скачивают обе стороны:** продавец из своего заказа, покупатель — из своего, каждый только по
своему. Чужой счёт не открыть даже прямой ссылкой, реквизиты чужого магазина в свой счёт не
подставить даже прямым запросом — на оба случая есть тесты.

## Оплата картой: эквайринг продавца

Деньги идут **напрямую магазину**, мы их не держим и не пересылаем. Ключи эквайринга продавец
подключает сам в разделе «Интеграции» — там для этого появилась группа **«Приём оплаты»** рядом с
CRM и мессенджерами.

**Почему провайдер оплаты — не транспорт доставки заказов.** Раздел интеграций уже умеет отправлять
события наружу (`IntegrationTransport`: send/ping/findExisting), и соблазн был втиснуть эквайринг
туда же. Задачи разные: транспорт отвечает на вопрос «куда сообщить о заказе», эквайринг — «как
получить по нему деньги и как узнать, что они пришли». Общего у них только хранилище ключей, поэтому
общим осталось оно: `SellerIntegration` с шифрованными `credentials`, а логика — в
`App\Services\Payments\YooKassaProvider`. Чтобы эквайринг случайно не попал в очередь доставки,
`SellerIntegration::isDeliverable()` отсекает его явно.

**Ключ идемпотентности считается от заказа и суммы** (`sha1(id:сумма)`). Повторное нажатие
«Выставить оплату» не создаёт второй платёж — ЮKassa вернёт прежний. А вот изменившаяся сумма даёт
другой ключ, то есть новый платёж: это ровно то, что нужно, когда доставку пересчитали.

**Отказ эквайринга откатывает выставление.** `issueCard()` идёт одной транзакцией: сначала
замораживает сумму, потом просит ссылку. Без транзакции неудачный запрос оставлял бы заказ
«выставленным» без ссылки — продавец видел бы «оплата выставлена», а покупателю нечего нажать. На
это есть тест, и он эту ошибку однажды поймал.

**Ссылка хранится в заказе**, а не запрашивается на каждый показ страницы: лишние обращения к чужому
API — плата ни за что. Снятие выставления стирает ссылку вместе с суммой, иначе покупатель ушёл бы
платить по старой.

### Уведомление об оплате

Подписи у уведомлений ЮKassa нет, поэтому **содержимому запроса мы не верим вовсе**: берём оттуда
только идентификатор платежа и сами спрашиваем у ЮKassa статус ключами этого магазина. Подделать
оплату, зная адрес приёмника, нельзя — платежа с таким номером у них просто не найдётся.

Адрес приёмника **свой у каждой интеграции** (`/payments/yookassa/{integration}`): так уведомление
приносит с собой, чьими ключами проверять, и не приходится угадывать магазин по номеру платежа. Он
же показан продавцу в настройках интеграции — его нужно вписать в ЮKassa в разделе
«HTTP-уведомления» с событием `payment.succeeded`.

Отвечаем `200` всегда, когда запрос дошёл: иначе ЮKassa будет повторять уведомление сутки, а чинить
нечего — платёж уже прошёл. Заказ чужого магазина своими ключами оплаченным не отметить.

### Чек по 54-ФЗ

Состав чека передаём мы, а не продавец: у нас есть позиции заказа и доставка отдельной строкой.
Из-за этого у магазина появились два поля в профиле — **система налогообложения** и **ставка НДС**.
Коды заданы эквайрингами, не нами: СНО 1..6, ставки НДС — `App\Enums\VatRate`. Без них ЮKassa
чек не пробьёт и платёж не примет.

**Нумерация ставок рваная, и это не опечатка.** С 1 января 2026 года основная ставка — 22 %, её
код у ЮKassa **11**, а не 5: пятый и шестой заняты расчётными ставками 10/110 и 20/120, которых
в нашем списке нет. Двадцать процентов остались доступны с прежним кодом 4 — переходный период
ещё идёт.

| Ставка | Код |
|---|---|
| Без НДС | 1 |
| 0 % | 2 |
| 10 % | 3 |
| 20 % | 4 |
| 22 % | 11 |

Список живёт в одном месте — enum: раньше он был продублирован в форме кабинета и в правилах
валидации, и добавить ставку требовалось в обоих. Ошибка здесь тихая и дорогая: неверный код в
чеке — это неправильный чек, и разбираться с этим продавцу.
Поля необязательные: пока приём оплаты не подключён, они не нужны.

### Т-Банк — отложен сознательно

Механику подписи (`Token`) публичная документация не отдаёт: страницы редиректят на портал
разработчика, где только обзор. Писать её по памяти в деньгах нельзя — ошибка в проверке подписи
уведомления означает не «не работает», а «поддельную оплату примут за настоящую». Каркас к
добавлению готов: нужен доступ к их спецификации или тестовый терминал для сверки живьём.

## Что дальше

Готово: ось оплаты, выбор способа в корзине, выставление и ручная отметка у продавца, отражение у
покупателя, счёт для юрлиц целиком, оплата картой через ЮKassa.

Впереди:

1. **Т-Банк** — как только будет спецификация подписи (см. выше).
2. **Возврат средств** — сейчас `refunded` есть в статусах, но кнопки нет: возвращают через
   личный кабинет эквайринга.

## Тесты

`tests/Feature/OrderPaymentTest.php` (10), `InvoiceTest.php` (8), `InvoicePagesTest.php` (7),
`AcquiringTest.php` (16): доставка входит в сумму, снимок не меняется при правке
доставки, снятие выставления позволяет пересчитать, отметка оплаты пишет кто и когда, оплата без
выставления фиксирует сумму, оплаченный заказ не откатывается, «напрямую» не имеет потока оплаты,
действия продавца и запрет для чужого магазина.

По эквайрингу проверяется настоящий запрос к ЮKassa, а не только его результат: адрес, Basic-авторизация
ключами магазина, заголовок идемпотентности, сумма с доставкой, чек с СНО и доставкой отдельной
строкой. Плюс отказ эквайринга не оставляет заказ выставленным, снятие стирает ссылку, поддельное
уведомление ничего не меняет, незавершённый платёж не считается оплатой, чужой заказ своими ключами
не отметить, ключи лежат зашифрованными.
