Files
whyrating/apps/web/src/app/api/business/search/route.ts
Alejandro Gutiérrez 49edf70235 feat: add purchase wizard flow at /get-started
5-step guided wizard: find business → confirm → email → payment → success.
Includes Google Places search API, Stripe checkout API, i18n (EN/ES),
and CTA links updated to point to the new wizard.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 23:21:09 +00:00

150 lines
3.9 KiB
TypeScript

import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
interface Business {
place_id: string;
name: string;
address: string;
rating: number;
review_count: number;
category: string;
maps_url: string;
}
const GOOGLE_MAPS_URL_PATTERN =
/(?:google\.com\/maps|maps\.google\.com|goo\.gl\/maps|maps\.app\.goo\.gl)/i;
function isGoogleMapsUrl(input: string): boolean {
return GOOGLE_MAPS_URL_PATTERN.test(input);
}
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const { query } = body as { query: string };
if (!query || typeof query !== "string" || query.trim().length < 2) {
return NextResponse.json(
{ error: "Query must be at least 2 characters" },
{ status: 400 },
);
}
const trimmed = query.trim();
if (isGoogleMapsUrl(trimmed)) {
return handleUrlSearch(trimmed);
}
return handleTextSearch(trimmed);
} catch {
return NextResponse.json(
{ error: "Invalid request body" },
{ status: 400 },
);
}
}
async function handleUrlSearch(url: string): Promise<NextResponse> {
const engineUrl = process.env.NEXT_PUBLIC_ENGINE_URL;
if (!engineUrl) {
return NextResponse.json(
{ error: "Engine service not configured" },
{ status: 503 },
);
}
try {
const response = await fetch(`${engineUrl}/api/check-reviews`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ url }),
});
if (!response.ok) {
return NextResponse.json(
{ error: "Could not find business from URL" },
{ status: 404 },
);
}
const data = (await response.json()) as Record<string, unknown>;
const business: Business = {
place_id: (data.place_id as string) || "",
name: (data.name as string) || "Unknown Business",
address: (data.address as string) || "",
rating: (data.rating as number) || 0,
review_count: (data.review_count as number) || (data.reviews_count as number) || 0,
category: (data.category as string) || (data.type as string) || "",
maps_url: url,
};
return NextResponse.json({ businesses: [business] });
} catch {
return NextResponse.json(
{ error: "Failed to fetch business details" },
{ status: 502 },
);
}
}
async function handleTextSearch(query: string): Promise<NextResponse> {
const apiKey = process.env.GOOGLE_PLACES_API_KEY;
if (!apiKey) {
return NextResponse.json(
{ error: "Places search not configured" },
{ status: 503 },
);
}
try {
const response = await fetch(
"https://places.googleapis.com/v1/places:searchText",
{
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Goog-Api-Key": apiKey,
"X-Goog-FieldMask":
"places.id,places.displayName,places.formattedAddress,places.rating,places.userRatingCount,places.primaryTypeDisplayName,places.googleMapsUri",
},
body: JSON.stringify({
textQuery: query,
languageCode: "en",
}),
},
);
if (!response.ok) {
return NextResponse.json(
{ error: "Places search failed" },
{ status: 502 },
);
}
const data = (await response.json()) as Record<string, unknown>;
const businesses: Business[] = ((data.places as any[]) || [])
.slice(0, 5)
.map((place: any) => ({
place_id: place.id || "",
name: place.displayName?.text || "Unknown",
address: place.formattedAddress || "",
rating: place.rating || 0,
review_count: place.userRatingCount || 0,
category: place.primaryTypeDisplayName?.text || "",
maps_url: place.googleMapsUri || "",
}));
return NextResponse.json({ businesses });
} catch {
return NextResponse.json(
{ error: "Failed to search businesses" },
{ status: 502 },
);
}
}