fix: use only actual entries as source of truth for registry numbers

The previous fix still used MAX(actualMax, counterVal) which meant a
stale counter (from entries deleted before the fix was deployed) would
override the actual entry count. Changed to use ONLY actualMax + 1.

The RegistrySequence counter is now just a cache that gets synced —
it never overrides the actual entries count.

Also added /api/registratura/debug-sequences endpoint:
- GET: shows all counters vs actual entry max (for diagnostics)
- POST: resets all counters to match actual entries (one-time fix)

After deploy, call POST /api/registratura/debug-sequences to reset
the stale counters, then delete the BTG-2026-OUT-00004 entry and
recreate it — it will get 00001.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
AI Assistant
2026-03-11 21:20:58 +02:00
parent 28bb395b06
commit 46de088423
2 changed files with 88 additions and 12 deletions
@@ -0,0 +1,81 @@
/**
* Debug endpoint for registry sequence counters.
*
* GET — Show all sequence counters + actual max from entries
* POST — Reset all counters to match actual entries (fixes stale counters)
*
* Auth: NextAuth session only.
*/
import { NextResponse } from "next/server";
import { prisma } from "@/core/storage/prisma";
import { getAuthSession } from "@/core/auth";
export async function GET() {
const session = await getAuthSession();
if (!session) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
// Get all sequence counters
const counters = await prisma.$queryRaw<
Array<{ company: string; year: number; type: string; lastSeq: number }>
>`SELECT company, year, type, "lastSeq" FROM "RegistrySequence" ORDER BY company, year, type`;
// Get actual max sequences from entries
const actuals = await prisma.$queryRaw<
Array<{ prefix: string; maxSeq: number; count: number }>
>`
SELECT
SUBSTRING(value::text FROM '"number":"([A-Z]{3}-\\d{4}-(?:IN|OUT|INT))-') AS prefix,
MAX(CAST(SUBSTRING(value::text FROM '"number":"[A-Z]{3}-\\d{4}-(?:IN|OUT|INT)-(\\d{5})"') AS INTEGER)) AS "maxSeq",
COUNT(*)::int AS count
FROM "KeyValueStore"
WHERE namespace = 'registratura'
AND key LIKE 'entry:%'
AND value::text ~ '"number":"[A-Z]{3}-\\d{4}-(IN|OUT|INT)-\\d{5}"'
GROUP BY prefix
ORDER BY prefix
`;
return NextResponse.json({
counters,
actualEntries: actuals,
note: "POST to this endpoint to reset all counters to match actual entries",
});
}
export async function POST() {
const session = await getAuthSession();
if (!session) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
// Delete all counters
const deleted = await prisma.$executeRaw`DELETE FROM "RegistrySequence"`;
// Re-create counters from actual entries
const inserted = await prisma.$executeRaw`
INSERT INTO "RegistrySequence" (id, company, year, type, "lastSeq", "createdAt", "updatedAt")
SELECT
gen_random_uuid()::text,
SUBSTRING(value::text FROM '"number":"([A-Z]+)-') AS company,
CAST(SUBSTRING(value::text FROM '"number":"[A-Z]+-(\d{4})-') AS INTEGER) AS year,
SUBSTRING(value::text FROM '"number":"[A-Z]+-\d{4}-([A-Z]+)-') AS type,
MAX(CAST(SUBSTRING(value::text FROM '"number":"[A-Z]+-\d{4}-[A-Z]+-(\\d{5})"') AS INTEGER)) AS "lastSeq",
NOW(),
NOW()
FROM "KeyValueStore"
WHERE namespace = 'registratura'
AND key LIKE 'entry:%'
AND value::text ~ '"number":"[A-Z]{3}-\\d{4}-(IN|OUT|INT)-\\d{5}"'
GROUP BY company, year, type
`;
return NextResponse.json({
success: true,
deletedCounters: deleted,
recreatedCounters: inserted,
message: "All sequence counters reset to match actual entries",
});
}