// 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 { const normCui = cui.replace(/^RO\s*/i, '').trim(); const aggR = await query( `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( `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 { const normCui = cui.replace(/^RO\s*/i, '').trim(); const aggR = await query( `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( `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 { const normCui = cui.replace(/^RO\s*/i, '').trim(); const authAggR = await query( `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( `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( `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( `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 = { '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 { 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), }; }