Repository Public
1// UQO Éval — Métré : adaptateur SEAO (Données Québec, OCDS 1.1, CC-BY 4.0). Module PUR (parse) + liste des fichiers.2/**3 * Entrée : un fichier JSON hebdomadaire `hebdo_AAAAMMJJ_AAAAMMJJ.json` (release package OCDS).4 * Sortie : observations de classe « awarded_contract_total » pour les releases de5 * catégorie « works » (travaux) ayant un octroi/contrat — JAMAIS converties en prix6 * unitaires. L'unité d'origine est « contrat » ; `normalized_amount` reste le total.7 */8export const SEAO_PARSER_VERSION = "seao-ocds-1.0";9export const SEAO_DATASET_URL = "https://www.donneesquebec.ca/recherche/api/3/action/package_show?id=systeme-electronique-dappel-doffres-seao";1011export interface SeaoObservation {12 externalId: string; // ocid + award id13 ocid: string;14 title: string;15 buyer: string | null;16 buyerMunicipal: boolean;17 procuringEntity: string | null;18 procurementMethod: string | null;19 category: string | null;20 unspscCode: string | null;21 unspscLabel: string | null;22 itemDescription: string | null;23 awardDate: string | null;24 amount: number;25 currency: string;26 supplier: string | null;27 supplierRegion: string | null;28 supplierLocality: string | null;29 tenderers: number | null;30 noticeUrl: string | null;31}3233interface Release { ocid?: string; tag?: string[]; parties?: { name?: string; roles?: string[]; details?: Record<string, unknown>; address?: { locality?: string; region?: string } }[]; buyer?: { name?: string }; tender?: { title?: string; procuringEntity?: { name?: string }; items?: { description?: string; classification?: { scheme?: string; id?: string; description?: string } }[]; procurementMethod?: string; procurementMethodDetails?: string; mainProcurementCategory?: string; numberOfTenderers?: number | string; documents?: { url?: string }[] }; awards?: { id?: string; status?: string; date?: string; value?: { amount?: number | string; currency?: string }; suppliers?: { name?: string }[] }[]; contracts?: { awardID?: string; value?: { amount?: number | string; currency?: string } }[] }3435export function parseSeaoPackage(json: unknown): { publishedDate: string | null; license: string | null; releases: number; observations: SeaoObservation[]; skipped: { reason: string; n: number }[] } {36 const pkg = json as { publishedDate?: string; license?: string; releases?: Release[] };37 const rel = Array.isArray(pkg.releases) ? pkg.releases : [];38 const out: SeaoObservation[] = [];39 const skip = new Map<string, number>();40 const bump = (k: string) => skip.set(k, (skip.get(k) ?? 0) + 1);41 for (const r of rel) {42 const t = r.tender ?? {};43 if (t.mainProcurementCategory !== "works") { bump("non-travaux"); continue; }44 const awards = (r.awards ?? []).filter((a) => a.value?.amount != null && (a.status == null || a.status === "active"));45 if (!awards.length) { bump("sans octroi"); continue; }46 const buyerParty = (r.parties ?? []).find((p) => p.roles?.includes("buyer"));47 const item = t.items?.[0];48 for (const a of awards) {49 const amount = Number(a.value?.amount);50 if (!Number.isFinite(amount) || amount <= 0) { bump("montant invalide"); continue; }51 const supplierName = a.suppliers?.[0]?.name ?? null;52 const supParty = (r.parties ?? []).find((p) => p.roles?.includes("supplier") && p.name === supplierName);53 const contract = (r.contracts ?? []).find((c) => c.awardID === a.id);54 out.push({55 externalId: `${r.ocid ?? "?"}#${a.id ?? "?"}`, ocid: r.ocid ?? "", title: (t.title ?? "").trim().slice(0, 300), buyer: r.buyer?.name ?? buyerParty?.name ?? null, buyerMunicipal: String(buyerParty?.details?.municipal ?? "") === "1",56 procuringEntity: t.procuringEntity?.name ?? null, procurementMethod: t.procurementMethodDetails ?? t.procurementMethod ?? null, category: t.mainProcurementCategory ?? null,57 unspscCode: item?.classification?.scheme === "UNSPSC" ? item.classification.id ?? null : null, unspscLabel: item?.classification?.description ?? null, itemDescription: item?.description ?? null,58 awardDate: a.date ? a.date.slice(0, 10) : null, amount: Number(contract?.value?.amount ?? amount), currency: a.value?.currency ?? "CAD", supplier: supplierName, supplierRegion: supParty?.address?.region ?? null, supplierLocality: supParty?.address?.locality ?? null,59 tenderers: t.numberOfTenderers != null ? Number(t.numberOfTenderers) : null, noticeUrl: t.documents?.[0]?.url ?? null,60 });61 }62 }63 return { publishedDate: pkg.publishedDate ?? null, license: pkg.license ?? null, releases: rel.length, observations: out, skipped: [...skip.entries()].map(([reason, n]) => ({ reason, n })) };64}6566/** Liste les ressources hebdomadaires du jeu de données (CKAN package_show), les plus récentes d'abord. */67export function listWeeklyResources(pkg: unknown): { url: string; name: string; from: string; to: string }[] {68 const res = ((pkg as { result?: { resources?: { url?: string; name?: string }[] } }).result?.resources ?? []);69 const out: { url: string; name: string; from: string; to: string }[] = [];70 for (const r of res) {71 const m = (r.url ?? "").match(/hebdo_(\d{8})_(\d{8})\.json$/);72 if (!m || !r.url) continue;73 const f = (s: string) => `${s.slice(0, 4)}-${s.slice(4, 6)}-${s.slice(6, 8)}`;74 out.push({ url: r.url, name: r.name ?? m[0], from: f(m[1]), to: f(m[2]) });75 }76 return out.sort((a, b) => (a.to < b.to ? 1 : -1));77}78