fix(capital): регистрация долей Благороста при создании проекта по событию (не ждать крон) #151

Merged
ant merged 2 commits from fix/capital-share-onproject-event into dev 2026-06-16 08:15:30 +00:00
Owner

Проблема

Доли пайщиков Благороста (regshare) регистрировались в проект двумя путями:

  1. периодический ProgramShareRegistrationSchedulerService (крон), и
  2. точечный листенер на дельту кошелька ledger2::userwallets[w.cap.blago] — но он реагирует только на изменение баланса.

Создание нового проекта баланс не меняет → wallet-листенер молчит, и наполнение нового проекта существующими пайщиками зависело только от крона. Если проект быстро прогнали … → result до тика крона — пайщики в него не попадали. Откат result → active контракт не допускает, поэтому доли в компоненте терялись безвозвратно.

Инцидент: voskhod, компонент 011bcd92… (id 53) создан 2026-06-16 06:14 и за ~45 мин уведён в result; в сегментах оказались только 2 автора, а 5 пайщиков с долями Благороста (ipesgnlxmnwx ~17.1M, mrgpikzesygk, spnpcpshemqp, ndrlqjeptxhh, lfyoloxkxwjc) — нет.

Что сделано

  • Новый листенер ProgramShareRegistrationOnProjectDeltaListener на delta::capital::projects: как только проект оказывается в статусе active — сразу регистрирует доли всех активных пайщиков. В pending не заводим — заход только в активные проекты; при переходе pending → active придёт новая дельта со status=active, на ней и зайдём. Неблокирующий (fire-and-forget, как wallet-листенер), промахи подбирает крон-бэкстоп.
  • ProgramShareRegistrationService.syncProgramSharesForProject(coopname, project_hash) — обход пайщиков по одному проекту; syncContributor переведён с ProjectDomainEntity[] на projectHashes: string[]. Тот же код-путь, что и обход по расписанию.
  • findActiveProjects (общий путь крона и wallet-листенера) сужен до active — pending больше не участвует нигде.
  • Крон оставлен как reconciliation-бэкстоп. Выяснено: он не только заводит доли, но и ДОобновляет уже зарегистрированные при дрейфе баланса (if (user_shares != g.capital_contributor_shares) в upsert_contributor_segment — контракт это допускает), а также подбирает события, потерянные при downtime. Поэтому сносить его нельзя.

Контракт regshare оставлен пермиссивным (pending|active) — контроллер просто строже и заводит долю только в active.

Тесты

tests/unit/capital/program-share-registration-on-project-delta.listener.test.ts — гейтинг: active → регистрирует; pending / result / present=false / чужой scope → нет; ошибка сервиса не пробрасывается. 6/6 зелёные локально (--runInBand).

Без codegen

Изменения чисто backend-внутренние (NestJS листенер/сервис), GraphQL-схема не менялась → SDK regen не требуется.

## Проблема Доли пайщиков **Благороста** (`regshare`) регистрировались в проект двумя путями: 1. периодический `ProgramShareRegistrationSchedulerService` (крон), и 2. точечный листенер на дельту кошелька `ledger2::userwallets[w.cap.blago]` — но он реагирует **только на изменение баланса**. Создание нового проекта баланс не меняет → wallet-листенер молчит, и наполнение **нового** проекта существующими пайщиками зависело **только от крона**. Если проект быстро прогнали `… → result` до тика крона — пайщики в него не попадали. Откат `result → active` контракт не допускает, поэтому доли в компоненте терялись **безвозвратно**. **Инцидент:** voskhod, компонент `011bcd92…` (id 53) создан 2026-06-16 06:14 и за ~45 мин уведён в `result`; в сегментах оказались только 2 автора, а 5 пайщиков с долями Благороста (`ipesgnlxmnwx` ~17.1M, `mrgpikzesygk`, `spnpcpshemqp`, `ndrlqjeptxhh`, `lfyoloxkxwjc`) — нет. ## Что сделано - **Новый листенер** `ProgramShareRegistrationOnProjectDeltaListener` на `delta::capital::projects`: как только проект оказывается в статусе **`active`** — сразу регистрирует доли всех активных пайщиков. В `pending` **не** заводим — заход только в активные проекты; при переходе `pending → active` придёт новая дельта со `status=active`, на ней и зайдём. Неблокирующий (fire-and-forget, как wallet-листенер), промахи подбирает крон-бэкстоп. - `ProgramShareRegistrationService.syncProgramSharesForProject(coopname, project_hash)` — обход пайщиков по одному проекту; `syncContributor` переведён с `ProjectDomainEntity[]` на `projectHashes: string[]`. Тот же код-путь, что и обход по расписанию. - `findActiveProjects` (общий путь крона и wallet-листенера) сужен до **`active`** — pending больше не участвует нигде. - **Крон оставлен** как reconciliation-бэкстоп. Выяснено: он не только заводит доли, но и **ДОобновляет** уже зарегистрированные при дрейфе баланса (`if (user_shares != g.capital_contributor_shares)` в `upsert_contributor_segment` — контракт это допускает), а также подбирает события, потерянные при downtime. Поэтому сносить его нельзя. Контракт `regshare` оставлен пермиссивным (`pending|active`) — контроллер просто строже и заводит долю только в active. ## Тесты `tests/unit/capital/program-share-registration-on-project-delta.listener.test.ts` — гейтинг: active → регистрирует; pending / result / present=false / чужой scope → нет; ошибка сервиса не пробрасывается. 6/6 зелёные локально (`--runInBand`). ## Без codegen Изменения чисто backend-внутренние (NestJS листенер/сервис), GraphQL-схема не менялась → SDK regen не требуется.
claude added 1 commit 2026-06-16 07:30:45 +00:00
Доли пайщиков Благороста регистрировались в проект только периодическим
scheduler'ом (regshare) и точечным листенером на дельту кошелька. Новый
проект баланс не меняет → wallet-листенер молчит, и наполнение нового
проекта зависело ТОЛЬКО от крона. Если компонент быстро прогнали
pending→…→result до тика крона, пайщики в него не попадали, а откат
result→active контракт не допускает — доли в компоненте терялись
безвозвратно (voskhod, компонент 011bcd92…, 2026-06-16).

- Новый ProgramShareRegistrationOnProjectDeltaListener на
  delta::capital::projects: при появлении проекта в статусе pending|active
  сразу регистрирует доли всех активных пайщиков (неблокирующе, fire-and-forget,
  переиспользуя ядро syncContributor — тот же путь, что и крон).
- ProgramShareRegistrationService.syncProgramSharesForProject — обход пайщиков
  по одному проекту; syncContributor переведён с ProjectDomainEntity[] на
  projectHashes: string[].
- Крон ОСТАВЛЕН как reconciliation-бэкстоп: он ещё и ДОобновляет уже
  зарегистрированные доли при дрейфе баланса (контракт upsert_contributor_segment
  это допускает) и подбирает события, потерянные при downtime контроллера.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
claude added 1 commit 2026-06-16 08:04:12 +00:00
fix(capital): заводить доли только в active-проектах, не в pending
Typecheck / desktop (pull_request) Has been cancelled
Typecheck / controller (pull_request) Has been cancelled
3d04f76446
По решению пользователя заход долей Благороста — только в активные проекты.
В pending больше не заводим: при переходе pending→active придёт новая дельта
capital::projects со status=active, на ней листенер и зарегистрирует.

- Листенер ProgramShareRegistrationOnProjectDeltaListener: гейт pending|active → только active.
- ProgramShareRegistrationService.findActiveProjects (путь крона и wallet-листенера): фильтр pending|active → только active.
- Тест: pending теперь НЕ реагирует; active — позитивный кейс.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ant approved these changes 2026-06-16 08:15:25 +00:00
ant merged commit d7e08785eb into dev 2026-06-16 08:15:30 +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#151