import prisma from "../config/database"; // Default patterns (backward compatible with existing numbers) const DEFAULT_OFFER_PATTERN = "{YYYY}/{PREFIX}/{NNN}"; const DEFAULT_ORDER_PATTERN = "{YY}{CODE}{NNNN}"; const DEFAULT_INVOICE_PATTERN = "{YY}{CODE}{NNNN}"; /** * Apply a numbering pattern template. * Placeholders: {YYYY}, {YY}, {PREFIX}, {CODE}, {N+} (padding = count of N's) */ function applyPattern( pattern: string, vars: { year: number; prefix: string; code: string; seq: number }, ): string { const yyyy = String(vars.year); const yy = yyyy.slice(-2); return pattern.replace(/\{(\w+)\}/g, (match, key: string) => { if (key === "YYYY") return yyyy; if (key === "YY") return yy; if (key === "PREFIX") return vars.prefix; if (key === "CODE") return vars.code; if (/^N+$/.test(key)) return String(vars.seq).padStart(key.length, "0"); return match; }); } /** * Extract the static prefix and sequence position from a pattern. * Used to build SQL LIKE patterns for MAX(seq) queries. */ function buildLikePattern( pattern: string, vars: { year: number; prefix: string; code: string }, ): { likePattern: string; prefixLen: number } { const yyyy = String(vars.year); const yy = yyyy.slice(-2); let staticPrefix = ""; let foundSeq = false; const parts = pattern.split(/(\{[^}]+\})/); for (const part of parts) { const m = part.match(/^\{(\w+)\}$/); if (!m) { staticPrefix += part; continue; } const key = m[1]; if (/^N+$/.test(key)) { foundSeq = true; break; } if (key === "YYYY") staticPrefix += yyyy; else if (key === "YY") staticPrefix += yy; else if (key === "PREFIX") staticPrefix += vars.prefix; else if (key === "CODE") staticPrefix += vars.code; } if (!foundSeq) { return { likePattern: staticPrefix + "%", prefixLen: staticPrefix.length }; } return { likePattern: staticPrefix + "%", prefixLen: staticPrefix.length }; } async function getSettings() { return prisma.company_settings.findFirst({ select: { quotation_prefix: true, order_type_code: true, invoice_type_code: true, offer_number_pattern: true, order_number_pattern: true, invoice_number_pattern: true, }, }); } /** * Next offer/quotation number. */ export async function generateOfferNumber(): Promise { const settings = await getSettings(); const pattern = settings?.offer_number_pattern || DEFAULT_OFFER_PATTERN; const prefix = settings?.quotation_prefix || "NA"; const year = new Date().getFullYear(); const { likePattern, prefixLen } = buildLikePattern(pattern, { year, prefix, code: "", }); const result = await prisma.$queryRaw<[{ max_seq: bigint | null }]>` SELECT COALESCE(MAX(CAST(SUBSTRING(quotation_number, ${prefixLen} + 1) AS UNSIGNED)), 0) as max_seq FROM quotations WHERE quotation_number LIKE ${likePattern} `; const nextNum = Number(result[0]?.max_seq ?? 0) + 1; return applyPattern(pattern, { year, prefix, code: "", seq: nextNum }); } /** * Shared number for orders and projects. */ export async function generateSharedNumber(): Promise { const settings = await getSettings(); const pattern = settings?.order_number_pattern || DEFAULT_ORDER_PATTERN; const code = settings?.order_type_code || "71"; const year = new Date().getFullYear(); const { likePattern, prefixLen } = buildLikePattern(pattern, { year, prefix: "", code, }); const result = await prisma.$queryRaw<[{ max_seq: bigint | null }]>` SELECT COALESCE(MAX(seq), 0) as max_seq FROM ( SELECT CAST(SUBSTRING(order_number, ${prefixLen} + 1) AS UNSIGNED) AS seq FROM orders WHERE order_number LIKE ${likePattern} UNION ALL SELECT CAST(SUBSTRING(project_number, ${prefixLen} + 1) AS UNSIGNED) AS seq FROM projects WHERE project_number LIKE ${likePattern} ) combined `; const nextNum = Number(result[0]?.max_seq ?? 0) + 1; return applyPattern(pattern, { year, prefix: "", code, seq: nextNum }); } /** * Next invoice number. */ export async function generateInvoiceNumber( _year?: number, ): Promise<{ number: string; next_number: string }> { const settings = await getSettings(); const pattern = settings?.invoice_number_pattern || DEFAULT_INVOICE_PATTERN; const code = settings?.invoice_type_code || "81"; const year = _year || new Date().getFullYear(); const { likePattern, prefixLen } = buildLikePattern(pattern, { year, prefix: "", code, }); const result = await prisma.$queryRaw<[{ max_seq: bigint | null }]>` SELECT COALESCE(MAX(CAST(SUBSTRING(invoice_number, ${prefixLen} + 1) AS UNSIGNED)), 0) as max_seq FROM invoices WHERE invoice_number LIKE ${likePattern} `; const nextNum = Number(result[0]?.max_seq ?? 0) + 1; const number = applyPattern(pattern, { year, prefix: "", code, seq: nextNum, }); return { number, next_number: number }; } /** Preview what a pattern would produce (for settings UI) */ export function previewPattern( pattern: string, prefix: string, code: string, ): string { return applyPattern(pattern, { year: new Date().getFullYear(), prefix, code, seq: 1, }); }