diff --git a/src/app/api/registratura/debug-sequences/route.ts b/src/app/api/registratura/debug-sequences/route.ts index 1024e41..4a478b2 100644 --- a/src/app/api/registratura/debug-sequences/route.ts +++ b/src/app/api/registratura/debug-sequences/route.ts @@ -27,7 +27,7 @@ export async function GET() { Array<{ key: string; num: string | null; snippet: string }> >(` SELECT key, - SUBSTRING(value::text FROM '"number":"([^"]+)"') AS num, + SUBSTRING(value::text FROM '"number": "([^"]+)"') AS num, SUBSTRING(value::text FROM 1 FOR 200) AS snippet FROM "KeyValueStore" WHERE namespace = 'registratura' @@ -42,13 +42,13 @@ export async function GET() { Array<{ prefix: string; maxSeq: number; count: number }> >(` SELECT - 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", + 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]-[0-9]{4}-[0-9]{5}"' + AND value::text ~ '"number": "[A-Z]-[0-9]{4}-[0-9]{5}"' GROUP BY prefix ORDER BY prefix `); @@ -58,13 +58,13 @@ export async function GET() { Array<{ prefix: string; maxSeq: number; count: number }> >(` SELECT - 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", + 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}-[0-9]{4}-(IN|OUT|INT)-[0-9]{5}"' + AND value::text ~ '"number": "[A-Z]{3}-[0-9]{4}-(IN|OUT|INT)-[0-9]{5}"' GROUP BY prefix ORDER BY prefix `); @@ -92,16 +92,16 @@ export async function POST() { 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]-([0-9]{4})-') AS INTEGER) AS year, + SUBSTRING(value::text FROM '"number": "([A-Z])-') AS company, + 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]-[0-9]{4}-([0-9]{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]-[0-9]{4}-[0-9]{5}"' + AND value::text ~ '"number": "[A-Z]-[0-9]{4}-[0-9]{5}"' GROUP BY company, year, type `); @@ -110,22 +110,22 @@ export async function POST() { INSERT INTO "RegistrySequence" (id, company, year, type, "lastSeq", "createdAt", "updatedAt") SELECT gen_random_uuid()::text, - CASE SUBSTRING(value::text FROM '"number":"([A-Z]{3})-') + 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})-') + 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, + 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", + 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}"' + 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"), @@ -140,3 +140,69 @@ export async function POST() { message: "All counters reset from actual entries (both old and new format).", }); } + +/** + * PATCH — Migrate old-format entries (BTG/SDT/USW/GRP) to new format (B/S/U/G). + * Rewrites the "number" field inside the JSONB value for matching entries. + */ +export async function PATCH() { + const session = await getAuthSession(); + if (!session) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + // Map old 3-letter prefixes to new single-letter + const migrations: Array<{ old: string; new: string }> = [ + { old: "BTG", new: "B" }, + { old: "SDT", new: "S" }, + { old: "USW", new: "U" }, + { old: "GRP", new: "G" }, + ]; + + const results: Array<{ prefix: string; updated: number }> = []; + + for (const m of migrations) { + // Find entries with old-format numbers: BTG-2026-IN-00001, SDT-2026-OUT-00002, etc. + const entries = 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:%' + AND value::text ~ '"number": "${m.old}-[0-9]{4}-(IN|OUT|INT)-[0-9]{5}"' + `); + + let updated = 0; + for (const entry of entries) { + if (!entry.num) continue; + + // Parse: SDT-2026-OUT-00001 → S-2026-00001 + const match = entry.num.match( + new RegExp(`^${m.old}-(\\d{4})-(?:IN|OUT|INT)-(\\d{5})$`) + ); + if (!match) continue; + + const newNumber = `${m.new}-${match[1]}-${match[2]}`; + + // Update the JSONB value — replace the number field + await prisma.$executeRawUnsafe(` + UPDATE "KeyValueStore" + SET value = jsonb_set(value, '{number}', $1::jsonb) + WHERE namespace = 'registratura' + AND key = $2 + `, JSON.stringify(newNumber), entry.key); + + updated++; + } + + results.push({ prefix: m.old, updated }); + } + + return NextResponse.json({ + success: true, + migrations: results, + message: "Old-format entries migrated to new format. Run POST to reset counters.", + }); +} diff --git a/src/modules/registratura/services/registry-service.ts b/src/modules/registratura/services/registry-service.ts index fcbd12a..16e2975 100644 --- a/src/modules/registratura/services/registry-service.ts +++ b/src/modules/registratura/services/registry-service.ts @@ -258,7 +258,8 @@ export async function allocateSequenceNumber( // New format: B-2026-00001 // NOTE: Use [0-9] instead of \d — PostgreSQL POSIX regex may not support \d - const newLike = `%"number":"${companyPrefix}-${yr}-%"%`; + // NOTE: JSONB value::text serializes with a space after colons ("number": "...") + const newLike = `%"number": "${companyPrefix}-${yr}-%"%`; const newRegex = `${companyPrefix}-${yr}-([0-9]{5})`; const newMaxRows = await tx.$queryRaw>` SELECT MAX( @@ -274,7 +275,7 @@ export async function allocateSequenceNumber( // Old format: BTG-2026-IN-00001 / BTG-2026-OUT-00001 let oldMax = 0; if (oldPrefix) { - const oldLike = `%"number":"${oldPrefix}-${yr}-%"%`; + const oldLike = `%"number": "${oldPrefix}-${yr}-%"%`; const oldRegex = `${oldPrefix}-${yr}-(?:IN|OUT|INT)-([0-9]{5})`; const oldMaxRows = await tx.$queryRaw>` SELECT MAX( @@ -335,7 +336,8 @@ export async function recalculateSequence( // 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}-%"%`; + // NOTE: JSONB value::text serializes with a space after colons ("number": "...") + const newLike = `%"number": "${companyPrefix}-${yr}-%"%`; const newRegex = `${companyPrefix}-${yr}-([0-9]{5})`; const newRows = await prisma.$queryRaw>` SELECT MAX( @@ -350,7 +352,7 @@ export async function recalculateSequence( // Also check old format (BTG-2026-OUT-00001) if (oldPrefix) { - const oldLike = `%"number":"${oldPrefix}-${yr}-%"%`; + const oldLike = `%"number": "${oldPrefix}-${yr}-%"%`; const oldRegex = `${oldPrefix}-${yr}-(?:IN|OUT|INT)-([0-9]{5})`; const oldRows = await prisma.$queryRaw>` SELECT MAX(