# F-P1 — Ретеншен ПДн удалённых лидов (анонимизация soft-deleted сделок)

## Цель

Закрыть 152-ФЗ блокер B1/F-P1: телефоны и имена контактов soft-deleted сделок
(`deals.deleted_at IS NOT NULL`) сейчас не вычищаются и хранятся неопределённо
долго. Ввести плановую команду-ретеншен, которая по истечении настраиваемого
срока после удаления анонимизирует ПДн таких сделок, сохраняя строку для
целостности биллинга и аудита.

## Контракт команды {#D1}

Artisan-команда `pd:scrub-soft-deleted-deals` с опцией `--dry-run`.

- Работает cross-tenant через соединение `pgsql_supplier` (BYPASSRLS), как `PdErasureService`.
- Находит сделки `deleted_at IS NOT NULL AND deleted_at < (now − N дней)`, у которых телефон ещё не анонимизирован.
- Анонимизирует пакетным UPDATE; печатает число затронутых строк.
- `--dry-run`: только считает и печатает кандидатов, ничего не меняет.
- Возвращает код успеха (0).

## Источник срока retention {#D2}

Срок N (дней) — из таблицы настроек, ключ `pd_scrub_soft_deleted_deals_days`.

- Ключ отсутствует или значение < 1 → команда не трогает ни одной строки (no-op), печатает «retention не настроен». Код не фиксирует юридический срок — он задаётся на проде.
- Безопасный паттерн, идентичный `partition_retention_months_*` в `PartitionsDropExpired`.

## Правило анонимизации {#D3}

Для каждой подпадающей сделки:

- `phone` → `'+7000XXXXXXX'`
- `contact_name` → `'Удалено'`
- `phones` (JSONB доп. телефонов) → `NULL`
- `updated_at` → now

Значения идентичны `PdErasureService::eraseSubject` (единый контракт анонимизации).
Денежные и связывающие сделку поля (`tenant_id`, `project_id`, ссылки `lead_charges`) не трогаются.

## Edge-cases и идемпотентность {#D4}

- Уже анонимизированные сделки (`phone = '+7000XXXXXXX'`) исключаются из выборки — повторный прогон ничего не меняет.
- Свежеудалённые сделки в пределах окна (`deleted_at >= now − N`) не трогаются.
- Живые сделки (`deleted_at IS NULL`) не трогаются.
- Пустая выборка — корректный no-op с кодом успеха.

## Журналирование {#D5}

После пакетной анонимизации — одна summary-запись в `pd_processing_log`:

- `action` = `deleted`, `purpose` = `152-FZ retention scrub`, `subject_type` = `deal`.
- Системное действие: `actor_tenant_user_id` и `actor_admin_user_id` оба NULL (допускается ограничением `chk_pd_actor`).
- Запись через `pgsql_supplier`; поле `log_hash` заполняется триггером БД (цепочка целостности аудита).

## Индекс и планировщик {#D6}

- Частичный индекс `deals_deleted_at_index ON deals (deleted_at) WHERE deleted_at IS NOT NULL` — для дешёвой выборки кандидатов по партициям. Добавляется Laravel-миграцией и отражается в `db/schema.sql` + `db/CHANGELOG_schema.md`.
- Планировщик: ежедневный запуск `pd:scrub-soft-deleted-deals` в 03:30 МСК с heartbeat-хуками, как `partitions:drop-expired`.

## Критерии приёмки {#D7}

Pest-тест `ScrubSoftDeletedDealsCommandTest` проверяет:

1. Сделка, удалённая раньше окна, анонимизирована (`phone`/`contact_name`/`phones`).
2. Свежеудалённая (в пределах окна) — не тронута.
3. Живая (`deleted_at IS NULL`) — не тронута.
4. Ключ не задан или < 1 → no-op (ничего не анонимизировано).
5. Создана запись в `pd_processing_log` (`action` = `deleted`, системный актор).
6. Повторный прогон идемпотентен (вторая итерация не меняет строк).

```verified-context-json
[
  {"id":"V1","kind":"EXTRACTED","ref":"app/app/Services/Pd/PdErasureService.php","anchor":"public function eraseSubject("},
  {"id":"V2","kind":"EXTRACTED","ref":"app/app/Console/Commands/PartitionsDropExpired.php","anchor":"partition_retention_months_"}
]
```
