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
14const COPY_FLESH: &[&str] = &["status", "call_number", "parts", "floating", "location"];
17
18pub 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#[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
121pub 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 pub circ_test_success: bool,
152 pub circ_policy_unlimited: bool,
153
154 pub circ_policy_rules: Option<CircPolicy>,
156
157 pub circ_policy_results: Option<Vec<EgValue>>,
159
160 pub exit_early: bool,
163
164 pub override_args: Option<Overrides>,
165
166 pub events: Vec<EgEvent>,
168
169 pub renewal_remaining: i64,
170 pub auto_renewal_remaining: Option<i64>,
171
172 pub failed_events: Vec<EgEvent>,
175
176 pub is_booking_enabled: Option<bool>,
178
179 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 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 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 pub fn editor(&mut self) -> &mut Editor {
301 self.editor
302 }
303
304 pub fn begin(&mut self) -> EgResult<()> {
307 self.editor.xact_begin()
308 }
309
310 pub fn rollback(&mut self) -> EgResult<()> {
313 self.editor.rollback()
314 }
315
316 pub fn commit(&mut self) -> EgResult<()> {
319 self.editor.commit()
320 }
321
322 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 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 pub fn copy(&self) -> &EgValue {
344 self.copy
345 .as_ref()
346 .expect("{self} self.copy() requires a copy")
347 }
348
349 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 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 pub fn exit_err_on_event_code(&mut self, code: &str) -> EgResult<()> {
380 self.exit_err_on_event(EgEvent::new(code))
381 }
382
383 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 pub fn exit_ok_on_event(&mut self, evt: EgEvent) -> EgResult<()> {
395 self.add_event(evt);
396 self.exit_ok()
397 }
398
399 pub fn exit_ok(&mut self) -> EgResult<()> {
401 self.exit_early = true;
402 Ok(())
403 }
404
405 pub fn add_event_code(&mut self, code: &str) {
407 self.add_event(EgEvent::new(code));
408 }
409
410 pub fn add_event(&mut self, evt: EgEvent) {
412 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 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 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", };
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 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 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 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 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 let wants_renew = atype["in_renew"].boolish();
531
532 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 if suppressions.iter().any(|a| a["alert_type"] == atype["id"]) {
553 continue;
554 }
555
556 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 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 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 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 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 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 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 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 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(); if let Some(stat) = atype["next_status"].members().next() {
717 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 ev.push(copy_override.to_string());
750 }
751
752 if copy_override.ne(&"CIRCULATION_EXISTS") {
753 continue;
754 }
755
756 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 CircOp::Checkout == self.circ_op && checkin_required {
776 self.checkin()?;
777 }
778 }
779
780 Ok(())
781 }
782
783 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 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 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 fn load_circ(&mut self) -> EgResult<()> {
824 if self.circ.is_some() {
825 log::info!("{self} found an open circulation");
826 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 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 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 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 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 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(); patron["card"] = card; self.patron_id = patron.id()?;
928 self.patron = Some(patron);
929
930 Ok(true)
931 }
932
933 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 pub fn init(&mut self) -> EgResult<()> {
957 if self.init_run {
958 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 pub fn post_commit_tasks(&mut self) -> EgResult<()> {
985 self.retarget_holds()?;
986 self.make_trigger_events()
987 }
988
989 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 self.load_copy()?;
1011
1012 Ok(self.copy.as_ref().unwrap())
1013 }
1014
1015 pub fn set_option_true(&mut self, name: &str) {
1017 self.options.insert(name.to_string(), true.into());
1018 }
1019
1020 pub fn clear_option(&mut self, name: &str) {
1022 self.options.remove(name);
1023 }
1024
1025 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 Overrides::Events(v) => v.iter().map(|s| s.as_str()).any(|s| s == textcode),
1051 }
1052 }
1053
1054 pub fn try_override_events(&mut self) -> EgResult<()> {
1060 if self.events.is_empty() {
1061 return Ok(());
1062 }
1063
1064 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 if !self.editor().allowed(&perm)? {
1086 if let Some(e) = self.editor().last_event().cloned() {
1087 self.failed_events.push(e);
1089 } else {
1090 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 let Some(evt) = success {
1103 self.events = vec![evt];
1104 };
1105
1106 Ok(())
1107 }
1108 }
1109
1110 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 self.is_booking_enabled = Some(false);
1127 }
1128
1129 Ok(())
1130 }
1131
1132 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 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 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 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 pub fn cleanup_events(&mut self) {
1187 if self.events.is_empty() {
1188 return;
1189 }
1190
1191 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 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 pub fn events(&self) -> &Vec<EgEvent> {
1216 &self.events
1217 }
1218
1219 pub fn take_events(&mut self) -> Vec<EgEvent> {
1221 std::mem::take(&mut self.events)
1222 }
1223
1224 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}