diff --git a/backend/app/api/v1/parcels.py b/backend/app/api/v1/parcels.py index da20595d..bc3f5f43 100644 --- a/backend/app/api/v1/parcels.py +++ b/backend/app/api/v1/parcels.py @@ -1626,10 +1626,15 @@ def build_parcel_report( analyze_at = _iso_or_none(getattr(analyze_run, "created_at", None)) forecast_at = _iso_or_none(getattr(forecast_run, "created_at", None)) - # Готовый кэш → 200 ready (не enqueue'им повторно). - if _cached_report_result(db, cad_num) is not None: + # Готовый кэш → 200 ready (не enqueue'им повторно). Отдаём и дату генерации PDF из + # метадаты рана — чтобы fast-path кнопка показала дату без отдельного GET /status. + cached = _cached_report_result(db, cad_num) + if cached is not None: return ReportBuildResponse( - status="ready", analyze_run_at=analyze_at, forecast_run_at=forecast_at + status="ready", + analyze_run_at=analyze_at, + forecast_run_at=forecast_at, + report_generated_at=cached.get("generated_at"), ) # Enqueue фоновой сборки. Lazy import таски (как forecast в analyze_parcel) — атрибут diff --git a/backend/app/schemas/parcel.py b/backend/app/schemas/parcel.py index 9f48e48b..d8a3988a 100644 --- a/backend/app/schemas/parcel.py +++ b/backend/app/schemas/parcel.py @@ -270,6 +270,9 @@ class ReportBuildResponse(BaseModel): # Даты базовых ранов (ISO) — контекст, по каким данным собирается/собран отчёт. analyze_run_at: str | None = None forecast_run_at: str | None = None + # Дата генерации готового PDF (ISO) — только при status="ready" (кэш-хит); на + # building-ветке None (файла ещё нет). Даёт fast-path кнопке дату без GET /status. + report_generated_at: str | None = None class ReportStatusResponse(BaseModel): diff --git a/backend/tests/api/v1/test_parcel_report.py b/backend/tests/api/v1/test_parcel_report.py index deb74db4..af2cb810 100644 --- a/backend/tests/api/v1/test_parcel_report.py +++ b/backend/tests/api/v1/test_parcel_report.py @@ -95,6 +95,8 @@ def test_post_report_enqueues_and_returns_202() -> None: body = resp.json() assert body["status"] == "building" assert body["analyze_run_at"] is not None + # Отчёт ещё не собран → даты генерации PDF нет. + assert body["report_generated_at"] is None delay_mock.assert_called_once_with(_CAD) finally: app.dependency_overrides.clear() @@ -127,7 +129,10 @@ def test_post_report_cache_hit_returns_200_ready(tmp_path: Path) -> None: client = TestClient(app) resp = client.post(f"/api/v1/parcels/{_CAD}/report") assert resp.status_code == 200, resp.text - assert resp.json()["status"] == "ready" + body = resp.json() + assert body["status"] == "ready" + # Fast-path: POST-ответ несёт дату генерации PDF из метадаты рана (без GET /status). + assert body["report_generated_at"] == "2026-07-03T09:00:00+00:00" delay_mock.assert_not_called() # кэш есть → не enqueue'им finally: app.dependency_overrides.clear() diff --git a/frontend/src/hooks/useFullReport.ts b/frontend/src/hooks/useFullReport.ts index bfa0cce7..6482c46e 100644 --- a/frontend/src/hooks/useFullReport.ts +++ b/frontend/src/hooks/useFullReport.ts @@ -294,7 +294,9 @@ export function useFullReport(cad: string): UseFullReportResult { } const reportGeneratedAt = - (uiState === "ready" ? statusData?.report_generated_at : null) ?? null; + (uiState === "ready" + ? (statusData?.report_generated_at ?? enqueue.data?.report_generated_at) + : null) ?? null; return { uiState, diff --git a/frontend/src/lib/api-types.ts b/frontend/src/lib/api-types.ts index e1d545ab..780690bb 100644 --- a/frontend/src/lib/api-types.ts +++ b/frontend/src/lib/api-types.ts @@ -5255,6 +5255,8 @@ export interface components { analyze_run_at?: string | null; /** Forecast Run At */ forecast_run_at?: string | null; + /** Report Generated At */ + report_generated_at?: string | null; }; /** * ReportStatusResponse