diff --git a/tradein-mvp/packages/scraper-kit/src/scraper_kit/providers/avito/houses.py b/tradein-mvp/packages/scraper-kit/src/scraper_kit/providers/avito/houses.py index 714235a7..87c024a5 100644 --- a/tradein-mvp/packages/scraper-kit/src/scraper_kit/providers/avito/houses.py +++ b/tradein-mvp/packages/scraper-kit/src/scraper_kit/providers/avito/houses.py @@ -919,10 +919,11 @@ async def fetch_house_catalog( # --------------------------------------------------------------------------- -def upsert_house(db: Session, h: HouseInfo, *, matcher: HouseMatcher) -> int: +def upsert_house(db: Session, h: HouseInfo, *, matcher: HouseMatcher) -> int | None: """Match-or-create канонический дом через matcher, затем обновить enrichment. - Source='avito'. Возвращает houses.id. + Source='avito'. Возвращает houses.id, либо None если matcher отказал (P1: + безномерный адрес без кадастра, method='no_house_number'). Делегирует в _persist_house (без url — url пуст как раньше для этого пути). `matcher` — инжектируемый `HouseMatcher` (scraper_kit.contracts, #2133) вместо прямого импорта app.services.matching.houses.match_or_create_house. @@ -941,15 +942,18 @@ def upsert_house(db: Session, h: HouseInfo, *, matcher: HouseMatcher) -> int: def _upsert_house_with_url( db: Session, h: HouseInfo, house_url: str, *, matcher: HouseMatcher -) -> int: +) -> int | None: """Внутренний вариант upsert_house с правильным URL. Делегирует в _persist_house — используй его вместо прямого вызова. + Возвращает None если matcher отказал (P1 numberless без кадастра). """ return _persist_house(db, h, house_url, matcher=matcher) -def _persist_house(db: Session, h: HouseInfo, house_url: str, *, matcher: HouseMatcher) -> int: +def _persist_house( + db: Session, h: HouseInfo, house_url: str, *, matcher: HouseMatcher +) -> int | None: """Канонический путь сохранения дома-avito через match_or_create_house. Шаги: @@ -978,6 +982,17 @@ def _persist_house(db: Session, h: HouseInfo, house_url: str, *, matcher: HouseM source_url=house_url or None, ) + # P1: matcher refuses a numberless address without a cadastral number + # (method 'no_house_number' → house_id None). No canonical row exists to enrich, + # so skip the UPDATE and signal the graceful skip up to save_house_catalog_enrichment. + if house_id is None: + logger.info( + "_persist_house: skip numberless avito house ext_id=%s addr=%r (no cadastral)", + h.ext_id, + h.short_address or h.full_address, + ) + return None + db.execute( text(""" UPDATE houses SET @@ -1281,6 +1296,25 @@ def save_house_catalog_enrichment( } house_id = _upsert_house_with_url(db, e.house, e.house_url, matcher=matcher) + # P1: matcher refused to create a canonical house for a numberless address without a + # cadastral number (house_id None). Downstream saves (reviews / placement history) are + # house-scoped and meaningless with a NULL house_id, so graceful-skip the whole + # enrichment — same zero-counter contract as the id-less guard above. + if house_id is None: + logger.warning( + "save_house_catalog_enrichment: пропуск дома с безномерным адресом без кадастра" + " (ext_id=%s addr=%r) — matcher вернул no_house_number", + e.house.ext_id, + e.house.short_address or e.house.full_address, + ) + return { + "house_id": 0, + "reviews": 0, + "sellers": 0, + "listings_linked": 0, + "placement_history": 0, + } + # 2. Сохранить отзывы reviews_count = save_house_reviews(db, house_id, e.reviews)