# Доработка 152-ФЗ erasure: хирургическая чистка телефона субъекта в сделках

Право субъекта на удаление (ст. 21 152-ФЗ) для портальных сделок (`deals`) сейчас
выполняется неполно: телефон субъекта остаётся в данных, если он записан как
дополнительный телефон в JSONB-массиве `deals.phones` чужой сделки. Эта спека
определяет корректное удаление, не повреждающее данные со-контактов.

## Цель

Удаление ПДн субъекта по телефону должно затрагивать **все** сделки, где этот
телефон присутствует — как в скалярной колонке `deals.phone`, так и внутри
JSONB-массива `deals.phones` (дополнительные телефоны). При этом телефоны
**других** людей в той же сделке должны сохраняться, а скалярные ПДн владельца
чужой сделки — оставаться нетронутыми. Удаление только по email не затрагивает
сделки, так как email в таблице `deals` не хранится.

## D1. Контракт ветки deals в eraseSubject {#D1}

`PdErasureService::eraseSubject($email, $phone, $tenantId, $actorAdminId, $requestId)`
обрабатывает сделки **только когда `$phone !== null`**. Множество затрагиваемых
сделок — это сделки, где (`deals.phone = $phone`) ИЛИ (JSONB-массив `deals.phones`
содержит элемент, равный `$phone`), с опциональным сужением по `tenant_id`, если он
задан. Каждая такая сделка обновляется по своему `id` (таблица партиционирована —
UPDATE по `id` на родительской таблице корректен на PostgreSQL 11+). Счётчик
`counts['deals']` равен числу затронутых сделок.

## D2. Хирургическое удаление из JSONB phones {#D2}

Для каждой затронутой сделки телефон субъекта удаляется из массива `deals.phones`
поэлементно: остаются все элементы, не равные `$phone`. Если после удаления массив
пуст или исходный `phones` был `NULL`, итоговое значение — `NULL`. Остальные
дополнительные телефоны (со-контакты) сохраняются без изменения порядка. Реализация
средствами PostgreSQL: пересборка массива через `JSONB_ARRAY_ELEMENTS` +
`JSONB_AGG` с фильтром `element <> TO_JSONB($phone::text)` и `COALESCE(phones,'[]')`
для NULL-безопасности.

## D3. Атрибуция: владелец сделки против со-контакта {#D3}

Если `deals.phone` сделки равен `$phone` — субъект является владельцем сделки:
его скалярные ПДн анонимизируются (`phone` → маска `+7000XXXXXXX`, `contact_name`
→ `Удалено`) дополнительно к хирургической чистке `phones`. Если же сделка попала
в выборку только по вхождению в JSONB `phones` (скалярный `phone` принадлежит
другому человеку) — скалярные `phone` и `contact_name` **не трогаются**, меняется
лишь массив `phones`. Поле `updated_at` обновляется в обоих случаях.

## D4. email-путь для deals — корректный no-op {#D4}

В таблице `deals` нет колонки email и нет JSONB-поля, хранящего email. Поэтому при
удалении субъекта только по email (`$phone === null`) сделки не подлежат
сопоставлению и остаются нетронутыми; `counts['deals']` равен 0. Это намеренное
поведение (нечего сопоставлять по схеме), а не пропущенный дефект — оно
фиксируется инвариантным тестом и комментарием в коде.

## D5. Критерий приёмки {#D5}

Тесты в `app/tests/Feature/Admin/AdminPdSubjectRequestsControllerTest.php`:

1. **Со-контакт (JSONB-only).** Сделка с `phone` = телефон владельца (другого
   человека) и `phones` = `[телефон_субъекта, телефон_со-контакта]`. После удаления
   по телефону субъекта: скалярный `phone` и `contact_name` сделки **не изменились**;
   массив `phones` **не содержит** телефона субъекта, но **содержит** телефон
   со-контакта; `counts.deals >= 1`.
2. **email no-op.** Удаление только по email при наличии сделки с непустым `phone`:
   сделка не изменена, `counts.deals = 0`.
3. **Сохранение со-контакта у владельца.** Существующий тест чистки `deals.phones`
   усилен: после удаления владельца дополнительный телефон со-контакта в том же
   массиве сохраняется.

Все тесты семейства erasure (`--filter="Erasure"`) зелёные.

```verified-context-json
[
  {
    "id": "svc-erase-method",
    "kind": "EXTRACTED",
    "ref": "app/app/Services/Pd/PdErasureService.php",
    "anchor": "public function eraseSubject("
  },
  {
    "id": "schema-deals-table",
    "kind": "EXTRACTED",
    "ref": "db/schema.sql",
    "anchor": "CREATE TABLE deals ("
  }
]
```
