gendesign/tradein-mvp/frontend/src/lib/trade-in-api.ts
Light1YT 63756c3c29
All checks were successful
Deploy Trade-In / changes (push) Successful in 5s
Deploy Trade-In / build-frontend (push) Successful in 1m38s
Deploy Trade-In / deploy (push) Successful in 35s
Deploy Trade-In / build-backend (push) Has been skipped
feat(tradein): StreetDealsCard с paired listings (PR L, #564 Phase 2) (#599)
2026-05-27 12:29:06 +00:00

190 lines
6.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import { useMutation, useQuery } from "@tanstack/react-query";
import { apiFetch } from "./api";
import type {
AggregatedEstimate,
CianPriceChangeStats,
HouseAnalyticsResponse,
HouseInfoForEstimate,
IMVBenchmarkResponse,
PlacementHistoryItem,
SalesVsListingsResponse,
SellTimeSensitivityResponse,
StreetDealsResponse,
TradeInEstimateInput,
} from "@/types/trade-in";
const BASE = "/api/v1/trade-in";
/**
* POST /api/v1/trade-in/estimate
* Sends apartment parameters and returns AggregatedEstimate (mock data for now).
*/
export function useEstimateMutation() {
return useMutation<AggregatedEstimate, Error, TradeInEstimateInput>({
mutationFn: (input) =>
apiFetch<AggregatedEstimate>(`${BASE}/estimate`, {
method: "POST",
body: JSON.stringify(input),
}),
});
}
/**
* GET /api/v1/trade-in/estimate/{id}
* Fetches a previously computed estimate by UUID (for shareable links / PDF).
*/
export function useEstimate(estimate_id: string | null) {
return useQuery<AggregatedEstimate>({
queryKey: ["trade-in", "estimate", estimate_id],
queryFn: () =>
apiFetch<AggregatedEstimate>(`${BASE}/estimate/${estimate_id}`),
enabled: estimate_id !== null && estimate_id.length > 0,
staleTime: 10 * 60_000, // 10 min — estimates expire at expires_at anyway
});
}
/**
* GET /api/v1/trade-in/estimate/{id}/houses
* Houses в радиусе 500m от target адреса estimate (Stage 2c data).
*/
export function useEstimateHouses(estimate_id: string | null) {
return useQuery<HouseInfoForEstimate[]>({
queryKey: ["trade-in", "estimate", estimate_id, "houses"],
queryFn: () =>
apiFetch<HouseInfoForEstimate[]>(`${BASE}/estimate/${estimate_id}/houses`),
enabled: estimate_id !== null && estimate_id.length > 0,
staleTime: 10 * 60_000,
});
}
/**
* GET /api/v1/trade-in/estimate/{id}/placement-history
* Historical listings from house_placement_history for the target house.
*/
export function useEstimatePlacementHistory(estimate_id: string | null) {
return useQuery<PlacementHistoryItem[]>({
queryKey: ["trade-in", "estimate", estimate_id, "placement-history"],
queryFn: () =>
apiFetch<PlacementHistoryItem[]>(
`${BASE}/estimate/${estimate_id}/placement-history`,
),
enabled: estimate_id !== null && estimate_id.length > 0,
staleTime: 10 * 60_000,
});
}
/**
* GET /api/v1/trade-in/estimate/{id}/imv-benchmark
* Avito IMV benchmark для UI badge (Stage 3 IMV cache lookup).
*/
export function useEstimateImvBenchmark(estimate_id: string | null) {
return useQuery<IMVBenchmarkResponse>({
queryKey: ["trade-in", "estimate", estimate_id, "imv-benchmark"],
queryFn: () =>
apiFetch<IMVBenchmarkResponse>(`${BASE}/estimate/${estimate_id}/imv-benchmark`),
enabled: estimate_id !== null && estimate_id.length > 0,
staleTime: 10 * 60_000,
});
}
/**
* GET /api/v1/trade-in/estimate/{id}/cian-price-changes
* Price change history for Cian analog listings (only those with >0 changes).
*/
export function useEstimateCianPriceChanges(estimate_id: string | null) {
return useQuery<CianPriceChangeStats[]>({
queryKey: ["trade-in", "estimate", estimate_id, "cian-price-changes"],
queryFn: () =>
apiFetch<CianPriceChangeStats[]>(
`${BASE}/estimate/${estimate_id}/cian-price-changes`,
),
enabled: estimate_id !== null && estimate_id.length > 0,
staleTime: 10 * 60_000,
});
}
/**
* GET /api/v1/trade-in/estimate/{id}/house-analytics
* Price history, KPI and recent sold lots for the target house.
*/
export function useEstimateHouseAnalytics(estimate_id: string | null) {
return useQuery<HouseAnalyticsResponse>({
queryKey: ["trade-in", "estimate", estimate_id, "house-analytics"],
queryFn: () =>
apiFetch<HouseAnalyticsResponse>(
`${BASE}/estimate/${estimate_id}/house-analytics`,
),
enabled: estimate_id !== null && estimate_id.length > 0,
staleTime: 10 * 60_000,
});
}
/**
* GET /api/v1/trade-in/street-deals
* ДКП-сделки Росреестра по улице target адреса. Per-street (open dataset
* без номера дома). Empty response если street не извлёкся.
*/
export function useStreetDeals(
address: string | null,
area_m2: number | null,
rooms: number | null,
) {
const params = new URLSearchParams();
if (address) params.set("address", address);
if (area_m2 !== null) params.set("area_m2", String(area_m2));
if (rooms !== null) params.set("rooms", String(rooms));
return useQuery<StreetDealsResponse>({
queryKey: ["trade-in", "street-deals", address, area_m2, rooms],
queryFn: () =>
apiFetch<StreetDealsResponse>(`${BASE}/street-deals?${params}`),
enabled: !!address && area_m2 !== null && rooms !== null,
staleTime: 5 * 60_000,
});
}
/**
* GET /api/v1/trade-in/sales-vs-listings
* ДКП-сделки + paired listings по улице (PR L / #564 Phase 2). Возвращает все
* пары LEFT JOIN — сделки без listing match сохраняются (listing_* = null),
* чтобы вычислить linkage_rate. Используется в StreetDealsCard для показа
* исторических ASK рядом с фактической ценой сделки.
*
* staleTime: 10 минут — данные обновляются раз в сутки (rosreestr import +
* scrapers), нет смысла часто рефетчить.
*/
export function useSalesVsListings(
address: string | null,
area_m2: number | null,
rooms: number | null,
) {
const params = new URLSearchParams();
if (address) params.set("address", address);
if (area_m2 !== null) params.set("area_m2", String(area_m2));
if (rooms !== null) params.set("rooms", String(rooms));
return useQuery<SalesVsListingsResponse>({
queryKey: ["trade-in", "sales-vs-listings", address, area_m2, rooms],
queryFn: () =>
apiFetch<SalesVsListingsResponse>(`${BASE}/sales-vs-listings?${params}`),
enabled: !!address && area_m2 !== null && rooms !== null,
staleTime: 10 * 60_000,
});
}
/**
* GET /api/v1/trade-in/estimate/{id}/sell-time-sensitivity
* Median exposure days bucketed by price premium (-5%, 0, +5%, +10%).
*/
export function useEstimateSellTimeSensitivity(estimate_id: string | null) {
return useQuery<SellTimeSensitivityResponse>({
queryKey: ["trade-in", "estimate", estimate_id, "sell-time-sensitivity"],
queryFn: () =>
apiFetch<SellTimeSensitivityResponse>(
`${BASE}/estimate/${estimate_id}/sell-time-sensitivity`,
),
enabled: estimate_id !== null && estimate_id.length > 0,
staleTime: 10 * 60_000,
});
}