diff --git a/src/app/api/v1/orders/create/route.ts b/src/app/api/v1/orders/create/route.ts index 1575bd8..943fa0a 100644 --- a/src/app/api/v1/orders/create/route.ts +++ b/src/app/api/v1/orders/create/route.ts @@ -130,7 +130,27 @@ export async function POST(req: NextRequest) { ); } - const message = error instanceof Error ? error.message : "Failed to create order"; + // PocketBase's ClientResponseError carries the real per-field + // validation reason (e.g. "email_body: Must be no more than 5000 + // character(s).") in `.response.data`/`.data`, not `.message` — + // `.message` is always the generic "Failed to create record." + // Surface the field details when present so callers (n8n, the + // agent API) don't have to go dig through PB's own request log to + // find out which field actually failed. + const pbData = (error as { response?: { data?: unknown }; data?: unknown })?.response?.data + ?? (error as { data?: unknown })?.data; + const fieldDetails = + pbData && typeof pbData === "object" && "data" in (pbData as Record) + ? (pbData as { data?: Record; message?: string }).data + : undefined; + + const baseMessage = error instanceof Error ? error.message : "Failed to create order"; + const message = + fieldDetails && Object.keys(fieldDetails).length > 0 + ? `${baseMessage}: ${Object.entries(fieldDetails) + .map(([field, info]) => `${field} - ${info?.message ?? "invalid"}`) + .join("; ")}` + : baseMessage; const status = (error as Error & { status?: number }).status || 500; // 404 from getOne propagates with status === 404 if PB throws it. // Otherwise we bucket by message: customer-not-found / company-not-found diff --git a/src/lib/server/orders-service.ts b/src/lib/server/orders-service.ts index 70dfa3b..3df9cb9 100644 --- a/src/lib/server/orders-service.ts +++ b/src/lib/server/orders-service.ts @@ -19,6 +19,14 @@ const QUOTE_COLLECTION = "quotes"; const QUOTE_ITEMS_COLLECTION = "quote_items"; const ORDER_COUNTERS_COLLECTION = "order_counters"; +// PB schema caps `orders.email_body` at 5000 chars. Long reply threads +// (quoted history, signatures, legal disclaimers) routinely exceed +// that, and PB's validation error only surfaces in `err.data`, not +// `err.message` — so an oversized body used to bubble up as an opaque +// "Failed to create record." Truncate defensively at the write site +// instead of trusting every caller to pre-trim. +const ORDER_EMAIL_BODY_MAX_LENGTH = 5000; + export function getOrderItemsCollectionName() { return process.env.ORDER_ITEMS_COLLECTION || "order_items"; } @@ -2123,7 +2131,7 @@ export async function createOrder(input: CreateOrderInput): Promise = {}; for (const [k, v] of Object.entries(patch)) { if ((ORDER_PATCHABLE_FIELDS as readonly string[]).includes(k)) { - cleaned[k] = v; + cleaned[k] = k === "email_body" && typeof v === "string" ? v.slice(0, ORDER_EMAIL_BODY_MAX_LENGTH) : v; } else { rejected.push(k); }