feat(numbering): projects get their own dedicated number sequence (decoupled from orders)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
BOHA
2026-06-09 19:24:51 +02:00
parent 9e9aacdf0b
commit ed9ea52a44
5 changed files with 148 additions and 22 deletions

View File

@@ -13,7 +13,10 @@ afterEach(async () => {
await prisma.orders.deleteMany({ where: { id } });
createdProjectIds.length = 0;
createdOrderIds.length = 0;
await prisma.number_sequences.deleteMany({ where: { type: "shared" } });
// Orders and projects now draw from SEPARATE sequences — reset both.
await prisma.number_sequences.deleteMany({
where: { type: { in: ["shared", "project"] } },
});
});
const baseOrder = {
@@ -35,7 +38,7 @@ function failResult(res: unknown): never {
}
describe("createOrder (manual, optional linked project)", () => {
it("creates an order AND a project sharing one number when create_project=true", async () => {
it("creates an order AND a project, each with its OWN number, linked by FK", async () => {
const res = await createOrder({
...baseOrder,
scope_title: "Ruční zakázka",
@@ -48,7 +51,11 @@ describe("createOrder (manual, optional linked project)", () => {
where: { id: res.project_id! },
});
if (project) createdProjectIds.push(project.id);
expect(project?.project_number).toBe(res.order_number);
// The auto-created project now draws from the dedicated project sequence,
// so its number is distinct from the order's shared number. They relate
// only via the order_id FK, not by sharing a number.
expect(project?.project_number).toBeTruthy();
expect(project?.project_number).not.toBe(res.order_number);
expect(project?.order_id).toBe(res.id);
});
@@ -60,8 +67,8 @@ describe("createOrder (manual, optional linked project)", () => {
});
});
describe("shared pool coherence (tracking)", () => {
it("order → standalone project → order produce three distinct shared numbers", async () => {
describe("separate order vs project sequences (coherence)", () => {
it("order → standalone project → order produce three distinct numbers (from separate sequences)", async () => {
const o1 = await createOrder({ ...baseOrder, create_project: true });
if (!("id" in o1)) failResult(o1);
createdOrderIds.push(o1.id!);
@@ -75,7 +82,9 @@ describe("shared pool coherence (tracking)", () => {
if (!("id" in o2)) failResult(o2);
createdOrderIds.push(o2.id!);
// All three numbers must be present (non-null) AND distinct.
// All three numbers must be present (non-null) AND distinct. Orders draw
// from the `shared` sequence (code 71); projects from the dedicated
// `project` sequence (code 73), so they never collide.
expect(o1.order_number).toBeTruthy();
expect(p.project_number).toBeTruthy();
expect(o2.order_number).toBeTruthy();
@@ -86,7 +95,7 @@ describe("shared pool coherence (tracking)", () => {
});
describe("createProject (manual, standalone)", () => {
it("assigns a shared number and is not linked to an order", async () => {
it("assigns a project number and is not linked to an order", async () => {
const result = await createProject({
name: "Test projekt",
status: "aktivni",

View File

@@ -6,6 +6,9 @@ import {
isSharedNumberTaken,
previewOfferNumber,
isOfferNumberTaken,
generateProjectNumber,
previewProjectNumber,
isProjectNumberTaken,
} from "../services/numbering.service";
import prisma from "../config/database";
@@ -77,6 +80,38 @@ describe("generateSharedNumber", () => {
});
});
describe("generateProjectNumber", () => {
beforeEach(async () => {
await prisma.number_sequences.deleteMany({ where: { type: "project" } });
});
afterEach(async () => {
await prisma.number_sequences.deleteMany({ where: { type: "project" } });
});
it("returns a non-empty string", async () => {
const num = await generateProjectNumber();
expect(typeof num).toBe("string");
expect(num.length).toBeGreaterThan(0);
});
it("increments the sequence by exactly 1 and preserves the year/format", async () => {
// Seed high so neither generated number collides with a real project →
// the skip-taken retry never fires and the increment is strictly +1.
await seedSequence("project", 900000);
const num1 = await generateProjectNumber();
const num2 = await generateProjectNumber();
assertStrictIncrement(num1, num2);
});
it("previewProjectNumber returns a number not yet taken by a project", async () => {
const preview = await previewProjectNumber();
expect(typeof preview).toBe("string");
expect(preview.length).toBeGreaterThan(0);
expect(await isProjectNumberTaken(preview)).toBe(false);
});
});
describe("generateOfferNumber", () => {
beforeEach(async () => {
await prisma.number_sequences.deleteMany({ where: { type: "offer" } });