previewOfferNumber returned applyPattern(previewNextSequence) directly; when the number_sequences 'offer' counter lags behind real quotations, that raw next number is already taken, so the offer editor showed an already-used number (e.g. NA-2026-0001 when 0001/0002 exist). Mirror generateOfferNumber's collision loop in the preview (without consuming the sequence). +2 tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
135 lines
4.4 KiB
TypeScript
135 lines
4.4 KiB
TypeScript
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
import {
|
|
generateSharedNumber,
|
|
generateOfferNumber,
|
|
previewSharedNumber,
|
|
isSharedNumberTaken,
|
|
previewOfferNumber,
|
|
isOfferNumberTaken,
|
|
} from "../services/numbering.service";
|
|
import prisma from "../config/database";
|
|
|
|
describe("generateSharedNumber", () => {
|
|
beforeEach(async () => {
|
|
await prisma.number_sequences.deleteMany({ where: { type: "shared" } });
|
|
});
|
|
|
|
afterEach(async () => {
|
|
await prisma.number_sequences.deleteMany({ where: { type: "shared" } });
|
|
});
|
|
|
|
it("returns a non-empty string", async () => {
|
|
const num = await generateSharedNumber();
|
|
expect(typeof num).toBe("string");
|
|
expect(num.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it("increments on consecutive calls", async () => {
|
|
const num1 = await generateSharedNumber();
|
|
const num2 = await generateSharedNumber();
|
|
expect(num1).not.toBe(num2);
|
|
});
|
|
});
|
|
|
|
describe("generateOfferNumber", () => {
|
|
beforeEach(async () => {
|
|
await prisma.number_sequences.deleteMany({ where: { type: "offer" } });
|
|
});
|
|
|
|
afterEach(async () => {
|
|
await prisma.number_sequences.deleteMany({ where: { type: "offer" } });
|
|
});
|
|
|
|
it("returns a non-empty string", async () => {
|
|
const num = await generateOfferNumber();
|
|
expect(typeof num).toBe("string");
|
|
expect(num.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it("increments on consecutive calls", async () => {
|
|
const num1 = await generateOfferNumber();
|
|
const num2 = await 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);
|
|
});
|
|
});
|
|
|
|
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);
|
|
});
|
|
});
|