From 49b13c6f4121e4e0f48107249bb0d59474d3ce30 Mon Sep 17 00:00:00 2001 From: admin Date: Tue, 16 Jun 2026 22:40:17 +0300 Subject: [PATCH] =?UTF-8?q?fix(orders):=20incoming=20payload=20=E2=80=94?= =?UTF-8?q?=20optional=20fields=20use=20.nullish(),=20accept=20ISO=20date?= =?UTF-8?q?=20OR=20datetime?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.7 (1M context) --- apps/api/src/modules/orders/routes.ts | 40 +++++++++++++++------------ 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/apps/api/src/modules/orders/routes.ts b/apps/api/src/modules/orders/routes.ts index 3cde965..7d45858 100644 --- a/apps/api/src/modules/orders/routes.ts +++ b/apps/api/src/modules/orders/routes.ts @@ -10,27 +10,33 @@ import { verifySiteKey } from '../sites/auth-verify.js'; const STATUSES = ['new', 'accepted', 'invoiced', 'paid', 'fulfilled', 'cancelled'] as const; const VAT_VALUES = ['none', 'vat_0', 'vat_5', 'vat_7', 'vat_10', 'vat_20'] as const; -// Внешний (S2S) payload от сайта — толерантный +// Внешний (S2S) payload от сайта — толерантный. Optional поля = .nullish() +// (можно опустить в JSON, прислать null или валидное значение). +const isoDateOrDateTime = z.string().regex( + /^\d{4}-\d{2}-\d{2}(?:T\d{2}:\d{2}(?::\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:?\d{2})?)?)?$/, + 'нужна дата ISO (YYYY-MM-DD) или datetime', +); + const IncomingItem = z.object({ name: z.string().min(1).max(500), qty: z.coerce.number().positive().default(1), unit: z.string().max(50).default('шт'), - priceRub: z.coerce.number().nonnegative(), // в рублях, чтобы сайту удобней + priceRub: z.coerce.number().nonnegative(), vat: z.enum(VAT_VALUES).default('none'), - eventDate: z.string().datetime().nullable().optional(), - serviceSlug: z.string().optional(), // если сайт знает наш каталог + eventDate: isoDateOrDateTime.nullish(), + serviceSlug: z.string().optional(), }); const IncomingOrder = z.object({ customerName: z.string().min(1).max(500), - customerEmail: optionalText(200), - customerPhone: optionalText(50), - customerInn: optionalText(20), - customerKpp: optionalText(20), - customerAddress: optionalText(1000), + customerEmail: z.string().email().nullish(), + customerPhone: z.string().max(50).nullish(), + customerInn: z.string().max(20).nullish(), + customerKpp: z.string().max(20).nullish(), + customerAddress: z.string().max(1000).nullish(), customerKind: z.enum(['ul', 'ip', 'fl']).default('ul'), - notes: optionalText(2000), - acceptedOfferAt: z.string().datetime().nullable().optional(), + notes: z.string().max(2000).nullish(), + acceptedOfferAt: isoDateOrDateTime.nullish(), items: z.array(IncomingItem).min(1), }); @@ -161,17 +167,17 @@ export async function ordersRoutes(app: FastifyInstance) { siteId: localSite?.id ?? null, status: 'new', customerName: data.customerName, - customerEmail: data.customerEmail, - customerPhone: data.customerPhone, - customerInn: data.customerInn, - customerKpp: data.customerKpp, - customerAddress: data.customerAddress, + customerEmail: data.customerEmail ?? null, + customerPhone: data.customerPhone ?? null, + customerInn: data.customerInn ?? null, + customerKpp: data.customerKpp ?? null, + customerAddress: data.customerAddress ?? null, customerKind: data.customerKind, totalCents: sums.totalCents, vatCents: sums.vatCents, currency: 'RUB', acceptedOfferAt: data.acceptedOfferAt ? new Date(data.acceptedOfferAt) : null, - notes: data.notes, + notes: data.notes ?? null, rawPayload: req.body as Prisma.InputJsonValue, items: { create: data.items.map((it, i) => {