/** * 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 = { '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); }