feat(core): add payment attempt lifecycle and webhook callback - #604
feat(core): add payment attempt lifecycle and webhook callback#604Ibochkarev wants to merge 3 commits into
Conversation
Async providers need a payment attempt distinct from msPayment and order status, plus an idempotent core webhook that maps events through OrderStatusService.
Checkout now fails when initiate() cannot record an attempt after send(), and getPaymentLink goes through PaymentService instead of a second gateway call.
Replay no longer re-runs change() when the order is already paid, and attempt fields plus the idempotency event persist in one write. Financial callbacks now require matching currency, current order cost, and externalId.
…k callback Conflict resolution: keep PR 596/603 status gate (lifecycle ports + inventory) in OrderStatusService, add ensure() as OrderStatusChanger impl; registry deps merged (order_status -> order_log, lifecycle_ports, inventory; payment_lifecycle -> order_status); docblock example switched to PaymentLifecycle API.
|
Этот PR включён в тестовую интеграционную сборку всех открытых PR MiniShop3: AgelxNash/MiniShop3, ветка Сборка нужна, чтобы проверить совместимость взаимозависимых серий PR до их мержа — при последовательном слиянии они конфликтуют друг с другом. Это не ревью и не конкурирующий PR: авторство сохранено (1 PR = 1 коммит с исходным автором), ветка пересобирается по мере обновления PR. Как вошёл в сборку: Конфликт разрешён: |
|
Привет! Просто пожелание: удачи с этим PR 🚀 Работа нужная — пусть рассмотрят и смержат как можно скорее. Успехов! |
Описание
У заказа был только
status_id. Async-провайдеру некуда было сохранитьexternal_id, и в core не было общего webhook-входа.Появились таблицы
ms3_payment_attemptsиms3_payment_attempt_events(уникальные(payment_method_id, provider, external_id)и(attempt_id, event_type, provider_event_id)), сервисPaymentLifecycleService(DIms3_payment_lifecycle) и публичныйPOST /api/v1/payment/webhook/{payment_method_id}без customer token. Тело только raw JSON, не$_POST. Невалидный JSON даёт 400, необработанныйThrowable— 500 и запись в лог.Смена статуса заказа идёт только через
OrderStatusChanger. Провайдеры не пишутstatus_id. Повторный успешный callback идемпотентен: unique event в одной транзакции с UPDATE попытки (writeWithEvent), затемensure(). Если заказ уже в целевом статусе,ensure()не вызываетchange()— replay paid на fixed-статусе не даёт 409.Статусы попытки:
pending,authorized,paid,failed,cancelled,refunded,partially_refunded.DefaultPaymentattempt не создаёт: вsend()нетpayment_id/external_id. Политикиms3_payment_on_failed_statusиms3_payment_on_refunded_status(по умолчанию 5,0оставляет заказ).getPaymentLinkидёт черезPaymentService::resolvePaymentLink: ссылка открытой попытки этого метода (pending/authorized), иначеsend()+initiate(). Еслиinitiate()после успешногоsend()упал, checkout получаетsuccess => falseиms3_err_payment_attempt_record.apply()иrefund()коммитят одинаково:writeWithEvent→ensure().Webhook
paid/refunded/partially_refundedтребуют non-emptyexternalIdиpaidещёamount. Если в webhook естьcurrency, она должна совпасть с attempt. Paid amount = attempt amount, attempt amount = текущийorder.cost. Refund требует provider event id. Over-refund не режется молча.external_idчужого метода не закрывает текущую попытку. Attempt на callback не создаётся с нуля.resolveAttemptтолько читает:external_idпишется вcommit.Секреты
msPayment.propertiesубраны изOrdersPageService,ms3_get_orderи писем (PaymentPublicFields). Эталонный HMAC:PaymentWebhookHmacиPayment::verifyWebhookHmac()(secret изpropertiessecret/secret_key/webhook_secret, затемms3_payment_secret). Конкретный эквайер в этот PR не входит.Тип изменений
Связанные Issues
Closes #590
Как это было протестировано?
Локальный CI-гейт (без полной установки MODX/MySQL):
Vue не менялся,
npm run lint:ciне запускался.composer ci:php,composer stan)Конфигурация тестирования:
feat/issue-590-payment-lifecycleотbeta(коммиты11721446,e7dcb7b0,b2ccc755)Скриншоты (если применимо)
N/A, backend/API.
Чеклист
PaymentProviderInterfaceи DefaultPayment без изменений контракта send/receive)composer stan)npm run lint:ci) — N/A, Vue не тронутДополнительные заметки
Контракт провайдера:
PaymentWebhookHandlerInterface::verifyWebhook($rawBody, $payload, $headers, $method)+PaymentLifecycleService. Старые провайдеры с собственным webhook продолжают работать. Новый вход opt-in.Fenom-чанки, которые читали
payment.properties, больше не получают секреты. Это цель AC.Follow-up
b2ccc755закрывает blocker/high из ревью #604: replay на fixed, атомарный commit, currency, stale cost,externalIdдля финансовых событий, read-onlyresolveAttempt, безpartialRefund().Вне scope этого issue: конкретный эквайер, 3DS, UI попыток в менеджере, timeout pending, #569 payment/list.