fix(numbering): preview the next FREE shared number, not a stale one

previewSharedNumber returned applyPattern(previewNextSequence) directly. When the number_sequences counter lags behind real data (e.g. orders/projects imported or seeded without bumping it), that raw next sequence is already taken, so the create-project modal showed an already-used number (e.g. OBJ-2026-001) even though generateSharedNumber would correctly assign the next free one via its collision-check loop. Mirror that loop in the preview (without consuming the sequence) so the hint matches what will actually be assigned. Adds two numbering tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
BOHA
2026-06-06 23:02:40 +02:00
parent 56bad52de5
commit 1b372b93ea
2 changed files with 54 additions and 1 deletions

View File

@@ -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);
});
});

View File

@@ -258,7 +258,18 @@ export async function previewSharedNumber(): Promise<string> {
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 });
}