feat(gateway,account): возврат вступительного взноса при отказе совета #97

Merged
ant merged 8 commits from feat/registration-refund into dev 2026-06-06 06:49:04 +00:00
Owner

Stacked на #95 (feat/registration-fsm). После последовательного мерджа (#91→#95→dev) перетаргетится на dev.

Суть

При отказе совета в приёме пайщика взнос по фондам ещё не разносился (confirmreg не было), но деньги физически у кооператива. Заводим исходящий платёж-возврат в реестр.

  • Новый PaymentType REGISTRATION_REFUND (исходящий), отдельный от WITHDRAWAL.
  • RegistrationDeclineListener на action::registrator::declinereg → исходящий PENDING-платёж, назначение «Возврат вступительного и мин.паевого взноса №<тот же № из QR>», идемпотентно.
  • Подтверждение возврата кассиром — off-chain no-op (COMPLETED, без on-chain проводок). Обратная проводка появится здесь же, когда введём проводки на приёме платежа.
  • В реестре спрятана кнопка «Отклонить» для возврата.
  • Codegen проброшен в Zeus.

Зависимость

Триггер declinereg (немедленный отказ по негативному консенсусу / истечению срока) даёт PR #94 (feat/soviet-negative-consensus) — мерджится в dev отдельно. Эта ветка #94 НЕ содержит (история чистая); для сквозного e2e нужен #94 в dev.

🤖 Generated with Claude Code

Stacked на #95 (feat/registration-fsm). После последовательного мерджа (#91→#95→dev) перетаргетится на dev. ## Суть При отказе совета в приёме пайщика взнос по фондам ещё не разносился (confirmreg не было), но деньги физически у кооператива. Заводим исходящий платёж-возврат в реестр. - Новый PaymentType REGISTRATION_REFUND (исходящий), отдельный от WITHDRAWAL. - RegistrationDeclineListener на action::registrator::declinereg → исходящий PENDING-платёж, назначение «Возврат вступительного и мин.паевого взноса №<тот же № из QR>», идемпотентно. - Подтверждение возврата кассиром — off-chain no-op (COMPLETED, без on-chain проводок). Обратная проводка появится здесь же, когда введём проводки на приёме платежа. - В реестре спрятана кнопка «Отклонить» для возврата. - Codegen проброшен в Zeus. ## Зависимость Триггер declinereg (немедленный отказ по негативному консенсусу / истечению срока) даёт PR #94 (feat/soviet-negative-consensus) — мерджится в dev отдельно. Эта ветка #94 НЕ содержит (история чистая); для сквозного e2e нужен #94 в dev. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
ant requested changes 2026-06-05 11:19:49 +00:00
Dismissed
@@ -0,0 +30,4 @@
@Inject(CANDIDATE_REPOSITORY) private readonly candidateRepository: CandidateRepository,
) {}
@OnEvent('action::registrator::declinereg')
Owner

тут через константы из Cooptypes подписываться надо!! Смотри как в других местах и запомни себе уже

тут через константы из Cooptypes подписываться надо!! Смотри как в других местах и запомни себе уже
claude changed target branch from feat/registration-fsm to dev 2026-06-05 19:08:12 +00:00
claude added 4 commits 2026-06-05 19:08:12 +00:00
Plan B (recovery + источник истины о шаге на сервере), поверх гарда A.

Backend:
- resetRegistration (self-scoped по токену): откат незавершённой регистрации
  к редактированию до отправки в блокчейн. Снимает заморозку профиля/e-mail
  (users.status → created), чистит документы кандидата и отпечаток заявления,
  удаляет непринятую попытку вступительного платежа. Принятые средства
  (PAID/COMPLETED) или уже созданный on-chain аккаунт → отказ (нужна
  перерегистрация/возврат — отдельная ветка).
- PaymentRepository.delete.

Frontend:
- SignUp реконструирует шаг с сервера: Joined → шаг оплаты (переживает
  перезагрузку и работу в другой вкладке), приоритет у registration_payment.
- WaitingRegistration «Исправить данные» дёргает resetRegistration перед
  возвратом к данным (после подписи профиль заморожен, локального отката мало).
- Фича Account/ResetRegistration + Zeus-клиент (codegen).

Существующие статусы (joined/registered/active) не меняем, новых не вводим:
фронтовые active-гейты висят на on-chain user_account.status — другая ось,
не пересекается с mono users.status, который трогает recovery.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Провайдер QR (Bank) не эмитит success-колбэк, деньги подтверждаются вебхуком
на бэке асинхронно — без поллинга экран висел на форме оплаты до перезагрузки.
Каждые 10с подтягиваем аккаунт; как только вступительный платёж покинул PENDING
(принят/отклонён) — уходим на шаг ожидания решения совета, который сам
показывает «платёж принят» либо причину отказа.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
При отказе совета в приёме пайщика (отрицательный консенсус >50% — PR #94 —
или истечение срока повестки) on-chain срабатывает registrator::declinereg.
Взнос по фондам на этот момент НЕ разносился (confirmreg не было), но реальные
деньги пайщика уже у кооператива на расчётном счёте — их нужно вернуть.

- Новый PaymentTypeEnum.REGISTRATION_REFUND (исходящий), отдельный от WITHDRAWAL
  (возврат паевого), чтобы не путать термины. Лейблы + OUTGOING_PAYMENT_TYPES.
- RegistrationDeclineListener на action::registrator::declinereg: резолвит
  кандидата по registration_hash, по исходному REGISTRATION-платежу берёт сумму
  и номер (hash8 = тот же № из QR), заводит исходящий платёж PENDING с
  назначением «Возврат вступительного и мин.паевого взноса №<hash8>».
  Идемпотентно (повторный приход события не плодит возвраты).
- processOutgoingPayment: для REGISTRATION_REFUND подтверждение кассиром —
  off-chain no-op (статус → COMPLETED, без completeOutcome). Взнос on-chain не
  двигался, откатывать нечего; обратная проводка по расчётному счёту появится
  здесь же, когда введём проводки на приёме платежа.
- Реестр платежей: для возврата спрятана кнопка «Отклонить» (отказать в
  возврате нельзя — совет уже отказал, деньги обязаны вернуть).
- Codegen: PaymentType.REGISTRATION_REFUND проброшен в Zeus (sdk + controller).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
По ревью @ant: не хардкодить 'action::registrator::declinereg' строкой.
Добавил action-константу RegistratorContract.Actions.DeclineRegistration
(actionName='declinereg', тип IDeclinereg) в cooptypes — её не было.
Листенер подписывается через
action::${RegistratorContract.contractName.production}::${...DeclineRegistration.actionName}
по образцу signed-document-ingestion (SovietContract).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
claude added 1 commit 2026-06-05 19:37:03 +00:00
merge dev into feat/registration-refund
Typecheck / desktop (pull_request) Successful in 13m36s
Typecheck / controller (pull_request) Successful in 13m16s
e3983c3d01
Конфликт только в сгенерированном Zeus-клиенте (controller/zeus + sdk/zeus) — разрешён
перегенерацией codegen, не ручной разводкой:
- пересобран cooptypes dist (протух: DeclineRegistration был в src, не в dist → TS2339)
- generate-schema (56 резолверов) + generate-client
- zeus теперь надмножество: dev + fsm-recovery (resetRegistration) + refund (REGISTRATION_REFUND)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
claude added 1 commit 2026-06-06 06:09:55 +00:00
fix(account): declinereg не находил кандидата из-за регистра registration_hash
Typecheck / controller (pull_request) Has been cancelled
Typecheck / desktop (pull_request) Has been cancelled
e2cf85dd6b
В БД registration_hash хранится lowercase (sha256().digest('hex')), а из
цепи checksum256 в declinereg.registration_hash приходит UPPERCASE — exact-match
findOneBy не находил кандидата, возврат взноса пропускался с warn.
Нормализуем входящий хэш .toLowerCase() в findByRegistrationHash.

Проверено на voskhod: candidate czeudubzzuql имеет registration_hash
f25ca396... (lowercase), warn пришёл с F25CA396... (uppercase) — тот же хэш.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
claude added 1 commit 2026-06-06 06:22:43 +00:00
feat(account,desktop): назначение возврата взноса — зеркало входящего + показ на развороте
Typecheck / desktop (pull_request) Successful in 13m31s
Typecheck / controller (pull_request) Has been cancelled
75a8068970
Backend: memo возвратного платежа теперь зеркалит назначение входящего взноса —
«Возврат вступительного и минимального паевого взносов №XXXX. Без НДС.» с тем же
номером (original.hash.slice(0,8), как в QR при приёме) и НДС-оговоркой, что
qrpay/sberpoll добавляют к Purpose. Кассир копирует as-is в платёжку.

Desktop: PaymentDetails показывал «Назначение платежа» с кнопкой копирования
только в ветке OUTGOING+payment_details. Возврат идёт без payment_details и падал
в «нет дополнительной информации». Добавлена ветка: если есть payment.memo —
показываем его через CopyableInput (с копированием).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
claude added 1 commit 2026-06-06 06:38:54 +00:00
fix(gateway,notifications): возврат взноса слал письмо «платёж принят»
Typecheck / desktop (pull_request) Successful in 13m24s
Typecheck / controller (pull_request) Successful in 13m15s
9e64238682
notifyPaymentStatus выбирал workflow только по статусу (PAID → PaymentPaid
«Платёж принят»), игнорируя direction. Исходящий PAID — это возврат взноса
пайщику, и он получал письмо как на приём. Баг общий для всех возвратов
(регистрационный + паевой — оба идут через gateway.setPaymentStatus).

- Новый workflow notifications/payment-refunded «Возврат взноса выполнен»
  (зеркало payment-paid, тот же payload).
- payment-notification.service: OUTGOING+PAID → PaymentRefunded, INCOMING+PAID →
  PaymentPaid, остальное → PaymentCancelled.

Workflow засинкан в Novu (pnpm sync, 24 workflow, 0 ошибок).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ant approved these changes 2026-06-06 06:48:58 +00:00
ant merged commit 41c77b3b0f into dev 2026-06-06 06:49:04 +00:00
Sign in to join this conversation.
No Reviewers
No Label
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: C9S/mono#97