// Отправка заявки на POST /api/lead и перевод ответа сервера в состояние формы. import type { FieldErrors, LeadField, LeadRequest } from '@kvarta/shared/lead'; export const SUBMIT_TIMEOUT_MS = 10_000; /** Что сказать посетителю после отправки. */ export type SubmitNotice = 'created' | 'clarified' | 'invalid' | 'rate_limited' | 'failed'; export type SubmitResult = { status: 'success' | 'error'; notice: SubmitNotice; /** подсказки под полями при 400 */ fieldErrors: FieldErrors; }; export type SendLead = (body: LeadRequest) => Promise; const FIELDS: readonly LeadField[] = ['name', 'phone', 'interest', 'comment']; const success = (notice: 'created' | 'clarified'): SubmitResult => ({ status: 'success', notice, fieldErrors: {} }); const error = (notice: SubmitNotice, fieldErrors: FieldErrors = {}): SubmitResult => ({ status: 'error', notice, fieldErrors, }); /** Сеть, таймаут, отказ fetch. */ export const FAILED: SubmitResult = error('failed'); function pickFieldErrors(value: unknown): FieldErrors { if (!value || typeof value !== 'object') return {}; const src = value as Record; const out: FieldErrors = {}; for (const field of FIELDS) { const message = src[field]; if (typeof message === 'string' && message !== '') out[field] = message; } return out; } /** Ответ сервера → состояние формы. Тело может быть чем угодно, в том числе null. */ export function resultFromResponse(status: number, body: unknown): SubmitResult { const data = (body && typeof body === 'object' ? body : {}) as Record; if (status === 201) return success('created'); if (status === 200 && data.ok === true) return success(data.clarified === true ? 'clarified' : 'created'); if (status === 400) { const fieldErrors = pickFieldErrors(data.fieldErrors); return Object.keys(fieldErrors).length > 0 ? error('invalid', fieldErrors) : FAILED; } if (status === 429) return error('rate_limited'); // 413, 415, 5xx, 503 и всё неожиданное — «попробуйте ещё раз или позвоните» return FAILED; } export function createSendLead({ fetchImpl = (...args: Parameters) => fetch(...args), timeoutMs = SUBMIT_TIMEOUT_MS, url = '/api/lead', }: { fetchImpl?: typeof fetch; timeoutMs?: number; url?: string } = {}): SendLead { return async (body) => { const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), timeoutMs); try { const res = await fetchImpl(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), signal: controller.signal, }); const data: unknown = await res.json().catch(() => null); return resultFromResponse(res.status, data); } catch { return FAILED; } finally { clearTimeout(timer); } }; } export const sendLead: SendLead = createSendLead();