diff --git a/docs/cli/indicators/R/055.md b/docs/cli/indicators/R/055.md new file mode 100644 index 0000000..33f822d --- /dev/null +++ b/docs/cli/indicators/R/055.md @@ -0,0 +1,60 @@ +# TODO The title of the indicator (R055) + +TODO A one-sentence description of the indicator. + +## Methodology + +TODO + +:::{admonition} Example +:class: seealso + +TODO +::: + +:::{admonition} Why is this a red flag? +:class: hint + +TODO +::: + +Based on "TODO" in [*TODO*](TODO). + +## Output + +The indicator's value is TODO. + +## Configuration + +All configuration is optional. To override the default TODO: + +```ini +[R055] +TODO +``` + +## Exclusions + +A contracting process is excluded if: + +- TODO + +## Assumptions + +TODO + +## Demonstration + +*Input* + +:::{literalinclude} ../../../examples/R/055.jsonl +:language: json +::: + +*Output* + +```console +$ ocdscardinal indicators --settings docs/examples/settings.ini --no-meta docs/examples/R/055.jsonl +{} + +``` diff --git a/docs/cli/init.md b/docs/cli/init.md index 1156fd2..c2a2c3f 100644 --- a/docs/cli/init.md +++ b/docs/cli/init.md @@ -118,6 +118,11 @@ $ ocdscardinal init - ; threshold = 10 ; minimum_contracting_processes = 20 +[R055] +; threshold = 66593 +; start_date = 2022-01-01 +; end_date = 2022-12-31 + [R058] ; threshold = 0.5 diff --git a/docs/examples/R/055.jsonl b/docs/examples/R/055.jsonl new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/docs/examples/R/055.jsonl @@ -0,0 +1 @@ +{} diff --git a/docs/examples/settings.ini b/docs/examples/settings.ini index fb1867d..517cfb6 100644 --- a/docs/examples/settings.ini +++ b/docs/examples/settings.ini @@ -8,4 +8,5 @@ [R036] [R038] [R048] +[R055] [R058] diff --git a/src/indicators/mod.rs b/src/indicators/mod.rs index 3a7836e..95cd3c1 100644 --- a/src/indicators/mod.rs +++ b/src/indicators/mod.rs @@ -8,15 +8,17 @@ pub mod r035; pub mod r036; pub mod r038; pub mod r048; +pub mod r055; pub mod r058; pub mod util; use std::collections::{HashMap, HashSet}; use std::ops::AddAssign; +use chrono::NaiveDate; use indexmap::IndexMap; use serde::ser::SerializeMap; -use serde::{Deserialize, Serialize, Serializer}; +use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; use serde_json::{Map, Value}; // Settings. @@ -119,6 +121,32 @@ pub struct R048 { pub minimum_contracting_processes: Option, } +#[derive(Clone, Debug, Default, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct R055 { + pub threshold: Option, + #[serde(deserialize_with = "naive_date_from_str")] + pub start_date: Option, + #[serde(deserialize_with = "naive_date_from_str")] + pub end_date: Option, +} + +// https://serde.rs/field-attrs.html#deserialize_with +fn naive_date_from_str<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + let s: Option = Option::deserialize(deserializer)?; + s.map_or_else( + || Ok(None), + |s| { + NaiveDate::parse_from_str(&s, "%Y-%m-%d") + .map(Some) + .map_err(de::Error::custom) + }, + ) +} + #[derive(Clone, Debug, Default, Deserialize)] #[serde(deny_unknown_fields)] #[allow(non_snake_case)] @@ -144,6 +172,7 @@ pub struct Settings { pub R036: Option, pub R038: Option, pub R048: Option, + pub R055: Option, pub R058: Option, // ratio } @@ -169,6 +198,7 @@ pub enum Indicator { R036, R038, R048, + R055, R058, } @@ -216,6 +246,12 @@ pub struct Indicators { pub r038_tenderer: HashMap, /// The item classifications for each `bids/details/tenderers/id`. pub r048_classifications: HashMap)>, + /// The `tender/value/amount` for each `ocid` when `tender/procurementMethod` is 'open'. + pub r055_open_tender_amount: HashMap, + /// The total awarded amount for each `buyer/id` and `awards/suppliers/id` when `tender/procurementMethod` is 'direct'. + pub r055_direct_awarded_amount_supplier_buyer: HashMap<(String, String), f64>, + /// The total awarded amount for each `tender/procuringEntity/id` and `awards/suppliers/id` when `tender/procurementMethod` is 'direct'. + pub r055_direct_awarded_amount_supplier_procuring_entity: HashMap<(String, String), f64>, /// Whether to map contracting processes to organizations. pub map: bool, } diff --git a/src/indicators/r055.rs b/src/indicators/r055.rs new file mode 100644 index 0000000..980917b --- /dev/null +++ b/src/indicators/r055.rs @@ -0,0 +1,133 @@ +use chrono::{Duration, NaiveDate, Utc}; +use log::warn; +use serde_json::{Map, Value}; + +use crate::indicators::{set_meta, set_result, sum, Calculate, Indicators, Settings}; + +#[derive(Default)] +pub struct R055 { + threshold: Option, // resolved in finalize() + start_date: NaiveDate, + end_date: NaiveDate, + currency: Option, +} + +impl Calculate for R055 { + fn new(settings: &mut Settings) -> Self { + let setting = std::mem::take(&mut settings.R055).unwrap_or_default(); + let today = Utc::now().date_naive(); + + Self { + threshold: setting.threshold, + start_date: setting.start_date.unwrap_or(today - Duration::days(365)), + end_date: setting.end_date.unwrap_or(today), + currency: settings.currency.clone(), + } + } + + fn fold(&self, item: &mut Indicators, release: &Map, ocid: &str) { + if let Some(Value::Object(tender)) = release.get("tender") + && let Some(Value::Object(tender_period)) = tender.get("tenderPeriod") + && let Some(Value::String(date)) = tender_period.get("startDate") + && let Some(Value::String(method)) = tender.get("procurementMethod") + && method == "open" + && let Ok(date) = NaiveDate::parse_from_str(date, "%Y-%m-%dT%H:%M:%S%z") + && date >= self.start_date + && date <= self.end_date + && let Some(Value::Object(value)) = tender.get("value") + && let Some(Value::Number(amount)) = value.get("amount") + && let Some(Value::String(currency)) = value.get("currency") + && let Some(amount) = amount.as_f64() + { + if currency + == item + .currency + .get_or_insert_with(|| self.currency.as_ref().map_or_else(|| currency.clone(), Clone::clone)) + { + item.r055_open_tender_amount.insert(ocid.to_owned(), amount); + } else { + warn!("{} is not {:?}, skipping.", currency, item.currency); + } + } + + if let Some(Value::Array(awards)) = release.get("awards") { + for award in awards { + if let Some(Value::String(status)) = award.get("status") + && let Some(Value::Array(suppliers)) = award.get("suppliers") + && suppliers.len() == 1 + && let Some(Value::String(supplier_id)) = suppliers[0].get("id") + && status == "active" + && let Some(Value::String(date)) = award.get("date") + && let Ok(date) = NaiveDate::parse_from_str(date, "%Y-%m-%dT%H:%M:%S%z") + && date >= self.start_date + && date <= self.end_date + && let Some(Value::Object(value)) = award.get("value") + && let Some(Value::Number(amount)) = value.get("amount") + && let Some(Value::String(currency)) = value.get("currency") + && let Some(amount) = amount.as_f64() + { + if currency + == item.currency.get_or_insert_with(|| { + self.currency.as_ref().map_or_else(|| currency.clone(), Clone::clone) + }) + { + if let Some(Value::Object(buyer)) = release.get("buyer") + && let Some(Value::String(id)) = buyer.get("id") + { + item.r055_direct_awarded_amount_supplier_buyer + .insert((id.clone(), supplier_id.clone()), amount); + } + if let Some(Value::Object(tender)) = release.get("tender") + && let Some(Value::Object(procuring_entity)) = tender.get("procuringEntity") + && let Some(Value::String(id)) = procuring_entity.get("id") + { + item.r055_direct_awarded_amount_supplier_procuring_entity + .insert((id.clone(), supplier_id.clone()), amount); + } + } else { + warn!("{} is not {:?}, skipping.", currency, item.currency); + } + } + } + } + } + + fn reduce(&self, item: &mut Indicators, other: &mut Indicators) { + let mut min = 0.0; + for (key, value) in std::mem::take(&mut other.r055_open_tender_amount) { + if min <= value { + item.r055_open_tender_amount.insert(key, value); + min = value; + } + } + sum!(item, other, r055_direct_awarded_amount_supplier_buyer); + sum!(item, other, r055_direct_awarded_amount_supplier_procuring_entity); + } + + fn finalize(&self, item: &mut Indicators) { + let min_amount = self.threshold.map_or_else( + || { + std::mem::take(&mut item.r055_open_tender_amount) + .values() + .copied() + .last() + .unwrap_or(0.0) + }, + |v| v, + ); + set_meta!(item, R055, "lower_open_amount", min_amount); + + for (id, amount) in &item.r055_direct_awarded_amount_supplier_buyer { + if *amount >= min_amount { + set_result!(item, Buyer, id.0, R055, *amount); + set_result!(item, Tenderer, id.1, R055, *amount); + } + } + for (id, amount) in &item.r055_direct_awarded_amount_supplier_procuring_entity { + if *amount >= min_amount { + set_result!(item, ProcuringEntity, id.0, R055, *amount); + set_result!(item, Tenderer, id.1, R055, *amount); + } + } + } +} diff --git a/src/lib.rs b/src/lib.rs index a62c08d..727f00a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -27,6 +27,7 @@ use crate::indicators::r035::R035; use crate::indicators::r036::R036; use crate::indicators::r038::R038; use crate::indicators::r048::R048; +use crate::indicators::r055::R055; use crate::indicators::r058::R058; use crate::indicators::util::{SecondLowestBidRatio, Tenderers}; pub use crate::indicators::{Calculate, Codelist, Exclusions, Group, Indicator, Indicators, Modifications, Settings}; @@ -144,6 +145,11 @@ pub fn init(path: &PathBuf, force: &bool) -> std::io::Result { ; threshold = 10 ; minimum_contracting_processes = 20 +[R055] +; threshold = 66593 +; start_date = 2022-01-01 +; end_date = 2022-12-31 + [R058] ; threshold = 0.5 "; @@ -255,6 +261,7 @@ impl Indicators { R036, R038, R048, + R055, R058, ); diff --git a/tests/fixtures/indicators/R055.expected b/tests/fixtures/indicators/R055.expected new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/tests/fixtures/indicators/R055.expected @@ -0,0 +1 @@ +{} diff --git a/tests/fixtures/indicators/R055.jsonl b/tests/fixtures/indicators/R055.jsonl new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/tests/fixtures/indicators/R055.jsonl @@ -0,0 +1 @@ +{}