From 3ee99efaa44d2a094e326502bd22b2a5863140fb Mon Sep 17 00:00:00 2001 From: bot-backend Date: Thu, 6 Aug 2026 19:10:27 +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?231=20=D0=B8=20merge=20main=20-=20=D0=BA=D0=BE=D0=BB=D0=BB?= =?UTF-8?q?=D0=B8=D0=B7=D0=B8=D1=8F=20=D0=BF=D1=80=D0=B5=D1=84=D0=B8=D0=BA?= =?UTF-8?q?=D1=81=D0=B0=20(#2547)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tradein-mvp/backend/app/api/v1/lead.py | 2 +- tradein-mvp/backend/app/core/config.py | 2 +- .../app/tasks/purge_expired_trade_in_data.py | 4 ++-- .../229_trade_in_estimates_consent_proof.sql | 2 +- ...sql => 231_trade_in_privacy_retention.sql} | 2 +- .../tests/test_purge_expired_trade_in_data.py | 24 +++++++++---------- 6 files changed, 18 insertions(+), 18 deletions(-) rename tradein-mvp/backend/data/sql/{230_trade_in_privacy_retention.sql => 231_trade_in_privacy_retention.sql} (99%) diff --git a/tradein-mvp/backend/app/api/v1/lead.py b/tradein-mvp/backend/app/api/v1/lead.py index 54795e32..6ac5ba07 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 230) — раньше лид хранился бессрочно + # ЭТАП 4 B2C: expires_at (migration 231) — раньше лид хранился бессрочно # (никакого 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 0e716969..84945e41 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 230). + # trade_in_leads.expires_at TTL (дни от момента создания, migration 231). # У trade_in_leads раньше вообще не было срока хранения — лид (телефон + # согласие) жил в БД бессрочно. 180 дней (6 месяцев) — рабочий default для # НЕконвертированных маркетинговых лидов (типичный индустриальный диапазон 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 dcdaad71..32dba363 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 230, trade_in_leads.expires_at) + trade_in_estimates.expires_at (и, начиная с migration 231, 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 230 -- seeded enabled=false, see that migration's docstring + row seeded by migration 231 -- 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/229_trade_in_estimates_consent_proof.sql b/tradein-mvp/backend/data/sql/229_trade_in_estimates_consent_proof.sql index ee3bfbf8..482f9405 100644 --- a/tradein-mvp/backend/data/sql/229_trade_in_estimates_consent_proof.sql +++ b/tradein-mvp/backend/data/sql/229_trade_in_estimates_consent_proof.sql @@ -41,7 +41,7 @@ -- defense-in-depth на случай будущего кода, который забудет про gate). -- -- Индекс на expires_at — обслуживает будущую retention-задачу --- purge_expired_trade_in_data (см. migration 230), которая физически +-- purge_expired_trade_in_data (см. migration 231), которая физически -- удаляет строки, чей 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/230_trade_in_privacy_retention.sql b/tradein-mvp/backend/data/sql/231_trade_in_privacy_retention.sql similarity index 99% rename from tradein-mvp/backend/data/sql/230_trade_in_privacy_retention.sql rename to tradein-mvp/backend/data/sql/231_trade_in_privacy_retention.sql index d0d5817b..c78c7780 100644 --- a/tradein-mvp/backend/data/sql/230_trade_in_privacy_retention.sql +++ b/tradein-mvp/backend/data/sql/231_trade_in_privacy_retention.sql @@ -1,4 +1,4 @@ --- 230_trade_in_privacy_retention.sql +-- 231_trade_in_privacy_retention.sql -- ЭТАП 4 B2C launch — retention enforcement (152-ФЗ): срок хранения ДОЛЖЕН -- приводить к физическому удалению, а не быть декоративным полем. -- 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 20a695e2..a953c278 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_230 = _SQL_DIR / "230_trade_in_privacy_retention.sql" +_MIGRATION_231 = _SQL_DIR / "231_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 230 ──────────────────────────────────────────────────────────── +# ── migration 231 ──────────────────────────────────────────────────────────── -def test_migration_230_exists() -> None: - assert _MIGRATION_230.is_file(), f"missing migration: {_MIGRATION_230}" +def test_migration_231_exists() -> None: + assert _MIGRATION_231.is_file(), f"missing migration: {_MIGRATION_231}" -def test_migration_230_is_transactional() -> None: - sql = _MIGRATION_230.read_text("utf-8") +def test_migration_231_is_transactional() -> None: + sql = _MIGRATION_231.read_text("utf-8") assert "BEGIN;" in sql assert "COMMIT;" in sql -def test_migration_230_backfills_and_sets_not_null() -> None: - sql = _MIGRATION_230.read_text("utf-8") +def test_migration_231_backfills_and_sets_not_null() -> None: + sql = _MIGRATION_231.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_230_seeds_purge_schedule_disabled_by_default() -> None: - sql = _MIGRATION_230.read_text("utf-8") +def test_migration_231_seeds_purge_schedule_disabled_by_default() -> None: + sql = _MIGRATION_231.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_230_seeds_purge_schedule_disabled_by_default() -> None: assert re.search(r"'purge_expired_trade_in_data',\s*\n\s*false,", sql) -def test_migration_230_no_psycopg_trap() -> None: - sql = _MIGRATION_230.read_text("utf-8") +def test_migration_231_no_psycopg_trap() -> None: + sql = _MIGRATION_231.read_text("utf-8") assert not re.search(r":\w+::", sql)