import type { Pool, PoolClient } from 'pg'; import type { BusinessUpsertRecord } from './google-places.js'; import { mapBusinessRow, mapSearchJobRow, type BusinessDto, type BusinessRow, type RunSearchInput, type SearchJobDto, type SearchJobResultLinkDto, type SearchJobRow, } from './types.js'; type DbClient = Pool | PoolClient; export async function createSearchJob(db: DbClient, userId: string, payload: RunSearchInput) { const now = new Date().toISOString(); const jobName = payload.name || `${payload.businessType} in ${payload.location}`; const result = await db.query( ` insert into public.search_jobs ( user_id, name, city, radius_km, business_type, keywords, status, total_results, started_at, created_at, updated_at ) values ($1, $2, $3, $4, $5, $6, 'running', 0, $7, $7, $7) returning id, user_id, name, city, address, postal_code, radius_km, business_type, keywords, status, total_results, started_at, completed_at, created_at, updated_at `, [userId, jobName, payload.location, payload.radiusKm, payload.businessType, payload.keywords ?? null, now], ); return mapSearchJobRow(result.rows[0]); } export async function updateSearchJobCenter(db: DbClient, jobId: string, lat: number, lng: number) { await db.query( ` update public.search_jobs set search_center_geom = ST_SetSRID(ST_MakePoint($2, $1), 4326)::geography, updated_at = now() where id = $3 `, [lat, lng, jobId], ); } export async function completeSearchJob(db: DbClient, jobId: string, totalResults: number) { const completedAt = new Date().toISOString(); const result = await db.query( ` update public.search_jobs set total_results = $2, status = 'completed', completed_at = $3, updated_at = $3 where id = $1 returning id, user_id, name, city, address, postal_code, radius_km, business_type, keywords, status, total_results, started_at, completed_at, created_at, updated_at `, [jobId, totalResults, completedAt], ); return mapSearchJobRow(result.rows[0]); } export async function failSearchJob(db: DbClient, jobId: string) { await db.query( ` update public.search_jobs set status = 'failed', updated_at = now() where id = $1 `, [jobId], ); } export async function upsertBusiness(db: DbClient, userId: string, business: BusinessUpsertRecord) { const result = await db.query<{ id: string }>( ` insert into public.businesses ( user_id, external_source_id, source, name, address, city, state_province, postal_code, country, phone, website, rating, review_count, category, latitude, longitude, geom, metadata_json, first_seen_at, last_seen_at, updated_at ) values ( $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, case when $15 is not null and $16 is not null then ST_SetSRID(ST_MakePoint($16, $15), 4326)::geography else null end, $17::jsonb, $18, $19, $20 ) on conflict (user_id, source, external_source_id) do update set name = excluded.name, address = excluded.address, city = excluded.city, state_province = excluded.state_province, postal_code = excluded.postal_code, country = excluded.country, phone = excluded.phone, website = excluded.website, rating = excluded.rating, review_count = excluded.review_count, category = excluded.category, latitude = excluded.latitude, longitude = excluded.longitude, geom = excluded.geom, metadata_json = excluded.metadata_json, last_seen_at = excluded.last_seen_at, updated_at = excluded.updated_at returning id `, [ userId, business.externalSourceId, business.source, business.name, business.address, business.city, business.stateProvince, business.postalCode, business.country, business.phone, business.website, business.rating, business.reviewCount, business.category, business.latitude, business.longitude, JSON.stringify(business.metadataJson), business.firstSeenAt, business.lastSeenAt, business.updatedAt, ], ); return result.rows[0].id; } export async function upsertSearchJobResult( db: DbClient, input: { userId: string; searchJobId: string; businessId: string; matchedKeywords: string[] | null; rank: number; capturedAt: string }, ) { await db.query( ` insert into public.search_job_results (user_id, search_job_id, business_id, matched_keywords, rank, captured_at) values ($1, $2, $3, $4, $5, $6) on conflict (search_job_id, business_id) do update set matched_keywords = excluded.matched_keywords, rank = excluded.rank, captured_at = excluded.captured_at `, [input.userId, input.searchJobId, input.businessId, input.matchedKeywords, input.rank, input.capturedAt], ); } export async function listSearchJobsForUser(db: DbClient, userId: string, limit = 100) { const result = await db.query( ` select id, user_id, name, city, address, postal_code, radius_km, business_type, keywords, status, total_results, started_at, completed_at, created_at, updated_at from public.search_jobs where user_id = $1 order by created_at desc limit $2 `, [userId, limit], ); return result.rows.map(mapSearchJobRow); } export async function getSearchJobForUser(db: DbClient, userId: string, jobId: string) { const result = await db.query( ` select id, user_id, name, city, address, postal_code, radius_km, business_type, keywords, status, total_results, started_at, completed_at, created_at, updated_at from public.search_jobs where user_id = $1 and id = $2 limit 1 `, [userId, jobId], ); if (result.rowCount === 0) { return null; } return mapSearchJobRow(result.rows[0]); } export async function listBusinessesForUser(db: DbClient, userId: string) { const result = await db.query( ` select id, user_id, external_source_id, source, name, address, city, state_province, postal_code, country, phone, website, rating, review_count, category, hours_json, latitude, longitude, general_info, metadata_json, first_seen_at, last_seen_at, created_at, updated_at from public.businesses where user_id = $1 order by created_at desc `, [userId], ); return result.rows.map(mapBusinessRow); } export async function listSearchJobResultLinksForUser(db: DbClient, userId: string): Promise { const result = await db.query<{ business_id: string; search_job_id: string }>( ` select business_id, search_job_id from public.search_job_results where user_id = $1 `, [userId], ); return result.rows.map((row) => ({ businessId: row.business_id, searchJobId: row.search_job_id })); } export async function listBusinessesForJobIds(db: DbClient, userId: string, jobIds: string[]): Promise { if (jobIds.length === 0) { return []; } const result = await db.query( ` select distinct b.id, b.user_id, b.external_source_id, b.source, b.name, b.address, b.city, b.state_province, b.postal_code, b.country, b.phone, b.website, b.rating, b.review_count, b.category, b.hours_json, b.latitude, b.longitude, b.general_info, b.metadata_json, b.first_seen_at, b.last_seen_at, b.created_at, b.updated_at from public.businesses b join public.search_job_results r on r.business_id = b.id where b.user_id = $1 and r.search_job_id = any($2::uuid[]) order by b.created_at desc `, [userId, jobIds], ); return result.rows.map(mapBusinessRow); }