[CL-1180] income-distribution: settled-gate на snapshot дивидендов (L-4 промежуточный) #1181

Merged
andrei merged 1 commit from feature/claude-1176-l4-dividend-settled-gate into master 2026-07-21 13:15:05 +00:00
Owner

Что сделано

  • api/src/services/p2p-order/settled-shares.ts: новый bulk-хелпер getUnsettledEscrowSharesByUser(tx, objectId) — один groupBy(buyerId) по non-SETTLED эскроу объекта (та же семантика, что существующий A1-GATE getUnsettledEscrowShares).
  • api/src/services/income-distribution/reports.ts: record-date snapshot в createMonthlyReport вычитает per-user unsettled-доли (clamp 0, нулевые холдеры выпадают). perShare и accrual-база считаются от settled-долей — netProfit сохраняется by construction.
  • Тесты: 4 новых кейса в reports.test.ts (вычитание, phantom-холдер из REFUNDED эскроу, clamp, скоуп groupBy) + mock-sync в income-distribution.test.ts.
  • Vault: docs/vault/modules/api-p2p.md — заметка о распространении settled-gate.

Зачем

Находка аудита L-4 (issue #1176): share_ownerships кредитуется при match, ДО settlement эскроу; при REFUNDED/EXPIRED доли остаются в ownership до ручной reconciliation. income-distribution/preparation.ts начислял дивиденды на эти phantom-доли. Это промежуточный безопасный фикс из issue — закрывает дивиденд-leak без разворота трейда. Полная модель (revertPlan, миграция, revert обеих ног) остаётся за #1176 и ждёт решения по open question.

Гейт стоит в момент СОЗДАНИЯ снапшота, а не дистрибуции: perShare выводится из суммы snapshot-долей, поэтому вычитание на этапе дистрибуции разбалансировало бы residual-инвариант SUM(accruals) = netProfit (W12-B2.2) и могло абортить дистрибуцию.

План тестирования

  • pnpm exec vitest run src/services/income-distribution/__tests__/reports.test.ts — 7/7 (TDD: 4 новых сперва red).
  • Затронутые сьюты (income-distribution, p2p-order, admin.controller, workers) — 269 passed / 0 failed.
  • Полный API-suite — exit 0. tsc --noEmit чисто, eslint чисто.

Где могу ошибаться

  • Семантика «эскроу pending на record-date, settle позже» — покупатель пропускает дивиденд этого месяца, доля перераспределяется на остальных холдеров. Считаю корректным для settled-only начисления, но это изменение распределения, не только фикс leak.
  • Существующие DRAFT-отчёты со старым (phantom) снапшотом фикс не трогает — гейт действует только на новые отчёты. Старые закроет полный revert из #1176.

Ref #1176 (не закрывает).

## Что сделано - `api/src/services/p2p-order/settled-shares.ts`: новый bulk-хелпер `getUnsettledEscrowSharesByUser(tx, objectId)` — один `groupBy(buyerId)` по non-SETTLED эскроу объекта (та же семантика, что существующий A1-GATE `getUnsettledEscrowShares`). - `api/src/services/income-distribution/reports.ts`: record-date snapshot в `createMonthlyReport` вычитает per-user unsettled-доли (clamp 0, нулевые холдеры выпадают). perShare и accrual-база считаются от settled-долей — netProfit сохраняется by construction. - Тесты: 4 новых кейса в `reports.test.ts` (вычитание, phantom-холдер из REFUNDED эскроу, clamp, скоуп groupBy) + mock-sync в `income-distribution.test.ts`. - Vault: `docs/vault/modules/api-p2p.md` — заметка о распространении settled-gate. ## Зачем Находка аудита L-4 (issue #1176): `share_ownerships` кредитуется при match, ДО settlement эскроу; при REFUNDED/EXPIRED доли остаются в ownership до ручной reconciliation. `income-distribution/preparation.ts` начислял дивиденды на эти phantom-доли. Это промежуточный безопасный фикс из issue — закрывает дивиденд-leak без разворота трейда. Полная модель (revertPlan, миграция, revert обеих ног) остаётся за #1176 и ждёт решения по open question. Гейт стоит в момент СОЗДАНИЯ снапшота, а не дистрибуции: perShare выводится из суммы snapshot-долей, поэтому вычитание на этапе дистрибуции разбалансировало бы residual-инвариант `SUM(accruals) = netProfit` (W12-B2.2) и могло абортить дистрибуцию. ## План тестирования - `pnpm exec vitest run src/services/income-distribution/__tests__/reports.test.ts` — 7/7 (TDD: 4 новых сперва red). - Затронутые сьюты (income-distribution, p2p-order, admin.controller, workers) — 269 passed / 0 failed. - Полный API-suite — exit 0. `tsc --noEmit` чисто, eslint чисто. ## Где могу ошибаться - Семантика «эскроу pending на record-date, settle позже» — покупатель пропускает дивиденд этого месяца, доля перераспределяется на остальных холдеров. Считаю корректным для settled-only начисления, но это изменение распределения, не только фикс leak. - Существующие DRAFT-отчёты со старым (phantom) снапшотом фикс не трогает — гейт действует только на новые отчёты. Старые закроет полный revert из #1176. Ref #1176 (не закрывает).
[CL-1180] income-distribution: settled-gate на snapshot дивидендов (L-4 промежуточный)
All checks were successful
CI / Contracts (pull_request) Successful in 2m49s
CI / Python SDK (pull_request) Successful in 31s
CI / Secrets Scan (pull_request) Successful in 15s
CI / Telegram Mini App (pull_request) Successful in 1m40s
CI / Prisma Migrate Gate (pull_request) Successful in 2m9s
CI / API (pull_request) Successful in 20m35s
CI / App (pull_request) Successful in 23m12s
871d67c2ac
Дивиденды начислялись на phantom-доли: share_ownerships кредитуется при match,
до settlement эскроу; REFUNDED/EXPIRED эскроу оставляет доли в ownership до
reconciliation, и record-date snapshot включал их в базу начисления.

Snapshot в createMonthlyReport теперь вычитает per-user доли из non-SETTLED
эскроу (bulk groupBy getUnsettledEscrowSharesByUser — та же семантика, что
A1-GATE на sell-путях). perShare и accrual-база считаются от settled-долей,
netProfit сохраняется by construction. Полный trade-unwind (revertPlan) — #1176.
andrei merged commit 8279d8b643 into master 2026-07-21 13:15:05 +00:00
andrei deleted branch feature/claude-1176-l4-dividend-settled-gate 2026-07-21 13:15:05 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
europa-tech-srl/europatech!1181
No description provided.