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:
@@ -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",
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -261,19 +261,14 @@ export async function allocateSequenceNumber(
|
|||||||
`;
|
`;
|
||||||
const actualMax = maxRows[0]?.maxSeq ?? 0;
|
const actualMax = maxRows[0]?.maxSeq ?? 0;
|
||||||
|
|
||||||
// 2. Also read the current counter as a safety net (should never be
|
// 2. Next sequence = actual entries max + 1.
|
||||||
// higher than actualMax if everything is consistent, but we take
|
// Entries in KeyValueStore are the SOLE source of truth.
|
||||||
// the MAX of both just in case).
|
// The RegistrySequence counter is only a cache — if it drifted
|
||||||
const counterRows = await tx.$queryRaw<Array<{ lastSeq: number }>>`
|
// (e.g. entries were deleted before the recalculate fix), we
|
||||||
SELECT "lastSeq" FROM "RegistrySequence"
|
// ignore it entirely and reset it to match reality.
|
||||||
WHERE company = ${companyPrefix} AND year = ${yr} AND type = ${typeCode}
|
const nextSeq = actualMax + 1;
|
||||||
`;
|
|
||||||
const counterVal = counterRows[0]?.lastSeq ?? 0;
|
|
||||||
|
|
||||||
// 3. Next sequence = max(actual entries, counter) + 1
|
// 3. Upsert the counter to the new value (keep it in sync)
|
||||||
const nextSeq = Math.max(actualMax, counterVal) + 1;
|
|
||||||
|
|
||||||
// 4. Upsert the counter to the new value
|
|
||||||
await tx.$executeRaw`
|
await tx.$executeRaw`
|
||||||
INSERT INTO "RegistrySequence" (id, company, year, type, "lastSeq", "createdAt", "updatedAt")
|
INSERT INTO "RegistrySequence" (id, company, year, type, "lastSeq", "createdAt", "updatedAt")
|
||||||
VALUES (gen_random_uuid()::text, ${companyPrefix}, ${yr}, ${typeCode}, ${nextSeq}, NOW(), NOW())
|
VALUES (gen_random_uuid()::text, ${companyPrefix}, ${yr}, ${typeCode}, ${nextSeq}, NOW(), NOW())
|
||||||
|
|||||||
Reference in New Issue
Block a user