diff --git a/src/__tests__/numbering.test.ts b/src/__tests__/numbering.test.ts index dfd8444..9c9316f 100644 --- a/src/__tests__/numbering.test.ts +++ b/src/__tests__/numbering.test.ts @@ -4,6 +4,8 @@ import { generateOfferNumber, previewSharedNumber, isSharedNumberTaken, + previewOfferNumber, + isOfferNumberTaken, } from "../services/numbering.service"; import prisma from "../config/database"; @@ -90,3 +92,43 @@ describe("previewSharedNumber", () => { expect(await isSharedNumberTaken(second)).toBe(false); }); }); + +describe("previewOfferNumber", () => { + let createdQuotationId: number | null = null; + + beforeEach(async () => { + await prisma.number_sequences.deleteMany({ where: { type: "offer" } }); + }); + + afterEach(async () => { + if (createdQuotationId !== null) { + await prisma.quotations + .delete({ where: { id: createdQuotationId } }) + .catch(() => {}); + createdQuotationId = null; + } + await prisma.number_sequences.deleteMany({ where: { type: "offer" } }); + }); + + it("returns a number not already used by a quotation", async () => { + const preview = await previewOfferNumber(); + expect(typeof preview).toBe("string"); + expect(await isOfferNumberTaken(preview)).toBe(false); + }); + + it("skips a number already taken when the sequence counter lags behind", async () => { + // Fresh counter → preview points at the first sequence number; with the + // skip-taken logic it's guaranteed free, so we can claim it. + const first = await previewOfferNumber(); + const quotation = await prisma.quotations.create({ + data: { quotation_number: first }, + }); + createdQuotationId = quotation.id; + + // The counter still lags, but `first` is now taken — the preview must + // advance past it instead of re-offering it. + const second = await previewOfferNumber(); + expect(second).not.toBe(first); + expect(await isOfferNumberTaken(second)).toBe(false); + }); +}); diff --git a/src/services/numbering.service.ts b/src/services/numbering.service.ts index 5a07c23..8ac6ec6 100644 --- a/src/services/numbering.service.ts +++ b/src/services/numbering.service.ts @@ -221,7 +221,17 @@ export async function previewOfferNumber(): Promise { const prefix = settings?.quotation_prefix || "NA"; const year = new Date().getFullYear(); - const seq = await previewNextSequence("offer", year); + // Mirror generateOfferNumber's collision check (without consuming the + // sequence). The number_sequences counter can lag behind real data, so the + // raw next sequence may already be taken — advance past any number already + // used by a quotation so the preview matches what generateOfferNumber assigns + // instead of showing a stale, already-used number. + let seq = await previewNextSequence("offer", year); + for (let attempt = 0; attempt < 1000; attempt++) { + const number = applyPattern(pattern, { year, prefix, code: "", seq }); + if (!(await isOfferNumberTaken(number))) return number; + seq++; + } return applyPattern(pattern, { year, prefix, code: "", seq }); }