From 34024404a595153979f915e2525f0db4fa1ac2a5 Mon Sep 17 00:00:00 2001 From: AI Assistant Date: Wed, 11 Mar 2026 22:38:51 +0200 Subject: [PATCH] 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 --- src/app/api/registratura/route.ts | 54 ++++++++++++++++++- .../components/registratura-module.tsx | 7 ++- .../registratura/hooks/use-registry.ts | 14 +++-- 3 files changed, 70 insertions(+), 5 deletions(-) diff --git a/src/app/api/registratura/route.ts b/src/app/api/registratura/route.ts index 5634645..603328f 100644 --- a/src/app/api/registratura/route.ts +++ b/src/app/api/registratura/route.ts @@ -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>` + 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>` + 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 diff --git a/src/modules/registratura/components/registratura-module.tsx b/src/modules/registratura/components/registratura-module.tsx index 4c67fb7..f68acd2 100644 --- a/src/modules/registratura/components/registratura-module.tsx +++ b/src/modules/registratura/components/registratura-module.tsx @@ -216,7 +216,12 @@ export function RegistraturaModule() { }; const handleDelete = async (id: string) => { - await removeEntry(id); + try { + await removeEntry(id); + } catch (err) { + const msg = err instanceof Error ? err.message : "Eroare la ștergere"; + alert(msg); + } }; /** All closes go through the close dialog */ diff --git a/src/modules/registratura/hooks/use-registry.ts b/src/modules/registratura/hooks/use-registry.ts index 1c19496..8196222 100644 --- a/src/modules/registratura/hooks/use-registry.ts +++ b/src/modules/registratura/hooks/use-registry.ts @@ -15,7 +15,6 @@ import { getAllEntries, getFullEntry, saveEntry, - deleteEntry, } from "../services/registry-service"; import type { RegistryAuditEvent } from "../types"; import { @@ -111,10 +110,19 @@ export function useRegistry() { const removeEntry = useCallback( async (id: string) => { - await deleteEntry(storage, blobStorage, id); + // Use the API for sequence validation + audit logging + const res = await fetch(`/api/registratura?id=${encodeURIComponent(id)}`, { + method: "DELETE", + }); + const result = await res.json(); + + if (!result.success) { + throw new Error(result.error ?? "Failed to delete entry"); + } + await refresh(); }, - [storage, blobStorage, refresh], + [refresh], ); const closeEntry = useCallback(