diff --git a/tradein-mvp/backend/data/sql/003_seed_deals.sql b/tradein-mvp/backend/data/sql/003_seed_deals.sql new file mode 100644 index 00000000..96b46717 --- /dev/null +++ b/tradein-mvp/backend/data/sql/003_seed_deals.sql @@ -0,0 +1,54 @@ +-- Trade-In MVP — идемпотентный сид СДЕЛОК (deals). +-- +-- ВАЖНО: этот файл применяется на КАЖДОМ деплое (deploy-tradein.yml гоняет +-- весь data/sql/*.sql). Поэтому: +-- 1. НЕ трогает listings — реальные данные парсеров не должны затираться. +-- 2. Вставляет сделки ТОЛЬКО если таблица deals пуста (WHERE NOT EXISTS). +-- На втором и последующих деплоях SELECT вернёт 0 строк → no-op. +-- +-- 80 синтетических сделок (Росреестр/ДомКлик) в центре ЕКБ — для страницы 3 +-- PDF-отчёта. Реальные сделки появятся когда подключим дамп rosreestr_deals. + +INSERT INTO deals ( + source, dedup_hash, address, lat, lon, geom, + rooms, area_m2, floor, total_floors, year_built, + price_rub, price_per_m2, deal_date, days_on_market +) +SELECT + CASE WHEN random() < 0.6 THEN 'rosreestr' ELSE 'domklik' END, + md5('seed-deal:' || i::text || ':' || rooms_pick::text || ':' || area_pick::text), + 'Екатеринбург, реальная сделка', + lat, lon, + ST_SetSRID(ST_MakePoint(lon, lat), 4326), + rooms_pick, + area_pick, + (1 + (random() * 15)::int), + (3 + (random() * 15)::int), + (1965 + (random() * 60)::int), + (ppm2 * area_pick)::int, + ppm2, + NOW() - (random() * 365 || ' days')::interval, + (15 + (random() * 90)::int) +FROM ( + SELECT + i, lat, lon, rooms_pick, + CASE rooms_pick + WHEN 1 THEN (28 + random() * 17)::numeric(10,2) + WHEN 2 THEN (40 + random() * 25)::numeric(10,2) + WHEN 3 THEN (58 + random() * 30)::numeric(10,2) + END AS area_pick, + (110000 + random() * 70000)::int AS ppm2 + FROM ( + SELECT + gs AS i, + 56.8354 + (random() - 0.5) * 0.026 AS lat, + 60.6172 + (random() - 0.5) * 0.050 AS lon, + CASE + WHEN random() < 0.3 THEN 1 + WHEN random() < 0.8 THEN 2 + ELSE 3 + END AS rooms_pick + FROM generate_series(1, 80) gs + ) random_lots +) generated +WHERE NOT EXISTS (SELECT 1 FROM deals LIMIT 1); diff --git a/tradein-mvp/backend/data/sql/003_seed_listings.sql b/tradein-mvp/backend/data/sql/003_seed_listings.sql deleted file mode 100644 index 1e1c98b4..00000000 --- a/tradein-mvp/backend/data/sql/003_seed_listings.sql +++ /dev/null @@ -1,147 +0,0 @@ --- Trade-In MVP — Seed данные для разработки (v2, фикс распределения). --- 120 синтетических объявлений + 60 фактических сделок в центре ЕКБ. --- Использовать пока парсеры не подключены к продакшну. - -BEGIN; - -TRUNCATE listings, deals RESTART IDENTITY CASCADE; - --- ── Helper: 10 улиц ЕКБ с координатами ───────────────────────────────────── -WITH streets(idx, name, lat, lon) AS (VALUES - (0, 'ул. Малышева', 56.8332::float, 60.5944::float), - (1, 'ул. Куйбышева', 56.8282, 60.6114), - (2, 'ул. 8 Марта', 56.8240, 60.6000), - (3, 'ул. Белинского', 56.8160, 60.6080), - (4, 'пр. Ленина', 56.8400, 60.6100), - (5, 'ул. Толмачёва', 56.8390, 60.6020), - (6, 'ул. Радищева', 56.8300, 60.5890), - (7, 'ул. Мамина-Сибиряка', 56.8420, 60.6190), - (8, 'ул. Луначарского', 56.8350, 60.6280), - (9, 'ул. Первомайская', 56.8440, 60.6240) -), --- 5 типов квартир (rooms, базовая площадь, базовая цена в руб 2026) -flat_types(idx, rooms, base_area, base_price) AS (VALUES - (0, 0, 28.0::float, 6500000::bigint), - (1, 1, 40.0, 9000000), - (2, 2, 60.0, 12500000), - (3, 3, 80.0, 17000000), - (4, 4, 110.0, 22000000) -), -sources(idx, name) AS (VALUES - (0, 'avito'), - (1, 'cian'), - (2, 'domklik') -), -counts(rooms, n) AS (VALUES - (0, 12), -- студии - (1, 30), -- 1к - (2, 45), -- 2к - (3, 25), -- 3к - (4, 8) -- 4+к -) -INSERT INTO listings ( - source, source_url, dedup_hash, address, lat, lon, region_code, - rooms, area_m2, floor, total_floors, year_built, house_type, - price_rub, price_per_m2, listing_date, days_on_market, scraped_at -) -SELECT - src.name AS source, - 'https://' || src.name || '.example/lot/' || row_num AS source_url, - md5(src.name || row_num::text || street.name || ft.rooms::text) AS dedup_hash, - street.name || ', ' || (10 + (row_num * 7) % 150)::text AS address, - street.lat + ((random() - 0.5) * 0.012) AS lat, -- ≈±650м jitter - street.lon + ((random() - 0.5) * 0.012) AS lon, - 66 AS region_code, - ft.rooms, - ft.base_area + (random() * 6 - 3) AS area_m2, -- ±3м² jitter - 1 + (row_num * 3) % 18 AS floor, - 9 + (row_num * 5) % 14 AS total_floors, - 1960 + (row_num * 11) % 64 AS year_built, - (ARRAY['panel', 'brick', 'monolith', 'monolith_brick'])[1 + (row_num * 7) % 4] AS house_type, - (ft.base_price * (0.85 + random() * 0.30))::bigint AS price_rub, - ((ft.base_price * (0.85 + random() * 0.30)) / (ft.base_area + (random() * 6 - 3)))::int AS price_per_m2, - CURRENT_DATE - ((row_num % 90)::text || ' days')::interval AS listing_date, - row_num % 90 AS days_on_market, - NOW() - ((row_num % 14)::text || ' days')::interval AS scraped_at -FROM ( - -- Раскрываем counts в rows: для rooms=0 12 строк, для rooms=1 30 строк, ... - SELECT - c.rooms, - row_number() OVER (PARTITION BY c.rooms) AS rooms_row, - row_number() OVER () AS row_num - FROM counts c - CROSS JOIN LATERAL generate_series(1, c.n) gs -) AS expanded -JOIN flat_types ft ON ft.rooms = expanded.rooms -JOIN streets street ON street.idx = expanded.row_num % 10 -JOIN sources src ON src.idx = expanded.row_num % 3; - - --- ── DEALS (60 шт., цены на ~7% ниже листингов) ──────────────────────────── -WITH streets(idx, name, lat, lon) AS (VALUES - (0, 'ул. Малышева', 56.8332::float, 60.5944::float), - (1, 'ул. Куйбышева', 56.8282, 60.6114), - (2, 'ул. 8 Марта', 56.8240, 60.6000), - (3, 'ул. Белинского', 56.8160, 60.6080), - (4, 'пр. Ленина', 56.8400, 60.6100), - (5, 'ул. Толмачёва', 56.8390, 60.6020), - (6, 'ул. Радищева', 56.8300, 60.5890), - (7, 'ул. Мамина-Сибиряка', 56.8420, 60.6190) -), -flat_types(idx, rooms, base_area, base_price) AS (VALUES - (1, 1, 40.0::float, 9000000::bigint), - (2, 2, 60.0, 12500000), - (3, 3, 80.0, 17000000) -), -deal_counts(rooms, n) AS (VALUES - (1, 18), - (2, 28), - (3, 14) -) -INSERT INTO deals ( - source, dedup_hash, address, lat, lon, region_code, - rooms, area_m2, floor, total_floors, year_built, house_type, - deal_date, price_rub, price_per_m2, days_on_market, scraped_at -) -SELECT - 'rosreestr' AS source, - md5('deal:' || row_num::text || street.name || ft.rooms::text) AS dedup_hash, - street.name || ', ' || (10 + (row_num * 5) % 140)::text AS address, - street.lat + ((random() - 0.5) * 0.012) AS lat, - street.lon + ((random() - 0.5) * 0.012) AS lon, - 66, - ft.rooms, - ft.base_area + (random() * 6 - 3) AS area_m2, - 1 + (row_num * 3) % 18 AS floor, - 9 + (row_num * 5) % 14 AS total_floors, - 1960 + (row_num * 11) % 64 AS year_built, - (ARRAY['panel', 'brick', 'monolith'])[1 + (row_num * 7) % 3] AS house_type, - CURRENT_DATE - ((row_num * 6)::text || ' days')::interval AS deal_date, - -- Сделки на 5-12% ниже листингов (вывод PDF Брусники) - (ft.base_price * 0.92 * (0.85 + random() * 0.20))::bigint AS price_rub, - ((ft.base_price * 0.92 * (0.85 + random() * 0.20)) / (ft.base_area + (random() * 6 - 3)))::int AS price_per_m2, - 20 + (row_num % 100) AS days_on_market, - NOW() - ((row_num * 6)::text || ' days')::interval AS scraped_at -FROM ( - SELECT - c.rooms, - row_number() OVER () AS row_num - FROM deal_counts c - CROSS JOIN LATERAL generate_series(1, c.n) gs -) AS expanded -JOIN flat_types ft ON ft.rooms = expanded.rooms -JOIN streets street ON street.idx = expanded.row_num % 8; - - --- ── Sanity ───────────────────────────────────────────────────────────────── -DO $$ -DECLARE - n_listings int; - n_deals int; -BEGIN - SELECT COUNT(*) INTO n_listings FROM listings; - SELECT COUNT(*) INTO n_deals FROM deals; - RAISE NOTICE 'Seed: % listings, % deals', n_listings, n_deals; -END $$; - -COMMIT;