chore(tradein/privacy): перенумерация 231 и merge main - коллизия префикса (#2547)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI / changes (pull_request) Successful in 9s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 3m21s

This commit is contained in:
bot-backend 2026-08-06 19:10:27 +03:00
parent b93bee5393
commit 3ee99efaa4
6 changed files with 18 additions and 18 deletions

View file

@ -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, не хардкод.

View file

@ -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 для
# НЕконвертированных маркетинговых лидов (типичный индустриальный диапазон

View file

@ -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).

View file

@ -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 таблицы

View file

@ -1,4 +1,4 @@
-- 230_trade_in_privacy_retention.sql
-- 231_trade_in_privacy_retention.sql
-- ЭТАП 4 B2C launch — retention enforcement (152-ФЗ): срок хранения ДОЛЖЕН
-- приводить к физическому удалению, а не быть декоративным полем.
--

View file

@ -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)