Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

R044 #100

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open

R044 #100

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
- [R018](cli/indicators/R/018): (*Single bid received*).
- [R028](cli/indicators/R/028): (*Identical bid prices*).
- [R030](cli/indicators/R/030): (*Late bid won*).
- [R044](cli/indicators/R/048): (*Business similarities between suppliers*).
- [R048](cli/indicators/R/048): (*Heterogeneous supplier*).
- [R058](cli/indicators/R/058): (*Heavily discounted bid*).
- Add `no_price_comparison_procurement_methods` configuration.
Expand Down
46 changes: 46 additions & 0 deletions docs/cli/indicators/R/044.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Business similarities between suppliers (R044)

Different tenderers bidding for the same contracting process have similar information.

## Methodology

A contracting process is flagged if different tenderers have either the exact same address or any contact point fields.
These tenderers are also flagged.

:::{admonition} Example
:class: seealso

BribeCorp and AnotherBribeCorp submit bids to the same procurement process. Both companies use "Collusion Street 1" as their street address in the Atlantis country.
:::

:::{admonition} Why is this a red flag?
:class: hint

Similarities between suppliers may indicate that the companies are connected.
:::

<small>Based on "Connections between bidders undermines competition" in [*Corruption in Public Procurement: Finding the Right Indicators*](https://www.researchgate.net/publication/303359108_Corruption_in_Public_Procurement_Finding_the_Right_Indicators), and "Similarity in Bids; Apparent Connections Between Bidders" in [*Guide to Combating Corruption & Fraud in Infrastructure Development Projects*](https://guide.iacrc.org/red-flag-similar-bids-apparent-connections-between-bidders/).</small>

## Output

The indicator’s value is always 1.0.

## Configuration

The indicator is not configurable.

## Demonstration

*Input*

:::{literalinclude} ../../../examples/R/044.jsonl
:language: json
:::

*Output*

```console
$ ocdscardinal indicators --settings docs/examples/settings.ini --no-meta docs/examples/R/044.jsonl
{"OCID":{"F1":{"R044":1.0},"F2":{"R044":1.0},"F3":{"R044":1.0},"F4":{"R044":1.0}},"Tenderer":{"GB-COH-09506232":{"R044":1.0},"GB-COH-09506234":{"R044":1.0}}}

```
3 changes: 3 additions & 0 deletions docs/cli/indicators/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,9 @@ R/index
* - [R038](R/038)
- [Excessive disqualified bids](R/038)
- The ratio of disqualified bids to submitted bids is a high outlier per buyer, procuring entity or tenderer.
* - [R044](R/044)
- [Business similarities between suppliers](R/044)
- Different tenderers bidding for the same contracting process have similar information.
* - [R048](R/048)
- [Heterogeneous supplier](R/048)
- The variety of items supplied by a tenderer is a high outlier.
Expand Down
2 changes: 2 additions & 0 deletions docs/cli/init.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,8 @@ $ ocdscardinal init -
; minimum_submitted_bids = 2
; minimum_contracting_processes = 2

[R044]

[R048]
; digits = 2
; threshold = 10
Expand Down
4 changes: 4 additions & 0 deletions docs/examples/R/044.jsonl
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{"ocid":"F1","parties":[{"address":{"streetAddress":"Street ABC","locality":"Atlantis City","region":"Moorland","postalCode":"1234","countryName":"Atlantis"},"roles":["tenderer"],"id":"GB-COH-09506232"},{"address":{"streetAddress":"Street ABC","locality":"Atlantis City","region":"Moorland","postalCode":"1234","countryName":"Atlantis"},"roles":["tenderer"],"id":"GB-COH-09506234"}]}
{"ocid":"F2","parties":[{"address":{"streetAddress":"Street ABC"},"roles":["tenderer"],"id":"GB-COH-09506232"},{"address":{"streetAddress":"Street ABC"},"roles":["tenderer"],"id":"GB-COH-09506234"}]}
{"ocid":"F3","parties":[{"contactPoint":{"name":"Person","email":"[email protected]","telephone":"+123456","faxNumber":"+123456","url":"http://example.com"},"roles":["tenderer"],"id":"GB-COH-09506232"},{"contactPoint":{"name":"Person","email":"[email protected]","telephone":"+123456","faxNumber":"+123456","url":"http://example.com"},"roles":["tenderer"],"id":"GB-COH-09506234"}]}
{"ocid":"F4","parties":[{"contactPoint":{"name":"Person"},"roles":["tenderer"],"id":"GB-COH-09506232"},{"contactPoint":{"name":"Person"},"roles":["tenderer"],"id":"GB-COH-09506234"}]}
1 change: 1 addition & 0 deletions docs/examples/settings.ini
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@
[R035]
[R036]
[R038]
[R044]
[R048]
[R058]
3 changes: 3 additions & 0 deletions src/indicators/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ pub mod r030;
pub mod r035;
pub mod r036;
pub mod r038;
pub mod r044;
pub mod r048;
pub mod r058;
pub mod util;
Expand Down Expand Up @@ -143,6 +144,7 @@ pub struct Settings {
pub R035: Option<IntegerThreshold>, // count
pub R036: Option<Empty>,
pub R038: Option<R038>,
pub R044: Option<Empty>,
pub R048: Option<R048>,
pub R058: Option<FloatThreshold>, // ratio
}
Expand All @@ -168,6 +170,7 @@ pub enum Indicator {
R035,
R036,
R038,
R044,
R048,
R058,
}
Expand Down
85 changes: 85 additions & 0 deletions src/indicators/r044.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
use std::collections::HashMap;

use serde_json::{Map, Value};

use crate::indicators::{set_result, Calculate, Indicators, Settings};

#[derive(Default)]
pub struct R044 {}

impl R044 {
fn get_address(party: &Value) -> Option<String> {
let mut full_address: String = String::new();
if let Some(Value::Object(address)) = party.get("address")
&& address.get("streetAddress").is_some()
{
for field in ["streetAddress", "locality", "region", "postalCode", "countryName"] {
if let Some(Value::String(value)) = address.get(field) {
full_address.push_str(value.trim().to_lowercase().as_str());
full_address.push(' ');
}
}
Some(full_address)
} else {
None
}
}
fn compare_contact_point(party1: Option<Value>, party2: Option<Value>) -> bool {
if let Some(Value::Object(contact_point_1)) = party1
&& let Some(Value::Object(contact_point_2)) = party2
{
for field in ["name", "email", "telephone", "faxNumber", "url"] {
if let Some(Value::String(field1)) = contact_point_1.get(field)
&& let Some(Value::String(field2)) = contact_point_2.get(field)
{
return field1 == field2;
}
}
}
false
}
}

impl Calculate for R044 {
fn new(_settings: &mut Settings) -> Self {
Self::default()
}

fn fold(&self, item: &mut Indicators, release: &Map<String, Value>, ocid: &str) {
if let Some(Value::Array(parties)) = release.get("parties") {
let tenderers = parties
.iter()
.filter_map(|tenderer| {
if let Some(Value::Array(roles)) = tenderer.get("roles")
&& roles.iter().any(|s| s == "tenderer")
&& let Some(Value::String(id)) = tenderer.get("id")
{
Some((id.clone(), (Self::get_address(tenderer), tenderer.get("contactPoint"))))
} else {
None
}
})
.collect::<HashMap<_, _>>();
for (id_to_compare, details_to_compare) in &tenderers {
for (id, details) in &tenderers {
if id_to_compare != id {
let address_match = if let Some(address_to_compare) = details_to_compare.0.as_deref()
&& let Some(address) = details.0.as_deref()
&& address_to_compare == address
{
true
} else {
false
};
let contact_point_match =
Self::compare_contact_point(details.1.cloned(), details_to_compare.1.cloned());
if address_match || contact_point_match {
set_result!(item, OCID, ocid, R044, 1.0);
set_result!(item, Tenderer, id_to_compare, R044, 1.0);
}
}
}
}
}
}
}
4 changes: 4 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ use crate::indicators::r030::R030;
use crate::indicators::r035::R035;
use crate::indicators::r036::R036;
use crate::indicators::r038::R038;
use crate::indicators::r044::R044;
use crate::indicators::r048::R048;
use crate::indicators::r058::R058;
use crate::indicators::util::{SecondLowestBidRatio, Tenderers};
Expand Down Expand Up @@ -139,6 +140,8 @@ pub fn init(path: &PathBuf, force: &bool) -> std::io::Result<bool> {
; minimum_submitted_bids = 2
; minimum_contracting_processes = 2

[R044]

[R048]
; digits = 2
; threshold = 10
Expand Down Expand Up @@ -254,6 +257,7 @@ impl Indicators {
R035,
R036,
R038,
R044,
R048,
R058,
);
Expand Down
1 change: 1 addition & 0 deletions tests/fixtures/indicators/R044.expected
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"OCID":{"F1":{"R044":1.0},"F2":{"R044":1.0},"F3":{"R044":1.0},"F4":{"R044":1.0}},"Tenderer":{"GB-COH-09506234":{"R044":1.0},"GB-COH-09506232":{"R044":1.0}}}
6 changes: 6 additions & 0 deletions tests/fixtures/indicators/R044.jsonl
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{"ocid":"F1","parties":[{"address":{"streetAddress":"Street ABC","locality":"Atlantis City","region":"Moorland","postalCode":"1234","countryName":"Atlantis"},"roles":["tenderer"],"id":"GB-COH-09506232"},{"address":{"streetAddress":"Street ABC","locality":"Atlantis City","region":"Moorland","postalCode":"1234","countryName":"Atlantis"},"roles":["tenderer"],"id":"GB-COH-09506234"}]}
{"ocid":"F2","parties":[{"address":{"streetAddress":"Street ABC"},"roles":["tenderer"],"id":"GB-COH-09506232"},{"address":{"streetAddress":"Street ABC"},"roles":["tenderer"],"id":"GB-COH-09506234"}]}
{"ocid":"F3","parties":[{"contactPoint":{"name":"Person","email":"[email protected]","telephone":"+123456","faxNumber":"+123456","url":"http://example.com"},"roles":["tenderer"],"id":"GB-COH-09506232"},{"contactPoint":{"name":"Person","email":"[email protected]","telephone":"+123456","faxNumber":"+123456","url":"http://example.com"},"roles":["tenderer"],"id":"GB-COH-09506234"}]}
{"ocid":"F4","parties":[{"contactPoint":{"name":"Person"},"roles":["tenderer"],"id":"GB-COH-09506232"},{"contactPoint":{"name":"Person"},"roles":["tenderer"],"id":"GB-COH-09506234"}]}
{"ocid":"Same country only","parties":[{"address":{"countryName":"Atlantis"},"roles":["tenderer"],"id":"GB-COH-09506232"},{"address":{"countryName":"Atlantis"},"roles":["tenderer"],"id":"GB-COH-09506234"}]}
{"ocid":"No address or contact point","parties":[{"roles":["tenderer"],"id":"GB-COH-09506232"},{"roles":["tenderer"],"id":"GB-COH-09506234"}]}
Loading