import { z } from "zod"; import { BusinessDataBusinessListingsSearchLiveRequestInfo, BusinessDataGoogleQuestionsAndAnswersLiveRequestInfo, type BusinessDataBusinessListingsSearchLiveItem, } from "dataforseo-client"; import { businessDataApi } from "@/server/lib/dataforseo/core"; import { assertOk, buildTaskBilling, type DataforseoApiResponse, } from "@/server/lib/dataforseo/envelope"; type BusinessListingItem = BusinessDataBusinessListingsSearchLiveItem; export async function fetchBusinessListingsSearch(input: { categories?: string[]; title?: string; locationCoordinate: string; orderBy?: string[]; limit: number; }): Promise> { const response = await businessDataApi().businessListingsSearchLive([ new BusinessDataBusinessListingsSearchLiveRequestInfo({ categories: input.categories, title: input.title, location_coordinate: input.locationCoordinate, order_by: input.orderBy, limit: input.limit, }), ]); // "No Search Results" (40501) is a valid empty result for obscure // businesses/keywords — DataForSEO still charges for it, so treat it as an // empty success instead of surfacing a charged-task error to the user. const task = assertOk(response, { treatNoResultsAsEmpty: true }); return { data: task.result?.[0]?.items ?? [], billing: buildTaskBilling(task), }; } // Q&A results carry both answered (`items`) and unanswered // (`items_without_answers`) rows; the SDK types this result as `any`, so we // validate a generic record shape and flatten both. const questionsResultSchema = z .object({ items: z.array(z.record(z.string(), z.unknown())).nullable().optional(), items_without_answers: z .array(z.record(z.string(), z.unknown())) .nullable() .optional(), }) .passthrough(); function combinedQuestionItems(results: unknown): Record[] { const list = Array.isArray(results) ? results : []; return list.flatMap((result) => { const parsed = questionsResultSchema.safeParse(result ?? {}); if (!parsed.success) return []; return [ ...(parsed.data.items ?? []), ...(parsed.data.items_without_answers ?? []), ]; }); } export async function fetchQuestionsAnswers(input: { keyword: string; locationCoordinate: string; languageCode: string; depth: number; }): Promise[]>> { const response = await businessDataApi().googleQuestionsAndAnswersLive([ new BusinessDataGoogleQuestionsAndAnswersLiveRequestInfo({ keyword: input.keyword, location_coordinate: input.locationCoordinate, language_code: input.languageCode, depth: input.depth, }), ]); // "No Search Results" (40501) is a valid empty result for obscure // businesses/keywords — DataForSEO still charges for it, so treat it as an // empty success instead of surfacing a charged-task error to the user. const task = assertOk(response, { treatNoResultsAsEmpty: true }); return { data: combinedQuestionItems(task.result), billing: buildTaskBilling(task), }; }