80 lines
2.2 KiB
TypeScript
80 lines
2.2 KiB
TypeScript
|
|
import {
|
||
|
|
DEFAULT_DISCLAIMER,
|
||
|
|
isPriceSearchResponse,
|
||
|
|
parseN8nResponseBody,
|
||
|
|
} from "@/src/lib/price-search/normalize";
|
||
|
|
import type { PriceSearchRequest, PriceSearchResponse } from "@/src/types/priceSearch";
|
||
|
|
|
||
|
|
function getWebhookConfig() {
|
||
|
|
const url =
|
||
|
|
process.env.N8N_PRICE_SEARCH_WEBHOOK_URL?.trim() ??
|
||
|
|
process.env.N8N_PRICE_WEBHOOK_URL?.trim();
|
||
|
|
const secret = process.env.N8N_PRICE_WEBHOOK_SECRET?.trim();
|
||
|
|
const timeoutMs = Number(process.env.N8N_PRICE_WEBHOOK_TIMEOUT_MS ?? 45_000);
|
||
|
|
|
||
|
|
if (!url || !secret) {
|
||
|
|
throw new Error("Ricerca prezzi n8n non configurata (URL o secret mancanti)");
|
||
|
|
}
|
||
|
|
|
||
|
|
return { url, secret, timeoutMs: Number.isFinite(timeoutMs) ? timeoutMs : 45_000 };
|
||
|
|
}
|
||
|
|
|
||
|
|
export async function searchPriceOffersViaN8n(
|
||
|
|
input: PriceSearchRequest,
|
||
|
|
meta?: { userId?: string; userName?: string },
|
||
|
|
): Promise<PriceSearchResponse> {
|
||
|
|
const { url, secret, timeoutMs } = getWebhookConfig();
|
||
|
|
|
||
|
|
const controller = new AbortController();
|
||
|
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
||
|
|
|
||
|
|
try {
|
||
|
|
const res = await fetch(url, {
|
||
|
|
method: "POST",
|
||
|
|
headers: {
|
||
|
|
"Content-Type": "application/json",
|
||
|
|
"X-Webhook-Secret": secret,
|
||
|
|
},
|
||
|
|
body: JSON.stringify({
|
||
|
|
...input,
|
||
|
|
meta: {
|
||
|
|
source: "magricambi",
|
||
|
|
userId: meta?.userId ?? null,
|
||
|
|
userName: meta?.userName ?? null,
|
||
|
|
requestedAt: new Date().toISOString(),
|
||
|
|
},
|
||
|
|
}),
|
||
|
|
signal: controller.signal,
|
||
|
|
cache: "no-store",
|
||
|
|
});
|
||
|
|
|
||
|
|
const text = await res.text();
|
||
|
|
const data = parseN8nResponseBody(text, res.status);
|
||
|
|
|
||
|
|
if (!res.ok) {
|
||
|
|
const err =
|
||
|
|
data && typeof data === "object" && "error" in data
|
||
|
|
? String((data as { error: unknown }).error)
|
||
|
|
: `Errore n8n (HTTP ${res.status})`;
|
||
|
|
throw new Error(err);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!isPriceSearchResponse(data)) {
|
||
|
|
throw new Error(
|
||
|
|
"Formato risposta n8n non conforme. Verifica il nodo Respond OK del workflow.",
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
return data;
|
||
|
|
} catch (error) {
|
||
|
|
if (error instanceof Error && error.name === "AbortError") {
|
||
|
|
throw new Error("Timeout ricerca prezzi: riprova tra poco");
|
||
|
|
}
|
||
|
|
throw error;
|
||
|
|
} finally {
|
||
|
|
clearTimeout(timer);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
export { DEFAULT_DISCLAIMER };
|