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:
@@ -87,9 +87,9 @@ model GisUat {
|
||||
|
||||
model RegistrySequence {
|
||||
id String @id @default(uuid())
|
||||
company String // BTG, SDT, USW, GRP
|
||||
company String // B, U, S, G (single-letter prefix)
|
||||
year Int
|
||||
type String // IN, OUT, INT
|
||||
type String // SEQ (shared across directions)
|
||||
lastSeq Int @default(0)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@ -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.",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -416,7 +416,7 @@ export async function sendTestDigest(): Promise<DigestResult> {
|
||||
title: "Termene urgente (5 zile sau mai putin)",
|
||||
items: [
|
||||
{
|
||||
entryNumber: "BTG-0001/2026",
|
||||
entryNumber: "B-2026-00001",
|
||||
subject: "[TEST] Certificat de urbanism - str. Exemplu nr. 10",
|
||||
label: "Emitere CU (30 zile lucratoare)",
|
||||
dueDate: today,
|
||||
@@ -424,7 +424,7 @@ export async function sendTestDigest(): Promise<DigestResult> {
|
||||
color: "yellow",
|
||||
},
|
||||
{
|
||||
entryNumber: "BTG-0005/2026",
|
||||
entryNumber: "B-2026-00005",
|
||||
subject: "[TEST] Aviz ISU - Proiect rezidential",
|
||||
label: "Raspuns aviz ISU (15 zile)",
|
||||
dueDate: today,
|
||||
@@ -438,7 +438,7 @@ export async function sendTestDigest(): Promise<DigestResult> {
|
||||
title: "Termene depasite",
|
||||
items: [
|
||||
{
|
||||
entryNumber: "BTG-0003/2026",
|
||||
entryNumber: "B-2026-00003",
|
||||
subject: "[TEST] Autorizatie construire - bloc P+4",
|
||||
label: "Emitere AC (30 zile lucratoare)",
|
||||
dueDate: "2026-03-01",
|
||||
@@ -452,7 +452,7 @@ export async function sendTestDigest(): Promise<DigestResult> {
|
||||
title: "Documente care expira",
|
||||
items: [
|
||||
{
|
||||
entryNumber: "BTG-0010/2025",
|
||||
entryNumber: "B-2025-00010",
|
||||
subject: "[TEST] CU nr. 123/2025 - proiect mixt",
|
||||
label: "Expira curand",
|
||||
dueDate: "2026-03-25",
|
||||
|
||||
@@ -69,7 +69,7 @@ export function defaultPreference(
|
||||
// ── Digest result types ──
|
||||
|
||||
export interface DigestItem {
|
||||
/** Entry number (e.g., "BTG-0042/2026") */
|
||||
/** Entry number (e.g., "B-2026-00042") */
|
||||
entryNumber: string;
|
||||
/** Entry subject */
|
||||
subject: string;
|
||||
|
||||
@@ -377,7 +377,7 @@ export function RegistraturaModule() {
|
||||
<div className="space-y-1.5">
|
||||
<p className="text-xs font-semibold text-muted-foreground uppercase tracking-wider">Numerotare</p>
|
||||
<ul className="text-xs text-muted-foreground space-y-1 list-disc pl-4">
|
||||
<li>Numerele se atribuie automat (BTG-2026-IN-00001)</li>
|
||||
<li>Numerele se atribuie automat (B-2026-00001)</li>
|
||||
<li>Nu modifica manual numărul de înregistrare</li>
|
||||
<li>Fiecare companie are propria secvență</li>
|
||||
</ul>
|
||||
|
||||
@@ -588,7 +588,7 @@ const COMPANY_BADGE: Record<string, { label: string; className: string }> = {
|
||||
};
|
||||
|
||||
function CompactNumber({ entry }: { entry: RegistryEntry }) {
|
||||
// Extract plain number with leading zeros: "BTG-0042/2026" → "0042/2026"
|
||||
// Extract plain number: "B-2026-00001" → "2026-00001"
|
||||
const plain = (entry.number ?? "").replace(/^[A-Z]+-/, "");
|
||||
const badge = COMPANY_BADGE[entry.company ?? ""] ?? { label: "B", className: "bg-blue-600 text-white" };
|
||||
|
||||
|
||||
@@ -21,4 +21,5 @@ export {
|
||||
DEFAULT_DOC_TYPE_LABELS,
|
||||
DIRECTION_TYPE_CODE,
|
||||
REGISTRY_COMPANY_PREFIX,
|
||||
OLD_COMPANY_PREFIX,
|
||||
} from "./types";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { CompanyId } from "@/core/auth/types";
|
||||
import type { RegistryEntry, RegistryAttachment, RegistryDirection } from "../types";
|
||||
import { REGISTRY_COMPANY_PREFIX, DIRECTION_TYPE_CODE } from "../types";
|
||||
import { REGISTRY_COMPANY_PREFIX, OLD_COMPANY_PREFIX } from "../types";
|
||||
|
||||
const STORAGE_PREFIX = "entry:";
|
||||
|
||||
@@ -212,11 +212,15 @@ export function generateRegistryNumber(
|
||||
return `${prefix}-${padded}/${year}`;
|
||||
}
|
||||
|
||||
// ── New-format numbering (server-side, atomic) ──
|
||||
// ── Registry numbering (server-side, atomic) ──
|
||||
//
|
||||
// Format: B-2026-00001 (single-letter prefix, year, 5-digit sequence)
|
||||
// Direction (intrat/iesit) is NOT part of the number — shown via icon.
|
||||
// Sequence is shared across directions within the same company+year.
|
||||
|
||||
/**
|
||||
* Allocate the next sequence number atomically via PostgreSQL.
|
||||
* Format: BTG-2026-IN-00001
|
||||
* Format: B-2026-00001
|
||||
*
|
||||
* Uses an interactive Prisma transaction with pg_advisory_xact_lock to
|
||||
* serialize concurrent allocations. Always reads the **actual max
|
||||
@@ -224,54 +228,75 @@ export function generateRegistryNumber(
|
||||
* never drifts — even after entry deletions, company reassignments, or
|
||||
* any other mutation that could leave the RegistrySequence counter stale.
|
||||
*
|
||||
* The `direction` parameter is kept in the signature for API compat but
|
||||
* is NOT used in the number format — all directions share one sequence.
|
||||
*
|
||||
* Must be called from server-side only (API routes).
|
||||
*/
|
||||
export async function allocateSequenceNumber(
|
||||
company: CompanyId,
|
||||
direction: RegistryDirection,
|
||||
_direction: RegistryDirection,
|
||||
year?: number,
|
||||
): Promise<{ number: string; sequence: number }> {
|
||||
// Dynamic import — prisma is only available server-side
|
||||
const { prisma } = await import("@/core/storage/prisma");
|
||||
|
||||
const companyPrefix = REGISTRY_COMPANY_PREFIX[company];
|
||||
const typeCode = DIRECTION_TYPE_CODE[direction];
|
||||
const yr = year ?? new Date().getFullYear();
|
||||
|
||||
const seq = await prisma.$transaction(async (tx) => {
|
||||
// Advisory lock scoped to this transaction — serializes concurrent
|
||||
// allocations for the same company/year/type combination.
|
||||
const lockKey = `registratura:${companyPrefix}-${yr}-${typeCode}`;
|
||||
// allocations for the same company/year combination.
|
||||
const lockKey = `registratura:${companyPrefix}-${yr}`;
|
||||
await tx.$executeRaw`SELECT pg_advisory_xact_lock(hashtext(${lockKey}))`;
|
||||
|
||||
// 1. Find actual max sequence from entries stored in KeyValueStore.
|
||||
// This is the SOURCE OF TRUTH — entries might have been deleted,
|
||||
// moved between companies, etc., making the RegistrySequence
|
||||
// counter stale.
|
||||
const numberLike = `%"number":"${companyPrefix}-${yr}-${typeCode}-%"%`;
|
||||
const regexPat = `${companyPrefix}-${yr}-${typeCode}-(\\d{5})`;
|
||||
const maxRows = await tx.$queryRaw<Array<{ maxSeq: number | null }>>`
|
||||
// This is the SOURCE OF TRUTH.
|
||||
// Search BOTH new format (B-2026-00001) and old format (BTG-2026-OUT-00001)
|
||||
// for backward compat with entries created before the format change.
|
||||
const oldPrefix = OLD_COMPANY_PREFIX[companyPrefix] ?? "";
|
||||
|
||||
// New format: B-2026-00001
|
||||
const newLike = `%"number":"${companyPrefix}-${yr}-%"%`;
|
||||
const newRegex = `${companyPrefix}-${yr}-(\\d{5})`;
|
||||
const newMaxRows = await tx.$queryRaw<Array<{ maxSeq: number | null }>>`
|
||||
SELECT MAX(
|
||||
CAST(SUBSTRING(value::text FROM ${regexPat}) AS INTEGER)
|
||||
CAST(SUBSTRING(value::text FROM ${newRegex}) AS INTEGER)
|
||||
) AS "maxSeq"
|
||||
FROM "KeyValueStore"
|
||||
WHERE namespace = 'registratura'
|
||||
AND key LIKE 'entry:%'
|
||||
AND value::text LIKE ${numberLike}
|
||||
AND value::text LIKE ${newLike}
|
||||
`;
|
||||
const actualMax = maxRows[0]?.maxSeq ?? 0;
|
||||
const newMax = newMaxRows[0]?.maxSeq ?? 0;
|
||||
|
||||
// Old format: BTG-2026-IN-00001 / BTG-2026-OUT-00001
|
||||
let oldMax = 0;
|
||||
if (oldPrefix) {
|
||||
const oldLike = `%"number":"${oldPrefix}-${yr}-%"%`;
|
||||
const oldRegex = `${oldPrefix}-${yr}-(?:IN|OUT|INT)-(\\d{5})`;
|
||||
const oldMaxRows = await tx.$queryRaw<Array<{ maxSeq: number | null }>>`
|
||||
SELECT MAX(
|
||||
CAST(SUBSTRING(value::text FROM ${oldRegex}) AS INTEGER)
|
||||
) AS "maxSeq"
|
||||
FROM "KeyValueStore"
|
||||
WHERE namespace = 'registratura'
|
||||
AND key LIKE 'entry:%'
|
||||
AND value::text LIKE ${oldLike}
|
||||
`;
|
||||
oldMax = oldMaxRows[0]?.maxSeq ?? 0;
|
||||
}
|
||||
|
||||
const actualMax = Math.max(newMax, oldMax);
|
||||
|
||||
// 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. Upsert the counter to the new value (keep it in sync)
|
||||
// 3. Upsert the counter (type = "SEQ" — shared across directions)
|
||||
const seqType = "SEQ";
|
||||
await tx.$executeRaw`
|
||||
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}, ${seqType}, ${nextSeq}, NOW(), NOW())
|
||||
ON CONFLICT (company, year, type)
|
||||
DO UPDATE SET "lastSeq" = ${nextSeq}, "updatedAt" = NOW()
|
||||
`;
|
||||
@@ -282,7 +307,7 @@ export async function allocateSequenceNumber(
|
||||
const padded = String(seq).padStart(5, "0");
|
||||
|
||||
return {
|
||||
number: `${companyPrefix}-${yr}-${typeCode}-${padded}`,
|
||||
number: `${companyPrefix}-${yr}-${padded}`,
|
||||
sequence: seq,
|
||||
};
|
||||
}
|
||||
@@ -291,43 +316,71 @@ export async function allocateSequenceNumber(
|
||||
* Recalculate a company's sequence counter to match actual entries in the DB.
|
||||
* Called when an entry is reassigned away from a company, so the counter
|
||||
* reflects the real max sequence instead of staying artificially high.
|
||||
*
|
||||
* The `direction` parameter is kept for API compat but ignored — sequence
|
||||
* is shared across directions.
|
||||
*/
|
||||
export async function recalculateSequence(
|
||||
company: CompanyId,
|
||||
direction: RegistryDirection,
|
||||
_direction: RegistryDirection,
|
||||
year?: number,
|
||||
): Promise<void> {
|
||||
const { prisma } = await import("@/core/storage/prisma");
|
||||
|
||||
const companyPrefix = REGISTRY_COMPANY_PREFIX[company];
|
||||
const typeCode = DIRECTION_TYPE_CODE[direction];
|
||||
const yr = year ?? new Date().getFullYear();
|
||||
const oldPrefix = OLD_COMPANY_PREFIX[companyPrefix] ?? "";
|
||||
const seqType = "SEQ";
|
||||
|
||||
// Find the actual max sequence from entries in KeyValueStore
|
||||
const pattern = `${companyPrefix}-${yr}-${typeCode}-%`;
|
||||
const rows = await prisma.$queryRaw<Array<{ maxSeq: number | null }>>`
|
||||
// Find max from new format (B-2026-00001)
|
||||
const newLike = `%"number":"${companyPrefix}-${yr}-%"%`;
|
||||
const newRegex = `${companyPrefix}-${yr}-(\\d{5})`;
|
||||
const newRows = await prisma.$queryRaw<Array<{ maxSeq: number | null }>>`
|
||||
SELECT MAX(
|
||||
CAST(SUBSTRING(value::text FROM ${`${companyPrefix}-${yr}-${typeCode}-(\\d{5})`}) AS INTEGER)
|
||||
CAST(SUBSTRING(value::text FROM ${newRegex}) AS INTEGER)
|
||||
) AS "maxSeq"
|
||||
FROM "KeyValueStore"
|
||||
WHERE namespace = 'registratura'
|
||||
AND key LIKE 'entry:%'
|
||||
AND value::text LIKE ${`%"number":"${pattern}"%`}
|
||||
AND value::text LIKE ${newLike}
|
||||
`;
|
||||
let actualMax = newRows[0]?.maxSeq ?? 0;
|
||||
|
||||
const actualMax = rows[0]?.maxSeq ?? 0;
|
||||
// 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 oldRows = await prisma.$queryRaw<Array<{ maxSeq: number | null }>>`
|
||||
SELECT MAX(
|
||||
CAST(SUBSTRING(value::text FROM ${oldRegex}) AS INTEGER)
|
||||
) AS "maxSeq"
|
||||
FROM "KeyValueStore"
|
||||
WHERE namespace = 'registratura'
|
||||
AND key LIKE 'entry:%'
|
||||
AND value::text LIKE ${oldLike}
|
||||
`;
|
||||
actualMax = Math.max(actualMax, oldRows[0]?.maxSeq ?? 0);
|
||||
}
|
||||
|
||||
// Reset the counter to the actual max (or delete if 0)
|
||||
if (actualMax === 0) {
|
||||
await prisma.$executeRaw`
|
||||
DELETE FROM "RegistrySequence"
|
||||
WHERE company = ${companyPrefix} AND year = ${yr} AND type = ${typeCode}
|
||||
WHERE company = ${companyPrefix} AND year = ${yr} AND type = ${seqType}
|
||||
`;
|
||||
// Also clean up old-format counters
|
||||
if (oldPrefix) {
|
||||
await prisma.$executeRaw`
|
||||
DELETE FROM "RegistrySequence"
|
||||
WHERE company = ${oldPrefix} AND year = ${yr}
|
||||
`;
|
||||
}
|
||||
} else {
|
||||
await prisma.$executeRaw`
|
||||
UPDATE "RegistrySequence"
|
||||
SET "lastSeq" = ${actualMax}, "updatedAt" = NOW()
|
||||
WHERE company = ${companyPrefix} AND year = ${yr} AND type = ${typeCode}
|
||||
INSERT INTO "RegistrySequence" (id, company, year, type, "lastSeq", "createdAt", "updatedAt")
|
||||
VALUES (gen_random_uuid()::text, ${companyPrefix}, ${yr}, ${seqType}, ${actualMax}, NOW(), NOW())
|
||||
ON CONFLICT (company, year, type)
|
||||
DO UPDATE SET "lastSeq" = ${actualMax}, "updatedAt" = NOW()
|
||||
`;
|
||||
}
|
||||
}
|
||||
@@ -339,38 +392,49 @@ export interface ParsedRegistryNumber {
|
||||
year: number;
|
||||
type?: string;
|
||||
sequence: number;
|
||||
format: "old" | "new";
|
||||
format: "current" | "v1" | "legacy";
|
||||
}
|
||||
|
||||
/** Detect whether a number uses the new format (BTG-2026-IN-00125) */
|
||||
/** Detect whether a number uses the current format (B-2026-00001) */
|
||||
export function isNewFormat(num: string): boolean {
|
||||
return /^(BTG|SDT|USW|GRP)-\d{4}-(IN|OUT|INT)-\d{5}$/.test(num);
|
||||
return /^[A-Z]-\d{4}-\d{5}$/.test(num);
|
||||
}
|
||||
|
||||
/** Parse a registry number in either old or new format */
|
||||
/** Parse a registry number in any supported format */
|
||||
export function parseRegistryNumber(num: string): ParsedRegistryNumber | null {
|
||||
// New format: BTG-2026-IN-00125
|
||||
const newMatch = num.match(
|
||||
/^(BTG|SDT|USW|GRP)-(\d{4})-(IN|OUT|INT)-(\d{5})$/,
|
||||
);
|
||||
if (newMatch) {
|
||||
// Current format: B-2026-00001
|
||||
const currentMatch = num.match(/^([A-Z])-(\d{4})-(\d{5})$/);
|
||||
if (currentMatch) {
|
||||
return {
|
||||
company: newMatch[1]!,
|
||||
year: parseInt(newMatch[2]!, 10),
|
||||
type: newMatch[3]!,
|
||||
sequence: parseInt(newMatch[4]!, 10),
|
||||
format: "new",
|
||||
company: currentMatch[1]!,
|
||||
year: parseInt(currentMatch[2]!, 10),
|
||||
sequence: parseInt(currentMatch[3]!, 10),
|
||||
format: "current",
|
||||
};
|
||||
}
|
||||
|
||||
// Old format: B-0001/2026
|
||||
const oldMatch = num.match(/^(B|US|SDT|G)-(\d+)\/(\d{4})$/);
|
||||
if (oldMatch) {
|
||||
// V1 format (3-letter prefix + direction): BTG-2026-IN-00125
|
||||
const v1Match = num.match(
|
||||
/^(BTG|SDT|USW|GRP)-(\d{4})-(IN|OUT|INT)-(\d{5})$/,
|
||||
);
|
||||
if (v1Match) {
|
||||
return {
|
||||
company: oldMatch[1]!,
|
||||
year: parseInt(oldMatch[3]!, 10),
|
||||
sequence: parseInt(oldMatch[2]!, 10),
|
||||
format: "old",
|
||||
company: v1Match[1]!,
|
||||
year: parseInt(v1Match[2]!, 10),
|
||||
type: v1Match[3]!,
|
||||
sequence: parseInt(v1Match[4]!, 10),
|
||||
format: "v1",
|
||||
};
|
||||
}
|
||||
|
||||
// Legacy format: B-0001/2026
|
||||
const legacyMatch = num.match(/^(B|US|SDT|G)-(\d+)\/(\d{4})$/);
|
||||
if (legacyMatch) {
|
||||
return {
|
||||
company: legacyMatch[1]!,
|
||||
year: parseInt(legacyMatch[3]!, 10),
|
||||
sequence: parseInt(legacyMatch[2]!, 10),
|
||||
format: "legacy",
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -4,18 +4,27 @@ import type { CompanyId } from "@/core/auth/types";
|
||||
/** Document direction */
|
||||
export type RegistryDirection = "intrat" | "iesit";
|
||||
|
||||
/** Maps direction to the numbering type code */
|
||||
/** Maps direction to the numbering type code (used internally, not in number format) */
|
||||
export const DIRECTION_TYPE_CODE: Record<RegistryDirection, string> = {
|
||||
intrat: "IN",
|
||||
iesit: "OUT",
|
||||
};
|
||||
|
||||
/** New-format company prefixes for registry numbering */
|
||||
/** Single-letter company prefixes for registry numbering.
|
||||
* Format: B-2026-00001 (no direction code in the number). */
|
||||
export const REGISTRY_COMPANY_PREFIX: Record<CompanyId, string> = {
|
||||
beletage: "BTG",
|
||||
"urban-switch": "USW",
|
||||
"studii-de-teren": "SDT",
|
||||
group: "GRP",
|
||||
beletage: "B",
|
||||
"urban-switch": "U",
|
||||
"studii-de-teren": "S",
|
||||
group: "G",
|
||||
};
|
||||
|
||||
/** Old 3-letter prefixes — for backward compat when scanning existing entries */
|
||||
export const OLD_COMPANY_PREFIX: Record<string, string> = {
|
||||
B: "BTG",
|
||||
U: "USW",
|
||||
S: "SDT",
|
||||
G: "GRP",
|
||||
};
|
||||
|
||||
/** Registration type — normal, late, or claimed from reserved slot */
|
||||
|
||||
Reference in New Issue
Block a user