fix(cian): persist detail rich fields (description) + stamp detail_enriched_at #1718

Merged
lekss361 merged 1 commit from fix/cian-detail-rich-fields into main 2026-06-17 19:42:59 +00:00
2 changed files with 70 additions and 1 deletions
Showing only changes of commit fe805ea528 - Show all commits

View file

@ -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,
},

View file

@ -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 = "<html></html>"
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 ───────────────────────────────────────