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:
@@ -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",
|
||||
|
||||
@@ -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" } });
|
||||
|
||||
Reference in New Issue
Block a user