All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI / changes (pull_request) Successful in 8s
CI Trade-In / browser-tests (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 / frontend-checks (pull_request) Successful in 1m3s
CI Trade-In / backend-tests (pull_request) Successful in 3m49s
Deep-review MEDIUM: предполётная проверка purge_expired_trade_in_data считала по базовому предикату без retain_until — здоровая оплаченная строка (retain_until проставлен, платёж есть) через сутки после продажи тоже попадала под счётчик, и джоба аварийно останавливалась на первой же честной продаже навсегда (вместе с ней — и 180-дневное удаление лидов, вызываемое из той же функции после этой проверки). - _PREFLIGHT_PAID_CANDIDATES_SQL: добавлен терм `retain_until IS NULL` — теперь считает только реальную аномалию (retain_until не проставлен, а платёж есть), а не штатное состояние. Докстринги функции/модуля поправлены под фактическое поведение. - Тест на неверный инвариант (`"retain_until" not in sql`) заменён на позитивный (`"retain_until IS NULL" in sql`) + добавлены live-DB тесты на оба случая из ревью (здоровая оплаченная строка не поднимает тревогу, джоба не блокируется). - privacy/page.tsx: константа "12 месяцев" вынесена в content.ts (PAID_REPORT_RETENTION_MONTHS) вместо литерала + расходящегося комментария; добавлен сверяющий тест (test_paid_retention_text_consistency.py) по образцу _CONSENT_TEXT_SNAPSHOT. Смягчена формулировка про автоматическое удаление — задача на проде выключена и ни разу не запускалась, текст теперь описывает установленный порядок, а не наблюдаемый факт. - Все 10 висячих ссылок на untracked `mera-pr-d-spec.md` (7 файлов) заменены на краткое изложение сути в комментарии + ссылку на PR #2754.
103 lines
4.6 KiB
Python
103 lines
4.6 KiB
Python
"""Payments retention (PR #2754) — "12 месяцев" text sync guard.
|
|
|
|
WHY:
|
|
mera-public/content.ts declares itself as the ONE place product promises
|
|
live (docstring at the top of that file: "ни одного утверждения, которого
|
|
не делает код"). The public retention promise ("оплаченный отчёт хранится
|
|
N месяцев") has THREE places it could quietly drift: the backend setting
|
|
(`settings.trade_in_paid_retention_days`), the frontend constant
|
|
(`PAID_REPORT_RETENTION_MONTHS` in content.ts), and any page that renders
|
|
it (today: privacy/page.tsx). Deep-review finding 2026-08-06 MEDIUM on
|
|
PR #2754 caught exactly this: a comment claimed the number "reads from the
|
|
same setting" while the page actually hardcoded a `12 месяцев` literal --
|
|
a comment cannot fail CI, same lesson as
|
|
test_consent_text_frontend_sync.py's _CONSENT_TEXT_SNAPSHOT guard (which
|
|
this file mirrors).
|
|
|
|
WHAT:
|
|
1. privacy/page.tsx imports PAID_REPORT_RETENTION_MONTHS from content.ts
|
|
and does NOT hardcode a "N месяцев" literal of its own.
|
|
2. The frontend months constant and the backend days setting stay within
|
|
a sane calendar tolerance of each other (28-31 days per month) -- this
|
|
does NOT enforce byte-identity (days and months are different units by
|
|
design, see content.ts docstring), only that nobody silently changes
|
|
one without the other drifting out of "still honestly ~12 months".
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import re
|
|
from pathlib import Path
|
|
|
|
os.environ.setdefault("DATABASE_URL", "postgresql+psycopg://test:test@localhost:5432/test")
|
|
|
|
_FRONTEND_CONTENT = (
|
|
Path(__file__).resolve().parents[2] / "frontend" / "src" / "app" / "mera-public" / "content.ts"
|
|
)
|
|
_FRONTEND_PRIVACY_PAGE = (
|
|
Path(__file__).resolve().parents[2]
|
|
/ "frontend"
|
|
/ "src"
|
|
/ "app"
|
|
/ "mera-public"
|
|
/ "privacy"
|
|
/ "page.tsx"
|
|
)
|
|
|
|
_MONTHS_CONST_RE = re.compile(r"PAID_REPORT_RETENTION_MONTHS\s*=\s*(\d+)\s*;")
|
|
_LITERAL_MONTHS_RE = re.compile(r'"?\d+ месяцев"?')
|
|
|
|
|
|
def _extract_months_constant(content_ts_source: str) -> int:
|
|
match = _MONTHS_CONST_RE.search(content_ts_source)
|
|
assert match is not None, (
|
|
"PAID_REPORT_RETENTION_MONTHS not found in content.ts -- "
|
|
"constant renamed/removed without updating this test"
|
|
)
|
|
return int(match.group(1))
|
|
|
|
|
|
def test_frontend_files_exist() -> None:
|
|
assert _FRONTEND_CONTENT.is_file(), f"missing frontend file: {_FRONTEND_CONTENT}"
|
|
assert _FRONTEND_PRIVACY_PAGE.is_file(), f"missing frontend file: {_FRONTEND_PRIVACY_PAGE}"
|
|
|
|
|
|
def test_privacy_page_imports_retention_constant_not_hardcoded() -> None:
|
|
"""The whole point: FAILS if privacy/page.tsx stops importing the shared
|
|
constant and goes back to a hardcoded '12 месяцев' literal (exactly the
|
|
drift the deep-review finding caught -- comment said 'reads from content.ts',
|
|
code said otherwise)."""
|
|
src = _FRONTEND_PRIVACY_PAGE.read_text(encoding="utf-8")
|
|
assert "PAID_REPORT_RETENTION_MONTHS" in src, (
|
|
"privacy/page.tsx no longer references PAID_REPORT_RETENTION_MONTHS -- "
|
|
"the retention promise must be rendered from the shared content.ts "
|
|
"constant, not written out again by hand"
|
|
)
|
|
assert re.search(r'from\s+"\.\./content"', src), (
|
|
"privacy/page.tsx must import from '../content' (content.ts), where "
|
|
"PAID_REPORT_RETENTION_MONTHS is declared"
|
|
)
|
|
literal_hits = _LITERAL_MONTHS_RE.findall(src)
|
|
assert not literal_hits, (
|
|
"privacy/page.tsx contains a hardcoded 'N месяцев' literal -- render the "
|
|
"PAID_REPORT_RETENTION_MONTHS constant instead: "
|
|
f"{literal_hits!r}"
|
|
)
|
|
|
|
|
|
def test_backend_days_setting_matches_frontend_months_within_calendar_tolerance() -> None:
|
|
"""Not byte-identity (days vs months are different units, deliberately --
|
|
see content.ts docstring on PAID_REPORT_RETENTION_MONTHS): just a sanity
|
|
bound that `trade_in_paid_retention_days` still honestly rounds to the
|
|
number of months the public page promises (28-31 days/month, generous)."""
|
|
from app.core.config import settings
|
|
|
|
days = settings.trade_in_paid_retention_days
|
|
months = _extract_months_constant(_FRONTEND_CONTENT.read_text(encoding="utf-8"))
|
|
|
|
assert 28 * months <= days <= 31 * months, (
|
|
f"settings.trade_in_paid_retention_days={days} no longer honestly rounds to "
|
|
f"content.ts PAID_REPORT_RETENTION_MONTHS={months} -- update both together "
|
|
"(and the offer text, when it exists) so the public promise stays true"
|
|
)
|