Initial commit - Route Commerce platform

This commit is contained in:
2026-06-01 19:40:55 +00:00
commit 53a9671461
617 changed files with 106132 additions and 0 deletions
+64
View File
@@ -0,0 +1,64 @@
import { NextRequest, NextResponse } from "next/server";
import { getAdminUser } from "@/lib/admin-permissions";
import { getAIClient } from "@/actions/integrations/ai-providers";
export async function POST(req: NextRequest) {
const adminUser = await getAdminUser();
if (!adminUser) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
if (!adminUser.can_manage_messages) {
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
}
const { topic, brandId, brandName } = await req.json();
if (!topic) {
return NextResponse.json({ error: "topic is required" }, { status: 400 });
}
// Brand scoping
const effectiveBrandId = adminUser.brand_id ?? brandId ?? null;
if (!effectiveBrandId) {
return NextResponse.json({ error: "Brand ID required" }, { status: 400 });
}
if (adminUser.role === "brand_admin" && adminUser.brand_id !== brandId) {
return NextResponse.json({ error: "Not authorized for this brand" }, { status: 403 });
}
const aiResult = await getAIClient(effectiveBrandId);
if (!aiResult.client) {
return NextResponse.json({ error: "error" in aiResult ? aiResult.error : "AI not configured" }, { status: 503 });
}
const systemPrompt = `You are a campaign idea generator for Route Commerce, a B2B fresh produce wholesale platform.
Given a topic, generate exactly 3 campaign ideas for a produce brand.
Each idea should include: angle (one sentence describing the campaign approach), subject line (compelling, max 60 chars), body (3-4 sentences, persuasive, fits brand voice).
Return valid JSON: { "ideas": [{ "angle": "...", "subject": "...", "body": "..." }] }
Brand voice: friendly, transparent, emphasizes freshness and local origin.
Audience: wholesale produce buyers, restaurant owners, farm stand operators.`;
try {
const client = aiResult.client as { chat: { completions: { create: (opts: unknown) => Promise<{ choices: Array<{ message: { content: string } }> }> } } };
const response = await client.chat.completions.create({
model: aiResult.model,
messages: [
{ role: "system", content: systemPrompt },
{ role: "user", content: `Topic: ${topic}\nBrand: ${brandName || "our produce brand"}` },
],
response_format: { type: "json_object" },
temperature: 0.8,
});
const content = response.choices[0]?.message?.content;
if (!content) {
return NextResponse.json({ error: "No response from AI" }, { status: 500 });
}
const parsed = JSON.parse(content);
return NextResponse.json({ ideas: parsed.ideas ?? [] });
} catch (err) {
return NextResponse.json({ error: "Generation failed" }, { status: 500 });
}
}
+132
View File
@@ -0,0 +1,132 @@
import { NextRequest, NextResponse } from "next/server";
import { getAdminUser } from "@/lib/admin-permissions";
import { getAIClient } from "@/actions/integrations/ai-providers";
import { svcHeaders } from "@/lib/svc-headers";
const SYSTEM = `You are a data analyst for a B2B produce wholesale platform.
Given a natural language customer query, return a JSON response indicating which predefined query to run and what parameters to use.
Query types (each has a predefined SQL template):
1. "dormant" — customers who haven't ordered in X days (parameter: days, default 45)
2. "trending" — products ordered most in the last N days (parameter: days, default 30)
3. "top_customers" — top customers by order total in last N days (parameter: days, default 90)
4. "recent_orders" — orders created in last N days (parameter: days, default 14)
5. "at_risk" — customers who had orders in past 90 days but none in last 30 days
Return JSON with:
{
"queryType": "dormant" | "trending" | "top_customers" | "recent_orders" | "at_risk",
"days": number (or null for at_risk which always uses 30/90),
"explanation": "plain English explanation of what this query will return",
"fallback": "plain language result if AI analysis fails"
}`;
export async function POST(req: NextRequest) {
const adminUser = await getAdminUser();
if (!adminUser) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
if (!adminUser.can_manage_reports) {
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
}
try {
const { brandId, nlQuery } = await req.json();
if (!brandId || !nlQuery) {
return NextResponse.json({ error: "Missing brandId or nlQuery" }, { status: 400 });
}
// Brand scoping: platform_admin passes explicit brandId; brand_admin limited to own brand
const effectiveBrandId = adminUser.brand_id ?? brandId ?? null;
if (!effectiveBrandId) {
return NextResponse.json({ error: "Brand ID required" }, { status: 400 });
}
if (adminUser.role === "brand_admin" && adminUser.brand_id !== brandId) {
return NextResponse.json({ error: "Not authorized for this brand" }, { status: 403 });
}
const aiResult = await getAIClient(effectiveBrandId);
if (!aiResult.client || !("client" in aiResult)) {
return NextResponse.json({ error: "error" in aiResult ? (aiResult as { error?: string }).error ?? "AI not configured" : "AI not configured" }, { status: 503 });
}
const userMessage = `Brand: ${effectiveBrandId}
User asked: "${nlQuery}"
Classify this query and return JSON with queryType, days, explanation, and fallback.
Use "at_risk" for customers who had orders in past 90 days but none in last 30 days.
Use "dormant" for customers who haven't ordered in a while.
Use "trending" for product popularity questions.
Use "top_customers" for customer ranking questions.
Use "recent_orders" for recent order questions.`;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const client = aiResult.client as { chat: { completions: { create: (opts: unknown) => Promise<{ choices: Array<{ message: { content: string } }> }> } } };
const response = await client.chat.completions.create({
model: aiResult.model,
messages: [
{ role: "system", content: SYSTEM },
{ role: "user", content: userMessage },
],
response_format: { type: "json_object" },
temperature: 0.2,
});
const content = response.choices[0]?.message?.content;
if (!content) {
return NextResponse.json({ error: "No response from AI" }, { status: 500 });
}
const parsed = JSON.parse(content);
const queryType = parsed.queryType ?? "recent_orders";
const days = parsed.days ?? 30;
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY;
// Route to appropriate RPC based on query type
let rpcName = "get_recent_orders_insights";
let rpcParams: Record<string, unknown> = { p_brand_id: effectiveBrandId, p_days: days };
if (queryType === "dormant") {
rpcName = "get_dormant_customers_insights";
rpcParams = { p_brand_id: effectiveBrandId, p_days: days };
} else if (queryType === "trending") {
rpcName = "get_trending_products_insights";
rpcParams = { p_brand_id: effectiveBrandId, p_days: days };
} else if (queryType === "top_customers") {
rpcName = "get_top_customers_insights";
rpcParams = { p_brand_id: effectiveBrandId, p_days: days };
} else if (queryType === "at_risk") {
rpcName = "get_at_risk_customers_insights";
rpcParams = { p_brand_id: effectiveBrandId };
}
const dbResponse = await fetch(
`${supabaseUrl}/rest/v1/rpc/${rpcName}`,
{
method: "POST",
headers: { ...svcHeaders(supabaseKey!), "Content-Type": "application/json" },
body: JSON.stringify(rpcParams),
}
);
let results: unknown[] = [];
if (dbResponse.ok) {
const data = await dbResponse.json();
results = Array.isArray(data) ? data.slice(0, 100) : [];
}
return NextResponse.json({
queryType,
explanation: parsed.explanation ?? "",
results,
count: results.length,
fallback: parsed.fallback ?? "",
nlQuery,
});
} catch (err) {
return NextResponse.json({ error: "AI analysis failed" }, { status: 500 });
}
}
+82
View File
@@ -0,0 +1,82 @@
import { NextRequest, NextResponse } from "next/server";
import { getAdminUser } from "@/lib/admin-permissions";
import { getAIClient } from "@/actions/integrations/ai-providers";
export async function POST(req: NextRequest) {
const adminUser = await getAdminUser();
if (!adminUser) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
if (!adminUser.can_manage_reports) {
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
}
try {
const { brandId, productName, historicalData, stopName } = await req.json();
if (!brandId) {
return NextResponse.json({ error: "Missing brandId" }, { status: 400 });
}
// Brand scoping
const effectiveBrandId = adminUser.brand_id ?? brandId ?? null;
if (!effectiveBrandId) {
return NextResponse.json({ error: "Brand ID required" }, { status: 400 });
}
if (adminUser.role === "brand_admin" && adminUser.brand_id !== brandId) {
return NextResponse.json({ error: "Not authorized for this brand" }, { status: 403 });
}
const aiResult = await getAIClient(effectiveBrandId);
if (!aiResult.client || !("client" in aiResult)) {
return NextResponse.json({ error: "error" in aiResult ? (aiResult as { error?: string }).error ?? "AI not configured" : "AI not configured" }, { status: 503 });
}
const SYSTEM = `You are a demand forecasting analyst for a B2B produce wholesale platform.
Given historical order data for a product or stop, predict future demand and recommend stock levels.
Return JSON with this exact shape:
{
"currentTrend": "description of recent demand pattern",
"prediction": {
"nextStopVolume": number,
"nextWeekVolume": number,
"confidence": "high" | "medium" | "low",
"confidenceReason": "why confidence is this level"
},
"recommendedStock": {
"units": number,
"reasoning": "why this level"
},
"seasonalFactors": ["factor 1", "factor 2"],
"riskFlags": ["concern 1"]
}`;
const userMessage = `Brand: ${effectiveBrandId}
Product: ${productName ?? "unknown"}
Stop: ${stopName ?? "general"}
Historical Order Data: ${JSON.stringify(historicalData ?? [])}
Analyze demand patterns and return JSON with currentTrend, prediction (nextStopVolume, nextWeekVolume, confidence, confidenceReason), recommendedStock (units, reasoning), seasonalFactors array, and riskFlags array.`;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const client = aiResult.client as { chat: { completions: { create: (opts: unknown) => Promise<{ choices: Array<{ message: { content: string } }> }> } } };
const response = await client.chat.completions.create({
model: aiResult.model,
messages: [
{ role: "system", content: SYSTEM },
{ role: "user", content: userMessage },
],
response_format: { type: "json_object" },
temperature: 0.4,
});
const content = response.choices[0]?.message?.content;
if (!content) {
return NextResponse.json({ error: "No response from AI" }, { status: 500 });
}
return NextResponse.json(JSON.parse(content));
} catch (err) {
return NextResponse.json({ error: "AI analysis failed" }, { status: 500 });
}
}
+82
View File
@@ -0,0 +1,82 @@
import { NextRequest, NextResponse } from "next/server";
import { getAdminUser } from "@/lib/admin-permissions";
import { getAIClient } from "@/actions/integrations/ai-providers";
export async function POST(req: NextRequest) {
const adminUser = await getAdminUser();
if (!adminUser) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
if (!adminUser.can_manage_reports) {
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
}
try {
const { brandId, productName, currentPriceTiers, historicalSales } = await req.json();
if (!brandId) {
return NextResponse.json({ error: "Missing brandId" }, { status: 400 });
}
// Brand scoping
const effectiveBrandId = adminUser.brand_id ?? brandId ?? null;
if (!effectiveBrandId) {
return NextResponse.json({ error: "Brand ID required" }, { status: 400 });
}
if (adminUser.role === "brand_admin" && adminUser.brand_id !== brandId) {
return NextResponse.json({ error: "Not authorized for this brand" }, { status: 403 });
}
const aiResult = await getAIClient(effectiveBrandId);
if (!aiResult.client || !("client" in aiResult)) {
return NextResponse.json({ error: "error" in aiResult ? (aiResult as { error?: string }).error ?? "AI not configured" : "AI not configured" }, { status: 503 });
}
const SYSTEM = `You are a pricing strategist for a B2B produce wholesale platform.
Given product sales data, recommend price adjustments with reasoning and estimated revenue impact.
Return JSON with exact shape:
{
"currentState": "brief description of current pricing situation",
"recommendations": [
{
"productName": "product name",
"currentPrice": number,
"suggestedPrice": number,
"direction": "increase" | "decrease" | "maintain",
"reasoning": "why this change",
"estimatedRevenueImpact": "±$X or 'minimal'"
}
],
"opportunities": ["high-level opportunity 1", "opportunity 2"],
"warnings": ["potential issue 1"]
}`;
const userMessage = `Brand: ${effectiveBrandId}
Product: ${productName ?? "unknown"}
Current Price Tiers: ${JSON.stringify(currentPriceTiers ?? [])}
Historical Sales (last 30-90 days): ${JSON.stringify(historicalSales ?? [])}
Analyze pricing and return JSON with currentState, recommendations array, opportunities, and warnings.`;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const client = aiResult.client as { chat: { completions: { create: (opts: unknown) => Promise<{ choices: Array<{ message: { content: string } }> }> } } };
const response = await client.chat.completions.create({
model: aiResult.model,
messages: [
{ role: "system", content: SYSTEM },
{ role: "user", content: userMessage },
],
response_format: { type: "json_object" },
temperature: 0.4,
});
const content = response.choices[0]?.message?.content;
if (!content) {
return NextResponse.json({ error: "No response from AI" }, { status: 500 });
}
return NextResponse.json(JSON.parse(content));
} catch (err) {
return NextResponse.json({ error: "AI analysis failed" }, { status: 500 });
}
}
+77
View File
@@ -0,0 +1,77 @@
import { NextRequest, NextResponse } from "next/server";
import { getAdminUser } from "@/lib/admin-permissions";
import { getAIClient } from "@/actions/integrations/ai-providers";
export async function POST(req: NextRequest) {
const adminUser = await getAdminUser();
if (!adminUser) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
if (!adminUser.can_manage_products) {
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
}
const { productName, category, price, unit, brandId } = await req.json();
if (!productName) {
return NextResponse.json({ error: "productName is required" }, { status: 400 });
}
// Brand scoping
const effectiveBrandId = adminUser.brand_id ?? brandId ?? null;
if (!effectiveBrandId) {
return NextResponse.json({ error: "Brand ID required" }, { status: 400 });
}
if (adminUser.role === "brand_admin" && adminUser.brand_id !== brandId) {
return NextResponse.json({ error: "Not authorized for this brand" }, { status: 403 });
}
const aiResult = await getAIClient(effectiveBrandId);
if (!aiResult.client || !("client" in aiResult)) {
return NextResponse.json({ error: "error" in aiResult ? (aiResult as { error?: string }).error ?? "AI not configured" : "AI not configured" }, { status: 503 });
}
const systemPrompt = `You are a product copywriter for Route Commerce, a B2B fresh produce wholesale platform.
Given a product, write: a product name (can improve on input), a marketing description (2-3 sentences, persuasive, highlights freshness/origin/quality), image alt text (max 125 chars, SEO-friendly), and a brief pricing note if relevant.
Return valid JSON: { "name": "...", "description": "...", "altText": "...", "priceNote": "..." (or null) }
Brand voice: friendly, transparent, professional.
Audience: wholesale buyers, restaurant owners, farm stand operators.`;
try {
const client = aiResult.client as { chat: { completions: { create: (opts: unknown) => Promise<{ choices: Array<{ message: { content: string } }> }> } } };
const response = await client.chat.completions.create({
model: aiResult.model,
messages: [
{ role: "system", content: systemPrompt },
{
role: "user",
content: [
`Product: ${productName}`,
category ? `Category: ${category}` : "",
price ? `Price: ${price}${unit ? ` ${unit}` : ""}` : "",
]
.filter(Boolean)
.join("\n"),
},
],
response_format: { type: "json_object" },
temperature: 0.7,
});
const content = response.choices[0]?.message?.content;
if (!content) {
return NextResponse.json({ error: "No response from AI" }, { status: 500 });
}
const parsed = JSON.parse(content);
return NextResponse.json({
name: parsed.name ?? productName,
description: parsed.description ?? "",
altText: parsed.altText ?? "",
priceNote: parsed.priceNote ?? null,
});
} catch (err) {
return NextResponse.json({ error: "Generation failed" }, { status: 500 });
}
}
+87
View File
@@ -0,0 +1,87 @@
import { NextRequest, NextResponse } from "next/server";
import { getAdminUser } from "@/lib/admin-permissions";
import { getAIClient } from "@/actions/integrations/ai-providers";
const REPORT_PROMPTS: Record<string, string> = {
"orders-by-stop": `This is an Orders by Stop report. Each row has: stop_name, city, state, date, order_count, gross_sales, pending_count, completed_count. Identify top-performing stops, underperforming stops, and patterns in pending vs completed orders.`,
"sales-by-product": `This is a Sales by Product report. Each row has: product_name, units_sold, gross_revenue, avg_price. Identify top sellers, underperformers, and pricing anomalies. Flag any products with unusually high/low average prices.`,
"fulfillment": `This is a Fulfillment Breakdown report. Each row has: fulfillment_type (pickup/shipping), order_count, revenue, pct_of_total. Analyze pickup vs shipping split and revenue distribution.`,
"pickup-status": `This is a Pickup Status report. Each row has: stop_name, city, date, total_orders, pending, completed, canceled. Identify stops with high pending rates, high cancellation rates, and completion speed.`,
"contact-growth": `This is a Contact Growth report. Each row has: date, new_contacts, imports, total. Identify growth trends, import spikes, and rate-of-change patterns.`,
"campaigns": `This is a Campaign Activity report. Each row has: campaign_name, status, campaign_type, sent_at, messages_logged. Identify which campaign types perform best, timing patterns, and engagement levels.`,
};
const SYSTEM = `You are a data analyst for a B2B produce wholesale distribution platform.
You analyze report data and return structured insights in plain English.
Always be specific and cite numbers from the data.
Format your response as JSON with this exact shape:
{
"summary": "2-3 sentence plain-English summary of what this report shows",
"keyInsights": ["insight 1", "insight 2", "insight 3"],
"suggestedActions": ["action 1", "action 2"]
}
Be direct and actionable. Do not use hedging language.`;
export async function POST(req: NextRequest) {
const adminUser = await getAdminUser();
if (!adminUser) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
if (!adminUser.can_manage_reports) {
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
}
try {
const { reportType, dateRange, brandId, reportData } = await req.json();
if (!reportType || !reportData) {
return NextResponse.json({ error: "Missing reportType or reportData" }, { status: 400 });
}
// Brand scoping: platform_admin passes explicit brandId; brand_admin limited to own brand
const effectiveBrandId = adminUser.brand_id ?? brandId ?? null;
if (!effectiveBrandId) {
return NextResponse.json({ error: "Brand ID required" }, { status: 400 });
}
const aiResult = await getAIClient(effectiveBrandId);
if (!aiResult.client || !("client" in aiResult)) {
return NextResponse.json({ error: "error" in aiResult ? (aiResult as { error?: string }).error ?? "AI not configured" : "AI not configured" }, { status: 503 });
}
const prompt = REPORT_PROMPTS[reportType] ?? `This is a ${reportType} report. Analyze it.`;
// Build a compact sample of rows (max 20 for token efficiency)
const sampleRows = Array.isArray(reportData) ? reportData.slice(0, 20) : [];
const userMessage = `Date range: ${dateRange?.start ?? "unknown"} to ${dateRange?.end ?? "unknown"}
Brand: ${effectiveBrandId}
${prompt}
Data:
${JSON.stringify(sampleRows, null, 2)}
Return JSON with summary, keyInsights (array), and suggestedActions (array).`;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const client = aiResult.client as { chat: { completions: { create: (opts: unknown) => Promise<{ choices: Array<{ message: { content: string } }> }> } } };
const response = await client.chat.completions.create({
model: aiResult.model,
messages: [
{ role: "system", content: SYSTEM },
{ role: "user", content: userMessage },
],
response_format: { type: "json_object" },
temperature: 0.3,
});
const content = response.choices[0]?.message?.content;
if (!content) {
return NextResponse.json({ error: "No response from AI" }, { status: 500 });
}
return NextResponse.json(JSON.parse(content));
} catch (err) {
return NextResponse.json({ error: "AI analysis failed" }, { status: 500 });
}
}
+86
View File
@@ -0,0 +1,86 @@
import { NextRequest, NextResponse } from "next/server";
import { getAdminUser } from "@/lib/admin-permissions";
import { getAIClient } from "@/actions/integrations/ai-providers";
export async function POST(req: NextRequest) {
const adminUser = await getAdminUser();
if (!adminUser) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
if (!adminUser.can_manage_reports) {
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
}
try {
const { brandId, stops, startLocation } = await req.json();
if (!brandId || !stops || !Array.isArray(stops) || stops.length < 2) {
return NextResponse.json({ error: "Need at least 2 stops to optimize" }, { status: 400 });
}
// Brand scoping
const effectiveBrandId = adminUser.brand_id ?? brandId ?? null;
if (!effectiveBrandId) {
return NextResponse.json({ error: "Brand ID required" }, { status: 400 });
}
if (adminUser.role === "brand_admin" && adminUser.brand_id !== brandId) {
return NextResponse.json({ error: "Not authorized for this brand" }, { status: 403 });
}
const aiResult = await getAIClient(effectiveBrandId);
if (!aiResult.client || !("client" in aiResult)) {
return NextResponse.json({ error: "error" in aiResult ? (aiResult as { error?: string }).error ?? "AI not configured" : "AI not configured" }, { status: 503 });
}
const SYSTEM = `You are a route optimization assistant for a B2B produce wholesale platform.
Given a list of stops with city/state/address and constraints, return an optimized delivery sequence.
Return JSON with this exact shape:
{
"optimizedSequence": [
{
"position": 1,
"stopName": "Stop A",
"city": "Greeley",
"state": "CO",
"reason": "why placed here (e.g., 'nearest to start point', 'time window constraint')"
}
],
"totalEstimatedDistance": "X miles",
"totalEstimatedDriveTime": "X hours Y minutes",
"warnings": ["issue 1"],
"suggestions": ["improvement 1"]
}`;
const stopsList = stops.map((s: { name?: string; city?: string; state?: string; address?: string; time_window?: string }, i: number) =>
`${i + 1}. ${s.name ?? "Stop"}${s.city ?? ""}, ${s.state ?? ""} ${s.address ? "(" + s.address + ")" : ""} ${s.time_window ? "[window: " + s.time_window + "]" : ""}`
).join("\n");
const userMessage = `Brand: ${effectiveBrandId}
Start location: ${startLocation ?? "First stop"}
Stops to sequence:
${stopsList}
Optimize the route for efficiency. Return JSON with optimizedSequence, totalEstimatedDistance, totalEstimatedDriveTime, warnings, and suggestions.`;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const client = aiResult.client as { chat: { completions: { create: (opts: unknown) => Promise<{ choices: Array<{ message: { content: string } }> }> } } };
const response = await client.chat.completions.create({
model: aiResult.model,
messages: [
{ role: "system", content: SYSTEM },
{ role: "user", content: userMessage },
],
response_format: { type: "json_object" },
temperature: 0.3,
});
const content = response.choices[0]?.message?.content;
if (!content) {
return NextResponse.json({ error: "No response from AI" }, { status: 500 });
}
return NextResponse.json(JSON.parse(content));
} catch (err) {
return NextResponse.json({ error: "AI analysis failed" }, { status: 500 });
}
}
@@ -0,0 +1,88 @@
import { NextRequest, NextResponse } from "next/server";
import { getAdminUser } from "@/lib/admin-permissions";
import { getAIClient } from "@/actions/integrations/ai-providers";
export async function POST(req: NextRequest) {
const adminUser = await getAdminUser();
if (!adminUser) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
if (!adminUser.can_manage_messages) {
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
}
try {
const { brandId, stopId, stopName, stopDate, city, state, recentOrders, customerCount } = await req.json();
if (!brandId || !stopName) {
return NextResponse.json({ error: "Missing required fields" }, { status: 400 });
}
// Brand scoping
const effectiveBrandId = adminUser.brand_id ?? brandId ?? null;
if (!effectiveBrandId) {
return NextResponse.json({ error: "Brand ID required" }, { status: 400 });
}
if (adminUser.role === "brand_admin" && adminUser.brand_id !== brandId) {
return NextResponse.json({ error: "Not authorized for this brand" }, { status: 403 });
}
const aiResult = await getAIClient(effectiveBrandId);
if (!aiResult.client || !("client" in aiResult)) {
return NextResponse.json({ error: "error" in aiResult ? (aiResult as { error?: string }).error ?? "AI not configured" : "AI not configured" }, { status: 503 });
}
const SYSTEM = `You are a marketing strategist for a B2B produce wholesale platform.
Given a stop's order history and customer data, recommend the optimal stop blast message.
Return JSON with exact shape:
{
"timingRecommendation": "e.g., 'Send 2 days before the stop, between 9-11am for best open rates'",
"subjectLine": "email subject line",
"bodyPreview": "2-3 sentence email body preview",
"audienceSize": "estimated number of recipients",
"audienceRecommendation": "who should receive this (e.g., 'all pickup customers for this stop' or 'customers who ordered from this stop in last 90 days')",
"contentAngles": [
{
"angle": "e.g., 'Freshness angle'",
"reasoning": "why this angle works for this stop"
},
{
"angle": "e.g., 'Urgency/discount angle'",
"reasoning": "why this angle works"
}
],
"warnings": ["potential issue 1"]
}`;
const userMessage = `Brand: ${effectiveBrandId}
Stop: ${stopName}
Date: ${stopDate ?? "unknown"}
City: ${city ?? ""}, ${state ?? ""}
Recent Orders: ${JSON.stringify(recentOrders ?? [])}
Customer Count: ${customerCount ?? "unknown"}
Analyze this stop's context and order history to recommend the optimal stop blast message.
Return JSON with timingRecommendation, subjectLine, bodyPreview, audienceSize, audienceRecommendation, contentAngles array, and warnings.`;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const client = aiResult.client as { chat: { completions: { create: (opts: unknown) => Promise<{ choices: Array<{ message: { content: string } }> }> } } };
const response = await client.chat.completions.create({
model: aiResult.model,
messages: [
{ role: "system", content: SYSTEM },
{ role: "user", content: userMessage },
],
response_format: { type: "json_object" },
temperature: 0.4,
});
const content = response.choices[0]?.message?.content;
if (!content) {
return NextResponse.json({ error: "No response from AI" }, { status: 500 });
}
return NextResponse.json(JSON.parse(content));
} catch (err) {
return NextResponse.json({ error: "AI analysis failed" }, { status: 500 });
}
}