evergreen/common/
circulator.rs

1use crate as eg;
2use eg::common::holds;
3use eg::common::org;
4use eg::common::settings::Settings;
5use eg::common::trigger;
6use eg::constants as C;
7use eg::editor::Editor;
8use eg::event::{EgEvent, Overrides};
9use eg::util;
10use eg::{EgError, EgResult, EgValue};
11use std::collections::{HashMap, HashSet};
12use std::fmt;
13
14/// Our copy is assumed to be fleshed just-so throughout.
15/// Changing these values can impact assumptions in the code.
16const COPY_FLESH: &[&str] = &["status", "call_number", "parts", "floating", "location"];
17
18/// Map of some newer override event types to simplified legacy override codes .
19/// First entry in each sub-array is the newer event, followed by one or more
20/// legacy event types.
21pub const COPY_ALERT_OVERRIDES: [&[&str]; 7] = [
22    &["CLAIMSRETURNED\tCHECKOUT", "CIRC_CLAIMS_RETURNED"],
23    &["CLAIMSRETURNED\tCHECKIN", "CIRC_CLAIMS_RETURNED"],
24    &["LOST\tCHECKOUT", "CIRCULATION_EXISTS"],
25    &["LONGOVERDUE\tCHECKOUT", "CIRCULATION_EXISTS"],
26    &["MISSING\tCHECKOUT", "COPY_NOT_AVAILABLE"],
27    &["DAMAGED\tCHECKOUT", "COPY_NOT_AVAILABLE"],
28    &[
29        "LOST_AND_PAID\tCHECKOUT",
30        "COPY_NOT_AVAILABLE",
31        "CIRCULATION_EXISTS",
32    ],
33];
34
35pub const LEGACY_CIRC_EVENT_MAP: [(&str, &str); 12] = [
36    ("no_item", "ITEM_NOT_CATALOGED"),
37    ("actor.usr.barred", "PATRON_BARRED"),
38    ("asset.copy.circulate", "COPY_CIRC_NOT_ALLOWED"),
39    ("asset.copy.status", "COPY_NOT_AVAILABLE"),
40    ("asset.copy_location.circulate", "COPY_CIRC_NOT_ALLOWED"),
41    ("config.circ_matrix_test.circulate", "COPY_CIRC_NOT_ALLOWED"),
42    (
43        "config.circ_matrix_test.max_items_out",
44        "PATRON_EXCEEDS_CHECKOUT_COUNT",
45    ),
46    (
47        "config.circ_matrix_test.max_overdue",
48        "PATRON_EXCEEDS_OVERDUE_COUNT",
49    ),
50    ("config.circ_matrix_test.max_fines", "PATRON_EXCEEDS_FINES"),
51    (
52        "config.circ_matrix_circ_mod_test",
53        "PATRON_EXCEEDS_CHECKOUT_COUNT",
54    ),
55    (
56        "config.circ_matrix_test.total_copy_hold_ratio",
57        "TOTAL_HOLD_COPY_RATIO_EXCEEDED",
58    ),
59    (
60        "config.circ_matrix_test.available_copy_hold_ratio",
61        "AVAIL_HOLD_COPY_RATIO_EXCEEDED",
62    ),
63];
64
65#[derive(Debug, PartialEq, Clone)]
66pub enum CircOp {
67    Checkout,
68    Checkin,
69    Renew,
70    Unset,
71}
72
73impl fmt::Display for CircOp {
74    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
75        let s: &str = self.into();
76        write!(f, "{}", s)
77    }
78}
79
80impl From<&CircOp> for &'static str {
81    fn from(op: &CircOp) -> &'static str {
82        match *op {
83            CircOp::Checkout => "checkout",
84            CircOp::Checkin => "checkin",
85            CircOp::Renew => "renewal",
86            CircOp::Unset => "unset",
87        }
88    }
89}
90
91/// Contains circ policy matchpoint data.
92#[derive(Debug)]
93pub struct CircPolicy {
94    pub max_fine: f64,
95    pub duration: String,
96    pub recurring_fine: f64,
97    pub matchpoint: EgValue,
98    pub duration_rule: EgValue,
99    pub recurring_fine_rule: EgValue,
100    pub max_fine_rule: EgValue,
101    pub hard_due_date: Option<EgValue>,
102    pub limit_groups: Option<EgValue>,
103}
104
105impl CircPolicy {
106    pub fn to_eg_value(&self) -> EgValue {
107        eg::hash! {
108            "max_fine": self.max_fine,
109            "duration": self.duration.as_str(),
110            "recurring_fine": self.recurring_fine,
111            "matchpoint": self.matchpoint.clone(),
112            "duration_rule": self.duration_rule.clone(),
113            "recurring_fine_rule": self.recurring_fine_rule.clone(),
114            "max_fine_rule": self.max_fine_rule.clone(),
115            "hard_due_date": self.hard_due_date.as_ref().cloned(),
116            "limit_groups": self.limit_groups.as_ref().cloned(),
117        }
118    }
119}
120
121/// Context and shared methods for circulation actions.
122///
123/// Innards are 'pub' since the impl's are spread across multiple files.
124pub struct Circulator<'a> {
125    pub editor: &'a mut Editor,
126    pub init_run: bool,
127    pub settings: Settings,
128    pub circ_lib: i64,
129    pub copy: Option<EgValue>,
130    pub copy_id: i64,
131    pub copy_barcode: Option<String>,
132    pub circ: Option<EgValue>,
133    pub hold: Option<EgValue>,
134    pub reservation: Option<EgValue>,
135    pub patron: Option<EgValue>,
136    pub patron_id: i64,
137    pub transit: Option<EgValue>,
138    pub hold_transit: Option<EgValue>,
139    pub is_noncat: bool,
140    pub system_copy_alerts: Vec<EgValue>,
141    pub runtime_copy_alerts: Vec<EgValue>,
142    pub is_override: bool,
143    pub is_inspect: bool,
144    pub circ_op: CircOp,
145    pub parent_circ: Option<i64>,
146    pub deposit_billing: Option<EgValue>,
147    pub rental_billing: Option<EgValue>,
148
149    /// A circ test can be successfull without a matched policy
150    /// if the matched policy is for
151    pub circ_test_success: bool,
152    pub circ_policy_unlimited: bool,
153
154    /// Compiled rule set for a successful policy match.
155    pub circ_policy_rules: Option<CircPolicy>,
156
157    /// Raw results from the database.
158    pub circ_policy_results: Option<Vec<EgValue>>,
159
160    /// When true, stop further processing and exit.
161    /// This is not necessarily an error condition.
162    pub exit_early: bool,
163
164    pub override_args: Option<Overrides>,
165
166    /// Events that need to be addressed.
167    pub events: Vec<EgEvent>,
168
169    pub renewal_remaining: i64,
170    pub auto_renewal_remaining: Option<i64>,
171
172    /// Override failures are tracked here so they can all be returned
173    /// to the caller.
174    pub failed_events: Vec<EgEvent>,
175
176    /// None until a status is determined one way or the other.
177    pub is_booking_enabled: Option<bool>,
178
179    /// List of hold IDs for holds that need to be retargeted.
180    pub retarget_holds: Option<Vec<i64>>,
181
182    pub checkout_is_for_hold: Option<EgValue>,
183    pub hold_found_for_alt_patron: Option<EgValue>,
184
185    pub fulfilled_hold_ids: Option<Vec<i64>>,
186
187    /// Storage for the large list of circulation API flags that we
188    /// don't explicitly define in this struct.
189    ///
190    /// General plan so far is if the value is only used by a specific
191    /// circ_op (e.g. checkin) then make it an option.  If it's used
192    /// more or less globally for circ stuff, make it part of the
193    /// Circulator proper.
194    pub options: HashMap<String, EgValue>,
195}
196
197impl fmt::Display for Circulator<'_> {
198    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
199        let mut patron_barcode = "null";
200        let mut copy_status = "null";
201
202        if let Some(p) = &self.patron {
203            if let Some(bc) = &p["card"]["barcode"].as_str() {
204                patron_barcode = bc;
205            }
206        }
207
208        let copy_barcode = match self.copy_barcode.as_ref() {
209            Some(b) => b,
210            None => "null",
211        };
212
213        if let Some(c) = &self.copy {
214            if let Some(s) = c["status"]["name"].as_str() {
215                copy_status = s;
216            }
217        }
218
219        write!(
220            f,
221            "Circ: op={} lib={} copy={} copy_status={} patron={}",
222            self.circ_op, self.circ_lib, copy_barcode, copy_status, patron_barcode
223        )
224    }
225}
226
227impl<'a> Circulator<'a> {
228    /// Create a new Circulator.
229    ///
230    pub fn new(
231        editor: &'a mut Editor,
232        options: HashMap<String, EgValue>,
233    ) -> EgResult<Circulator<'a>> {
234        if editor.requestor().is_none() {
235            Err("Circulator requires an authenticated requestor".to_string())?;
236        }
237
238        let settings = Settings::new(editor);
239        let circ_lib = editor.requestor_ws_ou().expect("Workstation Required");
240
241        Ok(Circulator {
242            editor,
243            init_run: false,
244            settings,
245            options,
246            circ_lib,
247            events: Vec::new(),
248            circ: None,
249            parent_circ: None,
250            hold: None,
251            reservation: None,
252            copy: None,
253            copy_id: 0,
254            copy_barcode: None,
255            patron: None,
256            patron_id: 0,
257            transit: None,
258            hold_transit: None,
259            is_noncat: false,
260            is_inspect: false,
261            renewal_remaining: 0,
262            deposit_billing: None,
263            rental_billing: None,
264            auto_renewal_remaining: None,
265            fulfilled_hold_ids: None,
266            checkout_is_for_hold: None,
267            hold_found_for_alt_patron: None,
268            circ_test_success: false,
269            circ_policy_unlimited: false,
270            circ_policy_rules: None,
271            circ_policy_results: None,
272            system_copy_alerts: Vec::new(),
273            runtime_copy_alerts: Vec::new(),
274            is_override: false,
275            override_args: None,
276            failed_events: Vec::new(),
277            exit_early: false,
278            is_booking_enabled: None,
279            retarget_holds: None,
280            circ_op: CircOp::Unset,
281        })
282    }
283
284    pub fn policy_to_eg_value(&self) -> EgValue {
285        let mut value = eg::hash! {};
286
287        if let Some(rules) = self.circ_policy_rules.as_ref() {
288            value["rules"] = rules.to_eg_value();
289        }
290
291        if let Some(results) = self.circ_policy_results.as_ref() {
292            let matches: Vec<EgValue> = results.to_vec();
293            value["matches"] = matches.into();
294        }
295
296        value
297    }
298
299    /// Panics if we have no editor
300    pub fn editor(&mut self) -> &mut Editor {
301        self.editor
302    }
303
304    /// Allow the caller to xact_begin the editor transaction via the
305    /// circulator, which can help to avoid some mutable cross-borrows.
306    pub fn begin(&mut self) -> EgResult<()> {
307        self.editor.xact_begin()
308    }
309
310    /// Allow the caller to rollback the editor transaction via the
311    /// circulator, which can help to avoid some mutable cross-borrows.
312    pub fn rollback(&mut self) -> EgResult<()> {
313        self.editor.rollback()
314    }
315
316    /// Allow the caller to commit the editor transaction via the
317    /// circulator, which can help to avoid some mutable cross-borrows.
318    pub fn commit(&mut self) -> EgResult<()> {
319        self.editor.commit()
320    }
321
322    /// Editor requestor id.
323    pub fn requestor_id(&self) -> EgResult<i64> {
324        self.editor.requestor_id()
325    }
326
327    pub fn is_renewal(&self) -> bool {
328        self.circ_op == CircOp::Renew
329    }
330
331    /// Panics if the booking status is unknown.
332    pub fn is_booking_enabled(&self) -> bool {
333        self.is_booking_enabled.unwrap()
334    }
335
336    pub fn is_inspect(&self) -> bool {
337        self.is_inspect
338    }
339
340    /// Unchecked copy getter.
341    ///
342    /// Panics if copy is None.
343    pub fn copy(&self) -> &EgValue {
344        self.copy
345            .as_ref()
346            .expect("{self} self.copy() requires a copy")
347    }
348
349    /// Returns the copy status ID.
350    ///
351    /// Panics if we have no copy.
352    pub fn copy_status(&self) -> i64 {
353        let copy = self
354            .copy
355            .as_ref()
356            .expect("{self} copy required for copy_status()");
357
358        copy["status"]
359            .id()
360            .expect("Circulator invalid fleshed copy status value")
361    }
362
363    /// Returns the copy circ lib ID.
364    ///
365    /// Panics if we have no copy.
366    pub fn copy_circ_lib(&self) -> i64 {
367        let copy = self
368            .copy
369            .as_ref()
370            .expect("{self} copy required for copy_circ_lib()");
371
372        copy["circ_lib"]
373            .int()
374            .expect("Circulator invalid copy circ lib")
375    }
376
377    /// Used for events that stop processing and should result in
378    /// a rollback on the main editor.
379    pub fn exit_err_on_event_code(&mut self, code: &str) -> EgResult<()> {
380        self.exit_err_on_event(EgEvent::new(code))
381    }
382
383    /// Used for events that stop processing and should result in
384    /// a rollback on the main editor.
385    pub fn exit_err_on_event(&mut self, evt: EgEvent) -> EgResult<()> {
386        self.add_event(evt.clone());
387        Err(EgError::from_event(evt))
388    }
389
390    /// Sets a final event and sets the exit_early flag.
391    ///
392    /// This is for non-Error events that occur when logic has
393    /// reached an endpoint that requires to further processing.
394    pub fn exit_ok_on_event(&mut self, evt: EgEvent) -> EgResult<()> {
395        self.add_event(evt);
396        self.exit_ok()
397    }
398
399    /// Exit now without adding any additional events.
400    pub fn exit_ok(&mut self) -> EgResult<()> {
401        self.exit_early = true;
402        Ok(())
403    }
404
405    /// Add a potentially overridable event to our events list (by code).
406    pub fn add_event_code(&mut self, code: &str) {
407        self.add_event(EgEvent::new(code));
408    }
409
410    /// Add a potentially overridable event to our events list.
411    pub fn add_event(&mut self, evt: EgEvent) {
412        // Avoid duplicate success events.
413        // Retain the most recent.
414        if evt.is_success() {
415            if let Some(pos) = self.events.iter().position(|e| e.is_success()) {
416                self.events.remove(pos);
417            }
418        }
419
420        self.events.push(evt);
421    }
422
423    /// Search for the copy in question
424    pub fn load_copy(&mut self) -> EgResult<()> {
425        let copy_flesh = eg::hash! {
426            flesh: 1,
427            flesh_fields: {
428                acp: COPY_FLESH
429            }
430        };
431
432        // If we have loaded our item before, we can reload it directly
433        // via its ID.
434        let copy_id = if self.copy_id > 0 {
435            self.copy_id
436        } else if let Some(id) = self.options.get("copy_id") {
437            id.int()?
438        } else {
439            0
440        };
441
442        if copy_id > 0 {
443            if let Some(copy) = self
444                .editor()
445                .retrieve_with_ops("acp", copy_id, copy_flesh)?
446            {
447                self.copy = Some(copy);
448            } else {
449                self.exit_err_on_event_code("ASSET_COPY_NOT_FOUND")?;
450            }
451        } else if let Some(copy_barcode) = self.options.get("copy_barcode") {
452            self.copy_barcode = Some(copy_barcode.string()?);
453
454            let query = eg::hash! {
455                barcode: copy_barcode.clone(),
456                deleted: "f", // cstore turns json false into NULL :\
457            };
458
459            if let Some(copy) = self
460                .editor()
461                .search_with_ops("acp", query, copy_flesh)?
462                .pop()
463            {
464                self.copy = Some(copy)
465            } else if self.circ_op != CircOp::Checkout {
466                // OK to checkout precat copies
467                self.exit_err_on_event_code("ASSET_COPY_NOT_FOUND")?;
468            }
469        }
470
471        if let Some(c) = self.copy.as_ref() {
472            self.copy_id = c.id()?;
473            if self.copy_barcode.is_none() {
474                self.copy_barcode = Some(c["barcode"].string()?);
475            }
476        }
477
478        Ok(())
479    }
480
481    /// Load copy alerts related to the copy we're working on.
482    pub fn load_runtime_copy_alerts(&mut self) -> EgResult<()> {
483        if self.copy.is_none() {
484            return Ok(());
485        }
486
487        let query = eg::hash! {
488            copy: self.copy_id,
489            ack_time: EgValue::Null,
490        };
491
492        let flesh = eg::hash! {
493            flesh: 1,
494            flesh_fields: {aca: ["alert_type"]}
495        };
496
497        for alert in self
498            .editor()
499            .search_with_ops("aca", query, flesh)?
500            .drain(..)
501        {
502            self.runtime_copy_alerts.push(alert);
503        }
504
505        self.filter_runtime_copy_alerts()
506    }
507
508    /// Filter copy alerts by circ action, location, etc.
509    fn filter_runtime_copy_alerts(&mut self) -> EgResult<()> {
510        if self.runtime_copy_alerts.is_empty() {
511            return Ok(());
512        }
513
514        let circ_lib = self.circ_lib;
515        let query = eg::hash! {
516            org: org::full_path(self.editor(), circ_lib, None)?
517        };
518
519        // actor.copy_alert_suppress
520        let suppressions = self.editor().search("acas", query)?;
521        let copy_circ_lib = self.copy()["circ_lib"].int()?;
522
523        let mut wanted_alerts = Vec::new();
524
525        let is_renewal = self.is_renewal();
526        while let Some(alert) = self.runtime_copy_alerts.pop() {
527            let atype = &alert["alert_type"];
528
529            // Does this alert type only apply to renewals?
530            let wants_renew = atype["in_renew"].boolish();
531
532            // Verify the alert type event matches what is currently happening.
533            if is_renewal {
534                if !wants_renew {
535                    continue;
536                }
537            } else {
538                if wants_renew {
539                    continue;
540                }
541                if let Some(event) = atype["event"].as_str() {
542                    if event.eq("CHECKOUT") && self.circ_op != CircOp::Checkout {
543                        continue;
544                    }
545                    if event.eq("CHECKIN") && self.circ_op != CircOp::Checkin {
546                        continue;
547                    }
548                }
549            }
550
551            // Verify this alert type is not locally suppressed.
552            if suppressions.iter().any(|a| a["alert_type"] == atype["id"]) {
553                continue;
554            }
555
556            // TODO below mimics load_system_copy_alerts - refactor?
557
558            // Filter on "only at circ lib"
559            if atype["at_circ"].boolish() {
560                let at_circ_orgs = org::descendants(self.editor(), copy_circ_lib)?;
561
562                if atype["invert_location"].boolish() {
563                    if at_circ_orgs.contains(&self.circ_lib) {
564                        continue;
565                    }
566                } else if !at_circ_orgs.contains(&self.circ_lib) {
567                    continue;
568                }
569            }
570
571            // filter on "only at owning lib"
572            if atype["at_owning"].boolish() {
573                let owner = self.copy.as_ref().unwrap()["call_number"]["owning_lib"].int()?;
574                let at_owner_orgs = org::descendants(self.editor(), owner)?;
575
576                if atype["invert_location"].boolish() {
577                    if at_owner_orgs.contains(&self.circ_lib) {
578                        continue;
579                    }
580                } else if !at_owner_orgs.contains(&self.circ_lib) {
581                    continue;
582                }
583            }
584
585            // The Perl code unnests the alert type's next_status value
586            // here, but I have not yet found where it uses it.
587            wanted_alerts.push(alert);
588        }
589
590        self.runtime_copy_alerts = wanted_alerts;
591
592        Ok(())
593    }
594
595    pub fn load_system_copy_alerts(&mut self) -> EgResult<()> {
596        if self.copy_id == 0 {
597            return Ok(());
598        }
599        let copy_id = self.copy_id;
600        let circ_lib = self.circ_lib;
601
602        // System events need event types to focus on.
603        let events: &[&str] = if self.circ_op == CircOp::Renew {
604            &["CHECKOUT", "CHECKIN"]
605        } else if self.circ_op == CircOp::Checkout {
606            &["CHECKOUT"]
607        } else if self.circ_op == CircOp::Checkin {
608            &["CHECKIN"]
609        } else {
610            return Ok(());
611        };
612
613        let list = self.editor().json_query(eg::hash! {
614            from: ["asset.copy_state", copy_id]
615        })?;
616
617        let mut copy_state = "NORMAL";
618        if let Some(hash) = list.first() {
619            if let Some(state) = hash["asset.copy_state"].as_str() {
620                copy_state = state;
621            }
622        }
623
624        // Avoid creating system copy alerts for "NORMAL" copies.
625        if copy_state.eq("NORMAL") {
626            return Ok(());
627        }
628
629        let copy_circ_lib = self.copy()["circ_lib"].int()?;
630
631        let query = eg::hash! {
632            org: org::full_path(self.editor(), circ_lib, None)?
633        };
634
635        // actor.copy_alert_suppress
636        let suppressions = self.editor().search("acas", query)?;
637
638        let alert_orgs = org::ancestors(self.editor(), circ_lib)?;
639
640        let is_renew_filter = if self.is_renewal() { "t" } else { "f" };
641
642        let query = eg::hash! {
643            "active": "t",
644            "scope_org": alert_orgs,
645            "event": events,
646            "state": copy_state,
647            "-or": [{"in_renew": is_renew_filter}, {"in_renew": EgValue::Null}]
648        };
649
650        // config.copy_alert_type
651        let mut alert_types = self.editor().search("ccat", query)?;
652        let mut wanted_types = Vec::new();
653
654        while let Some(atype) = alert_types.pop() {
655            // Filter on "only at circ lib"
656            if atype["at_circ"].boolish() {
657                let at_circ_orgs = org::descendants(self.editor(), copy_circ_lib)?;
658
659                if atype["invert_location"].boolish() {
660                    if at_circ_orgs.contains(&circ_lib) {
661                        continue;
662                    }
663                } else if !at_circ_orgs.contains(&circ_lib) {
664                    continue;
665                }
666            }
667
668            // filter on "only at owning lib"
669            if atype["at_owning"].boolish() {
670                let owner = self.copy()["call_number"]["owning_lib"].int()?;
671                let at_owner_orgs = org::descendants(self.editor(), owner)?;
672
673                if atype["invert_location"].boolish() {
674                    if at_owner_orgs.contains(&circ_lib) {
675                        continue;
676                    }
677                } else if !at_owner_orgs.contains(&circ_lib) {
678                    continue;
679                }
680            }
681
682            wanted_types.push(atype);
683        }
684
685        log::info!(
686            "{self} settled on {} final copy alert types",
687            wanted_types.len()
688        );
689
690        let mut auto_override_conditions = HashSet::new();
691
692        for mut atype in wanted_types {
693            if let Some(ns) = atype["next_status"].as_str() {
694                if suppressions.iter().any(|v| v["alert_type"] == atype["id"]) {
695                    atype["next_status"] = EgValue::new_array();
696                } else {
697                    atype["next_status"] = util::pg_unpack_int_array(ns).into();
698                }
699            }
700
701            let alert = eg::hash! {
702                alert_type: atype["id"].clone(),
703                copy: self.copy_id,
704                temp: "t",
705                create_staff: self.requestor_id()?,
706                create_time: "now",
707                ack_staff: self.requestor_id()?,
708                ack_time: "now",
709            };
710
711            let alert = EgValue::create("aca", alert)?;
712            let mut alert = self.editor().create(alert)?;
713
714            alert["alert_type"] = atype.clone(); // flesh
715
716            if let Some(stat) = atype["next_status"].members().next() {
717                // The Perl version tracks all of the next statuses,
718                // but only ever uses the first.  Just track the first.
719                self.options
720                    .insert("next_copy_status".to_string(), stat.clone());
721            }
722
723            if suppressions.iter().any(|a| a["alert_type"] == atype["id"]) {
724                auto_override_conditions.insert(format!("{}\t{}", atype["state"], atype["event"]));
725            } else {
726                self.system_copy_alerts.push(alert);
727            }
728        }
729
730        self.add_overrides_from_system_copy_alerts(auto_override_conditions)
731    }
732
733    fn add_overrides_from_system_copy_alerts(
734        &mut self,
735        conditions: HashSet<String>,
736    ) -> EgResult<()> {
737        for condition in conditions.iter() {
738            let map = match COPY_ALERT_OVERRIDES.iter().find(|m| m[0].eq(condition)) {
739                Some(m) => m,
740                None => continue,
741            };
742
743            self.is_override = true;
744            let mut checkin_required = false;
745
746            for copy_override in &map[1..] {
747                if let Some(Overrides::Events(ev)) = &mut self.override_args {
748                    // Only track specific events if we are not overriding "All".
749                    ev.push(copy_override.to_string());
750                }
751
752                if copy_override.ne(&"CIRCULATION_EXISTS") {
753                    continue;
754                }
755
756                // Special handling for lsot/long-overdue circs
757
758                let setting = match condition.split('\t').next().unwrap() {
759                    "LOST" | "LOST_AND_PAID" => "circ.copy_alerts.forgive_fines_on_lost_checkin",
760                    "LONGOVERDUE" => "circ.copy_alerts.forgive_fines_on_long_overdue_checkin",
761                    _ => continue,
762                };
763
764                if self.settings.get_value(setting)?.boolish() {
765                    self.set_option_true("void_overdues");
766                }
767
768                self.set_option_true("noop");
769                checkin_required = true;
770            }
771
772            // If we are mid-checkout (not checkin or renew), force
773            // a checkin here (which will be no-op) so the item can be
774            // reset before the checkout resumes.
775            if CircOp::Checkout == self.circ_op && checkin_required {
776                self.checkin()?;
777            }
778        }
779
780        Ok(())
781    }
782
783    /// Map alerts to events, which will be returned to the caller.
784    ///
785    /// Assumes new-style alerts are supported.
786    pub fn check_copy_alerts(&mut self) -> EgResult<()> {
787        if self.copy.is_none() {
788            return Ok(());
789        }
790
791        let mut alert_on = Vec::new();
792        for alert in self.runtime_copy_alerts.iter() {
793            alert_on.push(alert.clone());
794        }
795
796        for alert in self.system_copy_alerts.iter() {
797            alert_on.push(alert.clone());
798        }
799
800        if !alert_on.is_empty() {
801            // We have new-style alerts to reports.
802            let mut evt = EgEvent::new("COPY_ALERT_MESSAGE");
803            evt.set_payload(alert_on.into());
804            self.add_event(evt);
805            return Ok(());
806        }
807
808        // No new-style alerts.  See if the copy itself has one.
809        if self.is_renewal() {
810            return Ok(());
811        }
812
813        if let Some(msg) = self.copy()["alert_message"].as_str() {
814            let mut evt = EgEvent::new("COPY_ALERT_MESSAGE");
815            evt.set_payload(msg.into());
816            self.add_event(evt);
817        }
818
819        Ok(())
820    }
821
822    /// Find an open circulation linked to our copy if possible.
823    fn load_circ(&mut self) -> EgResult<()> {
824        if self.circ.is_some() {
825            log::info!("{self} found an open circulation");
826            // May have been set in load_patron()
827            return Ok(());
828        }
829
830        if let Some(copy) = self.copy.as_ref() {
831            let query = eg::hash! {
832                target_copy: copy["id"].clone(),
833                checkin_time: EgValue::Null,
834            };
835
836            if let Some(circ) = self.editor().search("circ", query)?.pop() {
837                self.circ = Some(circ);
838                log::info!("{self} found an open circulation");
839            }
840        }
841
842        Ok(())
843    }
844
845    /// Find the requested patron if possible.
846    ///
847    /// Also sets a value for self.circ if needed to find the patron.
848    fn load_patron(&mut self) -> EgResult<()> {
849        if self.load_patron_by_id()? {
850            return Ok(());
851        }
852
853        if self.load_patron_by_barcode()? {
854            return Ok(());
855        }
856
857        if self.load_patron_by_copy()? {
858            return Ok(());
859        }
860
861        Ok(())
862    }
863
864    /// Returns true if we were able to load the patron.
865    fn load_patron_by_copy(&mut self) -> EgResult<bool> {
866        let copy = match self.copy.as_ref() {
867            Some(c) => c,
868            None => return Ok(false),
869        };
870
871        // See if we can find the circulation / patron related
872        // to the provided copy.
873
874        let query = eg::hash! {
875            target_copy: copy["id"].clone(),
876            checkin_time: EgValue::Null,
877        };
878
879        let flesh = eg::hash! {
880            flesh: 2,
881            flesh_fields: {
882                circ: ["usr"],
883                au: ["card"],
884            }
885        };
886
887        let mut circ = match self.editor().search_with_ops("circ", query, flesh)?.pop() {
888            Some(c) => c,
889            None => return Ok(false),
890        };
891
892        // Flesh consistently
893        let patron = circ["usr"].take();
894
895        circ["usr"] = patron["id"].clone();
896
897        self.patron_id = patron.id()?;
898        self.patron = Some(patron);
899        self.circ = Some(circ);
900
901        Ok(true)
902    }
903
904    /// Returns true if we were able to load the patron.
905    fn load_patron_by_barcode(&mut self) -> EgResult<bool> {
906        let barcode = match self.options.get("patron_barcode") {
907            Some(b) => b,
908            None => return Ok(false),
909        };
910
911        let query = eg::hash! {barcode: barcode.clone()};
912        let flesh = eg::hash! {flesh: 1, flesh_fields: {"ac": ["usr"]}};
913
914        let mut card = match self.editor().search_with_ops("ac", query, flesh)?.pop() {
915            Some(c) => c,
916            None => {
917                self.exit_err_on_event_code("ACTOR_USER_NOT_FOUND")?;
918                return Ok(false);
919            }
920        };
921
922        let mut patron = card["usr"].take();
923
924        card["usr"] = patron["id"].clone(); // de-flesh card->user
925        patron["card"] = card; // flesh user->card
926
927        self.patron_id = patron.id()?;
928        self.patron = Some(patron);
929
930        Ok(true)
931    }
932
933    /// Returns true if we were able to load the patron by ID.
934    fn load_patron_by_id(&mut self) -> EgResult<bool> {
935        let patron_id = match self.options.get("patron_id") {
936            Some(id) => id.clone(),
937            None => return Ok(false),
938        };
939
940        let flesh = eg::hash! {flesh: 1, flesh_fields: {au: ["card"]}};
941
942        let patron = self
943            .editor()
944            .retrieve_with_ops("au", patron_id, flesh)?
945            .ok_or_else(|| self.editor().die_event())?;
946
947        self.patron_id = patron.id()?;
948        self.patron = Some(patron);
949
950        Ok(true)
951    }
952
953    /// Load data common to most/all circulation operations.
954    ///
955    /// This should be called before any other circulation actions.
956    pub fn init(&mut self) -> EgResult<()> {
957        if self.init_run {
958            // May be called multiple times, e.g. renewals.
959            return Ok(());
960        }
961
962        self.init_run = true;
963
964        if let Some(cl) = self.options.get("circ_lib") {
965            self.circ_lib = cl.int()?;
966        }
967
968        self.settings.set_org_id(self.circ_lib);
969        if let Some(v) = self.options.get("is_noncat") {
970            self.is_noncat = v.boolish();
971        }
972
973        self.load_copy()?;
974        self.load_patron()?;
975        self.load_circ()?;
976        self.set_booking_status()?;
977
978        Ok(())
979    }
980
981    /// Perform post-commit tasks and cleanup, i.e. jobs that can
982    /// be performed after one of our core actions (e.g. checkin) has
983    /// completed and produced a response.
984    pub fn post_commit_tasks(&mut self) -> EgResult<()> {
985        self.retarget_holds()?;
986        self.make_trigger_events()
987    }
988
989    /// Update our copy with the values provided.
990    ///
991    /// * `changes` - a JSON Object with key/value copy attributes to update.
992    pub fn update_copy(&mut self, mut changes: EgValue) -> EgResult<&EgValue> {
993        let mut copy = match self.copy.take() {
994            Some(c) => c,
995            None => return Err("We have no copy to update".into()),
996        };
997
998        copy["editor"] = self.requestor_id()?.into();
999        copy["edit_date"] = "now".into();
1000
1001        for (k, v) in changes.entries_mut() {
1002            copy[k] = v.take();
1003        }
1004
1005        copy.deflesh()?;
1006
1007        self.editor().update(copy)?;
1008
1009        // Load the updated copy with the usual fleshing.
1010        self.load_copy()?;
1011
1012        Ok(self.copy.as_ref().unwrap())
1013    }
1014
1015    /// Set a free-text option value to true.
1016    pub fn set_option_true(&mut self, name: &str) {
1017        self.options.insert(name.to_string(), true.into());
1018    }
1019
1020    /// Delete an option key and value from our options hash.
1021    pub fn clear_option(&mut self, name: &str) {
1022        self.options.remove(name);
1023    }
1024
1025    /// Get the value for a boolean option.
1026    ///
1027    /// Returns false if the value is unset or false-ish.
1028    pub fn get_option_bool(&self, name: &str) -> bool {
1029        if let Some(op) = self.options.get(name) {
1030            op.boolish()
1031        } else {
1032            false
1033        }
1034    }
1035
1036    pub fn can_override_event(&self, textcode: &str) -> bool {
1037        if !self.is_override {
1038            return false;
1039        }
1040
1041        let oargs = match self.override_args.as_ref() {
1042            Some(o) => o,
1043            None => return false,
1044        };
1045
1046        match oargs {
1047            Overrides::All => true,
1048            // True if the list of events that we want to override
1049            // contains the textcode provided.
1050            Overrides::Events(v) => v.iter().map(|s| s.as_str()).any(|s| s == textcode),
1051        }
1052    }
1053
1054    /// Attempts to override any events we have collected so far.
1055    ///
1056    /// Returns Err to exit early if any events exist that cannot
1057    /// be overridden either becuase we are not actively overriding
1058    /// or because an override permission check fails.
1059    pub fn try_override_events(&mut self) -> EgResult<()> {
1060        if self.events.is_empty() {
1061            return Ok(());
1062        }
1063
1064        // If we have a success event, keep it for returning later.
1065        let mut success: Option<EgEvent> = None;
1066        let selfstr = format!("{self}");
1067
1068        while let Some(evt) = self.events.pop() {
1069            if evt.is_success() {
1070                success = Some(evt);
1071                continue;
1072            }
1073
1074            let can_override = self.can_override_event(evt.textcode());
1075
1076            if !can_override {
1077                self.failed_events.push(evt);
1078                continue;
1079            }
1080
1081            let perm = format!("{}.override", evt.textcode());
1082            log::info!("{selfstr} attempting to override: {perm}");
1083
1084            // Override permissions are all global
1085            if !self.editor().allowed(&perm)? {
1086                if let Some(e) = self.editor().last_event().cloned() {
1087                    // Track the permission failure as the event to return.
1088                    self.failed_events.push(e);
1089                } else {
1090                    // Should not get here.
1091                    self.failed_events.push(evt);
1092                }
1093            }
1094        }
1095
1096        if !self.failed_events.is_empty() {
1097            log::info!("Exiting early on failed events: {:?}", self.failed_events);
1098            Err(EgError::from_event(self.failed_events[0].clone()))
1099        } else {
1100            // If all is well and we encountered a SUCCESS event, keep
1101            // it in place so it can ultimately be returned to the caller.
1102            if let Some(evt) = success {
1103                self.events = vec![evt];
1104            };
1105
1106            Ok(())
1107        }
1108    }
1109
1110    /// Sets the is_booking_enable flag if not previously set.
1111    ///
1112    /// TODO: make this a host setting so we can avoid the network call.
1113    pub fn set_booking_status(&mut self) -> EgResult<()> {
1114        if self.is_booking_enabled.is_some() {
1115            return Ok(());
1116        }
1117
1118        if let Some(services) = self.editor().client_mut().send_recv_one(
1119            "router",
1120            "opensrf.router.info.class.list",
1121            None,
1122        )? {
1123            self.is_booking_enabled = Some(services.contains("open-ils.booking"));
1124        } else {
1125            // Should not get here since it means the Router is not resonding.
1126            self.is_booking_enabled = Some(false);
1127        }
1128
1129        Ok(())
1130    }
1131
1132    /// True if the caller wants us to treat this as a precat circ/item.
1133    /// item must be a precat due to it using the precat call number.
1134    pub fn precat_requested(&self) -> bool {
1135        match self.options.get("is_precat") {
1136            Some(v) => v.boolish(),
1137            None => false,
1138        }
1139    }
1140
1141    /// True if we found a copy to work on and it's a precat item.
1142    pub fn is_precat_copy(&self) -> bool {
1143        if let Some(copy) = self.copy.as_ref() {
1144            if let Ok(cn) = copy["call_number"].int() {
1145                return cn == C::PRECAT_CALL_NUMBER;
1146            }
1147        }
1148        false
1149    }
1150
1151    /// Retarget holds in our collected list of holds to retarget.
1152    fn retarget_holds(&mut self) -> EgResult<()> {
1153        let hold_ids = match self.retarget_holds.as_ref() {
1154            Some(list) => list.clone(),
1155            None => return Ok(()),
1156        };
1157        holds::retarget_holds(self.editor, hold_ids.as_slice())
1158    }
1159
1160    /// Create A/T events for checkout/checkin/renewal actions.
1161    fn make_trigger_events(&mut self) -> EgResult<()> {
1162        let circ = match self.circ.as_ref() {
1163            Some(c) => c,
1164            None => return Ok(()),
1165        };
1166
1167        let action: &str = (&self.circ_op).into();
1168
1169        if action == "other" {
1170            return Ok(());
1171        }
1172
1173        trigger::create_events_for_object(
1174            self.editor,
1175            action,
1176            circ,
1177            self.circ_lib,
1178            None,
1179            None,
1180            false,
1181        )
1182    }
1183
1184    /// Remove duplicate events and remove any SUCCESS events if other
1185    /// event types are present.
1186    pub fn cleanup_events(&mut self) {
1187        if self.events.is_empty() {
1188            return;
1189        }
1190
1191        // Deduplicate
1192        let mut events: Vec<EgEvent> = Vec::new();
1193        for evt in self.events.drain(0..) {
1194            if !events.iter().any(|e| e.textcode() == evt.textcode()) {
1195                events.push(evt);
1196            }
1197        }
1198
1199        if events.len() > 1 {
1200            // Multiple events mean something failed somewhere.
1201            // Remove any success events to avoid confusion.
1202            let mut new_events = Vec::new();
1203            for e in events.drain(..) {
1204                if !e.is_success() {
1205                    new_events.push(e);
1206                }
1207            }
1208            events = new_events;
1209        }
1210
1211        self.events = events;
1212    }
1213
1214    /// Events we have accumulated so far.
1215    pub fn events(&self) -> &Vec<EgEvent> {
1216        &self.events
1217    }
1218
1219    /// Clears our list of compiled events and returns them to the caller.
1220    pub fn take_events(&mut self) -> Vec<EgEvent> {
1221        std::mem::take(&mut self.events)
1222    }
1223
1224    /// Make sure the requested item exists and is not marked deleted.
1225    pub fn basic_copy_checks(&mut self) -> EgResult<()> {
1226        if self.copy.is_none() {
1227            self.exit_err_on_event_code("ASSET_COPY_NOT_FOUND")?;
1228        }
1229        self.handle_deleted_copy();
1230        Ok(())
1231    }
1232
1233    pub fn handle_deleted_copy(&mut self) {
1234        if let Some(c) = self.copy.as_ref() {
1235            if c["deleted"].boolish() {
1236                self.options
1237                    .insert(String::from("capture"), "nocapture".into());
1238            }
1239        }
1240    }
1241}