[CL-1176] p2pEscrow: полная модель revertPlan — off-chain unwind после on-chain REFUND (L-4) #1183

Merged
andrei merged 2 commits from feature/claude-1176-revertplan into master 2026-07-22 10:00:05 +00:00
Owner

Что сделано

  • Миграция (additive, nullable): p2p_escrows.revertPlan JSONB + revertAppliedAt TIMESTAMP (20260722110309_escrow_revert_plan). Деструктива нет.
  • Захват плана на матче (позже sellerPP невосстановим): execute-trade.ts — обе ветки (SELL-ордер: locked-пропорция lockedPurchasePrice; taker-продавец: прямой sellerPP); auto-match.ts — locked-пропорция sell-стороны, консистентная с buildOrderFillUpdate. buildRevertPlancreateEscrowForTrade(trade, mode, revertPlan) → персист.
  • applyEscrowRefundRevert (p2pEscrow/revert.ts): применение РОВНО один раз после подтверждённого on-chain REFUND — вызов из refund.ts после finalize (non-fatal: refund уже случился, неудачный unwind остаётся retryable, revertAppliedAt NULL). Внутри: FOR UPDATE (escrow → users в детерминированном порядке → buyer ownership), Zod-валидация JSONB-плана, обе ноги в СВОБОДНОЕ состояние по решению #1176 (buyer → balance/coinBalance по currency; seller ownership upsert +shares/+sellerPP; дебеты seller/SYSTEM; reversing-ledger всем трём), W15-B1.5 zero-basis при опустошении позиции, идемпотентность revertAppliedAt.
  • EXPIRED сознательно исключён (уточнение спеки): EXPIRED = on-chain refund так и НЕ прошёл (cron исчерпал ретраи) — off-chain unwind там создал бы ОБРАТНЫЙ дрейф. Admin runbook, план сохранён.
  • Legacy escrow (plan=NULL) — skip, backward-safe. Audit action P2P_ESCROW_REVERTED.
  • Тесты: 10 юнитов revert.test.ts (happy EUR/EURT, идемпотентность, legacy-skip, wrong-status, шорт по долям → 409 без штампа, malformed plan → 500 без мутаций, zero-basis) + план-тест в auto-match.test.ts (sellerPP=locked-пропорция). Vault синхронизирован.

Зачем

Финал L-4 (issue #1176): REFUNDED-эскроу ревертил только on-chain, off-chain обе ноги оставались settled — phantom-доли (дивиденд-leak закрыт ранее гейтом #1181 симптоматически) и невозвращённые деньги. Open question решён сессией 2026-07-22 (комментарий в issue): свободный balance, без реанимации ордеров — вечная заморозка в lockedBalance у COMPLETED-ордера недостижима ни одним переходом.

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

  • TDD: red (модуль отсутствовал) → 9/9 green → +план-тест auto-match red→green.
  • P2P-сьюты: 181 passed; полный API-suite: exit 0; tsc --noEmit чисто; eslint 0 warnings; pre-commit гейты (audit-trail, migration-guard) пройдены.

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

  • Rule 118 / Rule 103.1: Prisma schema + financial logic — требуется CEO sign-off, сам не мержу.
  • Продавец мог потратить proceeds → дебет упрётся в DB CHECK (>=0) → транзакция откатится целиком, CRITICAL-лог, retryable/manual — строгий fail вместо частичного применения (осознанно).
  • sellerPP по locked-пропорции наследует класс 2-dp дрейфа W15-B1.5 (центы на многочастичных продажах) — компенсируется zero-basis паттерном на опустошении.
  • Существующие REFUNDED-эскроу до миграции не разворачиваются автоматически (plan=NULL) — их закрывает ручная reconciliation, как и раньше.

Closes #1176

## Что сделано - **Миграция (additive, nullable):** `p2p_escrows.revertPlan JSONB` + `revertAppliedAt TIMESTAMP` (`20260722110309_escrow_revert_plan`). Деструктива нет. - **Захват плана на матче** (позже sellerPP невосстановим): `execute-trade.ts` — обе ветки (SELL-ордер: locked-пропорция `lockedPurchasePrice`; taker-продавец: прямой `sellerPP`); `auto-match.ts` — locked-пропорция sell-стороны, консистентная с `buildOrderFillUpdate`. `buildRevertPlan` → `createEscrowForTrade(trade, mode, revertPlan)` → персист. - **`applyEscrowRefundRevert`** (`p2pEscrow/revert.ts`): применение РОВНО один раз после подтверждённого on-chain REFUND — вызов из `refund.ts` после finalize (non-fatal: refund уже случился, неудачный unwind остаётся retryable, `revertAppliedAt` NULL). Внутри: FOR UPDATE (escrow → users в детерминированном порядке → buyer ownership), Zod-валидация JSONB-плана, обе ноги в **СВОБОДНОЕ** состояние по решению #1176 (buyer → `balance`/`coinBalance` по currency; seller ownership upsert +shares/+sellerPP; дебеты seller/SYSTEM; reversing-ledger всем трём), W15-B1.5 zero-basis при опустошении позиции, идемпотентность `revertAppliedAt`. - **EXPIRED сознательно исключён** (уточнение спеки): EXPIRED = on-chain refund так и НЕ прошёл (cron исчерпал ретраи) — off-chain unwind там создал бы ОБРАТНЫЙ дрейф. Admin runbook, план сохранён. - Legacy escrow (plan=NULL) — skip, backward-safe. Audit action `P2P_ESCROW_REVERTED`. - Тесты: 10 юнитов `revert.test.ts` (happy EUR/EURT, идемпотентность, legacy-skip, wrong-status, шорт по долям → 409 без штампа, malformed plan → 500 без мутаций, zero-basis) + план-тест в `auto-match.test.ts` (sellerPP=locked-пропорция). Vault синхронизирован. ## Зачем Финал L-4 (issue #1176): REFUNDED-эскроу ревертил только on-chain, off-chain обе ноги оставались settled — phantom-доли (дивиденд-leak закрыт ранее гейтом #1181 симптоматически) и невозвращённые деньги. Open question решён сессией 2026-07-22 (комментарий в issue): свободный balance, без реанимации ордеров — вечная заморозка в lockedBalance у COMPLETED-ордера недостижима ни одним переходом. ## План тестирования - TDD: red (модуль отсутствовал) → 9/9 green → +план-тест auto-match red→green. - P2P-сьюты: 181 passed; **полный API-suite: exit 0**; `tsc --noEmit` чисто; eslint 0 warnings; pre-commit гейты (audit-trail, migration-guard) пройдены. ## Где могу ошибаться - **Rule 118 / Rule 103.1:** Prisma schema + financial logic — **требуется CEO sign-off, сам не мержу.** - Продавец мог потратить proceeds → дебет упрётся в DB CHECK (>=0) → транзакция откатится целиком, CRITICAL-лог, retryable/manual — строгий fail вместо частичного применения (осознанно). - sellerPP по locked-пропорции наследует класс 2-dp дрейфа W15-B1.5 (центы на многочастичных продажах) — компенсируется zero-basis паттерном на опустошении. - Существующие REFUNDED-эскроу до миграции не разворачиваются автоматически (plan=NULL) — их закрывает ручная reconciliation, как и раньше. Closes #1176
[CL-1176] p2pEscrow: полная модель revertPlan — off-chain unwind после on-chain REFUND (L-4)
Some checks failed
CI / App (pull_request) Failing after 1m46s
CI / Telegram Mini App (pull_request) Successful in 2m27s
CI / API (pull_request) Failing after 2m27s
CI / Python SDK (pull_request) Successful in 43s
CI / Secrets Scan (pull_request) Successful in 13s
CI / Contracts (pull_request) Successful in 2m55s
CI / Prisma Migrate Gate (pull_request) Successful in 2m8s
b0399f3f91
Миграция (additive): p2p_escrows.revertPlan JSONB + revertAppliedAt.
План собирается НА МАТЧЕ из уже вычисленных дельт (sellerPP позже невосстановим):
execute-trade (обе ветки: locked-пропорция SELL-ордера / прямой decrement),
auto-match (locked-пропорция sell-стороны) → buildRevertPlan → персист в
createEscrowForTrade. applyEscrowRefundRevert (p2pEscrow/revert.ts) применяет
разворот РОВНО один раз после подтверждённого on-chain REFUND: обе ноги в
СВОБОДНОЕ состояние (решение #1176 — buyer на balance/coinBalance по currency,
ордера не реанимируются), reversing-ledger всем трём сторонам, FOR UPDATE в
детерминированном порядке, Zod-валидация JSONB, идемпотентность revertAppliedAt.
EXPIRED сознательно исключён: там on-chain разворота НЕ было — off-chain unwind
создал бы обратный дрейф (admin runbook, план сохранён). Legacy escrow
(plan=NULL) — skip, backward-safe. Closes #1176
andrei force-pushed feature/claude-1176-revertplan from b0399f3f91
Some checks failed
CI / App (pull_request) Failing after 1m46s
CI / Telegram Mini App (pull_request) Successful in 2m27s
CI / API (pull_request) Failing after 2m27s
CI / Python SDK (pull_request) Successful in 43s
CI / Secrets Scan (pull_request) Successful in 13s
CI / Contracts (pull_request) Successful in 2m55s
CI / Prisma Migrate Gate (pull_request) Successful in 2m8s
to 3f5dd3d783
Some checks failed
CI / Contracts (pull_request) Successful in 2m20s
CI / Telegram Mini App (pull_request) Successful in 1m42s
CI / Python SDK (pull_request) Successful in 27s
CI / Secrets Scan (pull_request) Successful in 12s
CI / Prisma Migrate Gate (pull_request) Successful in 1m51s
CI / API (pull_request) Failing after 18m11s
CI / App (pull_request) Successful in 21m25s
2026-07-22 08:49:40 +00:00
Compare
[CL-1176] tests: mock-sync post-settlement — createEscrowForTrade теперь получает revertPlan третьим аргументом
All checks were successful
CI / Contracts (pull_request) Successful in 3m0s
CI / Telegram Mini App (pull_request) Successful in 1m55s
CI / Python SDK (pull_request) Successful in 30s
CI / Secrets Scan (pull_request) Successful in 13s
CI / Prisma Migrate Gate (pull_request) Successful in 1m46s
CI / API (pull_request) Successful in 20m52s
CI / App (pull_request) Successful in 23m42s
aed34d6da5
Rule E промах первого прохода: тест лежит в services/__tests__/, а не в
p2p-trade/__tests__/ — grep-скоуп его не покрыл, поймал CI. Фикстура дополнена
match-дельтами, ассерты ожидают план.
andrei merged commit bc32780695 into master 2026-07-22 10:00:05 +00:00
andrei deleted branch feature/claude-1176-revertplan 2026-07-22 10:00: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!1183
No description provided.