diff --git a/src/__tests__/numbering.test.ts b/src/__tests__/numbering.test.ts index aa39246..dfd8444 100644 --- a/src/__tests__/numbering.test.ts +++ b/src/__tests__/numbering.test.ts @@ -2,6 +2,8 @@ import { describe, it, expect, beforeEach, afterEach } from "vitest"; import { generateSharedNumber, generateOfferNumber, + previewSharedNumber, + isSharedNumberTaken, } from "../services/numbering.service"; import prisma from "../config/database"; @@ -48,3 +50,43 @@ describe("generateOfferNumber", () => { expect(num1).not.toBe(num2); }); }); + +describe("previewSharedNumber", () => { + let createdProjectId: number | null = null; + + beforeEach(async () => { + await prisma.number_sequences.deleteMany({ where: { type: "shared" } }); + }); + + afterEach(async () => { + if (createdProjectId !== null) { + await prisma.projects + .delete({ where: { id: createdProjectId } }) + .catch(() => {}); + createdProjectId = null; + } + await prisma.number_sequences.deleteMany({ where: { type: "shared" } }); + }); + + it("returns a number not already used by an order or project", async () => { + const preview = await previewSharedNumber(); + expect(typeof preview).toBe("string"); + expect(await isSharedNumberTaken(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 this is guaranteed free, so we can safely claim it. + const first = await previewSharedNumber(); + const project = await prisma.projects.create({ + data: { project_number: first, name: "test-preview-skip-taken" }, + }); + createdProjectId = project.id; + + // The counter still lags behind (no row was consumed), but `first` is now + // taken — the preview must advance past it instead of re-offering it. + const second = await previewSharedNumber(); + expect(second).not.toBe(first); + expect(await isSharedNumberTaken(second)).toBe(false); + }); +}); diff --git a/src/services/numbering.service.ts b/src/services/numbering.service.ts index 333f5d2..5a07c23 100644 --- a/src/services/numbering.service.ts +++ b/src/services/numbering.service.ts @@ -258,7 +258,18 @@ export async function previewSharedNumber(): Promise { const code = settings?.order_type_code || "71"; const year = new Date().getFullYear(); - const seq = await previewNextSequence("shared", year); + // Mirror generateSharedNumber's collision check (without consuming the + // sequence). The number_sequences counter can lag behind real data — e.g. + // when orders/projects were imported or seeded without bumping it — so the + // raw next sequence may already be taken. Advance past any number already + // used by an order or project, so the preview matches what generateSharedNumber + // would actually assign instead of showing a stale, already-used number. + let seq = await previewNextSequence("shared", year); + for (let attempt = 0; attempt < 1000; attempt++) { + const number = applyPattern(pattern, { year, prefix: "", code, seq }); + if (!(await isSharedNumberTaken(number))) return number; + seq++; + } return applyPattern(pattern, { year, prefix: "", code, seq }); }