SPB Git

Repository Public

seao.ts 5.3 KB 78 lines typescript

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
Files main
Menu

Theme