import { aggregateAcrossTeams, formatAggregationResult, loopGet, } from "../lib/loop-api.js"; // ─── Loop API V2: /people ────────────────────────────────────── // Fields: id, title, firstName, lastName, contactNotes, // primaryPhone, primaryPhoneType, primaryPhoneNotes, // primaryEmail, primaryEmailType, primaryEmailNotes, // gdprGeneralMarketing, gdprPropertyMatching, gdprThirdParties, // communicationCall, communicationEmail, communicationPost, communicationText // ──────────────────────────────────────────────────────────────── interface LoopPersonSummary { id: number; title?: string; firstName?: string; lastName?: string; contactNotes?: string; primaryPhone?: string; primaryPhoneType?: string; primaryEmail?: string; primaryEmailType?: string; [key: string]: unknown; } // ─── Loop API V2: /people/buyers ─────────────────────────────── // Fields: id, buyerGroupName, buyerIsGroup, maxPrice, minBeds, // position, sellingPosition, purchaseReason, financiallyVerified, // mortgageOffered, insuranceOffered, offerCount, acceptedOfferCount, // viewingCount, upcomingViewingCount // ──────────────────────────────────────────────────────────────── interface LoopBuyerSummary { id: number; buyerGroupName?: string; buyerIsGroup?: boolean; maxPrice?: number | null; minBeds?: number; position?: string; sellingPosition?: string | null; purchaseReason?: string | null; financiallyVerified?: string | null; mortgageOffered?: string | null; insuranceOffered?: string | null; offerCount?: number; acceptedOfferCount?: number; viewingCount?: number; upcomingViewingCount?: number; [key: string]: unknown; } // ─── Loop API V2: /people/sellers ────────────────────────────── // Fields: id, sellerGroupName, sellerIsGroup, propertyId, // propertyStatus, propertyType, propertyAddress, price, // offerCount, acceptedOfferCount, viewingCount, upcomingViewingCount // ──────────────────────────────────────────────────────────────── interface LoopSellerSummary { id: number; sellerGroupName?: string; sellerIsGroup?: boolean; propertyId?: number; propertyStatus?: string; propertyType?: string; propertyAddress?: string; price?: number; offerCount?: number; acceptedOfferCount?: number; viewingCount?: number; upcomingViewingCount?: number; [key: string]: unknown; } type PeopleRole = "buyers" | "sellers" | "renters" | "landlords"; export async function peopleSearch(params: { accountId: string; role?: PeopleRole; searchTerm?: string; // Role-specific filters (buyers) maxPrice?: number; minBeds?: number; searchAreas?: string; propertyTypes?: string; // Role-specific filters (sellers/landlords) startDate?: string; endDate?: string; minPrice?: number; // Role-specific filters (renters) maxRent?: number; teamName?: string; limit?: number; }): Promise { const { accountId, role, searchTerm, maxPrice, minBeds, searchAreas, propertyTypes, startDate, endDate, minPrice, maxRent, teamName, limit, } = params; // Role-specific endpoints return different schemas — use a generic record // type for aggregation, then cast per-role in the formatter. const result = await aggregateAcrossTeams>( accountId, "people", "loop-people-search", async (apiKey, team) => { const qp: string[] = []; if (searchTerm) qp.push(`SearchTerm=${encodeURIComponent(searchTerm)}`); if (role === "buyers") { if (maxPrice != null) qp.push(`MaxPrice=${maxPrice}`); if (minBeds != null) qp.push(`MinBeds=${minBeds}`); if (searchAreas) qp.push(`SearchAreas=${encodeURIComponent(searchAreas)}`); if (propertyTypes) qp.push(`PropertyTypes=${encodeURIComponent(propertyTypes)}`); } else if (role === "sellers" || role === "landlords") { if (startDate) qp.push(`StartDate=${encodeURIComponent(startDate)}`); if (endDate) qp.push(`EndDate=${encodeURIComponent(endDate)}`); if (minPrice != null) qp.push(`MinPrice=${minPrice}`); if (maxPrice != null) qp.push(`MaxPrice=${maxPrice}`); } else if (role === "renters") { if (maxRent != null) qp.push(`MaxRent=${maxRent}`); if (minBeds != null) qp.push(`MinBeds=${minBeds}`); if (searchAreas) qp.push(`SearchAreas=${encodeURIComponent(searchAreas)}`); if (propertyTypes) qp.push(`PropertyTypes=${encodeURIComponent(propertyTypes)}`); } else { // Generic /people endpoint if (startDate) qp.push(`StartDate=${encodeURIComponent(startDate)}`); if (endDate) qp.push(`EndDate=${encodeURIComponent(endDate)}`); } const query = qp.length > 0 ? `?${qp.join("&")}` : ""; const basePath = role ? `/people/${role}` : "/people"; const path = `${basePath}${query}`; const data = await loopGet[]>(apiKey, path, "loop-people-search", team); return Array.isArray(data) ? data : []; }, { teamName, limitPerTeam: limit ?? 50, limitTotal: limit ?? 200 } ); const entityName = role ?? "people"; return formatAggregationResult( result, (record) => formatPersonRecord(record, role), entityName ); } function formatPersonRecord(record: Record, role?: PeopleRole): string { if (role === "buyers") { const p = record as unknown as LoopBuyerSummary; const name = p.buyerGroupName ?? "Unknown"; const id = p.id != null ? ` [ID: ${p.id}]` : ""; const price = p.maxPrice != null ? ` — max £${p.maxPrice.toLocaleString("en-GB")}` : ""; const beds = p.minBeds ? ` ${p.minBeds}+ beds` : ""; const position = p.position && p.position !== "none" ? ` (${p.position})` : ""; const viewings = p.viewingCount ? ` | ${p.viewingCount} viewing${p.viewingCount !== 1 ? "s" : ""}` : ""; const offers = p.offerCount ? ` | ${p.offerCount} offer${p.offerCount !== 1 ? "s" : ""}` : ""; return `- ${name}${id}${price}${beds}${position}${viewings}${offers}`; } if (role === "sellers" || role === "landlords") { const p = record as unknown as LoopSellerSummary; const name = p.sellerGroupName ?? "Unknown"; const id = p.id != null ? ` [ID: ${p.id}]` : ""; const addr = p.propertyAddress ? ` — ${p.propertyAddress}` : ""; const price = p.price != null ? ` £${p.price.toLocaleString("en-GB")}` : ""; const status = p.propertyStatus ? ` [${p.propertyStatus}]` : ""; const viewings = p.viewingCount ? ` | ${p.viewingCount} viewing${p.viewingCount !== 1 ? "s" : ""}` : ""; const offers = p.offerCount ? ` | ${p.offerCount} offer${p.offerCount !== 1 ? "s" : ""}` : ""; return `- ${name}${id}${addr}${price}${status}${viewings}${offers}`; } // Generic /people or /people/renters — uses PersonSummary schema const p = record as unknown as LoopPersonSummary; const name = [p.firstName, p.lastName].filter(Boolean).join(" ") || "Unknown"; const id = p.id != null ? ` [ID: ${p.id}]` : ""; const contact = p.primaryEmail ?? p.primaryPhone ?? ""; const title = p.title?.trim() ? `${p.title.trim()} ` : ""; return `- ${title}${name}${id}${contact ? ` <${contact}>` : ""}`; }