diff --git a/src/app/api/registratura/debug-sequences/route.ts b/src/app/api/registratura/debug-sequences/route.ts index 1d7576a..39bebef 100644 --- a/src/app/api/registratura/debug-sequences/route.ts +++ b/src/app/api/registratura/debug-sequences/route.ts @@ -1,7 +1,7 @@ /** * Debug endpoint for registry sequence counters. * - * GET — Show all sequence counters + actual max from entries + * GET — Show all sequence counters + actual max from entries + sample numbers * POST — Reset all counters to match actual entries (fixes stale counters) * * Auth: NextAuth session only. @@ -22,18 +22,32 @@ export async function GET() { 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 (current format: B-2026-00001) + // Sample: show actual number values from entries (for debugging regex issues) + const samples = await prisma.$queryRawUnsafe< + Array<{ key: string; num: string }> + >(` + SELECT key, + SUBSTRING(value::text FROM '"number":"([^"]+)"') AS num + FROM "KeyValueStore" + WHERE namespace = 'registratura' + AND key LIKE 'entry:%' + ORDER BY key + LIMIT 20 + `); + + // Get actual max sequences from entries — current format: B-2026-00001 + // Use [0-9] instead of \d for PostgreSQL POSIX regex compatibility const actuals = await prisma.$queryRawUnsafe< Array<{ prefix: string; maxSeq: number; count: number }> >(` SELECT - SUBSTRING(value::text FROM '"number":"([A-Z]-\\d{4})-') AS prefix, - MAX(CAST(SUBSTRING(value::text FROM '"number":"[A-Z]-\\d{4}-(\\d{5})"') AS INTEGER)) AS "maxSeq", + SUBSTRING(value::text FROM '"number":"([A-Z]-[0-9]{4})-') AS prefix, + MAX(CAST(SUBSTRING(value::text FROM '"number":"[A-Z]-[0-9]{4}-([0-9]{5})"') AS INTEGER)) AS "maxSeq", COUNT(*)::int AS count FROM "KeyValueStore" WHERE namespace = 'registratura' AND key LIKE 'entry:%' - AND value::text ~ '"number":"[A-Z]-\\d{4}-\\d{5}"' + AND value::text ~ '"number":"[A-Z]-[0-9]{4}-[0-9]{5}"' GROUP BY prefix ORDER BY prefix `); @@ -43,19 +57,20 @@ export async function GET() { Array<{ prefix: string; maxSeq: number; count: number }> >(` SELECT - SUBSTRING(value::text FROM '"number":"([A-Z]{3}-\\d{4})-') AS prefix, - MAX(CAST(SUBSTRING(value::text FROM '"number":"[A-Z]{3}-\\d{4}-(?:IN|OUT|INT)-(\\d{5})"') AS INTEGER)) AS "maxSeq", + SUBSTRING(value::text FROM '"number":"([A-Z]{3}-[0-9]{4})-') AS prefix, + MAX(CAST(SUBSTRING(value::text FROM '"number":"[A-Z]{3}-[0-9]{4}-(?:IN|OUT|INT)-([0-9]{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}"' + AND value::text ~ '"number":"[A-Z]{3}-[0-9]{4}-(IN|OUT|INT)-[0-9]{5}"' GROUP BY prefix ORDER BY prefix `); return NextResponse.json({ counters, + samples, currentFormatEntries: actuals, oldFormatEntries: oldFormatActuals, note: "POST to this endpoint to reset all counters to match actual entries", @@ -68,31 +83,59 @@ export async function POST() { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } - // Delete ALL old counters (including old-format BTG/SDT/USW/GRP and IN/OUT types) + // Delete ALL old counters const deleted = await prisma.$executeRaw`DELETE FROM "RegistrySequence"`; - // Re-create counters from actual entries in current format (B-2026-00001) + // Re-create counters from current format entries (B-2026-00001) const insertedNew = await prisma.$executeRawUnsafe(` 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, + CAST(SUBSTRING(value::text FROM '"number":"[A-Z]-([0-9]{4})-') AS INTEGER) AS year, 'SEQ' AS type, - MAX(CAST(SUBSTRING(value::text FROM '"number":"[A-Z]-\\d{4}-(\\d{5})"') AS INTEGER)) AS "lastSeq", + MAX(CAST(SUBSTRING(value::text FROM '"number":"[A-Z]-[0-9]{4}-([0-9]{5})"') AS INTEGER)) AS "lastSeq", NOW(), NOW() FROM "KeyValueStore" WHERE namespace = 'registratura' AND key LIKE 'entry:%' - AND value::text ~ '"number":"[A-Z]-\\d{4}-\\d{5}"' + AND value::text ~ '"number":"[A-Z]-[0-9]{4}-[0-9]{5}"' GROUP BY company, year, type `); + // Also handle old-format entries (BTG→B, USW→U, SDT→S, GRP→G) + const insertedOld = await prisma.$executeRawUnsafe(` + INSERT INTO "RegistrySequence" (id, company, year, type, "lastSeq", "createdAt", "updatedAt") + SELECT + gen_random_uuid()::text, + CASE SUBSTRING(value::text FROM '"number":"([A-Z]{3})-') + WHEN 'BTG' THEN 'B' + WHEN 'USW' THEN 'U' + WHEN 'SDT' THEN 'S' + WHEN 'GRP' THEN 'G' + ELSE SUBSTRING(value::text FROM '"number":"([A-Z]{3})-') + END AS company, + CAST(SUBSTRING(value::text FROM '"number":"[A-Z]{3}-([0-9]{4})-') AS INTEGER) AS year, + 'SEQ' AS type, + MAX(CAST(SUBSTRING(value::text FROM '"number":"[A-Z]{3}-[0-9]{4}-(?:IN|OUT|INT)-([0-9]{5})"') AS INTEGER)) AS "lastSeq", + NOW(), + NOW() + FROM "KeyValueStore" + WHERE namespace = 'registratura' + AND key LIKE 'entry:%' + AND value::text ~ '"number":"[A-Z]{3}-[0-9]{4}-(IN|OUT|INT)-[0-9]{5}"' + GROUP BY company, year, type + ON CONFLICT (company, year, type) + DO UPDATE SET "lastSeq" = GREATEST("RegistrySequence"."lastSeq", EXCLUDED."lastSeq"), + "updatedAt" = NOW() + `); + return NextResponse.json({ success: true, deletedCounters: deleted, - recreatedCounters: insertedNew, - message: "All sequence counters reset. Old-format counters (BTG/SDT/USW/GRP, IN/OUT) removed.", + recreatedFromNewFormat: insertedNew, + recreatedFromOldFormat: insertedOld, + message: "All counters reset from actual entries (both old and new format).", }); } diff --git a/src/modules/registratura/services/registry-service.ts b/src/modules/registratura/services/registry-service.ts index e9b1a16..fcbd12a 100644 --- a/src/modules/registratura/services/registry-service.ts +++ b/src/modules/registratura/services/registry-service.ts @@ -257,8 +257,9 @@ export async function allocateSequenceNumber( const oldPrefix = OLD_COMPANY_PREFIX[companyPrefix] ?? ""; // New format: B-2026-00001 + // NOTE: Use [0-9] instead of \d — PostgreSQL POSIX regex may not support \d const newLike = `%"number":"${companyPrefix}-${yr}-%"%`; - const newRegex = `${companyPrefix}-${yr}-(\\d{5})`; + const newRegex = `${companyPrefix}-${yr}-([0-9]{5})`; const newMaxRows = await tx.$queryRaw>` SELECT MAX( CAST(SUBSTRING(value::text FROM ${newRegex}) AS INTEGER) @@ -274,7 +275,7 @@ export async function allocateSequenceNumber( let oldMax = 0; if (oldPrefix) { const oldLike = `%"number":"${oldPrefix}-${yr}-%"%`; - const oldRegex = `${oldPrefix}-${yr}-(?:IN|OUT|INT)-(\\d{5})`; + const oldRegex = `${oldPrefix}-${yr}-(?:IN|OUT|INT)-([0-9]{5})`; const oldMaxRows = await tx.$queryRaw>` SELECT MAX( CAST(SUBSTRING(value::text FROM ${oldRegex}) AS INTEGER) @@ -333,8 +334,9 @@ export async function recalculateSequence( const seqType = "SEQ"; // Find max from new format (B-2026-00001) + // NOTE: Use [0-9] instead of \d — PostgreSQL POSIX regex may not support \d const newLike = `%"number":"${companyPrefix}-${yr}-%"%`; - const newRegex = `${companyPrefix}-${yr}-(\\d{5})`; + const newRegex = `${companyPrefix}-${yr}-([0-9]{5})`; const newRows = await prisma.$queryRaw>` SELECT MAX( CAST(SUBSTRING(value::text FROM ${newRegex}) AS INTEGER) @@ -349,7 +351,7 @@ export async function recalculateSequence( // Also check old format (BTG-2026-OUT-00001) if (oldPrefix) { const oldLike = `%"number":"${oldPrefix}-${yr}-%"%`; - const oldRegex = `${oldPrefix}-${yr}-(?:IN|OUT|INT)-(\\d{5})`; + const oldRegex = `${oldPrefix}-${yr}-(?:IN|OUT|INT)-([0-9]{5})`; const oldRows = await prisma.$queryRaw>` SELECT MAX( CAST(SUBSTRING(value::text FROM ${oldRegex}) AS INTEGER)