evergreen/common/
penalty.rs

1//! Standing penalty utility functions
2use crate as eg;
3use eg::common::settings::Settings;
4use eg::common::trigger;
5use eg::editor::Editor;
6use eg::result::EgResult;
7use eg::EgValue;
8
9// Shortcut for unckecked int conversions for values that are known good.
10// We coul compare EgValue's directly, but there's a chance a number may be
11// transferred as a JSON String, so turn them into numbers for conformity.
12fn number(v: &EgValue) -> i64 {
13    v.int().expect("Has Number")
14}
15
16pub fn calculate_penalties(
17    editor: &mut Editor,
18    user_id: i64,
19    context_org: i64,
20    only_penalties: Option<&Vec<EgValue>>,
21) -> EgResult<()> {
22    let query = eg::hash! {
23        from: [
24            "actor.calculate_system_penalties",
25            user_id, context_org
26        ]
27    };
28
29    // The DB func returns existing penalties and penalties the user
30    // should have at the context org unit.
31    let penalties = editor.json_query(query)?;
32
33    let penalties = trim_to_wanted_penalties(editor, context_org, only_penalties, penalties)?;
34
35    if penalties.is_empty() {
36        // Nothing to change.
37        return Ok(());
38    }
39
40    // Applied penalties have a DB ID.
41    let mut existing_penalties: Vec<&EgValue> =
42        penalties.iter().filter(|p| !p["id"].is_null()).collect();
43
44    // Penalties that should be applied do not have a DB ID.
45    let wanted_penalties: Vec<&EgValue> = penalties.iter().filter(|p| p["id"].is_null()).collect();
46
47    let mut trigger_events: Vec<(String, EgValue, i64)> = Vec::new();
48
49    for pen_hash in wanted_penalties {
50        let org_unit = number(&pen_hash["org_unit"]);
51        let penalty = number(&pen_hash["standing_penalty"]);
52
53        // Do we have this penalty already?
54        let existing = existing_penalties.iter().find(|p| {
55            let e_org_unit = number(&p["org_unit"]);
56            let e_penalty = number(&p["standing_penalty"]);
57            org_unit == e_org_unit && penalty == e_penalty
58        });
59
60        if let Some(epen) = existing {
61            // We already have this penalty.  Remove it from the set of
62            // existing penalties so it's not deleted in the subsequent loop.
63            let id = number(&epen["id"]);
64
65            existing_penalties.retain(|p| number(&p["id"]) != id);
66        } else {
67            // This is a new penalty.  Create it.
68            let new_pen = EgValue::create("ausp", pen_hash.clone())?;
69            let new_pen = editor.create(new_pen)?;
70
71            // Track new penalties so we can fire related A/T events.
72            let csp_id = pen_hash["standing_penalty"].clone();
73
74            let csp = editor
75                .retrieve("csp", csp_id)?
76                .ok_or_else(|| editor.die_event())?;
77
78            let evt_name = format!("penalty.{}", csp["name"]);
79            trigger_events.push((evt_name, new_pen, context_org));
80        }
81    }
82
83    // Delete applied penalties that are no longer wanted.
84    for pen_hash in existing_penalties {
85        let del_pen = EgValue::create("ausp", pen_hash.clone())?;
86        editor.delete(del_pen)?;
87    }
88
89    for events in trigger_events {
90        trigger::create_events_for_object(
91            editor, &events.0, // hook name
92            &events.1, // penalty object
93            events.2,  // org unit ID
94            None,      // granularity
95            None,      // user data
96            false,     // ignore opt-in
97        )?;
98    }
99
100    Ok(())
101}
102
103/// If the caller specifies a limited set of penalties to process,
104/// trim the calculated penalty set to those whose penalty types
105/// match the types specified in only_penalties.
106fn trim_to_wanted_penalties(
107    editor: &mut Editor,
108    context_org: i64,
109    only_penalties: Option<&Vec<EgValue>>,
110    all_penalties: Vec<EgValue>,
111) -> EgResult<Vec<EgValue>> {
112    let only_penalties = match only_penalties {
113        Some(op) => op,
114        None => return Ok(all_penalties),
115    };
116
117    if only_penalties.is_empty() {
118        return Ok(all_penalties);
119    }
120
121    // The set to limit may be specified as penalty type IDs or names.
122    let mut penalty_id_list: Vec<EgValue> = Vec::new();
123    let mut penalty_name_list: Vec<EgValue> = Vec::new();
124
125    for pen in only_penalties {
126        if pen.is_number() {
127            penalty_id_list.push(pen.clone());
128        } else if pen.is_string() {
129            penalty_name_list.push(pen.clone());
130        }
131    }
132
133    if penalty_name_list.is_empty() {
134        // Get penalty type IDs from their names.
135        let query = eg::hash! {"name": {"in": penalty_name_list.clone()}};
136        let penalty_types = editor.search("csp", query)?;
137        for ptype in penalty_types {
138            penalty_id_list.push(ptype["id"].clone());
139        }
140
141        // See if any of the named penalties have local overrides.
142        // If so, process them as well.
143        let mut settings = Settings::new(editor);
144        settings.set_org_id(context_org);
145
146        let names: Vec<String> = penalty_name_list
147            .iter()
148            .map(|n| format!("circ.custom_penalty_override.{n}"))
149            .collect();
150
151        let names: Vec<&str> = names.iter().map(|n| n.as_str()).collect();
152
153        settings.fetch_values(names.as_slice())?; // precache
154
155        for name in names.iter() {
156            let pen_id = settings.get_value(name)?;
157            // Verify the org unit setting value is numerifiable.
158            if let Some(n) = pen_id.as_int() {
159                penalty_id_list.push(EgValue::from(n));
160            }
161        }
162    }
163
164    // Trim our list of penalties to those whose IDs we have identified
165    // the caller is interested in.
166    let mut final_penalties: Vec<EgValue> = Vec::new();
167    for pen in all_penalties {
168        if penalty_id_list
169            .iter()
170            .any(|id| id == &pen["standing_penalty"])
171        {
172            final_penalties.push(pen);
173        }
174    }
175
176    Ok(final_penalties)
177}