import { db } from "../db"; import { eq, and, sql, isNull } from "drizzle-orm"; import { tasks, taskFieldValues, contractNumberCounters, forms, formFields } from "@shared/schema"; export interface ContractNumberConfig { legalEntityFormId: number; prefixFieldCode: string; startNumber: number; sequencePadding: number; } export interface GenerateContractNumberOpts { organizationId: number; formId: number; legalEntityTaskId: number; prefixFieldCode: string; startNumber: number; sequencePadding: number; } export interface PreviewContractNumberOpts { organizationId: number; formId: number; legalEntityTaskId: number; config: ContractNumberConfig; } /** * Получить значение поля задачи по коду поля */ async function getTaskFieldValueByCode( taskId: number, fieldCode: string, organizationId: number ): Promise { // Найти задачу const [task] = await db .select() .from(tasks) .innerJoin(forms, eq(tasks.formId, forms.id)) .where(and(eq(tasks.id, taskId), eq(forms.organizationId, organizationId))) .limit(1); if (!task) return undefined; // Найти поле по коду в форме задачи const [field] = await db .select() .from(formFields) .where( and( eq(formFields.formId, task.tasks.formId), eq(formFields.code, fieldCode), isNull(formFields.deletedAt) ) ) .limit(1); if (!field) return undefined; // Получить значение const [value] = await db .select() .from(taskFieldValues) .where( and( eq(taskFieldValues.taskId, taskId), eq(taskFieldValues.fieldId, field.id) ) ) .limit(1); return value ? String(value.value) : undefined; } /** * Сгенерировать номер договора атомарно (с резервацией счётчика) */ export async function generateContractNumber( opts: GenerateContractNumberOpts ): Promise { const { organizationId, formId, legalEntityTaskId, prefixFieldCode, startNumber, sequencePadding } = opts; // Получить префикс из задачи-юрлица по коду поля const prefix = await getTaskFieldValueByCode(legalEntityTaskId, prefixFieldCode, organizationId); if (!prefix) { throw new Error(`Префикс не найден: поле ${prefixFieldCode} в задаче ${legalEntityTaskId}`); } const year = new Date().getFullYear() % 100; // Атомарно получить/создать счётчик const result = await db.execute(sql` INSERT INTO contract_number_counters (organization_id, form_id, prefix, year, last_number) VALUES (${organizationId}, ${formId}, ${prefix}, ${year}, ${startNumber}) ON CONFLICT (organization_id, form_id, prefix, year) DO UPDATE SET last_number = contract_number_counters.last_number + 1, updated_at = NOW() RETURNING last_number; `); const nextNumber = Number((result.rows[0] as any)?.last_number ?? startNumber); const yearStr = year.toString().padStart(2, '0'); const seqStr = nextNumber.toString().padStart(sequencePadding ?? 3, '0'); return `${yearStr}${prefix}-${seqStr}`; } /** * Предпросмотр номера (без резервации счётчика) */ export async function previewContractNumber( opts: PreviewContractNumberOpts ): Promise<{ preview: string; year: number; prefix: string; nextNumber: number }> { const { organizationId, formId, legalEntityTaskId, config } = opts; const prefix = await getTaskFieldValueByCode( legalEntityTaskId, config.prefixFieldCode, organizationId ); if (!prefix) { throw new Error(`Префикс не найден: поле ${config.prefixFieldCode} в задаче ${legalEntityTaskId}`); } const year = new Date().getFullYear() % 100; // Заглянуть в существующий счётчик (без изменения) const [counter] = await db .select() .from(contractNumberCounters) .where( and( eq(contractNumberCounters.organizationId, organizationId), eq(contractNumberCounters.formId, formId), eq(contractNumberCounters.prefix, prefix), eq(contractNumberCounters.year, year) ) ) .limit(1); const startNumber = config.startNumber ?? 1; const sequencePadding = config.sequencePadding ?? 3; const nextNumber = counter ? counter.lastNumber + 1 : startNumber; const yearStr = year.toString().padStart(2, '0'); const seqStr = nextNumber.toString().padStart(sequencePadding, '0'); return { preview: `${yearStr}${prefix}-${seqStr}`, year, prefix, nextNumber, }; }