159 lines
4.7 KiB
TypeScript
159 lines
4.7 KiB
TypeScript
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<string | undefined> {
|
||
// Найти задачу
|
||
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<string> {
|
||
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,
|
||
};
|
||
}
|