a6c03a091e
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)
321 lines
11 KiB
TypeScript
321 lines
11 KiB
TypeScript
/**
|
|
* Map seap.announcements rows to OCDS 1.1.5 Release Package format.
|
|
* Schema: https://standard.open-contracting.org/1.1/en/schema/release/
|
|
*
|
|
* Coverage notes:
|
|
* - We map: tender (procuringEntity, value, procurementMethod, mainProcurementCategory),
|
|
* awards[] (suppliers, value, date), contracts[] (period, value, dateSigned), parties[].
|
|
* - We do NOT have yet: bids[], amendments[], milestones[], documents[]. Those fields
|
|
* are emitted as `null` or omitted to remain spec-valid.
|
|
*/
|
|
|
|
const PUBLISHER = {
|
|
name: 'vreaudigital.ro',
|
|
uri: 'https://vreaudigital.ro',
|
|
scheme: 'RO-PROCUREMENT-AGGREGATOR',
|
|
uid: 'vreaudigital-ro',
|
|
};
|
|
|
|
const OCID_PREFIX = 'ocds-vreaudigital-';
|
|
const LICENSE = 'https://creativecommons.org/licenses/by/4.0/';
|
|
const PUBLICATION_POLICY = 'https://vreaudigital.ro/api/ocds/policy';
|
|
|
|
// SEAP procedure codes → OCDS procurement methods
|
|
// https://standard.open-contracting.org/1.1/en/schema/codelists/#procurement-method
|
|
// Keys are normalized lowercase + ASCII (diacritics stripped) for matching.
|
|
const PROCUREMENT_METHOD_MAP: Record<string, { method: string; details?: string }> = {
|
|
'licitatie deschisa': { method: 'open' },
|
|
'licitatie deschisa accelerata': { method: 'open', details: 'accelerated' },
|
|
'licitatie restransa': { method: 'selective' },
|
|
'licitatie restransa accelerata': { method: 'selective', details: 'accelerated' },
|
|
'negociere fara publicare prealabila': { method: 'limited', details: 'negociere-fara-publicare' },
|
|
'negociere cu publicare': { method: 'selective', details: 'negociere-cu-publicare' },
|
|
'procedura competitiva cu negociere': { method: 'selective', details: 'competitiva-cu-negociere' },
|
|
'procedura simplificata': { method: 'open', details: 'procedura-simplificata' },
|
|
'procedura simplificata proprie': { method: 'open', details: 'procedura-simplificata-proprie' },
|
|
'norme proprii (anexa 2b)': { method: 'limited', details: 'norme-proprii-anexa-2b' },
|
|
'cerere de oferta': { method: 'selective', details: 'cerere-de-oferte' },
|
|
'cerere de oferte': { method: 'selective', details: 'cerere-de-oferte' },
|
|
'concurs de solutii': { method: 'open', details: 'concurs-de-solutii' },
|
|
'dialog competitiv': { method: 'selective', details: 'dialog-competitiv' },
|
|
'achizitie directa': { method: 'limited', details: 'achizitie-directa' },
|
|
};
|
|
|
|
function normProcedureKey(s: string): string {
|
|
return s
|
|
.normalize('NFD')
|
|
.replace(/[̀-ͯ]/g, '')
|
|
.toLowerCase()
|
|
.trim();
|
|
}
|
|
|
|
// SEAP type → OCDS release tags
|
|
// https://standard.open-contracting.org/1.1/en/schema/codelists/#release-tag
|
|
function tagsForType(seapType: string): string[] {
|
|
switch (seapType) {
|
|
case 'initiere':
|
|
case 'pi_notice':
|
|
return ['planning'];
|
|
case 'c_notice':
|
|
case 'rfq_invitation':
|
|
return ['tender'];
|
|
case 'ca_notice':
|
|
case 'rfq_notice':
|
|
case 'ted_notice':
|
|
return ['tender', 'award'];
|
|
case 'da':
|
|
case 'contract':
|
|
case 'atribuire_fara':
|
|
return ['tender', 'award', 'contract'];
|
|
case 'notificare':
|
|
return ['contractAmendment'];
|
|
default:
|
|
return ['tender'];
|
|
}
|
|
}
|
|
|
|
export interface AnnouncementRow {
|
|
id: number;
|
|
ref_number: string | null;
|
|
type: string;
|
|
source: string;
|
|
title: string | null;
|
|
description: string | null;
|
|
publication_date: Date | string | null;
|
|
contract_date: Date | string | null;
|
|
estimated_value: number | string | null;
|
|
awarded_value: number | string | null;
|
|
currency: string | null;
|
|
procedure_type: string | null;
|
|
procedure_state: string | null;
|
|
framework_agreement: boolean | null;
|
|
modification_desc: string | null;
|
|
cpv_code: string | null;
|
|
cpv_name: string | null;
|
|
cpv_division: string | null;
|
|
authority_cui: string | null;
|
|
authority_name: string | null;
|
|
authority_address: string | null;
|
|
authority_email: string | null;
|
|
authority_phone: string | null;
|
|
authority_url: string | null;
|
|
authority_county: string | null;
|
|
authority_siruta: string | null;
|
|
supplier_cui: string | null;
|
|
supplier_name: string | null;
|
|
supplier_address: string | null;
|
|
supplier_county: string | null;
|
|
supplier_siruta: string | null;
|
|
supplier_is_sme: boolean | null;
|
|
contract_has_lots: boolean | null;
|
|
lots_count: number | null;
|
|
seap_url: string | null;
|
|
contract_period_start?: Date | string | null;
|
|
contract_period_end?: Date | string | null;
|
|
}
|
|
|
|
function isoDate(d: Date | string | null | undefined): string | null {
|
|
if (!d) return null;
|
|
const date = typeof d === 'string' ? new Date(d) : d;
|
|
if (isNaN(date.getTime())) return null;
|
|
return date.toISOString();
|
|
}
|
|
|
|
function normCui(cui: string | null | undefined): string | null {
|
|
if (!cui) return null;
|
|
return cui.toUpperCase().replace(/^RO/, '').replace(/\s+/g, '').trim() || null;
|
|
}
|
|
|
|
function partyId(prefix: 'BUYER' | 'SUPPLIER', cui: string | null): string {
|
|
const norm = normCui(cui);
|
|
return norm ? `RO-CUI-${norm}` : `${prefix}-UNKNOWN`;
|
|
}
|
|
|
|
export function ocidFor(announcementId: number): string {
|
|
return `${OCID_PREFIX}${announcementId}`;
|
|
}
|
|
|
|
export function announcementToRelease(row: AnnouncementRow): any {
|
|
const ocid = ocidFor(row.id);
|
|
const releaseDate = isoDate(row.publication_date) || isoDate(row.contract_date) || new Date().toISOString();
|
|
const tags = tagsForType(row.type);
|
|
|
|
// Build parties[] — buyer + supplier (if present)
|
|
const parties: any[] = [];
|
|
|
|
if (row.authority_cui) {
|
|
const pid = partyId('BUYER', row.authority_cui);
|
|
parties.push({
|
|
id: pid,
|
|
name: row.authority_name,
|
|
identifier: {
|
|
scheme: 'RO-CUI',
|
|
id: normCui(row.authority_cui),
|
|
legalName: row.authority_name,
|
|
},
|
|
roles: ['buyer', 'procuringEntity'],
|
|
address: row.authority_address ? {
|
|
streetAddress: row.authority_address,
|
|
region: row.authority_county || undefined,
|
|
countryName: 'Romania',
|
|
} : undefined,
|
|
contactPoint: (row.authority_email || row.authority_phone || row.authority_url) ? {
|
|
email: row.authority_email || undefined,
|
|
telephone: row.authority_phone || undefined,
|
|
url: row.authority_url || undefined,
|
|
} : undefined,
|
|
details: row.authority_siruta ? { siruta: row.authority_siruta } : undefined,
|
|
});
|
|
}
|
|
|
|
if (row.supplier_cui) {
|
|
const pid = partyId('SUPPLIER', row.supplier_cui);
|
|
parties.push({
|
|
id: pid,
|
|
name: row.supplier_name,
|
|
identifier: {
|
|
scheme: 'RO-CUI',
|
|
id: normCui(row.supplier_cui),
|
|
legalName: row.supplier_name,
|
|
},
|
|
roles: ['supplier', 'tenderer'],
|
|
address: (row.supplier_address || row.supplier_county) ? {
|
|
streetAddress: row.supplier_address || undefined,
|
|
region: row.supplier_county || undefined,
|
|
countryName: 'Romania',
|
|
} : undefined,
|
|
details: {
|
|
scale: row.supplier_is_sme === true ? 'sme' : (row.supplier_is_sme === false ? 'large' : undefined),
|
|
siruta: row.supplier_siruta || undefined,
|
|
},
|
|
});
|
|
}
|
|
|
|
// Tender block
|
|
const procMap = row.procedure_type
|
|
? PROCUREMENT_METHOD_MAP[normProcedureKey(row.procedure_type)]
|
|
: undefined;
|
|
|
|
const tender: any = {
|
|
id: row.ref_number || `tender-${row.id}`,
|
|
title: row.title || undefined,
|
|
description: row.description || undefined,
|
|
status: row.procedure_state ? row.procedure_state.toLowerCase() : undefined,
|
|
procuringEntity: row.authority_cui ? {
|
|
id: partyId('BUYER', row.authority_cui),
|
|
name: row.authority_name,
|
|
} : undefined,
|
|
procurementMethod: procMap?.method,
|
|
procurementMethodDetails: procMap?.details || row.procedure_type || undefined,
|
|
mainProcurementCategory: cpvToCategory(row.cpv_code),
|
|
value: row.estimated_value != null ? {
|
|
amount: Number(row.estimated_value),
|
|
currency: row.currency || 'RON',
|
|
} : undefined,
|
|
items: row.cpv_code ? [{
|
|
id: '1',
|
|
description: row.cpv_name || row.title || undefined,
|
|
classification: {
|
|
scheme: 'CPV',
|
|
id: row.cpv_code,
|
|
description: row.cpv_name || undefined,
|
|
},
|
|
}] : undefined,
|
|
hasFrameworkAgreement: row.framework_agreement || undefined,
|
|
numberOfLots: row.lots_count || undefined,
|
|
};
|
|
|
|
// Awards block — only if we have an awarded value or supplier
|
|
const awards: any[] = [];
|
|
if (row.awarded_value != null || row.supplier_cui) {
|
|
awards.push({
|
|
id: `award-${row.id}`,
|
|
title: row.title || undefined,
|
|
status: row.contract_date ? 'active' : 'pending',
|
|
date: isoDate(row.publication_date) || isoDate(row.contract_date),
|
|
value: row.awarded_value != null ? {
|
|
amount: Number(row.awarded_value),
|
|
currency: row.currency || 'RON',
|
|
} : undefined,
|
|
suppliers: row.supplier_cui ? [{
|
|
id: partyId('SUPPLIER', row.supplier_cui),
|
|
name: row.supplier_name,
|
|
}] : [],
|
|
relatedLots: undefined,
|
|
});
|
|
}
|
|
|
|
// Contracts block
|
|
const contracts: any[] = [];
|
|
if (row.contract_date || row.modification_desc) {
|
|
contracts.push({
|
|
id: `contract-${row.id}`,
|
|
awardID: `award-${row.id}`,
|
|
title: row.title || undefined,
|
|
status: 'active',
|
|
dateSigned: isoDate(row.contract_date),
|
|
value: row.awarded_value != null ? {
|
|
amount: Number(row.awarded_value),
|
|
currency: row.currency || 'RON',
|
|
} : undefined,
|
|
period: (row.contract_period_start || row.contract_period_end) ? {
|
|
startDate: isoDate(row.contract_period_start),
|
|
endDate: isoDate(row.contract_period_end),
|
|
} : undefined,
|
|
hasAmendments: !!row.modification_desc,
|
|
amendments: row.modification_desc ? [{
|
|
id: 'amendment-1',
|
|
rationale: row.modification_desc,
|
|
}] : undefined,
|
|
});
|
|
}
|
|
|
|
return {
|
|
ocid,
|
|
id: `${row.id}`,
|
|
date: releaseDate,
|
|
tag: tags,
|
|
initiationType: 'tender',
|
|
language: 'ro',
|
|
parties,
|
|
buyer: row.authority_cui ? {
|
|
id: partyId('BUYER', row.authority_cui),
|
|
name: row.authority_name,
|
|
} : undefined,
|
|
tender,
|
|
awards: awards.length > 0 ? awards : undefined,
|
|
contracts: contracts.length > 0 ? contracts : undefined,
|
|
sources: [{
|
|
id: 'seap',
|
|
url: row.seap_url || `https://e-licitatie.ro`,
|
|
title: 'SEAP / e-licitatie.ro',
|
|
}],
|
|
};
|
|
}
|
|
|
|
function cpvToCategory(cpv: string | null): string | undefined {
|
|
if (!cpv || cpv.length < 2) return undefined;
|
|
const div = parseInt(cpv.slice(0, 2));
|
|
if (isNaN(div)) return undefined;
|
|
if (div >= 45 && div <= 45) return 'works';
|
|
if (div >= 14 || (div >= 18 && div <= 44)) return 'goods';
|
|
return 'services';
|
|
}
|
|
|
|
export function buildReleasePackage(rows: AnnouncementRow[], requestUrl: string): any {
|
|
return {
|
|
uri: requestUrl,
|
|
version: '1.1',
|
|
extensions: [],
|
|
publishedDate: new Date().toISOString(),
|
|
publisher: PUBLISHER,
|
|
license: LICENSE,
|
|
publicationPolicy: PUBLICATION_POLICY,
|
|
releases: rows.map(announcementToRelease),
|
|
};
|
|
}
|
|
|
|
export function buildSingleReleasePackage(row: AnnouncementRow, requestUrl: string): any {
|
|
return buildReleasePackage([row], requestUrl);
|
|
}
|