evergreen/common/trigger/validator/
mod.rs

1//! Base module for A/T Validators
2use crate as eg;
3use eg::common::holdings;
4use eg::common::trigger::{Event, EventState, Processor};
5use eg::constants as C;
6use eg::date;
7use eg::EgResult;
8
9/// Add validation routines to the Processor.
10impl Processor<'_> {
11    /// Validate an event.
12    ///
13    /// TODO stacked validators.
14    ///
15    /// Loading modules dynamically is not as simple in Rust as in Perl.
16    /// Hard-code a module-mapping instead. (*shrug* They all require
17    /// code changes).
18    pub fn validate(&mut self, event: &mut Event) -> EgResult<bool> {
19        log::info!("{self} validating {event}");
20
21        self.set_event_state(event, EventState::Validating)?;
22
23        let validator = self.validator();
24
25        let validate_result = match validator {
26            "NOOP_True" => Ok(true),
27            "NOOP_False" => Ok(false),
28            "CircIsOpen" => self.circ_is_open(event),
29            "CircIsOverdue" => self.circ_is_overdue(event),
30            "HoldIsAvailable" => self.hold_is_available(event),
31            "HoldIsCancelled" => self.hold_is_canceled(event),
32            "HoldNotifyCheck" => self.hold_notify_check(event),
33            "MinPassiveTargetAge" => self.min_passive_target_age(event),
34            "PatronBarred" => self.patron_is_barred(event),
35            "PatronNotBarred" => self.patron_is_barred(event).map(|val| !val),
36            "ReservationIsAvailable" => self.reservation_is_available(event),
37            _ => Err(format!("No such validator: {validator}").into()),
38        };
39
40        if let Ok(valid) = validate_result {
41            if valid {
42                self.set_event_state(event, EventState::Validating)?;
43            } else {
44                self.set_event_state(event, EventState::Invalid)?;
45            }
46        }
47
48        validate_result
49    }
50
51    /// True if the target circulation is still open.
52    fn circ_is_open(&mut self, event: &Event) -> EgResult<bool> {
53        if event.target()["checkin_time"].is_string() {
54            return Ok(false);
55        }
56
57        if event.target()["xact_finish"].is_string() {
58            return Ok(false);
59        }
60
61        if self.param_value("min_target_age").is_some() {
62            if let Some(fname) = self.param_value_as_str("target_age_field") {
63                if fname == "xact_start" {
64                    return self.min_passive_target_age(event);
65                }
66            }
67        }
68
69        Ok(true)
70    }
71
72    fn min_passive_target_age(&mut self, event: &Event) -> EgResult<bool> {
73        let min_target_age = self
74            .param_value_as_str("min_target_age")
75            .ok_or_else(|| {
76                "'min_target_age' parameter required for MinPassiveTargetAge".to_string()
77            })?
78            .to_string();
79
80        let age_field = self.param_value_as_str("target_age_field").ok_or_else(|| {
81            "'target_age_field' parameter or delay_field required for MinPassiveTargetAge"
82                .to_string()
83        })?;
84
85        let age_field_val = &event.target()[age_field];
86        let age_date_str = age_field_val.as_str().ok_or_else(|| {
87            format!(
88                "MinPassiveTargetAge age field {age_field} has unexpected value: {}",
89                age_field_val.dump()
90            )
91        })?;
92
93        let age_field_ts =
94            date::add_interval(date::parse_datetime(age_date_str)?, &min_target_age)?;
95
96        Ok(age_field_ts <= date::now())
97    }
98
99    fn circ_is_overdue(&mut self, event: &Event) -> EgResult<bool> {
100        if event.target()["checkin_time"].is_string() {
101            return Ok(false);
102        }
103
104        if let Some(stop_fines) = event.target()["stop_fines"].as_str() {
105            if stop_fines == "MAXFINES" || stop_fines == "LONGOVERDUE" {
106                return Ok(false);
107            }
108        }
109
110        if self.param_value("min_target_age").is_some() {
111            if let Some(fname) = self.param_value_as_str("target_age_field") {
112                if fname == "xact_start" {
113                    return self.min_passive_target_age(event);
114                }
115            }
116        }
117
118        // due_date is a required string field.
119        let due_date = event.target()["due_date"].as_str().unwrap();
120        let due_date_ts = date::parse_datetime(due_date)?;
121
122        Ok(due_date_ts < date::now())
123    }
124
125    /// True if the hold is ready for pickup.
126    fn hold_is_available(&mut self, event: &Event) -> EgResult<bool> {
127        if !self.hold_notify_check(event)? {
128            return Ok(false);
129        }
130
131        let hold = event.target();
132
133        // Start with some simple tests.
134        let canceled = hold["cancel_time"].is_string();
135        let fulfilled = hold["fulfillment_time"].is_string();
136        let captured = hold["capture_time"].is_string();
137        let shelved = hold["shelf_time"].is_string();
138
139        if canceled || fulfilled || !captured || !shelved {
140            return Ok(false);
141        }
142
143        // Verify shelf lib matches pickup lib -- it's not sitting on
144        // the wrong shelf somewhere.
145        //
146        // Accommodate fleshing
147        let shelf_lib = match hold["current_shelf_lib"].as_i64() {
148            Some(id) => id,
149            None => match hold["current_shelf_lib"]["id"].as_i64() {
150                Some(id) => id,
151                None => return Ok(false),
152            },
153        };
154
155        let pickup_lib = hold["pickup_lib"]
156            .as_int()
157            .unwrap_or(hold["pickup_lib"].id()?);
158
159        if shelf_lib != pickup_lib {
160            return Ok(false);
161        }
162
163        // Verify we have a targted copy and it has the expected status.
164        let copy_status = if let Some(copy_id) = hold["current_copy"].as_i64() {
165            holdings::copy_status(self.editor, Some(copy_id), None)?
166        } else if hold["current_copy"].is_object() {
167            holdings::copy_status(self.editor, None, Some(&hold["current_copy"]))?
168        } else {
169            -1
170        };
171
172        Ok(copy_status == C::COPY_STATUS_ON_HOLDS_SHELF)
173    }
174
175    fn hold_is_canceled(&mut self, event: &Event) -> EgResult<bool> {
176        if self.hold_notify_check(event)? {
177            Ok(event.target()["cancel_time"].is_string())
178        } else {
179            Ok(false)
180        }
181    }
182
183    /// Returns false if a notification parameter is present and the
184    /// hold in question is inconsistent with the parameter.
185    ///
186    /// In general, if this test fails, the event should not proceed
187    /// to reacting.
188    ///
189    /// Assumes the hold in question == the event.target().
190    fn hold_notify_check(&mut self, event: &Event) -> EgResult<bool> {
191        let hold = event.target();
192
193        if self.param_value_as_bool("check_email_notify") && !hold["email_notify"].boolish() {
194            return Ok(false);
195        }
196
197        if self.param_value_as_bool("check_sms_notify") && !hold["sms_notify"].boolish() {
198            return Ok(false);
199        }
200
201        if self.param_value_as_bool("check_phone_notify") && !hold["phone_notify"].boolish() {
202            return Ok(false);
203        }
204
205        Ok(true)
206    }
207
208    fn reservation_is_available(&mut self, event: &Event) -> EgResult<bool> {
209        let res = event.target();
210        Ok(res["cancel_time"].is_null()
211            && !res["capture_time"].is_null()
212            && !res["current_resource"].is_null())
213    }
214
215    fn patron_is_barred(&mut self, event: &Event) -> EgResult<bool> {
216        Ok(event.target()["barred"].boolish())
217    }
218
219    // Perl has CircIsAutoRenewable but it oddly creates the same
220    // events (hook 'autorenewal') that the autorenewal reactor creates,
221    // and it's not used in the default A/T definitions.  Guessing that
222    // validator should be removed from the Perl.
223
224    // TODO PatronNotInCollections
225}