import type { Interest, ValidLead } from '@kvarta/shared/lead'; import type { Db } from './db/index.js'; import type { ClarifiedField, LeadEvent } from './notifier.js'; /** Окно уточнений: от created_at заявки, уточнения его не продлевают. */ export const CLARIFY_WINDOW_MS = 10 * 60 * 1000; export type SaveMeta = { now: Date; userAgent: string | null }; export type SaveResult = | { kind: 'created'; event: LeadEvent } | { kind: 'clarified'; event: LeadEvent } | { kind: 'unchanged'; leadId: number }; /** Версия заявки: сама заявка или уточнение к ней. */ export type Version = { name: string; interest: Interest; comment: string | null }; const CLARIFIED_FIELDS: readonly ClarifiedField[] = ['name', 'interest', 'comment']; export function changedFields(previous: Version, next: Version): ClarifiedField[] { return CLARIFIED_FIELDS.filter((field) => previous[field] !== next[field]); } /** * Новая заявка, уточнение к заявке из окна или повтор без изменений. * Поиск и запись в одной IMMEDIATE-транзакции: два одновременных запроса с одного телефона * не создадут две заявки, даже если процессов станет больше одного. */ export function saveLead(db: Db, lead: ValidLead, meta: SaveMeta): SaveResult { const nowIso = meta.now.toISOString(); // Время хранится в одном формате toISOString, поэтому строки сравниваются как даты. const windowStart = new Date(meta.now.getTime() - CLARIFY_WINDOW_MS).toISOString(); return db .transaction((): SaveResult => { const open = db .prepare( `SELECT id, name, interest, comment FROM leads WHERE phone = ? AND created_at > ? ORDER BY created_at DESC, id DESC LIMIT 1`, ) .get(lead.phone, windowStart) as ({ id: number } & Version) | undefined; if (!open) { const { lastInsertRowid } = db .prepare( `INSERT INTO leads (created_at, phone, name, interest, comment, first_touch, last_touch, user_agent) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, ) .run( nowIso, lead.phone, lead.name, lead.interest, lead.comment, lead.firstTouch ? JSON.stringify(lead.firstTouch) : null, JSON.stringify(lead.lastTouch), meta.userAgent, ); return { kind: 'created', event: { kind: 'lead', leadId: Number(lastInsertRowid) } }; } const previous = (db .prepare( `SELECT name, interest, comment FROM lead_clarifications WHERE lead_id = ? ORDER BY id DESC LIMIT 1`, ) .get(open.id) as Version | undefined) ?? open; const changed = changedFields(previous, lead); if (changed.length === 0) return { kind: 'unchanged', leadId: open.id }; const { lastInsertRowid } = db .prepare( `INSERT INTO lead_clarifications (lead_id, created_at, name, interest, comment, last_touch) VALUES (?, ?, ?, ?, ?, ?)`, ) .run(open.id, nowIso, lead.name, lead.interest, lead.comment, JSON.stringify(lead.lastTouch)); return { kind: 'clarified', event: { kind: 'clarification', leadId: open.id, clarificationId: Number(lastInsertRowid), changed }, }; }) .immediate(); }