From fe805ea528a20566ae16d9b847759c0c50543be4 Mon Sep 17 00:00:00 2001 From: bot-backend Date: Wed, 17 Jun 2026 22:32:18 +0300 Subject: [PATCH] fix(cian): persist detail rich fields (description/kitchen/ceiling) + stamp detail_enriched_at --- .../app/services/scrapers/cian_detail.py | 7 +- tradein-mvp/backend/tests/test_cian_detail.py | 64 +++++++++++++++++++ 2 files changed, 70 insertions(+), 1 deletion(-) diff --git a/tradein-mvp/backend/app/services/scrapers/cian_detail.py b/tradein-mvp/backend/app/services/scrapers/cian_detail.py index 55b54092..e7b4749c 100644 --- a/tradein-mvp/backend/app/services/scrapers/cian_detail.py +++ b/tradein-mvp/backend/app/services/scrapers/cian_detail.py @@ -48,6 +48,7 @@ class DetailEnrichment: repair_type: str | None = None # 'cosmetic' / 'design' / 'no' — raw Cian value repair_state: str | None = None # enum: needs_repair/standard/good/excellent kitchen_area_m2: float | None = None # offer.kitchenArea (м²) + description: str | None = None # offer.description (текст объявления) views_total: int | None = None # Cian stats.totalViewsFormattedString → int views_today: int | None = None @@ -144,6 +145,7 @@ async def fetch_detail( repair_type=_extract_repair_type(offer), repair_state=_extract_repair_state(offer), kitchen_area_m2=_parse_float(offer.get("kitchenArea")), + description=offer.get("description") or None, raw_offer=offer, ) @@ -302,8 +304,10 @@ def save_detail_enrichment(db: Session, listing_id: int, enrichment: DetailEnric repair_type = COALESCE(:rt, repair_type), repair_state = COALESCE(:rs, repair_state), kitchen_area_m2 = COALESCE(CAST(:ka AS double precision), kitchen_area_m2), + description = COALESCE(:descr, description), views_total = COALESCE(:vt, views_total), - views_today = COALESCE(:vd, views_today) + views_today = COALESCE(:vd, views_today), + detail_enriched_at = NOW() WHERE id = CAST(:lid AS bigint) """), { @@ -315,6 +319,7 @@ def save_detail_enrichment(db: Session, listing_id: int, enrichment: DetailEnric "rt": enrichment.repair_type, "rs": enrichment.repair_state, "ka": enrichment.kitchen_area_m2, + "descr": enrichment.description, "vt": enrichment.views_total, "vd": enrichment.views_today, }, diff --git a/tradein-mvp/backend/tests/test_cian_detail.py b/tradein-mvp/backend/tests/test_cian_detail.py index b303373f..d084da48 100644 --- a/tradein-mvp/backend/tests/test_cian_detail.py +++ b/tradein-mvp/backend/tests/test_cian_detail.py @@ -462,6 +462,40 @@ async def test_fetch_detail_kitchen_area_none_when_missing(monkeypatch): assert result.kitchen_area_m2 is None +@pytest.mark.asyncio +async def test_fetch_detail_extracts_description(monkeypatch): + """offer.description → DetailEnrichment.description (для persist в listings).""" + fake_state = { + "offerData": { + "offer": { + "cianId": 555555, + "description": "Продаётся уютная квартира с видом на парк.", + } + } + } + + monkeypatch.setattr( + "app.services.scrapers.cian_detail.extract_state", + lambda html, mfe, key: fake_state if key == "defaultState" else None, + ) + monkeypatch.setattr( + "app.services.scrapers.cian_detail.extract_all_states", + lambda html: {}, + ) + + response = MagicMock() + response.status_code = 200 + response.text = "" + + session = MagicMock() + session.get = AsyncMock(return_value=response) + session.close = AsyncMock() + + result = await fetch_detail("https://ekb.cian.ru/sale/flat/555555/", session=session) + assert result is not None + assert result.description == "Продаётся уютная квартира с видом на парк." + + # ── save_detail_enrichment — kitchen_area_m2 persisted ─────────────────────── @@ -504,6 +538,36 @@ def test_save_detail_enrichment_kitchen_area_none_passthrough(): assert update_call_params["ka"] is None +# ── save_detail_enrichment — description + detail_enriched_at stamp ─────────── + + +def test_save_detail_enrichment_writes_description(): + """save_detail_enrichment передаёт description в UPDATE listings.""" + db = MagicMock() + db.execute.return_value.fetchone.return_value = None + + enrichment = DetailEnrichment(description="Светлая квартира рядом с парком") + + save_detail_enrichment(db, listing_id=1002, enrichment=enrichment) + + update_call_params = db.execute.call_args_list[0][0][1] + assert update_call_params["descr"] == "Светлая квартира рядом с парком" + + +def test_save_detail_enrichment_stamps_detail_enriched_at(): + """UPDATE listings содержит detail_enriched_at = NOW() — иначе строка + переобогащается каждый run (detail_enriched_at IS NULL filter).""" + db = MagicMock() + db.execute.return_value.fetchone.return_value = None + + enrichment = DetailEnrichment(kitchen_area_m2=8.3) + + save_detail_enrichment(db, listing_id=1003, enrichment=enrichment) + + update_sql = str(db.execute.call_args_list[0][0][0]) + assert "detail_enriched_at = NOW()" in update_sql + + # ── fetch_detail browser_fetcher param ─────────────────────────────────────── -- 2.45.3