Files
iistwin/server/services/contract-number.service.ts

159 lines
4.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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,
};
}