fix(numbering): preview the next FREE offer number, not a stale one
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>
This commit is contained in:
@@ -4,6 +4,8 @@ import {
|
||||
generateOfferNumber,
|
||||
previewSharedNumber,
|
||||
isSharedNumberTaken,
|
||||
previewOfferNumber,
|
||||
isOfferNumberTaken,
|
||||
} from "../services/numbering.service";
|
||||
import prisma from "../config/database";
|
||||
|
||||
@@ -90,3 +92,43 @@ describe("previewSharedNumber", () => {
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -221,7 +221,17 @@ export async function previewOfferNumber(): Promise<string> {
|
||||
const prefix = settings?.quotation_prefix || "NA";
|
||||
const year = new Date().getFullYear();
|
||||
|
||||
const seq = await previewNextSequence("offer", year);
|
||||
// Mirror generateOfferNumber's collision check (without consuming the
|
||||
// sequence). The number_sequences counter can lag behind real data, so the
|
||||
// raw next sequence may already be taken — advance past any number already
|
||||
// used by a quotation so the preview matches what generateOfferNumber assigns
|
||||
// instead of showing a stale, already-used number.
|
||||
let seq = await previewNextSequence("offer", year);
|
||||
for (let attempt = 0; attempt < 1000; attempt++) {
|
||||
const number = applyPattern(pattern, { year, prefix, code: "", seq });
|
||||
if (!(await isOfferNumberTaken(number))) return number;
|
||||
seq++;
|
||||
}
|
||||
return applyPattern(pattern, { year, prefix, code: "", seq });
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user