fix(tradein/cian): sanitize valuation cache writes (047 UNIQUE, fill 026+044 cols)
- 047_cian_history_sanitize.sql: dedup existing rows then add UNIQUE(listing_id, change_time) so ON CONFLICT DO NOTHING in cian_detail.save_detail_enrichment suppresses real duplicates, not just PK collisions. Idempotent via DO $$ pg_constraint guard. - cian_valuation._save_to_cache: fill price_rub/accuracy (mig 026), low_price/high_price (mig 044), house_id/listing_id (mig 044). COALESCE on house_id/listing_id preserves existing FK on cache refresh. - cian_valuation._load_from_cache: restore low_price/high_price from DB into sale_price_from/sale_price_to so callers get the full band. - estimate_via_cian_valuation: add house_id/listing_id kwargs (default None), pass through to _save_to_cache. No callsite changes needed (defaults preserve existing behavior; PR 4 can supply IDs when context is available).
This commit is contained in:
parent
45ee0a2945
commit
d04ee38347
2 changed files with 56 additions and 1 deletions
|
|
@ -135,6 +135,8 @@ async def estimate_via_cian_valuation(
|
||||||
repair_type: str = "cosmetic",
|
repair_type: str = "cosmetic",
|
||||||
deal_type: str = "sale",
|
deal_type: str = "sale",
|
||||||
use_cache: bool = True,
|
use_cache: bool = True,
|
||||||
|
house_id: int | None = None,
|
||||||
|
listing_id: int | None = None,
|
||||||
) -> CianValuationResult | None:
|
) -> CianValuationResult | None:
|
||||||
"""Estimate value via Cian's authenticated Valuation Calculator.
|
"""Estimate value via Cian's authenticated Valuation Calculator.
|
||||||
|
|
||||||
|
|
@ -219,6 +221,8 @@ async def estimate_via_cian_valuation(
|
||||||
repair_type=repair_type,
|
repair_type=repair_type,
|
||||||
deal_type=deal_type,
|
deal_type=deal_type,
|
||||||
result=result,
|
result=result,
|
||||||
|
house_id=house_id,
|
||||||
|
listing_id=listing_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
|
|
@ -355,7 +359,9 @@ def _load_from_cache(db: Session, cache_key: str) -> CianValuationResult | None:
|
||||||
chart_change_pct,
|
chart_change_pct,
|
||||||
chart_change_direction,
|
chart_change_direction,
|
||||||
external_house_id,
|
external_house_id,
|
||||||
filters_hash
|
filters_hash,
|
||||||
|
low_price,
|
||||||
|
high_price
|
||||||
FROM external_valuations
|
FROM external_valuations
|
||||||
WHERE source = 'cian_valuation'
|
WHERE source = 'cian_valuation'
|
||||||
AND cache_key = :ck
|
AND cache_key = :ck
|
||||||
|
|
@ -379,6 +385,8 @@ def _load_from_cache(db: Session, cache_key: str) -> CianValuationResult | None:
|
||||||
chart_change_direction=row["chart_change_direction"],
|
chart_change_direction=row["chart_change_direction"],
|
||||||
external_house_id=row["external_house_id"],
|
external_house_id=row["external_house_id"],
|
||||||
filters_hash=row["filters_hash"],
|
filters_hash=row["filters_hash"],
|
||||||
|
sale_price_from=_parse_num(row["low_price"]),
|
||||||
|
sale_price_to=_parse_num(row["high_price"]),
|
||||||
raw_state=row["raw_payload"] if isinstance(row["raw_payload"], dict) else None,
|
raw_state=row["raw_payload"] if isinstance(row["raw_payload"], dict) else None,
|
||||||
is_authenticated=True, # cache только authenticated results
|
is_authenticated=True, # cache только authenticated results
|
||||||
)
|
)
|
||||||
|
|
@ -396,6 +404,8 @@ def _save_to_cache(
|
||||||
repair_type: str,
|
repair_type: str,
|
||||||
deal_type: str,
|
deal_type: str,
|
||||||
result: CianValuationResult,
|
result: CianValuationResult,
|
||||||
|
house_id: int | None = None,
|
||||||
|
listing_id: int | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""INSERT result into external_valuations (source='cian_valuation') с 24h expiry.
|
"""INSERT result into external_valuations (source='cian_valuation') с 24h expiry.
|
||||||
|
|
||||||
|
|
@ -409,6 +419,9 @@ def _save_to_cache(
|
||||||
source, cache_key, address,
|
source, cache_key, address,
|
||||||
total_area, rooms_count, floor, total_floors,
|
total_area, rooms_count, floor, total_floors,
|
||||||
repair_type, deal_type,
|
repair_type, deal_type,
|
||||||
|
price_rub, accuracy,
|
||||||
|
low_price, high_price,
|
||||||
|
house_id, listing_id,
|
||||||
sale_price_rub, sale_accuracy,
|
sale_price_rub, sale_accuracy,
|
||||||
rent_price_rub, rent_accuracy,
|
rent_price_rub, rent_accuracy,
|
||||||
chart, chart_change_pct, chart_change_direction,
|
chart, chart_change_pct, chart_change_direction,
|
||||||
|
|
@ -419,12 +432,21 @@ def _save_to_cache(
|
||||||
CAST(:ta AS numeric), :rc, :fl, :tf,
|
CAST(:ta AS numeric), :rc, :fl, :tf,
|
||||||
:rt, :dt,
|
:rt, :dt,
|
||||||
CAST(:sp AS numeric), CAST(:sa AS numeric),
|
CAST(:sp AS numeric), CAST(:sa AS numeric),
|
||||||
|
CAST(:lp AS numeric), CAST(:hp AS numeric),
|
||||||
|
CAST(:hid AS bigint), CAST(:lid AS bigint),
|
||||||
|
CAST(:sp AS numeric), CAST(:sa AS numeric),
|
||||||
CAST(:rp AS numeric), CAST(:ra AS numeric),
|
CAST(:rp AS numeric), CAST(:ra AS numeric),
|
||||||
CAST(:ch AS jsonb), CAST(:ccp AS numeric), :ccd,
|
CAST(:ch AS jsonb), CAST(:ccp AS numeric), :ccd,
|
||||||
:ehi, :fh,
|
:ehi, :fh,
|
||||||
CAST(:raw AS jsonb), NOW(), NOW() + INTERVAL '24 hours'
|
CAST(:raw AS jsonb), NOW(), NOW() + INTERVAL '24 hours'
|
||||||
)
|
)
|
||||||
ON CONFLICT (source, cache_key) DO UPDATE SET
|
ON CONFLICT (source, cache_key) DO UPDATE SET
|
||||||
|
price_rub = EXCLUDED.price_rub,
|
||||||
|
accuracy = EXCLUDED.accuracy,
|
||||||
|
low_price = EXCLUDED.low_price,
|
||||||
|
high_price = EXCLUDED.high_price,
|
||||||
|
house_id = COALESCE(EXCLUDED.house_id, external_valuations.house_id),
|
||||||
|
listing_id = COALESCE(EXCLUDED.listing_id, external_valuations.listing_id),
|
||||||
sale_price_rub = EXCLUDED.sale_price_rub,
|
sale_price_rub = EXCLUDED.sale_price_rub,
|
||||||
sale_accuracy = EXCLUDED.sale_accuracy,
|
sale_accuracy = EXCLUDED.sale_accuracy,
|
||||||
rent_price_rub = EXCLUDED.rent_price_rub,
|
rent_price_rub = EXCLUDED.rent_price_rub,
|
||||||
|
|
@ -449,6 +471,10 @@ def _save_to_cache(
|
||||||
"dt": deal_type,
|
"dt": deal_type,
|
||||||
"sp": result.sale_price_rub,
|
"sp": result.sale_price_rub,
|
||||||
"sa": result.sale_accuracy,
|
"sa": result.sale_accuracy,
|
||||||
|
"lp": result.sale_price_from,
|
||||||
|
"hp": result.sale_price_to,
|
||||||
|
"hid": house_id,
|
||||||
|
"lid": listing_id,
|
||||||
"rp": result.rent_price_rub,
|
"rp": result.rent_price_rub,
|
||||||
"ra": result.rent_accuracy,
|
"ra": result.rent_accuracy,
|
||||||
"ch": json.dumps(result.chart),
|
"ch": json.dumps(result.chart),
|
||||||
|
|
|
||||||
29
tradein-mvp/backend/data/sql/047_cian_history_sanitize.sql
Normal file
29
tradein-mvp/backend/data/sql/047_cian_history_sanitize.sql
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
-- 047_cian_history_sanitize.sql
|
||||||
|
-- Purpose: Add UNIQUE(listing_id, change_time) to offer_price_history so that
|
||||||
|
-- the `ON CONFLICT DO NOTHING` in cian_detail.save_detail_enrichment
|
||||||
|
-- is semantically correct (currently only suppresses PK conflicts).
|
||||||
|
-- Dependencies: 023_offer_price_history.sql
|
||||||
|
-- Deploy order: Apply after 046.
|
||||||
|
|
||||||
|
BEGIN;
|
||||||
|
|
||||||
|
-- Dedupe existing rows before adding constraint (safe if table empty in prod).
|
||||||
|
DELETE FROM offer_price_history a
|
||||||
|
USING offer_price_history b
|
||||||
|
WHERE a.id > b.id
|
||||||
|
AND a.listing_id = b.listing_id
|
||||||
|
AND a.change_time = b.change_time;
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM pg_constraint
|
||||||
|
WHERE conname = 'offer_price_history_listing_change_uq'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE offer_price_history
|
||||||
|
ADD CONSTRAINT offer_price_history_listing_change_uq
|
||||||
|
UNIQUE (listing_id, change_time);
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
Loading…
Add table
Reference in a new issue