diff --git a/src/app/api/registratura/debug-sequences/route.ts b/src/app/api/registratura/debug-sequences/route.ts new file mode 100644 index 0000000..79dc193 --- /dev/null +++ b/src/app/api/registratura/debug-sequences/route.ts @@ -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", + }); +} diff --git a/src/modules/registratura/services/registry-service.ts b/src/modules/registratura/services/registry-service.ts index 8df092c..9e606c9 100644 --- a/src/modules/registratura/services/registry-service.ts +++ b/src/modules/registratura/services/registry-service.ts @@ -261,19 +261,14 @@ export async function allocateSequenceNumber( `; const actualMax = maxRows[0]?.maxSeq ?? 0; - // 2. Also read the current counter as a safety net (should never be - // higher than actualMax if everything is consistent, but we take - // the MAX of both just in case). - const counterRows = await tx.$queryRaw>` - SELECT "lastSeq" FROM "RegistrySequence" - WHERE company = ${companyPrefix} AND year = ${yr} AND type = ${typeCode} - `; - const counterVal = counterRows[0]?.lastSeq ?? 0; + // 2. Next sequence = actual entries max + 1. + // Entries in KeyValueStore are the SOLE source of truth. + // The RegistrySequence counter is only a cache — if it drifted + // (e.g. entries were deleted before the recalculate fix), we + // ignore it entirely and reset it to match reality. + const nextSeq = actualMax + 1; - // 3. Next sequence = max(actual entries, counter) + 1 - const nextSeq = Math.max(actualMax, counterVal) + 1; - - // 4. Upsert the counter to the new value + // 3. Upsert the counter to the new value (keep it in sync) await tx.$executeRaw` INSERT INTO "RegistrySequence" (id, company, year, type, "lastSeq", "createdAt", "updatedAt") VALUES (gen_random_uuid()::text, ${companyPrefix}, ${yr}, ${typeCode}, ${nextSeq}, NOW(), NOW())