Commit graph

106 commits

Author SHA1 Message Date
714d4fd3be feat(#978b): survivorship-free Source A + detrend control in rate-sensitivity backtest
Cleaner OOS validation of the §9.6 negative verdict:
- --source {B,A,both}: Source A = objective_corpus_room_month SUM(deals) monthly
  aggregate (survivorship-FREE, ~13mo); Source B path unchanged.
- --detrend: linearly detrend ln(units) before differencing so a spurious
  survivorship trend can't drive β (_detrend_log helper).
- cross-source verdict compares B-raw / B-detrended / A.

DEFINITIVE RESULT (run vs prod 2026-06-03): NO variant beats coin-flip OOS —
B-raw 0.148, B-detrended 0.148, A too thin (13mo<18). The §9.6 negative verdict
is a REAL 'no signal', NOT a survivorship artifact (detrend + clean Source A
agree). monthly key_rate→sales is not OOS-predictive on available EKB data →
§9.6 stays advisory; forecast should lean on measured signals (supply/absorption).
Tests 38→60.
2026-06-03 14:51:43 +05:00
6f06ca7f16 feat(forecasting): read-only OOS backtest of §9.6 rate-sensitivity (#978) (#1024)
All checks were successful
Deploy / changes (push) Successful in 5s
Deploy / build-frontend (push) Has been skipped
Deploy / build-backend (push) Successful in 41s
Deploy / build-worker (push) Successful in 44s
Deploy / deploy (push) Successful in 1m0s
2026-06-03 09:33:02 +00:00
12162a0abb feat(exporters): Site Finder report PDF exporter (#989, 955-A3) (#1023)
All checks were successful
Deploy / changes (push) Successful in 5s
Deploy / build-frontend (push) Has been skipped
Deploy / build-worker (push) Successful in 3m23s
Deploy / build-backend (push) Successful in 3m30s
Deploy / deploy (push) Successful in 1m7s
2026-06-03 09:09:30 +00:00
299f2365fb feat(exporters): Excel exporter for SiteFinderReport (#991, 955-A5) (#1022)
All checks were successful
Deploy / changes (push) Successful in 5s
Deploy / build-frontend (push) Has been skipped
Deploy / build-backend (push) Successful in 1m24s
Deploy / deploy (push) Successful in 1m24s
Deploy / build-worker (push) Successful in 2m40s
2026-06-03 08:57:00 +00:00
fc312fe606 feat(forecasting): §13 report assembler (#988, 955-A2) (#1021)
All checks were successful
Deploy / build-backend (push) Successful in 1m22s
Deploy / build-worker (push) Successful in 2m36s
Deploy / deploy (push) Successful in 1m13s
Deploy / changes (push) Successful in 5s
Deploy / build-frontend (push) Has been skipped
2026-06-03 08:51:23 +00:00
47fe5a55eb feat(forecasting): §15 confidence engine v2 (#990, 955-A4) (#1020)
All checks were successful
Deploy / changes (push) Successful in 6s
Deploy / build-frontend (push) Has been skipped
Deploy / build-backend (push) Successful in 1m24s
Deploy / build-worker (push) Successful in 2m58s
Deploy / deploy (push) Successful in 1m16s
2026-06-03 08:41:07 +00:00
75c9b34987 feat(forecasting): §13 SiteFinderReport object (#987, 955-A1) (#1019)
All checks were successful
Deploy / changes (push) Successful in 6s
Deploy / build-frontend (push) Has been skipped
Deploy / build-backend (push) Successful in 1m24s
Deploy / build-worker (push) Successful in 2m43s
Deploy / deploy (push) Successful in 1m14s
2026-06-03 08:33:01 +00:00
008f9d9ed1 feat(forecasting): §25 six special indices (#986, 954-C) (#1018)
All checks were successful
Deploy / changes (push) Successful in 5s
Deploy / build-frontend (push) Has been skipped
Deploy / build-backend (push) Successful in 1m28s
Deploy / build-worker (push) Successful in 2m31s
Deploy / deploy (push) Successful in 1m8s
2026-06-03 08:24:08 +00:00
98dea0315a feat(forecasting): §14.2 product scoring card (#985, 954-B) (#1017)
All checks were successful
Deploy / changes (push) Successful in 6s
Deploy / build-backend (push) Successful in 1m33s
Deploy / build-worker (push) Successful in 2m30s
Deploy / deploy (push) Successful in 1m10s
Deploy / build-frontend (push) Has been skipped
2026-06-03 08:11:54 +00:00
8057468c13 feat(forecasting): §11 macro-scenarios (#984, 954-A) (#1016)
All checks were successful
Deploy / changes (push) Successful in 5s
Deploy / build-frontend (push) Has been skipped
Deploy / build-backend (push) Successful in 2m29s
Deploy / build-worker (push) Successful in 3m37s
Deploy / deploy (push) Successful in 1m21s
2026-06-03 07:59:22 +00:00
691ccef4b7 feat(forecasting): class/commercial/USP §10.2/10.4/10.5 + §16 (#983, 953-B) (#1015)
All checks were successful
Deploy / changes (push) Successful in 6s
Deploy / build-frontend (push) Has been skipped
Deploy / build-backend (push) Successful in 1m32s
Deploy / build-worker (push) Successful in 2m32s
Deploy / deploy (push) Successful in 1m10s
2026-06-03 07:52:24 +00:00
72e9b24f2c feat(recommend): horizon-aware recommend_mix opt-in overlay (#982, 953-A) (#1014)
All checks were successful
Deploy / changes (push) Successful in 5s
Deploy / build-frontend (push) Has been skipped
Deploy / build-backend (push) Successful in 1m24s
Deploy / build-worker (push) Successful in 2m33s
Deploy / deploy (push) Successful in 1m4s
2026-06-03 07:40:33 +00:00
489e380f1e feat(forecasting): what-to-build ranker + MAI proxy (#981, 952-B) (#1013)
All checks were successful
Deploy / build-backend (push) Successful in 1m31s
Deploy / build-worker (push) Successful in 2m43s
Deploy / deploy (push) Successful in 1m9s
Deploy / changes (push) Successful in 5s
Deploy / build-frontend (push) Has been skipped
2026-06-03 07:07:17 +00:00
70ffa399fc feat(forecasting): demand-supply forecast engine (#980, 952-A) (#1012)
All checks were successful
Deploy / changes (push) Successful in 5s
Deploy / build-frontend (push) Has been skipped
Deploy / build-backend (push) Successful in 1m24s
Deploy / build-worker (push) Successful in 2m28s
Deploy / deploy (push) Successful in 1m10s
2026-06-03 06:56:33 +00:00
9758e21cbd feat(forecasting): §9.4 demand-normalization coefficient (#951f, advisory) (#1011)
All checks were successful
Deploy / changes (push) Successful in 5s
Deploy / build-frontend (push) Has been skipped
Deploy / build-backend (push) Successful in 1m22s
Deploy / build-worker (push) Successful in 2m34s
Deploy / deploy (push) Successful in 1m5s
2026-06-03 06:28:14 +00:00
69ad6c87fc feat(forecasting): §9.5 macro coefficient (#951e, advisory) (#1010)
All checks were successful
Deploy / changes (push) Successful in 5s
Deploy / build-frontend (push) Has been skipped
Deploy / build-backend (push) Successful in 1m22s
Deploy / build-worker (push) Successful in 3m27s
Deploy / deploy (push) Successful in 1m11s
2026-06-03 06:20:04 +00:00
f41e2536d8 feat(forecasting): §9.6 key-rate sensitivity module (#951d, advisory) (#1009)
All checks were successful
Deploy / changes (push) Successful in 5s
Deploy / build-frontend (push) Has been skipped
Deploy / build-backend (push) Successful in 1m30s
Deploy / build-worker (push) Successful in 2m39s
Deploy / deploy (push) Successful in 1m13s
2026-06-03 06:06:16 +00:00
c7bfc9e22a feat(forecasting): monthly sales series builder for §9.6 (#951c) (#1008)
All checks were successful
Deploy / changes (push) Successful in 5s
Deploy / build-frontend (push) Has been skipped
Deploy / build-backend (push) Successful in 1m20s
Deploy / build-worker (push) Successful in 2m17s
Deploy / deploy (push) Successful in 1m6s
2026-06-03 05:52:33 +00:00
ace3b99508 feat(forecasting): monthly macro series + regime classifier (#951b) (#1007)
All checks were successful
Deploy / changes (push) Successful in 5s
Deploy / build-frontend (push) Has been skipped
Deploy / build-backend (push) Successful in 1m23s
Deploy / build-worker (push) Successful in 2m43s
Deploy / deploy (push) Successful in 1m5s
2026-06-03 05:37:43 +00:00
a10592847d feat(site_finder): future-supply-pressure index (#950 Step 6) (#1006)
All checks were successful
Deploy / changes (push) Successful in 5s
Deploy / build-frontend (push) Has been skipped
Deploy / build-backend (push) Successful in 1m20s
Deploy / build-worker (push) Successful in 2m23s
Deploy / deploy (push) Successful in 1m5s
2026-06-03 05:10:08 +00:00
ae85f19993 feat(workers): supply-layers refresh task + weekly beat (#950 Step 5) (#1005)
All checks were successful
Deploy / build-frontend (push) Has been skipped
Deploy / build-backend (push) Successful in 1m22s
Deploy / deploy (push) Successful in 1m9s
Deploy / changes (push) Successful in 6s
Deploy / build-worker (push) Successful in 2m22s
2026-06-02 20:47:05 +00:00
900802264a feat(site-finder): supply_layers v2 compute service (#950 EPIC6 step3+4) (#1004)
All checks were successful
Deploy / build-worker (push) Successful in 2m30s
Deploy / changes (push) Successful in 5s
Deploy / build-frontend (push) Has been skipped
Deploy / build-backend (push) Successful in 1m40s
Deploy / deploy (push) Successful in 1m6s
2026-06-02 20:34:26 +00:00
59be55f80e feat(site-finder): per-competitor relevance_weight (#949 PR B) (#1000)
All checks were successful
Deploy / changes (push) Successful in 6s
Deploy / build-worker (push) Successful in 2m34s
Deploy / build-frontend (push) Has been skipped
Deploy / build-backend (push) Successful in 1m26s
Deploy / deploy (push) Successful in 1m10s
2026-06-02 19:45:38 +00:00
45d61ecff0 feat(site_finder): market-metrics service (#949 PR A, §9.2) (#997)
All checks were successful
Deploy / build-frontend (push) Has been skipped
Deploy / build-backend (push) Successful in 1m30s
Deploy / changes (push) Successful in 5s
Deploy / build-worker (push) Successful in 3m49s
Deploy / deploy (push) Successful in 1m10s
2026-06-02 19:26:26 +00:00
c78c6ec2d0 feat(site-finder): live CBR key-rate scraper → macro_indicator (#945 PR B) (#964)
All checks were successful
Deploy / build-frontend (push) Has been skipped
Deploy / deploy (push) Successful in 1m10s
Deploy / changes (push) Successful in 6s
Deploy / build-backend (push) Successful in 1m24s
Deploy / build-worker (push) Successful in 2m53s
2026-06-02 19:05:57 +00:00
dbae4b0bda feat(site-finder): macro_indicator table + backfill + reader (#945 PR A) (#963)
All checks were successful
Deploy / build-frontend (push) Has been skipped
Deploy / build-worker (push) Successful in 2m20s
Deploy / build-backend (push) Successful in 2m32s
Deploy / deploy (push) Successful in 1m14s
Deploy / changes (push) Successful in 6s
2026-06-02 18:41:28 +00:00
aa8ca9bcfc feat(backend): add /api/v1/ping liveness endpoint (#634)
All checks were successful
Deploy / changes (push) Successful in 6s
Deploy / build-frontend (push) Has been skipped
Deploy / build-backend (push) Successful in 1m28s
Deploy / build-worker (push) Successful in 3m2s
Deploy / deploy (push) Successful in 1m11s
Co-authored-by: bot-backend <bot-backend@gendsgn.local>
Co-committed-by: bot-backend <bot-backend@gendsgn.local>
2026-05-29 15:53:29 +00:00
6cb3d7515c fix(tradein): per-bucket velocity formula + realistic срок продажи (#574) (#631)
All checks were successful
Deploy / build-backend (push) Successful in 1m25s
Deploy / build-worker (push) Successful in 2m57s
Deploy / deploy (push) Successful in 1m10s
Deploy / changes (push) Successful in 5s
Deploy / build-frontend (push) Has been skipped
2026-05-28 15:35:16 +00:00
07106d744a chore(mv): regression test + sql rule for weighted AVG (#295) (#626)
Some checks failed
Deploy / changes (push) Successful in 5s
Deploy / build-frontend (push) Has been skipped
Deploy / build-worker (push) Successful in 33s
Deploy / build-backend (push) Successful in 37s
Deploy / deploy (push) Has been cancelled
2026-05-28 13:49:54 +00:00
c08e4d7811 feat(rbac): pilot scope → только /trade-in/** (#587)
All checks were successful
Deploy Trade-In / changes (push) Successful in 6s
Deploy / changes (push) Successful in 5s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-backend (push) Successful in 26s
Deploy / build-backend (push) Successful in 28s
Deploy / build-worker (push) Successful in 28s
Deploy / build-frontend (push) Successful in 27s
Deploy Trade-In / deploy (push) Successful in 36s
Deploy / deploy (push) Successful in 55s
Decision 2026-05-26: pilot не видит landing/analytics/site-finder/concept, только Trade-In.
2026-05-26 07:40:44 +00:00
88cdfd6adb feat(rbac): role-based access control via X-Authenticated-User middleware (#585)
All checks were successful
Deploy Trade-In / changes (push) Successful in 5s
Deploy / changes (push) Successful in 6s
Deploy Trade-In / build-frontend (push) Successful in 27s
Deploy / build-frontend (push) Successful in 30s
Deploy Trade-In / build-backend (push) Successful in 1m8s
Deploy Trade-In / deploy (push) Successful in 45s
Deploy / build-backend (push) Successful in 2m40s
Deploy / build-worker (push) Successful in 3m20s
Deploy / deploy (push) Successful in 1m16s
Backend RBAC + Caddy header_up. Closes part of #fixes-rbac-pra.
2026-05-26 06:18:40 +00:00
f9adb5cc2f feat(22d): catalog scraper — skip-today filter + force flag (#348)
All checks were successful
Deploy / changes (push) Successful in 5s
Deploy / build-frontend (push) Has been skipped
Deploy / build-backend (push) Successful in 1m19s
Deploy / build-worker (push) Successful in 2m26s
Deploy / deploy (push) Successful in 49s
2026-05-17 23:15:22 +00:00
0567ad2130 feat(22d): domrf_catalog_object scraper — fill ~25 NULL kn_objects cols from SSR __NEXT_DATA__ (#335)
Some checks failed
Deploy / changes (push) Successful in 7s
Deploy / build-backend (push) Successful in 1m43s
Deploy / build-frontend (push) Has been skipped
Deploy / build-worker (push) Successful in 2m49s
Deploy / deploy (push) Failing after 44s
2026-05-17 21:26:22 +00:00
c61c8c86af feat(sf-b1): GET /parcels/by-bbox — map entry endpoint + parcel_user_status table (#336)
Some checks failed
Deploy / deploy (push) Blocked by required conditions
Deploy / changes (push) Successful in 5s
Deploy / build-frontend (push) Has been skipped
Deploy / build-worker (push) Has been cancelled
Deploy / build-backend (push) Has been cancelled
2026-05-17 21:25:48 +00:00
b013fd886c feat(sf-b6): GET /parcels/{cad}/poi-score — weighted top-7 (#333)
Some checks failed
Deploy / deploy (push) Blocked by required conditions
Deploy / changes (push) Successful in 5s
Deploy / build-frontend (push) Has been skipped
Deploy / build-backend (push) Successful in 2m35s
Deploy / build-worker (push) Has been cancelled
2026-05-17 21:11:25 +00:00
7a8d59beb4 feat(obj-3): SF backend migrate to objective_lots ground truth (price 0.3% -> 81%) (#324)
Some checks failed
Deploy / build-frontend (push) Blocked by required conditions
Deploy / deploy (push) Blocked by required conditions
Deploy / changes (push) Successful in 5s
Deploy / build-backend (push) Has been cancelled
Deploy / build-worker (push) Has been cancelled
2026-05-17 20:08:17 +00:00
lekss361
689327909c fix(sf-09): capacity-aware redistribute (round 2) — bounded + correct invariant
Round 1 (commit bcd7dc8) был broken: на 2-bucket входах surplus уходил в free
полностью без учёта capacity → free превышал cap → следующая итерация
clamp'ировала его и наоборот. Infinite oscillation в FastAPI handler.

Round 2 fix per review BLOCK (#282 comment):
- Surplus распределяется пропорционально available capacity (cap - v),
  не текущему v. Free никогда не вылетит выше cap.
- free = строго < cap (не <=) — иначе деление на 0 capacity.
- Hard guard `for _ in range(N+1)` — гарантированно завершается.
- Pathological (surplus > total_capacity): возвращаем оригинальный pct_map
  + cap_skipped=True (sum=100 invariant сохранён).
- Hamilton round вынесен в _hamilton_round() helper.

Tests:
- 2-bucket cases (90/10, 70/30, 99/1) expected cap_skipped=True
- test_cap_iteration_count_bounded — все pathological завершаются < 100ms
- All 13 cases verified standalone (3 fast-path + 7 reproduced + 3 pathological)
2026-05-17 15:30:50 +03:00
lekss361
bcd7dc8f75 fix(sf-09): iterative cap algorithm + cap_skipped flag + unit tests
Review verdict MINOR (PR #282) — single-pass cap_and_redistribute
не гарантировал invariant max ≤ MAX_BUCKET_SHARE_PCT. Surplus от clamped
поднимал free выше cap (например {1k:75, studio:15, 2k:10} → studio=39).

- backend/app/services/site_finder/best_layouts.py
  - _cap_and_redistribute → iterative while-loop до сходимости
  - Returns (result_map, cap_skipped) — флаг для pathological all-clamped
  - Float work map, Hamilton финальный pass для invariant sum=100
- backend/app/schemas/parcel.py
  - LayoutTzRecommendation.cap_skipped: bool = False
- backend/tests/services/site_finder/test_best_layouts.py
  - 6 parametrized tests: invariants, 7 failing cases from review,
    no-dominant unchanged, empty, cap_skipped propagation, normal-case
- frontend/src/types/best-layouts.ts — cap_skipped: boolean
- frontend/src/components/site-finder/BestLayoutsBlock.tsx
  - banner условие сменилось с maxPct > 60 на rec.cap_skipped (honest signal)
2026-05-17 15:21:51 +03:00
lekss361
1799e1fb30 fix(sf-02): добавить site_status/ready_dt в /analyze competitors + sort строящиеся first
SQL в /analyze не выбирал site_status, ready_dt из domrf_kn_objects;
ORDER BY flat_count DESC ставил сданные ЖК в топ.

Closes (epic part) #271 item 2
2026-05-17 13:29:59 +03:00
lekss361
c701636515 Merge forgejo/main into fix/sf-01-velocity-time-window
Resolve conflict in best_layouts.py: keep SF-01 inline velocity
(deals_window numerator) + add SF-05 sold_pct clamp at 100% and
is_oversold flag from main.
2026-05-17 12:15:40 +03:00
lekss361
5a4408e0ab fix(sf-01): time_window honest velocity — inline SQL с реальным фильтром report_month
Раньше _VELOCITY_DIVISORS делил агрегаты mv_layout_velocity (24 мес)
на 4/12 для quarter/year, не меняя реальное окно данных. Теперь
inline SQL из objective_corpus_room_month с CAST(:window_interval AS interval).

velocity_per_month = deals_window / months_in_window (1.0/3.0/12.0).
Разные time_window → разные строки из БД → разный mix/velocity/jk_count.

Closes (epic part) #271 item 1
2026-05-17 12:12:19 +03:00
lekss361
247c252b04 fix(sf-05): clamp sold_pct_of_supply at 100% + add is_oversold flag
Symptom: sold_pct_of_supply = sum_deals_24mo / supply_count_snapshot * 100
yields >100% (e.g. 199%) for fast-selling small formats due to incompatible
time windows (24-month deals vs point-in-time supply snapshot).

Option A (hotfix): clamp to 100.0, expose is_oversold=True for UI badge.

- best_layouts.py: compute sold_pct_raw, clamp with min(..., 100.0),
  set is_oversold = raw > 100%
- parcel.py TopLayoutRow: add is_oversold: bool field
- best-layouts.ts TopLayoutRow: add is_oversold: boolean
- BestLayoutsBlock.tsx: show warn badge ">100%" when is_oversold=True
- tests: two new cases — raw 199% clamps + is_oversold=True;
  raw 50% passes through + is_oversold=False

Closes (epic part) #271 item 5
2026-05-17 11:57:29 +03:00
f160c5efaf Merge pull request 'feat(#263 sub-A): cad_* overlay schemas + cadastre territorial_zones (ПЗЗ)' (#265) from feat/263-sub-a-cad-nspd-overlay-schemas-and-pzz into main
Some checks failed
Deploy / build-frontend (push) Blocked by required conditions
Deploy / deploy (push) Blocked by required conditions
Deploy / changes (push) Successful in 6s
Deploy / build-worker (push) Has been cancelled
Deploy / build-backend (push) Has been cancelled
2026-05-17 07:50:08 +00:00
lekss361
8e377059a4 fix(#265 review): CRS 3857→4326 + stable zone_id hash + rename 102 + tx comment
- #1 (BLOCK): ST_GeogFromGeoJSON → ST_Transform(ST_SetSRID(..., 3857), 4326)::geography
  so EPSG:3857 features from get_territorial_zones_in_bbox are correctly
  stored in GEOGRAPHY(POLYGON, 4326) column (matches cad_parcels/cad_buildings pattern)
- #2: zone_id fallback changed from counter-based f"{quarter_cad}_{inserted}"
  to md5(json(props))[:12] — stable between runs, prevents wrong-row ON CONFLICT
- #3: rename 101_cad_nspd_overlay_tables.sql → 102_* (101_user_custom_pois.sql exists)
- #4: add comment clarifying SQLAlchemy autobegin guarantees outer tx before begin_nested()
- Tests: updated test_feature_without_zone_id_uses_fallback to assert md5 format + idempotency
2026-05-17 10:45:52 +03:00
lekss361
f08db43145 feat(#263 sub-A): cad_* overlay schemas + cadastre territorial_zones (ПЗЗ)
Phase 1 (migration 101): создаёт 5 новых таблиц cad_territorial_zones,
cad_red_lines, cad_engineering_structures, cad_risk_zones,
cad_opportunity_parcels. Idempotent (IF NOT EXISTS), GIST-индексы.

Phase 2 partial (ПЗЗ only):
- NSPDBulkClient.get_features_in_bbox_grid — async grid-walk WMS GetFeatureInfo
- NSPDBulkClient.get_territorial_zones_in_bbox — convenience wrapper layer 875838
- harvest_quarter Phase 2.5 — вызов get_territorial_zones_in_bbox после grid-walk
- _save_territorial_zones — UPSERT loop с SAVEPOINT per-row, ON CONFLICT zone_id

9 unit tests (mock DB): insert/upsert/no-geom/no-id/error-isolation/savepoint pattern.
2026-05-17 10:28:22 +03:00
26ee3b015c Merge pull request 'fix(#261): custom_pois — regression tests для db.commit() в create/update/delete (P0 persistence)' (#262) from fix/261-custom-pois-missing-commit into main
All checks were successful
Deploy / changes (push) Successful in 5s
Deploy / build-worker (push) Successful in 2m23s
Deploy / deploy (push) Successful in 49s
Deploy / build-backend (push) Successful in 27s
Deploy / build-frontend (push) Has been skipped
2026-05-17 07:26:54 +00:00
8ebcd15ed6 Merge pull request 'fix(#231): switch search_by_quarter area layers to grid-walk (Sub-PR B)' (#260) from fix/231-nspd-search-by-quarter-grid into main
Some checks failed
Deploy / build-worker (push) Has been cancelled
Deploy / build-frontend (push) Blocked by required conditions
Deploy / deploy (push) Blocked by required conditions
Deploy / changes (push) Successful in 6s
Deploy / build-backend (push) Has been cancelled
2026-05-17 07:26:08 +00:00
lekss361
570c987e78 fix(#231): repair test_rate_limit_semaphore regression + docstring sync
Re-review feedback (PR #260 head 8191a85):

1. tests/scrapers/test_nspd_bulk_client.py:360 — test referenced bulk_mod._SEMAPHORE
   which was renamed to _SEMAPHORE_LIMIT in previous commit. AttributeError на
   полном pytest run. Заменено: assert bulk_mod._SEMAPHORE_LIMIT == 3 (smoke).
   max_concurrent <= 3 уже валидирует throttling — capacity check был лишним.

2. app/services/scrapers/nspd_client.py docstrings (lines 483, 506) — обновлены
   ссылки NSPDBulkClient._SEMAPHORE(3) → NSPDBulkClient._sem (per-instance,
   capacity=3) чтобы соответствовать новой архитектуре.
2026-05-17 10:22:16 +03:00
lekss361
c94545f0eb fix(#261): add db.commit() regression tests для custom_pois service
Проблема: POST /custom-pois возвращал 201 с id=N, но row не сохранялась
в БД — user_custom_pois оставалась пустой после request close.

Симптом возникает если service-функция не вызывает db.commit() явно:
get_db.finally → db.close() без commit → psycopg3 rollbacks pending tx.

Код (#257) содержит db.commit() во всех 3 мутациях, но тесты полностью
мокировали service layer через patch() — regression в commit не ловился.

Fix: добавлены 6 service-level тестов с паттерном "два сеанса":
- MagicMock(db) + db.commit.assert_called_once() для create/update/delete
- db.commit.assert_not_called() для not-found и empty-payload случаев

Closes #261
2026-05-17 10:16:36 +03:00
lekss361
8191a85102 fix(#231): address re-review blockers — per-instance semaphore + time_limit + test mocks
PR #260 second-opinion review 🔴 BLOCK → fixes:

1. nspd_bulk_client.py: _SEMAPHORE module-level → per-instance self._sem
   создаётся в __aenter__ под текущий event loop. Sequential asyncio.run()
   в search_by_quarter (11 layers × asyncio.run() per layer) bind'ил
   module-level semaphore к first loop'у → RuntimeError на втором вызове →
   все cells return_exceptions=True проглатывали → grid возвращал 0 features.

2. nspd_sync.py: harvest_quarter добавлен time_limit=900 (hard) рядом с
   soft_time_limit=600. Safety net если task игнорит SoftTimeLimitExceeded.

3. tests/test_nspd_client.py: добавлен _make_fake_grid_walk helper +
   monkeypatch NSPDClient.get_features_in_bbox_grid в 3 тестах
   (search_by_quarter_core_only / _with_zouit / _layers_fetched_with_risks).
   Без этого area layers били бы по живому NSPD API → CI flaky.

Tests: 37 passed, 1 skipped (3 area-dispatch + 34 grid-walk).
2026-05-17 10:13:34 +03:00