From dccd2d42728160adf8383d936fdc4cbcf4745255 Mon Sep 17 00:00:00 2001 From: bot-backend Date: Thu, 6 Aug 2026 15:56:02 +0300 Subject: [PATCH] =?UTF-8?q?chore(tradein/privacy):=20=D0=BF=D0=B5=D1=80?= =?UTF-8?q?=D0=B5=D0=BD=D1=83=D0=BC=D0=B5=D1=80=D0=B0=D1=86=D0=B8=D1=8F=20?= =?UTF-8?q?=D0=BC=D0=B8=D0=B3=D1=80=D0=B0=D1=86=D0=B8=D0=B9=20=D0=B8=20mer?= =?UTF-8?q?ge=20main=20-=20=D1=80=D0=B0=D0=B7=D0=B1=D0=BB=D0=BE=D0=BA?= =?UTF-8?q?=D0=B8=D1=80=D0=BE=D0=B2=D0=BA=D0=B0=20PR=20(#2547)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 192/193 -> 229/230: main занял 192_tradein_users_auth.sql и 193_tradein_users_seed.sql за время простоя PR. 228 зарезервирован открытым PR #2732 (228_payments.sql) - следующие реально свободные 229/230, порядок consent_proof -> retention сохранён. Правки ссылок на старые имена/префиксы: docstring-заголовки самих SQL-файлов, перекрёстная ссылка 229 -> 230 в комментарии-докстринге, комментарии migration 192/193 в lead.py / config.py / schemas/trade_in.py / purge_expired_trade_in_data.py, переменные и имена тестов в test_estimate_consent_gate.py / test_purge_expired_trade_in_data.py. (Оставлены нетронутыми ссылки на migration 192/193 в auth_session.py и test_team_api.py - это про другие, уже существующие на main миграции 192_tradein_users_auth.sql / 193_tradein_users_seed.sql, не про эту пару.) --- tradein-mvp/backend/app/api/v1/lead.py | 2 +- tradein-mvp/backend/app/core/config.py | 2 +- tradein-mvp/backend/app/schemas/trade_in.py | 2 +- .../app/tasks/purge_expired_trade_in_data.py | 4 ++-- ... 229_trade_in_estimates_consent_proof.sql} | 4 ++-- ...sql => 230_trade_in_privacy_retention.sql} | 4 ++-- .../tests/test_estimate_consent_gate.py | 24 +++++++++---------- .../tests/test_purge_expired_trade_in_data.py | 24 +++++++++---------- 8 files changed, 33 insertions(+), 33 deletions(-) rename tradein-mvp/backend/data/sql/{192_trade_in_estimates_consent_proof.sql => 229_trade_in_estimates_consent_proof.sql} (98%) rename tradein-mvp/backend/data/sql/{193_trade_in_privacy_retention.sql => 230_trade_in_privacy_retention.sql} (98%) diff --git a/tradein-mvp/backend/app/api/v1/lead.py b/tradein-mvp/backend/app/api/v1/lead.py index 41ebe082..54795e32 100644 --- a/tradein-mvp/backend/app/api/v1/lead.py +++ b/tradein-mvp/backend/app/api/v1/lead.py @@ -118,7 +118,7 @@ async def create_trade_in_lead( # ранее — только audit-лог, #2497 TODO). client_ip может быть None (нет # X-Forwarded-For и request.client) — колонка nullable, CAST(NULL AS inet) валиден. # - # ЭТАП 4 B2C: expires_at (migration 193) — раньше лид хранился бессрочно + # ЭТАП 4 B2C: expires_at (migration 230) — раньше лид хранился бессрочно # (никакого TTL вообще не было, в отличие от trade_in_estimates.expires_at). # Считаем на insert-time тем же паттерном, что estimator.py делает для # trade_in_estimates — retention-период вынесен в settings, не хардкод. diff --git a/tradein-mvp/backend/app/core/config.py b/tradein-mvp/backend/app/core/config.py index e2d55fa3..0e716969 100644 --- a/tradein-mvp/backend/app/core/config.py +++ b/tradein-mvp/backend/app/core/config.py @@ -824,7 +824,7 @@ class Settings(BaseSettings): # к задаче. ENV: TRADE_IN_ESTIMATE_RETENTION_HOURS. trade_in_estimate_retention_hours: int = 24 - # trade_in_leads.expires_at TTL (дни от момента создания, migration 193). + # trade_in_leads.expires_at TTL (дни от момента создания, migration 230). # У trade_in_leads раньше вообще не было срока хранения — лид (телефон + # согласие) жил в БД бессрочно. 180 дней (6 месяцев) — рабочий default для # НЕконвертированных маркетинговых лидов (типичный индустриальный диапазон diff --git a/tradein-mvp/backend/app/schemas/trade_in.py b/tradein-mvp/backend/app/schemas/trade_in.py index 63e33cd5..ad4811d0 100644 --- a/tradein-mvp/backend/app/schemas/trade_in.py +++ b/tradein-mvp/backend/app/schemas/trade_in.py @@ -48,7 +48,7 @@ class TradeInEstimateInput(BaseModel): has_mortgage: bool | None = None # client_name / client_phone удалены (PII purge #1969, DROP COLUMN 167). - # ЭТАП 4 B2C launch — anonymous consent-before-save (152-ФЗ, migration 192). + # ЭТАП 4 B2C launch — anonymous consent-before-save (152-ФЗ, migration 229). # Enforcement (НЕ здесь): app.services.estimator.estimate_quality проверяет # `created_by is None and not consent -> 422` ДО первого INSERT адреса в # trade_in_estimates. Здесь поле намеренно `bool | None = None`, а НЕ diff --git a/tradein-mvp/backend/app/tasks/purge_expired_trade_in_data.py b/tradein-mvp/backend/app/tasks/purge_expired_trade_in_data.py index 17a99508..dcdaad71 100644 --- a/tradein-mvp/backend/app/tasks/purge_expired_trade_in_data.py +++ b/tradein-mvp/backend/app/tasks/purge_expired_trade_in_data.py @@ -1,7 +1,7 @@ """Physically delete expired personal data — ЭТАП 4 B2C retention enforcement (152-ФЗ). WHY: - trade_in_estimates.expires_at (и, начиная с migration 193, trade_in_leads.expires_at) + trade_in_estimates.expires_at (и, начиная с migration 230, trade_in_leads.expires_at) defined a retention window, but neither table had any background job that actually DELETEd rows once expired -- expires_at was used ONLY as a read-time filter (GET /estimate/{id}: "AND expires_at > NOW()"). Personal data (address / phone) @@ -11,7 +11,7 @@ WHY: WHAT: Batched physical DELETE for both tables, run nightly by the kit-scheduler (see app.services.product_handlers._job_purge_expired_trade_in_data, scrape_schedules - row seeded by migration 193 -- seeded enabled=false, see that migration's docstring + row seeded by migration 230 -- seeded enabled=false, see that migration's docstring for why). Same architecture as app/tasks/deactivate_stale_avito.py (sync, DB-only, invoked via run_in_executor from the async kit handler). diff --git a/tradein-mvp/backend/data/sql/192_trade_in_estimates_consent_proof.sql b/tradein-mvp/backend/data/sql/229_trade_in_estimates_consent_proof.sql similarity index 98% rename from tradein-mvp/backend/data/sql/192_trade_in_estimates_consent_proof.sql rename to tradein-mvp/backend/data/sql/229_trade_in_estimates_consent_proof.sql index 78ff74e5..ee3bfbf8 100644 --- a/tradein-mvp/backend/data/sql/192_trade_in_estimates_consent_proof.sql +++ b/tradein-mvp/backend/data/sql/229_trade_in_estimates_consent_proof.sql @@ -1,4 +1,4 @@ --- 192_trade_in_estimates_consent_proof.sql +-- 229_trade_in_estimates_consent_proof.sql -- ЭТАП 4 B2C launch — правовая рамка для анонимных пользователей (152-ФЗ). -- -- WHY: @@ -41,7 +41,7 @@ -- defense-in-depth на случай будущего кода, который забудет про gate). -- -- Индекс на expires_at — обслуживает будущую retention-задачу --- purge_expired_trade_in_data (см. migration 193), которая физически +-- purge_expired_trade_in_data (см. migration 230), которая физически -- удаляет строки, чей expires_at истёк (сегодня expires_at используется -- ТОЛЬКО как read-time фильтр, см. GET /estimate/{id}: "AND expires_at > -- NOW()" — без индекса такой batched-DELETE делал бы full scan таблицы diff --git a/tradein-mvp/backend/data/sql/193_trade_in_privacy_retention.sql b/tradein-mvp/backend/data/sql/230_trade_in_privacy_retention.sql similarity index 98% rename from tradein-mvp/backend/data/sql/193_trade_in_privacy_retention.sql rename to tradein-mvp/backend/data/sql/230_trade_in_privacy_retention.sql index 822a0526..d0d5817b 100644 --- a/tradein-mvp/backend/data/sql/193_trade_in_privacy_retention.sql +++ b/tradein-mvp/backend/data/sql/230_trade_in_privacy_retention.sql @@ -1,4 +1,4 @@ --- 193_trade_in_privacy_retention.sql +-- 230_trade_in_privacy_retention.sql -- ЭТАП 4 B2C launch — retention enforcement (152-ФЗ): срок хранения ДОЛЖЕН -- приводить к физическому удалению, а не быть декоративным полем. -- @@ -47,7 +47,7 @@ -- - INSERT ... ON CONFLICT (source) DO NOTHING — безопасный re-run seed'а. -- -- Dependencies: 172_trade_in_leads.sql (таблица), 052_scrape_schedules.sql --- (scrape_schedules), 192_trade_in_estimates_consent_proof.sql (соседняя +-- (scrape_schedules), 229_trade_in_estimates_consent_proof.sql (соседняя -- часть той же ЭТАП 4 инициативы — индекс на trade_in_estimates.expires_at -- уже создан там). diff --git a/tradein-mvp/backend/tests/test_estimate_consent_gate.py b/tradein-mvp/backend/tests/test_estimate_consent_gate.py index 28a3d13a..4dc9316c 100644 --- a/tradein-mvp/backend/tests/test_estimate_consent_gate.py +++ b/tradein-mvp/backend/tests/test_estimate_consent_gate.py @@ -44,7 +44,7 @@ import pytest from fastapi import HTTPException _SQL_DIR = Path(__file__).resolve().parents[1] / "data" / "sql" -_MIGRATION_192 = _SQL_DIR / "192_trade_in_estimates_consent_proof.sql" +_MIGRATION_229 = _SQL_DIR / "229_trade_in_estimates_consent_proof.sql" def _make_payload(**overrides: Any) -> Any: @@ -288,32 +288,32 @@ def test_api_handler_wires_require_consent_from_auth_header() -> None: assert "require_consent=x_authenticated_user is None" in src -# ── Migration 192 sanity ──────────────────────────────────────────────────────── +# ── Migration 229 sanity ──────────────────────────────────────────────────────── -def test_migration_192_exists() -> None: - assert _MIGRATION_192.is_file(), f"missing migration: {_MIGRATION_192}" +def test_migration_229_exists() -> None: + assert _MIGRATION_229.is_file(), f"missing migration: {_MIGRATION_229}" -def test_migration_192_is_transactional() -> None: - sql = _MIGRATION_192.read_text("utf-8") +def test_migration_229_is_transactional() -> None: + sql = _MIGRATION_229.read_text("utf-8") assert "BEGIN;" in sql assert "COMMIT;" in sql -def test_migration_192_is_idempotent() -> None: - sql = _MIGRATION_192.read_text("utf-8") +def test_migration_229_is_idempotent() -> None: + sql = _MIGRATION_229.read_text("utf-8") assert "ADD COLUMN IF NOT EXISTS consent" in sql assert "ADD COLUMN IF NOT EXISTS client_ip" in sql assert "CREATE INDEX IF NOT EXISTS" in sql assert "pg_constraint" in sql # DO-block guard, not bare ADD CONSTRAINT -def test_migration_192_no_psycopg_trap() -> None: - sql = _MIGRATION_192.read_text("utf-8") +def test_migration_229_no_psycopg_trap() -> None: + sql = _MIGRATION_229.read_text("utf-8") assert not re.search(r":\w+::", sql) -def test_migration_192_check_constraint_allows_null_or_true() -> None: - sql = _MIGRATION_192.read_text("utf-8") +def test_migration_229_check_constraint_allows_null_or_true() -> None: + sql = _MIGRATION_229.read_text("utf-8") assert "consent IS NULL OR consent IS TRUE" in sql diff --git a/tradein-mvp/backend/tests/test_purge_expired_trade_in_data.py b/tradein-mvp/backend/tests/test_purge_expired_trade_in_data.py index e32b4570..20a695e2 100644 --- a/tradein-mvp/backend/tests/test_purge_expired_trade_in_data.py +++ b/tradein-mvp/backend/tests/test_purge_expired_trade_in_data.py @@ -26,7 +26,7 @@ os.environ.setdefault("DATABASE_URL", "postgresql+psycopg://test:test@localhost: from app.tasks import purge_expired_trade_in_data as task_mod _SQL_DIR = Path(__file__).resolve().parents[1] / "data" / "sql" -_MIGRATION_193 = _SQL_DIR / "193_trade_in_privacy_retention.sql" +_MIGRATION_230 = _SQL_DIR / "230_trade_in_privacy_retention.sql" class _FakeResult: @@ -197,29 +197,29 @@ def test_idempotent_zero_rowcount_is_not_an_error(monkeypatch: pytest.MonkeyPatc assert marked["kind"] == "done" -# ── migration 193 ──────────────────────────────────────────────────────────── +# ── migration 230 ──────────────────────────────────────────────────────────── -def test_migration_193_exists() -> None: - assert _MIGRATION_193.is_file(), f"missing migration: {_MIGRATION_193}" +def test_migration_230_exists() -> None: + assert _MIGRATION_230.is_file(), f"missing migration: {_MIGRATION_230}" -def test_migration_193_is_transactional() -> None: - sql = _MIGRATION_193.read_text("utf-8") +def test_migration_230_is_transactional() -> None: + sql = _MIGRATION_230.read_text("utf-8") assert "BEGIN;" in sql assert "COMMIT;" in sql -def test_migration_193_backfills_and_sets_not_null() -> None: - sql = _MIGRATION_193.read_text("utf-8") +def test_migration_230_backfills_and_sets_not_null() -> None: + sql = _MIGRATION_230.read_text("utf-8") assert "ADD COLUMN IF NOT EXISTS expires_at" in sql assert "WHERE expires_at IS NULL" in sql assert "SET NOT NULL" in sql assert "180 days" in sql -def test_migration_193_seeds_purge_schedule_disabled_by_default() -> None: - sql = _MIGRATION_193.read_text("utf-8") +def test_migration_230_seeds_purge_schedule_disabled_by_default() -> None: + sql = _MIGRATION_230.read_text("utf-8") assert "'purge_expired_trade_in_data'" in sql assert "ON CONFLICT (source) DO NOTHING" in sql # Seeded disabled -- first automated PII-DELETE job in trade-in deserves a @@ -227,6 +227,6 @@ def test_migration_193_seeds_purge_schedule_disabled_by_default() -> None: assert re.search(r"'purge_expired_trade_in_data',\s*\n\s*false,", sql) -def test_migration_193_no_psycopg_trap() -> None: - sql = _MIGRATION_193.read_text("utf-8") +def test_migration_230_no_psycopg_trap() -> None: + sql = _MIGRATION_230.read_text("utf-8") assert not re.search(r":\w+::", sql)