fix: prevent deleting registry entries that would create sequence gaps
Only the last entry in a company+year sequence can be deleted. Trying to delete an earlier number (e.g. #2 when #3 exists) returns a 409 error with a Romanian message explaining the restriction. Also routes UI deletes through the API (like create/update) so they get proper audit logging and sequence recalculation. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -14,7 +14,8 @@ import type { Prisma } from "@prisma/client";
|
||||
import { v4 as uuid } from "uuid";
|
||||
import { prisma } from "@/core/storage/prisma";
|
||||
import { getAuthSession } from "@/core/auth";
|
||||
import { allocateSequenceNumber, recalculateSequence } from "@/modules/registratura/services/registry-service";
|
||||
import { allocateSequenceNumber, recalculateSequence, parseRegistryNumber } from "@/modules/registratura/services/registry-service";
|
||||
import { REGISTRY_COMPANY_PREFIX, OLD_COMPANY_PREFIX } from "@/modules/registratura/types";
|
||||
import {
|
||||
logAuditEvent,
|
||||
computeEntryDiff,
|
||||
@@ -429,6 +430,57 @@ export async function DELETE(req: NextRequest) {
|
||||
return NextResponse.json({ error: "Not found" }, { status: 404 });
|
||||
}
|
||||
|
||||
// Only allow deleting the LAST entry in the sequence (prevent gaps)
|
||||
const parsed = parseRegistryNumber(existing.number);
|
||||
if (parsed) {
|
||||
const companyPrefix =
|
||||
parsed.format === "current"
|
||||
? parsed.company
|
||||
: REGISTRY_COMPANY_PREFIX[existing.company as CompanyId] ?? parsed.company;
|
||||
const yr = parsed.year;
|
||||
const oldPrefix = OLD_COMPANY_PREFIX[companyPrefix] ?? "";
|
||||
|
||||
// Find the actual max sequence for this company+year
|
||||
// NOTE: JSONB value::text serializes with space after colons
|
||||
const newLike = `%"number": "${companyPrefix}-${yr}-%"%`;
|
||||
const newRegex = `${companyPrefix}-${yr}-([0-9]{5})`;
|
||||
const newMaxRows = await prisma.$queryRaw<Array<{ maxSeq: number | null }>>`
|
||||
SELECT MAX(
|
||||
CAST(SUBSTRING(value::text FROM ${newRegex}) AS INTEGER)
|
||||
) AS "maxSeq"
|
||||
FROM "KeyValueStore"
|
||||
WHERE namespace = 'registratura'
|
||||
AND key LIKE 'entry:%'
|
||||
AND value::text LIKE ${newLike}
|
||||
`;
|
||||
let maxSeq = newMaxRows[0]?.maxSeq ?? 0;
|
||||
|
||||
// Also check old format
|
||||
if (oldPrefix) {
|
||||
const oldLike = `%"number": "${oldPrefix}-${yr}-%"%`;
|
||||
const oldRegex = `${oldPrefix}-${yr}-(?:IN|OUT|INT)-([0-9]{5})`;
|
||||
const oldMaxRows = await prisma.$queryRaw<Array<{ maxSeq: number | null }>>`
|
||||
SELECT MAX(
|
||||
CAST(SUBSTRING(value::text FROM ${oldRegex}) AS INTEGER)
|
||||
) AS "maxSeq"
|
||||
FROM "KeyValueStore"
|
||||
WHERE namespace = 'registratura'
|
||||
AND key LIKE 'entry:%'
|
||||
AND value::text LIKE ${oldLike}
|
||||
`;
|
||||
maxSeq = Math.max(maxSeq, oldMaxRows[0]?.maxSeq ?? 0);
|
||||
}
|
||||
|
||||
if (parsed.sequence < maxSeq) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: `Nu poți șterge ${existing.number} — există înregistrări cu numere mai mari (max: ${maxSeq}). Doar ultimul număr din secvență poate fi șters.`,
|
||||
},
|
||||
{ status: 409 },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
await deleteEntryFromDB(id);
|
||||
|
||||
// Recalculate counter so next allocation reads the correct max
|
||||
|
||||
Reference in New Issue
Block a user