From 85761a869863657cd79da15c6973b9ef62cc695a Mon Sep 17 00:00:00 2001 From: Dave Date: Fri, 4 Sep 2026 12:55:53 +0200 Subject: [PATCH] fix(orders): improve error handling for order creation and enforce email body length limit Enhanced error messages to include specific field validation details from PocketBase, allowing callers to identify issues more easily. Additionally, implemented a safeguard to truncate the email body to a maximum of 5000 characters to prevent validation errors from surfacing as generic messages. --- src/app/api/v1/orders/create/route.ts | 22 +++++++++++++++++++++- src/lib/server/orders-service.ts | 12 ++++++++++-- 2 files changed, 31 insertions(+), 3 deletions(-) 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); }