Files
vreau-digital/src/lib/profile-queries-utilities.ts
T
Claude VM a6c03a091e initial: split from gov-agreg — vreau.digital standalone platform
Moved from gov-agreg/src/pages/achizitii/* to root (drop prefix).
- 22 pages migrated, 127 files total
- All internal links: /achizitii/X → /X (176 occurrences fixed)
- AchizitiiLayout subnav rewritten: /X paths, top-right link to vreaudigital.ro hub
- BaseLayout new (vreau.digital branding, OG tags, site URL)
- astro.config.mjs: site https://vreau.digital, server output (was static)
- docker-compose: port 5096 (vreaudigital is 5095), container vreau-digital
- deploy.sh: paths /opt/vreau-digital, log /var/log/vreau-digital-deploy.log

Backend shared with gov-agreg:
- PostgreSQL satra (same schemas: seap, firms, anaf, anre, ...)
- Photon, Martin tiles
- Infisical /vreaudigital path (DATABASE_URL etc. shared)

build: PASS (npx astro check 0 errors, npm run build 5s vite + 10s server)
2026-05-13 00:10:32 +03:00

397 lines
14 KiB
TypeScript

// Utility/regulator profile helpers — ANRE (energy), ANCOM (telco), CNSC (procurement
// contestations). Each returns null when the CUI has no presence in the source so the
// UI can skip the badge/section entirely. Mirrors the style of the existing helpers
// in profile-queries.ts (RegAS / AEP / ANAF). Always uses query() from './db.js' and
// PostgreSQL parameter binding ($1) with the standard normCui regex normalization.
import { query } from './db.js';
// ─────────────────────────────────────────────────────────────
// ANRE — Autoritatea Națională de Reglementare în domeniul Energiei.
// Source: anre.licente (corporate licenses, 3 sub-registries) and the
// per-CUI rollup anre.mv_licente_per_cui.
// ─────────────────────────────────────────────────────────────
export interface AnreLicentaPreview {
license_source: string; // 'electricitate' | 'gaze' | 'atestat'
license_no: string;
license_type: string | null; // 'Licenta' | 'Autorizatie de Infiintare' | 'Atestat' | …
license_subtype: string | null; // 'Furnizare' | 'Producere' | 'Distributie' | …
stare: string | null; // 'Acordata' | 'Expirata' | 'Retrasa' | …
data_emitere: string | null;
data_expirare: string | null;
}
export interface AnreStatus {
nr_licente_total: number;
nr_active: number;
nr_expirate: number;
nr_retrase: number;
nr_electricitate: number;
nr_gaze: number;
nr_atestate: number;
surse: string[];
subtipuri: string[];
prima_emitere: string | null;
ultima_emitere: string | null;
ultima_expirare: string | null;
recent: AnreLicentaPreview[];
}
export async function getAnreStatus(cui: string): Promise<AnreStatus | null> {
const normCui = cui.replace(/^RO\s*/i, '').trim();
const aggR = await query<any>(
`SELECT nr_licente_total::int,
nr_active::int,
nr_expirate::int,
nr_retrase::int,
nr_electricitate::int,
nr_gaze::int,
nr_atestate::int,
surse,
subtipuri,
to_char(prima_emitere, 'YYYY-MM-DD') AS prima_emitere,
to_char(ultima_emitere, 'YYYY-MM-DD') AS ultima_emitere,
to_char(ultima_expirare, 'YYYY-MM-DD') AS ultima_expirare
FROM anre.mv_licente_per_cui
WHERE cui = $1`,
[normCui],
);
const a = aggR.rows[0];
if (!a || Number(a.nr_licente_total) === 0) return null;
const recentR = await query<AnreLicentaPreview>(
`SELECT license_source,
license_no,
license_type,
license_subtype,
stare,
to_char(data_emitere, 'YYYY-MM-DD') AS data_emitere,
to_char(data_expirare, 'YYYY-MM-DD') AS data_expirare
FROM anre.licente
WHERE titular_cui = $1
ORDER BY (CASE WHEN stare ILIKE 'Acord%' OR stare ILIKE 'Activ%' THEN 0 ELSE 1 END),
data_emitere DESC NULLS LAST
LIMIT 10`,
[normCui],
);
return {
nr_licente_total: Number(a.nr_licente_total),
nr_active: Number(a.nr_active) || 0,
nr_expirate: Number(a.nr_expirate) || 0,
nr_retrase: Number(a.nr_retrase) || 0,
nr_electricitate: Number(a.nr_electricitate) || 0,
nr_gaze: Number(a.nr_gaze) || 0,
nr_atestate: Number(a.nr_atestate) || 0,
surse: a.surse || [],
subtipuri: a.subtipuri || [],
prima_emitere: a.prima_emitere,
ultima_emitere: a.ultima_emitere,
ultima_expirare: a.ultima_expirare,
recent: recentR.rows,
};
}
// ─────────────────────────────────────────────────────────────
// ANCOM — Autoritatea Națională pentru Administrare și Reglementare
// în Comunicații. CUI is direct on the registry detail page.
// Source: ancom.operatori + ancom.mv_operatori_per_cui (rollup).
// ─────────────────────────────────────────────────────────────
export interface AncomOperatorPreview {
ancom_id: number;
titular_name: string;
status: string;
judet: string | null;
detail_url: string;
}
export interface AncomStatus {
nr_autorizatii: number;
retele: string[]; // ['R1','R3', …] (codes)
servicii: string[]; // ['S1','S2', …] (codes)
are_internet_fix: boolean;
are_mobil: boolean;
are_fibra: boolean;
are_status_activ: boolean;
prima_autorizare: string | null;
ultima_autorizare: string | null;
operatori: AncomOperatorPreview[];
}
export async function getAncomStatus(cui: string): Promise<AncomStatus | null> {
const normCui = cui.replace(/^RO\s*/i, '').trim();
const aggR = await query<any>(
`SELECT nr_autorizatii::int,
retele,
servicii,
are_internet_fix,
are_mobil,
are_fibra,
are_status_activ,
to_char(prima_autorizare, 'YYYY-MM-DD') AS prima_autorizare,
to_char(ultima_autorizare, 'YYYY-MM-DD') AS ultima_autorizare
FROM ancom.mv_operatori_per_cui
WHERE cui = $1`,
[normCui],
);
const a = aggR.rows[0];
if (!a || Number(a.nr_autorizatii) === 0) return null;
const opR = await query<AncomOperatorPreview>(
`SELECT ancom_id,
titular_name,
status,
judet,
detail_url
FROM ancom.operatori
WHERE titular_cui = $1
ORDER BY (CASE WHEN status = 'autorizat' THEN 0 ELSE 1 END), ancom_id
LIMIT 5`,
[normCui],
);
return {
nr_autorizatii: Number(a.nr_autorizatii),
retele: a.retele || [],
servicii: a.servicii || [],
are_internet_fix: !!a.are_internet_fix,
are_mobil: !!a.are_mobil,
are_fibra: !!a.are_fibra,
are_status_activ: !!a.are_status_activ,
prima_autorizare: a.prima_autorizare,
ultima_autorizare: a.ultima_autorizare,
operatori: opR.rows,
};
}
// ─────────────────────────────────────────────────────────────
// CNSC — Consiliul Național de Soluționare a Contestațiilor.
// A CUI can appear as either authority (the public buyer being contested)
// or contestator (the bidder filing the complaint). We expose both sides;
// return null only when neither side has any rows.
// Stage 2 PDF parse hasn't run yet → decision_type is mostly NULL; UI hides
// fields that are NULL.
// ─────────────────────────────────────────────────────────────
export interface CnscDeciziePreview {
decision_no: number;
decision_year: number;
registration_no_cnsc: string | null;
registration_date: string | null;
contestator_names: string[];
contestator_cuis: string[];
authority_name: string | null;
authority_cuis: string[];
decision_type: string | null;
pdf_url: string | null;
}
export interface CnscAuthoritySide {
contestation_count: number;
admis_count: number;
admis_in_parte_count: number;
respins_count: number;
resolved_count: number;
first_contestation_date: string | null;
last_contestation_date: string | null;
recent: CnscDeciziePreview[];
}
export interface CnscContestatorSide {
contestations_filed: number;
won_admis: number;
won_partial: number;
lost_respins: number;
resolved_count: number;
first_contestation_date: string | null;
last_contestation_date: string | null;
recent: CnscDeciziePreview[];
}
export interface CnscStatus {
asAuthority: CnscAuthoritySide | null;
asContestator: CnscContestatorSide | null;
}
export async function getCnscStatus(cui: string): Promise<CnscStatus | null> {
const normCui = cui.replace(/^RO\s*/i, '').trim();
const authAggR = await query<any>(
`SELECT contestation_count::int,
admis_count::int,
admis_in_parte_count::int,
respins_count::int,
resolved_count::int,
to_char(first_contestation_date, 'YYYY-MM-DD') AS first_contestation_date,
to_char(last_contestation_date, 'YYYY-MM-DD') AS last_contestation_date
FROM cnsc.mv_per_authority_cui
WHERE cui = $1`,
[normCui],
);
const contAggR = await query<any>(
`SELECT contestations_filed::int,
won_admis::int,
won_partial::int,
lost_respins::int,
resolved_count::int,
to_char(first_contestation_date, 'YYYY-MM-DD') AS first_contestation_date,
to_char(last_contestation_date, 'YYYY-MM-DD') AS last_contestation_date
FROM cnsc.mv_per_contestator_cui
WHERE cui = $1`,
[normCui],
);
const authAgg = authAggR.rows[0];
const contAgg = contAggR.rows[0];
const hasAuth = !!authAgg && Number(authAgg.contestation_count) > 0;
const hasCont = !!contAgg && Number(contAgg.contestations_filed) > 0;
if (!hasAuth && !hasCont) return null;
let asAuthority: CnscAuthoritySide | null = null;
if (hasAuth) {
const recentR = await query<any>(
`SELECT decision_no,
decision_year,
registration_no_cnsc,
to_char(registration_date, 'YYYY-MM-DD') AS registration_date,
contestator_names,
contestator_cuis,
authority_name,
authority_cuis,
decision_type,
pdf_url
FROM cnsc.decizii
WHERE $1 = ANY(authority_cuis)
ORDER BY decision_year DESC, decision_no DESC
LIMIT 10`,
[normCui],
);
asAuthority = {
contestation_count: Number(authAgg.contestation_count),
admis_count: Number(authAgg.admis_count) || 0,
admis_in_parte_count: Number(authAgg.admis_in_parte_count) || 0,
respins_count: Number(authAgg.respins_count) || 0,
resolved_count: Number(authAgg.resolved_count) || 0,
first_contestation_date: authAgg.first_contestation_date,
last_contestation_date: authAgg.last_contestation_date,
recent: recentR.rows.map(r => ({
decision_no: Number(r.decision_no),
decision_year: Number(r.decision_year),
registration_no_cnsc: r.registration_no_cnsc,
registration_date: r.registration_date,
contestator_names: r.contestator_names || [],
contestator_cuis: r.contestator_cuis || [],
authority_name: r.authority_name,
authority_cuis: r.authority_cuis || [],
decision_type: r.decision_type,
pdf_url: r.pdf_url,
})),
};
}
let asContestator: CnscContestatorSide | null = null;
if (hasCont) {
const recentR = await query<any>(
`SELECT decision_no,
decision_year,
registration_no_cnsc,
to_char(registration_date, 'YYYY-MM-DD') AS registration_date,
contestator_names,
contestator_cuis,
authority_name,
authority_cuis,
decision_type,
pdf_url
FROM cnsc.decizii
WHERE $1 = ANY(contestator_cuis)
ORDER BY decision_year DESC, decision_no DESC
LIMIT 10`,
[normCui],
);
asContestator = {
contestations_filed: Number(contAgg.contestations_filed),
won_admis: Number(contAgg.won_admis) || 0,
won_partial: Number(contAgg.won_partial) || 0,
lost_respins: Number(contAgg.lost_respins) || 0,
resolved_count: Number(contAgg.resolved_count) || 0,
first_contestation_date: contAgg.first_contestation_date,
last_contestation_date: contAgg.last_contestation_date,
recent: recentR.rows.map(r => ({
decision_no: Number(r.decision_no),
decision_year: Number(r.decision_year),
registration_no_cnsc: r.registration_no_cnsc,
registration_date: r.registration_date,
contestator_names: r.contestator_names || [],
contestator_cuis: r.contestator_cuis || [],
authority_name: r.authority_name,
authority_cuis: r.authority_cuis || [],
decision_type: r.decision_type,
pdf_url: r.pdf_url,
})),
};
}
return { asAuthority, asContestator };
}
// ─────────────────────────────────────────────────────────────
// Bugetar — Transparență Bugetară MFP. Phase 1 ingest covers the
// universe of reporting public entities (18,822 rows) but not
// actual execuții data (Phase 2 needs 2captcha). Surfacing
// registry membership on autoritate profile is still useful:
// confirms this is a "real" public budget entity + links to
// the MFP search UI for manual lookup.
// ─────────────────────────────────────────────────────────────
const BUGETAR_SECTOR_LABELS: Record<string, string> = {
'01': 'buget de stat',
'02': 'bugete locale',
'03': 'asigurări sociale',
'04': 'șomaj',
'05': 'sănătate (FNUASS)',
};
export interface BugetarStatus {
sector_bugetar: string;
sector_label: string;
judet: string | null;
entity_name: string;
is_ordonator_principal: boolean;
cui_match_method: string | null;
cui_match_score: number | null;
}
export async function getBugetarStatus(cui: string): Promise<BugetarStatus | null> {
const norm = cui.replace(/^RO\s*/i, '').trim();
const r = await query<{
sector_bugetar: string;
judet: string;
entity_name: string;
is_ordonator_principal: boolean;
cui_match_method: string | null;
cui_match_score: number | null;
}>(
`SELECT sector_bugetar, judet, entity_name, is_ordonator_principal,
cui_match_method, cui_match_score
FROM bugetar.entitate
WHERE cui = $1
ORDER BY is_ordonator_principal DESC, sector_bugetar
LIMIT 1`,
[norm],
);
const row = r.rows[0];
if (!row) return null;
return {
sector_bugetar: row.sector_bugetar,
sector_label: BUGETAR_SECTOR_LABELS[row.sector_bugetar] || row.sector_bugetar,
judet: row.judet,
entity_name: row.entity_name,
is_ordonator_principal: row.is_ordonator_principal,
cui_match_method: row.cui_match_method,
cui_match_score: row.cui_match_score == null ? null : Number(row.cui_match_score),
};
}