feat: simplify registry number format to B-2026-00001

New format: single-letter prefix + year + 5-digit sequence.
No direction code (IN/OUT) in the number — shown via arrow icon.
Sequence is shared across directions within the same company+year.

Changes:
- REGISTRY_COMPANY_PREFIX: BTG→B, USW→U, SDT→S, GRP→G
- OLD_COMPANY_PREFIX map for backward compat with existing entries
- allocateSequenceNumber: searches both old and new format entries
  to find the actual max sequence (backward compat)
- recalculateSequence: same dual-format search
- parseRegistryNumber: supports 3 formats (current, v1, legacy)
- isNewFormat: updated regex for B-2026-00001
- CompactNumber: already used single-letter badges, just updated comment
- debug-sequences endpoint: updated for new format
- Notification test data: updated to new format
- RegistrySequence.type: now "SEQ" (shared) instead of "IN"/"OUT"

After deploy: POST /api/registratura/debug-sequences to clean up
old counters, then recreate test entries.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
AI Assistant
2026-03-11 21:49:35 +02:00
parent eb39024548
commit 0f555c55ee
9 changed files with 175 additions and 85 deletions
@@ -22,13 +22,28 @@ 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
// Use Prisma.$queryRawUnsafe to avoid tagged-template escaping issues with regex
// Get actual max sequences from entries (current format: B-2026-00001)
const actuals = await prisma.$queryRawUnsafe<
Array<{ prefix: string; maxSeq: number; count: number }>
>(`
SELECT
SUBSTRING(value::text FROM '"number":"([A-Z]{3}-\\d{4}-(?:IN|OUT|INT))-') AS prefix,
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",
COUNT(*)::int AS count
FROM "KeyValueStore"
WHERE namespace = 'registratura'
AND key LIKE 'entry:%'
AND value::text ~ '"number":"[A-Z]-\\d{4}-\\d{5}"'
GROUP BY prefix
ORDER BY prefix
`);
// Also check for old-format entries (BTG-2026-OUT-00001)
const oldFormatActuals = await prisma.$queryRawUnsafe<
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",
COUNT(*)::int AS count
FROM "KeyValueStore"
@@ -41,7 +56,8 @@ export async function GET() {
return NextResponse.json({
counters,
actualEntries: actuals,
currentFormatEntries: actuals,
oldFormatEntries: oldFormatActuals,
note: "POST to this endpoint to reset all counters to match actual entries",
});
}
@@ -52,31 +68,31 @@ export async function POST() {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
// Delete all counters
// Delete ALL old counters (including old-format BTG/SDT/USW/GRP and IN/OUT types)
const deleted = await prisma.$executeRaw`DELETE FROM "RegistrySequence"`;
// Re-create counters from actual entries
const inserted = await prisma.$executeRawUnsafe(`
// Re-create counters from actual entries in current format (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,
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",
SUBSTRING(value::text FROM '"number":"([A-Z])-') AS company,
CAST(SUBSTRING(value::text FROM '"number":"[A-Z]-(\\d{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",
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}"'
AND value::text ~ '"number":"[A-Z]-\\d{4}-\\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",
recreatedCounters: insertedNew,
message: "All sequence counters reset. Old-format counters (BTG/SDT/USW/GRP, IN/OUT) removed.",
});
}