1use crate as eg;
2use chrono::Timelike;
3use eg::common::billing;
4use eg::common::circulator::{CircOp, Circulator};
5use eg::common::holds;
6use eg::common::penalty;
7use eg::common::targeter;
8use eg::common::transit;
9use eg::constants as C;
10use eg::date;
11use eg::event::EgEvent;
12use eg::result::EgResult;
13use eg::EgValue;
14use std::collections::HashSet;
15
16impl Circulator<'_> {
18 pub fn checkin(&mut self) -> EgResult<()> {
23 if self.circ_op == CircOp::Unset {
24 self.circ_op = CircOp::Checkin;
25 }
26
27 self.init()?;
28
29 if !self.is_renewal() && !self.editor.allowed_at("COPY_CHECKIN", self.circ_lib)? {
30 return Err(self.editor().die_event());
31 }
32
33 log::info!("{self} starting checkin");
34
35 self.basic_copy_checks()?;
36
37 self.fix_broken_transit_status()?;
38 self.check_transit_checkin_interval()?;
39 self.checkin_retarget_holds()?;
40 self.cancel_transit_if_circ_exists()?;
41 self.hold_revert_sanity_checks()?;
42 self.set_dont_change_lost_zero()?;
43 self.set_can_float()?;
44 self.do_inventory_update()?;
45
46 if self.check_is_on_holds_shelf()? {
47 return Ok(());
49 }
50
51 self.load_system_copy_alerts()?;
52 self.load_runtime_copy_alerts()?;
53 self.check_copy_alerts()?;
54
55 self.check_claims_returned();
56 self.check_circ_deposit(false)?;
57 self.try_override_events()?;
58
59 if self.exit_early {
60 return Ok(());
61 }
62
63 if self.circ.is_some() {
64 self.checkin_handle_circ()?;
65 } else if self.transit.is_some() {
66 self.checkin_handle_transit()?;
67 self.checkin_handle_received_hold()?;
68 } else if self.copy_status() == C::COPY_STATUS_IN_TRANSIT {
69 log::warn!("{self} copy is in-transit but there is no transit");
70 self.reshelve_copy(true)?;
71 }
72
73 if self.exit_early {
74 return Ok(());
75 }
76
77 if self.is_renewal() {
78 self.finish_fines_and_voiding()?;
79 self.add_event_code("SUCCESS");
80 return Ok(());
81 }
82
83 if self.revert_hold_fulfillment()? {
84 return Ok(());
85 }
86
87 let mut item_is_needed = false;
92 if self.get_option_bool("noop") {
93 if self.get_option_bool("can_float") {
94 self.update_copy(eg::hash! {"circ_lib": self.circ_lib})?;
98 }
99 } else {
100 item_is_needed = self.try_to_capture()?;
101 if !item_is_needed {
102 self.try_to_transit()?;
103 }
104 }
105
106 if !self.handle_claims_never()? && !item_is_needed {
107 self.reshelve_copy(false)?;
108 }
109
110 if self.editor().has_pending_changes() {
111 if self.events.is_empty() {
112 self.add_event(EgEvent::success());
113 }
114 } else {
115 self.add_event(EgEvent::new("NO_CHANGE"));
116 }
117
118 self.finish_fines_and_voiding()?;
119
120 if self.patron.is_some() {
121 penalty::calculate_penalties(self.editor, self.patron_id, self.circ_lib, None)?;
122 }
123
124 self.cleanup_events();
125 self.flesh_checkin_events()?;
126
127 Ok(())
128 }
129
130 fn handle_claims_never(&mut self) -> EgResult<bool> {
132 if !self.get_option_bool("claims_never_checked_out") {
133 return Ok(false);
134 }
135
136 let circ = match self.circ.as_ref() {
137 Some(c) => c, None => return Ok(false),
139 };
140
141 if !self
142 .settings
143 .get_value_at_org(
144 "circ.claim_never_checked_out.mark_missing",
145 circ["circ_lib"].int()?,
146 )?
147 .boolish()
148 {
149 return Ok(false);
150 }
151
152 let next_status = match self.options.get("next_copy_status") {
157 Some(s) => s.int()?,
158 None => C::COPY_STATUS_MISSING,
159 };
160
161 self.update_copy(eg::hash! {"status": next_status})?;
162
163 Ok(true)
164 }
165
166 fn capture_state(&self) -> &str {
168 match self.options.get("capture") {
169 Some(c) => c.as_str().unwrap_or(""),
170 None => "",
171 }
172 }
173
174 fn fix_broken_transit_status(&mut self) -> EgResult<()> {
177 let query = eg::hash! {
178 target_copy: self.copy()["id"].clone(),
179 dest_recv_time: EgValue::Null,
180 cancel_time: EgValue::Null,
181 };
182
183 let mut results = self.editor().search("atc", query)?;
184
185 let transit = match results.pop() {
186 Some(t) => t,
187 None => return Ok(()),
188 };
189
190 if self.copy_status() != C::COPY_STATUS_IN_TRANSIT {
191 log::warn!("{self} Copy has an open transit, but incorrect status");
192 let changes = eg::hash! {status: C::COPY_STATUS_IN_TRANSIT};
193 self.update_copy(changes)?;
194 }
195
196 self.transit = Some(transit);
197
198 Ok(())
199 }
200
201 fn check_transit_checkin_interval(&mut self) -> EgResult<()> {
205 if self.copy_status() != C::COPY_STATUS_IN_TRANSIT {
206 return Ok(());
208 }
209
210 let interval = self
211 .settings
212 .get_value("circ.transit.min_checkin_interval")?;
213
214 if interval.is_null() {
215 return Ok(());
217 }
218
219 let transit = match self.transit.as_ref() {
220 Some(t) => t,
221 None => {
222 log::warn!("Copy has in-transit status but no matching transit!");
223 return Ok(());
224 }
225 };
226
227 if transit["source"] == transit["dest"] {
228 return Ok(());
231 }
232
233 let interval = interval.string()?;
235
236 let send_time_str = transit["source_send_time"].as_str().unwrap();
238 let send_time = date::parse_datetime(send_time_str)?;
239
240 let horizon = date::add_interval(send_time, &interval)?;
241
242 if horizon > date::now() {
243 self.add_event_code("TRANSIT_CHECKIN_INTERVAL_BLOCK");
244 }
245
246 Ok(())
247 }
248
249 fn checkin_retarget_holds(&mut self) -> EgResult<()> {
254 let copy = self.copy();
255
256 let retarget_mode = self
257 .options
258 .get("retarget_mode")
259 .map(|v| v.as_str().unwrap_or(""))
260 .unwrap_or("");
261
262 if !retarget_mode.contains("retarget")
264 || self.get_option_bool("revert_hold_fulfillment")
265 || self.capture_state() == "nocapture"
266 || self.is_precat_copy()
267 || copy["circ_lib"].int()? != self.circ_lib
268 || copy["deleted"].boolish()
269 || !copy["holdable"].boolish()
270 || !copy["status"]["holdable"].boolish()
271 || !copy["location"]["holdable"].boolish()
272 {
273 return Ok(());
274 }
275
276 if !retarget_mode.contains(".all") && self.copy_status() != C::COPY_STATUS_IN_PROCESS {
278 return Ok(());
279 }
280
281 let query = eg::hash! {target_copy: EgValue::from(self.copy_id)};
282 let parts = self.editor().search("acpm", query)?;
283 let parts = parts
284 .into_iter()
285 .map(|p| p.id().expect("ID Required"))
286 .collect::<HashSet<_>>();
287
288 let copy_id = self.copy_id;
289 let circ_lib = self.circ_lib;
290 let vol_id = self.copy()["call_number"].id()?;
291
292 let hold_data = holds::related_to_copy(
293 self.editor(),
294 copy_id,
295 Some(circ_lib),
296 None,
297 None,
298 Some(false), )?;
300
301 let mut editor = self.editor().clone();
306 let mut hold_targeter = targeter::HoldTargeter::new(&mut editor);
307
308 for hold in hold_data.iter() {
309 let target = hold.target();
310 let hold_type: &str = hold.hold_type().into();
311
312 if target != copy_id && (hold_type.eq("C") || hold_type.eq("R") || hold_type.eq("F")) {
314 continue;
315 }
316
317 if target != vol_id && hold_type.eq("V") {
319 continue;
320 }
321
322 if !parts.is_empty() {
323 if hold_type.eq("T") {
325 continue;
326 } else if hold_type.eq("P") {
327 if !parts.contains(&target) {
329 continue;
330 }
331 }
332 } else if hold_type.eq("P") {
333 continue;
335 }
336
337 let ctx = hold_targeter.target_hold(hold.id(), Some(copy_id))?;
338
339 if ctx.success() && ctx.found_copy() {
340 log::info!("checkin_retarget_holds() successfully targeted a hold");
341 break;
342 }
343 }
344
345 Ok(())
346 }
347
348 fn cancel_transit_if_circ_exists(&mut self) -> EgResult<()> {
351 if self.circ.is_none() {
352 return Ok(());
353 }
354
355 if let Some(transit) = self.transit.as_ref() {
356 let transit_id = transit.id()?;
357 log::info!(
358 "{self} copy is both checked out and in transit. Canceling transit {transit_id}"
359 );
360 transit::cancel_transit(self.editor(), transit_id, false)?;
361 self.transit = None;
362 }
363
364 Ok(())
365 }
366
367 fn set_dont_change_lost_zero(&mut self) -> EgResult<()> {
370 match self.copy_status() {
371 C::COPY_STATUS_LOST | C::COPY_STATUS_LOST_AND_PAID | C::COPY_STATUS_LONG_OVERDUE => {
372 }
374 _ => return Ok(()), }
376
377 let value = self.settings.get_value_at_org(
380 "circ.checkin.lost_zero_balance.do_not_change",
381 self.copy()["circ_lib"].int()?,
382 )?;
383
384 let mut dont_change = value.boolish();
385
386 if dont_change {
387 if let Some(circ) = self.circ.as_ref() {
391 let circ_id = circ["id"].clone();
392 if let Some(mbts) = self.editor().retrieve("mbts", circ_id)? {
393 dont_change = mbts["balance_owed"].float()? == 0.0;
394 }
395 }
396 }
397
398 if dont_change {
399 self.set_option_true("dont_change_lost_zero");
400 }
401
402 Ok(())
403 }
404
405 fn set_can_float(&mut self) -> EgResult<()> {
407 let float_id = &self.copy()["floating"];
408
409 if float_id.is_null() {
410 return Ok(());
412 }
413
414 let float_id = float_id.clone();
415
416 let float_group = self
419 .editor()
420 .retrieve("cfg", float_id)?
421 .ok_or_else(|| self.editor().die_event())?;
422
423 let query = eg::hash! {
424 from: [
425 "evergreen.can_float",
426 float_group["id"].clone(),
427 self.copy()["circ_lib"].clone(),
428 self.circ_lib
429 ]
430 };
431
432 if let Some(resp) = self.editor().json_query(query)?.first() {
433 if resp["evergreen.can_float"].boolish() {
434 self.set_option_true("can_float");
435 }
436 }
437
438 Ok(())
439 }
440
441 fn do_inventory_update(&mut self) -> EgResult<()> {
443 if !self.get_option_bool("do_inventory_update") {
444 return Ok(());
445 }
446
447 let ws_id = match self.editor().requestor_ws_id() {
448 Some(i) => i,
449 None => return Ok(()),
451 };
452
453 if self.copy()["circ_lib"].int()? != self.circ_lib && !self.get_option_bool("can_float") {
454 return Ok(());
456 }
457
458 let aci = eg::hash! {
460 inventory_date: "now",
461 inventory_workstation: ws_id,
462 copy: self.copy()["id"].clone(),
463 };
464
465 self.editor().create(aci)?;
466
467 Ok(())
468 }
469
470 fn check_is_on_holds_shelf(&mut self) -> EgResult<bool> {
475 if self.copy_status() != C::COPY_STATUS_ON_HOLDS_SHELF {
476 return Ok(false);
477 }
478
479 let copy_id = self.copy_id;
480
481 if self.get_option_bool("clear_expired") {
482 let params = vec![
486 EgValue::from(self.editor().authtoken()),
487 EgValue::from(self.circ_lib),
488 self.copy()["id"].clone(),
489 ];
490
491 self.editor().client_mut().send_recv_one(
492 "open-ils.circ",
493 "open-ils.circ.hold.clear_shelf.process",
494 params,
495 )?;
496 }
497
498 let hold = match holds::captured_hold_for_copy(self.editor(), copy_id)? {
499 Some(h) => h,
500 None => {
501 log::warn!("{self} Copy on holds shelf but there is no hold");
502 self.reshelve_copy(false)?;
503 return Ok(false);
504 }
505 };
506
507 let pickup_lib = hold["pickup_lib"].int()?;
508
509 log::info!("{self} we found a captured, un-fulfilled hold");
510
511 if pickup_lib != self.circ_lib && !self.get_option_bool("hold_as_transit") {
512 let suppress_here = self.settings.get_value("circ.transit.suppress_hold")?;
513
514 let suppress_here = suppress_here.string().unwrap_or_default();
515
516 let suppress_there = self
517 .settings
518 .get_value_at_org("circ.transit.suppress_hold", pickup_lib)?;
519
520 let suppress_there = suppress_there.string().unwrap_or_default();
521
522 if suppress_here == suppress_there && !suppress_here.is_empty() {
523 log::info!("{self} hold is within transit suppress group: {suppress_here}");
524 self.set_option_true("fake_hold_dest");
525 return Ok(true);
526 }
527 }
528
529 if pickup_lib == self.circ_lib && !self.get_option_bool("hold_as_transit") {
530 log::info!("{self} hold is for here");
531 return Ok(true);
532 }
533
534 log::info!("{self} hold is not for here");
535 self.options.insert(String::from("remote_hold"), hold);
536
537 Ok(false)
538 }
539
540 fn reshelve_copy(&mut self, force: bool) -> EgResult<()> {
543 let force = force || self.get_option_bool("force");
544
545 let status = self.copy_status();
546
547 let next_status = match self.options.get("next_copy_status") {
548 Some(s) => s.int()?,
549 None => C::COPY_STATUS_RESHELVING,
550 };
551
552 if force
553 || (status != C::COPY_STATUS_ON_HOLDS_SHELF
554 && status != C::COPY_STATUS_CATALOGING
555 && status != C::COPY_STATUS_IN_TRANSIT
556 && status != next_status)
557 {
558 self.update_copy(eg::hash! {status: EgValue::from(next_status)})?;
559 }
560
561 Ok(())
562 }
563
564 fn check_claims_returned(&mut self) {
566 if let Some(circ) = self.circ.as_ref() {
567 if let Some(sf) = circ["stop_fines"].as_str() {
568 if sf == "CLAIMSRETURNED" {
569 self.add_event_code("CIRC_CLAIMS_RETURNED");
570 }
571 }
572 }
573 }
574
575 fn check_circ_deposit(&mut self, void: bool) -> EgResult<()> {
578 let circ_id = match self.circ.as_ref() {
579 Some(c) => c["id"].clone(),
580 None => return Ok(()),
581 };
582
583 let query = eg::hash! {
584 btype: C::BTYPE_DEPOSIT,
585 voided: "f",
586 xact: circ_id,
587 };
588
589 let mut results = self.editor().search("mb", query)?;
590 let deposit = match results.pop() {
591 Some(d) => d,
592 None => return Ok(()),
593 };
594
595 if void {
596 if self.settings.get_value("circ.void_item_deposit")?.boolish() {
598 let bill_id = deposit.id()?;
599 billing::void_bills(self.editor(), &[bill_id], Some("DEPOSIT ITEM RETURNED"))?;
600 }
601 } else {
602 let mut evt = EgEvent::new("ITEM_DEPOSIT_PAID");
603 evt.set_payload(deposit);
604 self.add_event(evt);
605 }
606
607 Ok(())
608 }
609
610 fn checkin_handle_circ(&mut self) -> EgResult<()> {
613 let selfstr: String = self.to_string();
614
615 if self.get_option_bool("claims_never_checked_out") {
616 let xact_start = &self.circ.as_ref().unwrap()["xact_start"];
617 self.options
618 .insert("backdate".to_string(), xact_start.clone());
619 }
620
621 if self.options.contains_key("backdate") {
622 self.checkin_compile_backdate()?;
623 }
624
625 let copy_status = self.copy_status();
626 let copy_circ_lib = self.copy_circ_lib();
627
628 let req_id = self.requestor_id()?;
629 let req_ws_id = self.editor().requestor_ws_id();
630
631 let circ = self.circ.as_mut().unwrap();
632 let circ_id = circ.id()?;
633
634 circ["checkin_time"] = self
635 .options
636 .get("backdate")
637 .cloned()
638 .unwrap_or(EgValue::from("now"));
639
640 circ["checkin_scan_time"] = EgValue::from("now");
641 circ["checkin_staff"] = EgValue::from(req_id);
642 circ["checkin_lib"] = EgValue::from(self.circ_lib);
643 if let Some(id) = req_ws_id {
644 circ["checkin_workstation"] = EgValue::from(id);
645 }
646
647 log::info!(
648 "{selfstr} checking item in with checkin_time {}",
649 circ["checkin_time"]
650 );
651
652 match copy_status {
653 C::COPY_STATUS_LOST => self.checkin_handle_lost()?,
654 C::COPY_STATUS_LOST_AND_PAID => self.checkin_handle_lost()?,
655 C::COPY_STATUS_LONG_OVERDUE => self.checkin_handle_long_overdue()?,
656 C::COPY_STATUS_MISSING => {
657 if copy_circ_lib == self.circ_lib {
658 self.reshelve_copy(true)?
659 } else {
660 log::info!("{self} leaving copy in missing status on remote checkin");
661 }
662 }
663 _ => {
664 if !self.is_renewal() {
665 self.reshelve_copy(true)?;
668 }
669 }
670 }
671
672 if self.get_option_bool("dont_change_lost_zero") {
673 let circ = self.circ.as_ref().unwrap().clone();
677 self.editor().update(circ)?;
678 } else {
679 if self.get_option_bool("claims_never_checked_out") {
680 let circ = self.circ.as_mut().unwrap();
681 circ["stop_fines"] = EgValue::from("CLAIMSNEVERCHECKEDOUT");
682 } else if copy_status == C::COPY_STATUS_LOST {
683 if self.get_option_bool("circ.lost.generate_overdue_on_checkin") {
687 let circ = self.circ.as_mut().unwrap();
696 circ["stop_fines"].take();
697 }
698 }
699
700 let circ = self.circ.as_ref().unwrap().clone();
701 self.editor().update(circ)?;
702 self.handle_checkin_fines()?;
703 }
704
705 self.check_circ_deposit(true)?;
706
707 log::debug!("{selfstr} checking open transaction state");
708
709 billing::check_open_xact(self.editor(), circ_id)?;
711
712 self.circ = self.editor().retrieve("circ", circ_id)?;
714
715 Ok(())
716 }
717
718 fn checkin_handle_lost(&mut self) -> EgResult<()> {
720 log::info!("{self} processing LOST checkin...");
721
722 let billing_options = eg::hash! {
723 ous_void_item_cost: "circ.void_lost_on_checkin",
724 ous_void_proc_fee: "circ.void_lost_proc_fee_on_checkin",
725 ous_restore_overdue: "circ.restore_overdue_on_lost_return",
726 void_cost_btype: C::BTYPE_LOST_MATERIALS,
727 void_fee_btype: C::BTYPE_LOST_MATERIALS_PROCESSING_FEE,
728 };
729
730 self.options
731 .insert("lost_or_lo_billing_options".to_string(), billing_options);
732
733 self.checkin_handle_lost_or_long_overdue(
734 "circ.max_accept_return_of_lost",
735 "circ.lost_immediately_available",
736 None, )
738 }
739
740 fn checkin_handle_long_overdue(&mut self) -> EgResult<()> {
742 let billing_options = eg::hash! {
743 is_longoverdue: true,
744 ous_void_item_cost: "circ.void_longoverdue_on_checkin",
745 ous_void_proc_fee: "circ.void_longoverdue_proc_fee_on_checkin",
746 ous_restore_overdue: "circ.restore_overdue_on_longoverdue_return",
747 void_cost_btype: C::BTYPE_LONG_OVERDUE_MATERIALS,
748 void_fee_btype: C::BTYPE_LONG_OVERDUE_MATERIALS_PROCESSING_FEE,
749 };
750
751 self.options
752 .insert("lost_or_lo_billing_options".to_string(), billing_options);
753
754 self.checkin_handle_lost_or_long_overdue(
755 "circ.max_accept_return_of_longoverdue",
756 "circ.longoverdue_immediately_available",
757 Some("circ.longoverdue.use_last_activity_date_on_return"),
758 )
759 }
760
761 fn checkin_handle_lost_or_long_overdue(
764 &mut self,
765 ous_max_return: &str,
766 ous_immediately_available: &str,
767 ous_use_last_activity: Option<&str>,
768 ) -> EgResult<()> {
769 let copy_circ_lib = self.copy_circ_lib();
771 let max_return = self
772 .settings
773 .get_value_at_org(ous_max_return, copy_circ_lib)?
774 .clone(); let mut too_late = false;
776
777 if let Some(max) = max_return.as_str() {
778 let last_activity = self.circ_last_billing_activity(ous_use_last_activity)?;
779 let last_activity = date::parse_datetime(&last_activity)?;
780
781 let last_chance = date::add_interval(last_activity, max)?;
782 too_late = last_chance > date::now();
783 }
784
785 if too_late {
786 log::info!(
787 "{self} check-in of lost/lo item exceeds max
788 return interval. skipping fine/fee voiding, etc."
789 );
790 } else if self.get_option_bool("dont_change_lost_zero") {
791 log::info!(
792 "{self} check-in of lost/lo item having a balance
793 of zero, skipping fine/fee voiding and reinstatement."
794 );
795 } else {
796 log::info!(
797 "{self} check-in of lost/lo item is within the
798 max return interval (or no interval is defined). Proceeding
799 with fine/fee voiding, etc."
800 );
801
802 self.set_option_true("needs_lost_bill_handling");
803 }
804
805 if self.circ_lib == copy_circ_lib {
806 return self.reshelve_copy(true);
809 }
810
811 let available_now = self
813 .settings
814 .get_value_at_org(ous_immediately_available, copy_circ_lib)?
815 .boolish();
816
817 if available_now {
818 self.reshelve_copy(true)
821 } else {
822 log::info!("{self}: leaving lost/longoverdue copy status in place on checkin");
823 Ok(())
824 }
825 }
826
827 fn circ_last_billing_activity(&mut self, maybe_setting: Option<&str>) -> EgResult<String> {
835 let copy_circ_lib = self.copy_circ_lib();
836 let circ = self.circ.as_ref().unwrap();
837 let circ_id = circ["id"].clone();
838
839 let due_date = circ["due_date"].as_str().unwrap().to_string();
841
842 let setting = match maybe_setting {
843 Some(s) => s,
844 None => return Ok(due_date),
845 };
846
847 let use_activity = self.settings.get_value_at_org(setting, copy_circ_lib)?;
848
849 if !use_activity.boolish() {
850 return Ok(due_date);
851 }
852
853 if let Some(mbts) = self.editor().retrieve("mbts", circ_id)? {
854 if let Some(last_payment) = mbts["last_payment_ts"].as_str() {
855 return Ok(last_payment.to_string());
856 }
857 if let Some(last_billing) = mbts["last_billing_ts"].as_str() {
858 return Ok(last_billing.to_string());
859 }
860 }
861
862 Ok(due_date)
864 }
865
866 fn checkin_compile_backdate(&mut self) -> EgResult<()> {
870 let duedate = match self.circ.as_ref() {
871 Some(circ) => circ["due_date"]
872 .as_str()
873 .ok_or_else(|| format!("{self} circ has no due date?"))?,
874 None => return Ok(()),
875 };
876
877 let backdate = match self.options.get("backdate") {
878 Some(bd) => bd
879 .as_str()
880 .ok_or_else(|| format!("{self} bad backdate value: {bd}"))?,
881 None => return Ok(()),
882 };
883
884 let orig_date = date::parse_datetime(duedate)?;
887 let mut new_date = date::parse_datetime(backdate)?;
888
889 new_date = new_date
890 .with_hour(orig_date.hour())
891 .ok_or_else(|| "Could not set backdate hours".to_string())?;
892
893 new_date = new_date
894 .with_minute(orig_date.minute())
895 .ok_or_else(|| "Could not set backdate minutes".to_string())?;
896
897 if new_date > date::now() {
898 log::info!("{self} ignoring future backdate: {new_date}");
899 self.options.remove("backdate");
900 } else {
901 self.options.insert(
902 "backdate".to_string(),
903 EgValue::from(date::to_iso(&new_date)),
904 );
905 }
906
907 Ok(())
908 }
909
910 fn handle_checkin_fines(&mut self) -> EgResult<()> {
913 let copy_circ_lib = self.copy_circ_lib();
914
915 if let Some(ops) = self.options.get("lost_or_lo_billing_options") {
916 if !self.get_option_bool("void_overdues") {
917 if let Some(setting) = ops["ous_restore_overdue"].as_str() {
918 if self
919 .settings
920 .get_value_at_org(setting, copy_circ_lib)?
921 .boolish()
922 {
923 self.checkin_handle_lost_or_lo_now_found_restore_od(false)?;
924 }
925 }
926 }
927 }
928
929 let mut is_circ = false;
930 let xact_id = match self.circ.as_ref() {
931 Some(c) => {
932 is_circ = true;
933 c.id()?
934 }
935 None => match self.reservation.as_ref() {
936 Some(r) => r.id()?,
937 None => Err(format!(
938 "{self} we have no transaction to generate fines for"
939 ))?,
940 },
941 };
942 if is_circ {
943 if self.circ.as_ref().unwrap()["stop_fines"].is_null() {
944 billing::generate_fines_for_circ(self.editor(), xact_id)?;
945
946 self.circ = self.editor().retrieve("circ", xact_id)?;
949 }
950
951 self.set_circ_stop_fines()?;
952 } else {
953 billing::generate_fines_for_resv(self.editor(), xact_id)?;
954 }
955
956 if !self.get_option_bool("needs_lost_bill_handling") {
957 return Ok(());
959 }
960
961 let ops = match self.options.get("lost_or_lo_billing_options") {
962 Some(o) => o,
963 None => Err("Cannot handle lost/lo billing without options".to_string())?,
964 };
965
966 let tag = if ops["is_longoverdue"].boolish() {
968 "LONGOVERDUE"
969 } else {
970 "LOST"
971 };
972 let note = format!("{tag} ITEM RETURNED");
973
974 let mut void_cost = 0.0;
975 if let Some(set) = ops["ous_void_item_cost"].as_str() {
976 if let Ok(c) = self.settings.get_value_at_org(set, copy_circ_lib)?.float() {
977 void_cost = c;
978 }
979 }
980
981 let mut void_proc_fee = 0.0;
982 if let Some(set) = ops["ous_void_proc_fee"].as_str() {
983 if let Ok(c) = self.settings.get_value_at_org(set, copy_circ_lib)?.float() {
984 void_proc_fee = c;
985 }
986 }
987
988 let void_cost_btype = ops["void_cost_btype"].as_i64().unwrap_or(0);
989 let void_fee_btype = ops["void_fee_btype"].as_i64().unwrap_or(0);
990
991 if void_cost > 0.0 {
992 if void_cost_btype == 0 {
993 log::warn!("Cannot zero {tag} circ without a billing type");
994 return Ok(());
995 }
996
997 billing::void_or_zero_bills_of_type(
998 self.editor(),
999 xact_id,
1000 copy_circ_lib,
1001 void_cost_btype,
1002 ¬e,
1003 )?;
1004 }
1005
1006 if void_proc_fee > 0.0 {
1007 if void_fee_btype == 0 {
1008 log::warn!("Cannot zero {tag} circ without a billing type");
1009 return Ok(());
1010 }
1011
1012 billing::void_or_zero_bills_of_type(
1013 self.editor(),
1014 xact_id,
1015 copy_circ_lib,
1016 void_fee_btype,
1017 ¬e,
1018 )?;
1019 }
1020
1021 Ok(())
1022 }
1023
1024 fn set_circ_stop_fines(&mut self) -> EgResult<()> {
1028 let circ = self.circ.as_ref().unwrap();
1029
1030 if !circ["stop_fines"].is_null() {
1031 return Ok(());
1032 }
1033
1034 let stop_fines = if self.is_renewal() {
1036 "RENEW"
1037 } else if self.get_option_bool("claims_never_checked_out") {
1038 "CLAIMSNEVERCHECKEDOUT"
1039 } else {
1040 "CHECKIN"
1041 };
1042
1043 let stop_fines = EgValue::from(stop_fines);
1044
1045 let stop_fines_time = match self.options.get("backdate") {
1046 Some(bd) => bd.clone(),
1047 None => EgValue::from("now"),
1048 };
1049
1050 let mut circ = circ.clone();
1051
1052 let circ_id = circ["id"].clone();
1053
1054 circ["stop_fines"] = stop_fines;
1055 circ["stop_fines_time"] = stop_fines_time;
1056
1057 self.editor().update(circ)?;
1058
1059 self.circ = self.editor().retrieve("circ", circ_id)?;
1061
1062 Ok(())
1063 }
1064
1065 fn checkin_handle_lost_or_lo_now_found_restore_od(
1067 &mut self,
1068 is_longoverdue: bool,
1069 ) -> EgResult<()> {
1070 let circ = self.circ.as_ref().unwrap();
1071 let circ_id = circ.id()?;
1072 let void_max = circ["max_fine"].float()?;
1073
1074 let query = eg::hash! {xact: circ_id, btype: C::BTYPE_OVERDUE_MATERIALS};
1075 let ops = eg::hash! {"order_by": {"mb": "billing_ts desc"}};
1076 let overdues = self.editor().search_with_ops("mb", query, ops)?;
1077
1078 if overdues.is_empty() {
1079 log::info!("{self} no overdues to reinstate on lost/lo checkin");
1080 return Ok(());
1081 }
1082
1083 let tag = if is_longoverdue {
1084 "LONGOVERRDUE"
1085 } else {
1086 "LOST"
1087 };
1088 log::info!("{self} re-instating {} pre-{tag} overdues", overdues.len());
1089
1090 let mut void_amount = 0.0;
1091
1092 let billing_ids: Vec<EgValue> = overdues.iter().map(|b| b["id"].clone()).collect();
1093 let voids = self
1094 .editor()
1095 .search("maa", eg::hash! {"billing": billing_ids})?;
1096
1097 if !voids.is_empty() {
1098 for void in voids.iter() {
1100 void_amount += void["amount"].float()?;
1101 }
1102 } else {
1103 for bill in overdues.iter() {
1105 if bill["voided"].boolish() {
1106 void_amount += bill["amount"].float()?;
1107 }
1108 }
1109 }
1110
1111 if void_amount == 0.0 {
1112 log::info!("{self} voided overdues amounted to $0.00. Nothing to restore");
1113 return Ok(());
1114 }
1115
1116 if void_amount > void_max {
1117 void_amount = void_max;
1118 }
1119
1120 let first_od = overdues.first().unwrap();
1122 let last_od = overdues.last().unwrap();
1123
1124 let btype_label = first_od["billing_type"].as_str().unwrap(); let period_start = first_od["period_start"].as_str();
1126 let period_end = last_od["period_end"].as_str();
1127
1128 let note = format!("System: {tag} RETURNED - OVERDUES REINSTATED");
1129
1130 billing::create_bill(
1131 self.editor(),
1132 void_amount,
1133 billing::BillingType {
1134 id: C::BTYPE_OVERDUE_MATERIALS,
1135 label: btype_label.to_string(),
1136 },
1137 circ_id,
1138 Some(¬e),
1139 period_start,
1140 period_end,
1141 )?;
1142
1143 Ok(())
1144 }
1145
1146 fn checkin_handle_transit(&mut self) -> EgResult<()> {
1150 log::info!("{self} attempting to receive transit");
1151
1152 let transit = self.transit.as_ref().unwrap();
1153 let transit_id = transit.id()?;
1154 let transit_dest = transit["dest"].int()?;
1155 let transit_copy_status = transit["copy_status"].int()?;
1156
1157 let for_hold = transit_copy_status == C::COPY_STATUS_ON_HOLDS_SHELF;
1158 let suppress_transit = self.should_suppress_transit(transit_dest, for_hold)?;
1159
1160 if for_hold && suppress_transit {
1161 self.set_option_true("fake_hold_dest");
1162 }
1163
1164 self.hold_transit = self.editor().retrieve("ahtc", transit_id)?;
1165
1166 if let Some(ht) = self.hold_transit.as_ref() {
1167 let hold_id = ht["hold"].clone();
1168 if !ht["hold"].is_null() {
1171 self.hold = self.editor().retrieve("ahr", hold_id)?;
1172 }
1173 }
1174
1175 let hold_as_transit = self.get_option_bool("hold_as_transit")
1176 && transit_copy_status == C::COPY_STATUS_ON_HOLDS_SHELF;
1177
1178 if !suppress_transit && (transit_dest != self.circ_lib || hold_as_transit) {
1179 log::info!(
1183 "{self}: Fowarding transit on copy which is destined
1184 for a different location. transit={transit_id} destination={transit_dest}"
1185 );
1186
1187 let mut evt = EgEvent::new("ROUTE_ITEM");
1188 evt.set_org(transit_dest);
1189
1190 return self.exit_ok_on_event(evt);
1191 }
1192
1193 let mut transit = self.transit.take().unwrap();
1195 transit["dest_recv_time"] = EgValue::from("now");
1196 self.editor().update(transit)?;
1197
1198 self.transit = self.editor().retrieve("atc", transit_id)?;
1200
1201 self.update_copy(eg::hash! {"status": transit_copy_status})?;
1203
1204 if self.hold.is_some() {
1205 self.put_hold_on_shelf()?;
1206 } else {
1207 self.hold_transit = None;
1208 self.reshelve_copy(true)?;
1209 self.clear_option("fake_hold_dest");
1210 }
1211
1212 let mut payload = eg::hash! {
1213 transit: self.transit.as_ref().unwrap().clone()
1214 };
1215
1216 if let Some(ht) = self.hold_transit.as_ref() {
1217 payload["holdtransit"] = ht.clone();
1218 }
1219
1220 let mut evt = EgEvent::success();
1221 evt.set_payload(payload);
1222 evt.set_ad_hoc_value("ishold", EgValue::from(self.hold.is_some()));
1223
1224 self.add_event(evt);
1225
1226 Ok(())
1227 }
1228
1229 fn checkin_handle_received_hold(&mut self) -> EgResult<()> {
1233 if self.hold_transit.is_none() && self.copy_status() != C::COPY_STATUS_ON_HOLDS_SHELF {
1234 return Ok(());
1236 }
1237
1238 let copy_id = self.copy_id;
1239
1240 let mut alt_hold;
1241 let hold = match self.hold.as_mut() {
1242 Some(h) => h,
1243 None => match holds::captured_hold_for_copy(self.editor(), copy_id)? {
1244 Some(h) => {
1245 alt_hold = Some(h);
1246 alt_hold.as_mut().unwrap()
1247 }
1248 None => {
1249 log::warn!("{self} item should be captured, but isn't, skipping");
1250 return Ok(());
1251 }
1252 },
1253 };
1254
1255 if !hold["cancel_time"].is_null() || !hold["fulfillment_time"].is_null() {
1256 self.reshelve_copy(false)?;
1258 self.clear_option("fake_hold_dest");
1259 return Ok(());
1260 }
1261
1262 if hold["hold_type"].as_str().unwrap() == "R" {
1263 self.update_copy(eg::hash! {status: C::COPY_STATUS_CATALOGING})?;
1265 self.clear_option("fake_hold_dest");
1266 self.set_option_true("noop");
1268
1269 let mut hold = self.hold.take().unwrap();
1270 let hold_id = hold.id()?;
1271 hold["fulfillment_time"] = EgValue::from("now");
1272 self.editor().update(hold)?;
1273
1274 self.hold = self.editor().retrieve("ahr", hold_id)?;
1275
1276 return Ok(());
1277 }
1278
1279 if self.get_option_bool("fake_hold_dest") {
1280 let hold = self.hold.as_mut().unwrap();
1281 hold["pickup_lib"] = EgValue::from(self.circ_lib);
1284
1285 return Ok(());
1286 }
1287
1288 Ok(())
1289 }
1290
1291 fn should_suppress_transit(&mut self, destination: i64, for_hold: bool) -> EgResult<bool> {
1296 if destination == self.circ_lib {
1297 return Ok(false);
1298 }
1299
1300 if for_hold && self.get_option_bool("hold_as_transit") {
1301 return Ok(false);
1302 }
1303
1304 let setting = if for_hold {
1305 "circ.transit.suppress_hold"
1306 } else {
1307 "circ.transit.suppress_non_hold"
1308 };
1309
1310 let suppress_for_here = self.settings.get_value(setting)?.clone();
1314 if suppress_for_here.is_null() {
1315 return Ok(false);
1316 }
1317
1318 let suppress_for_dest = self
1319 .settings
1320 .get_value_at_org(setting, self.circ_lib)?
1321 .clone();
1322 if suppress_for_dest.is_null() {
1323 return Ok(false);
1324 }
1325
1326 if suppress_for_here != suppress_for_dest {
1328 return Ok(false);
1329 }
1330
1331 Ok(true)
1332 }
1333
1334 fn put_hold_on_shelf(&mut self) -> EgResult<()> {
1336 let mut hold = self.hold.take().unwrap();
1337 let hold_id = hold.id()?;
1338
1339 hold["shelf_time"] = EgValue::from("now");
1340 hold["current_shelf_lib"] = EgValue::from(self.circ_lib);
1341
1342 if let Some(date) = holds::calc_hold_shelf_expire_time(self.editor(), &hold, None)? {
1343 hold["shelf_expire_time"] = EgValue::from(date);
1344 }
1345
1346 self.editor().update(hold)?;
1347 self.hold = self.editor().retrieve("ahr", hold_id)?;
1348
1349 Ok(())
1350 }
1351
1352 fn try_to_capture(&mut self) -> EgResult<bool> {
1354 if self.get_option_bool("remote_hold") {
1355 return Ok(false);
1356 }
1357
1358 if !self.is_booking_enabled() {
1359 return self.attempt_checkin_hold_capture();
1360 }
1361
1362 let maybe_hold = self.hold_capture_is_possible()?;
1367 let maybe_resv = self.reservation_capture_is_possible()?;
1368
1369 if let Some(hold) = maybe_hold {
1370 if let Some(resv) = maybe_resv {
1371 let mut evt = EgEvent::new("HOLD_RESERVATION_CONFLICT");
1373 evt.set_ad_hoc_value("hold", hold);
1374 evt.set_ad_hoc_value("reservation", resv);
1375 self.exit_err_on_event(evt)?;
1376 Ok(false)
1377 } else {
1378 self.attempt_checkin_hold_capture()
1380 }
1381 } else if maybe_resv.is_some() {
1382 self.attempt_checkin_reservation_capture()
1384 } else {
1385 Ok(false)
1387 }
1388 }
1389
1390 fn attempt_checkin_hold_capture(&mut self) -> EgResult<bool> {
1392 if self.capture_state() == "nocapture" {
1393 return Ok(false);
1394 }
1395
1396 let copy_id = self.copy_id;
1397
1398 let maybe_found = holds::find_nearest_permitted_hold(self.editor(), copy_id, false)?;
1399
1400 let (mut hold, retarget) = match maybe_found {
1401 Some(info) => info,
1402 None => {
1403 log::info!("{self} no permitted holds found for copy");
1404 return Ok(false);
1405 }
1406 };
1407
1408 if self.capture_state() != "capture" {
1409 if self.copy()["location"]["hold_verify"].boolish() {
1411 let mut evt = EgEvent::new("HOLD_CAPTURE_DELAYED");
1412 evt.set_ad_hoc_value("copy_location", self.copy()["location"].clone());
1413 self.exit_err_on_event(evt)?;
1414 }
1415 }
1416
1417 if !retarget.is_empty() {
1418 self.retarget_holds = Some(retarget);
1419 }
1420
1421 let pickup_lib = hold["pickup_lib"].int()?;
1422 let suppress_transit = self.should_suppress_transit(pickup_lib, true)?;
1423
1424 hold["hopeless_date"].take();
1425 hold["current_copy"] = EgValue::from(self.copy_id);
1426 hold["capture_time"] = EgValue::from("now");
1427
1428 hold["fulfillment_time"].take();
1430 hold["fulfillment_staff"].take();
1431 hold["fulfillment_lib"].take();
1432 hold["expire_time"].take();
1433 hold["cancel_time"].take();
1434
1435 if suppress_transit
1436 || (pickup_lib == self.circ_lib && !self.get_option_bool("hold_as_transit"))
1437 {
1438 self.hold = Some(hold);
1439 self.put_hold_on_shelf()?;
1441 } else {
1442 let hold_id = hold.id()?;
1443 self.editor().update(hold)?;
1444 self.hold = self.editor().retrieve("ahr", hold_id)?;
1445 }
1446
1447 Ok(true)
1448 }
1449
1450 fn attempt_checkin_reservation_capture(&mut self) -> EgResult<bool> {
1451 if self.capture_state() == "nocapture" {
1452 return Ok(false);
1453 }
1454
1455 let params = vec![
1456 EgValue::from(self.editor().authtoken()),
1457 self.copy()["barcode"].clone(),
1458 EgValue::from(true), ];
1460
1461 let result = self.editor().client_mut().send_recv_one(
1462 "open-ils.booking",
1463 "open-ils.booking.resources.capture_for_reservation",
1464 params,
1465 )?;
1466
1467 let resp = result.ok_or_else(|| "Booking capture failed to return event".to_string())?;
1468
1469 let mut evt = EgEvent::parse(&resp)
1470 .ok_or_else(|| "Booking capture failed to return event".to_string())?;
1471
1472 if evt.textcode() == "RESERVATION_NOT_FOUND" {
1473 if let Some(cause) = evt.payload()["fail_cause"].as_str() {
1474 if cause == "not-transferable" {
1475 log::warn!(
1476 "{self} reservation capture attempted against non-transferable item"
1477 );
1478 self.add_event(evt);
1479 return Ok(false);
1480 }
1481 }
1482 }
1483
1484 if !evt.is_success() {
1485 return Ok(false);
1487 }
1488
1489 log::info!("{self} booking capture succeeded");
1490
1491 if let Ok(stat) = evt.payload()["new_copy_status"].int() {
1492 self.update_copy(eg::hash! {"status": stat})?;
1493 }
1494
1495 let reservation = evt.payload_mut()["reservation"].take();
1496 if reservation.is_object() {
1497 self.reservation = Some(reservation);
1498 }
1499
1500 let transit = evt.payload_mut()["transit"].take();
1501 if transit.is_object() {
1502 let mut e = EgEvent::new("ROUTE_ITEM");
1503 e.set_org(transit["dest"].int()?);
1504 self.add_event(e);
1505 }
1506
1507 Ok(true)
1508 }
1509
1510 fn hold_capture_is_possible(&mut self) -> EgResult<Option<EgValue>> {
1513 if self.capture_state() == "nocapture" {
1514 return Ok(None);
1515 }
1516
1517 let copy_id = self.copy_id;
1518 let maybe_found =
1519 holds::find_nearest_permitted_hold(self.editor(), copy_id, true )?;
1520
1521 let (hold, retarget) = match maybe_found {
1522 Some(info) => info,
1523 None => {
1524 log::info!("{self} no permitted holds found for copy");
1525 return Ok(None);
1526 }
1527 };
1528
1529 if !retarget.is_empty() {
1530 self.retarget_holds = Some(retarget);
1531 }
1532
1533 Ok(Some(hold))
1534 }
1535
1536 fn reservation_capture_is_possible(&mut self) -> EgResult<Option<EgValue>> {
1539 if self.capture_state() == "nocapture" {
1540 return Ok(None);
1541 }
1542
1543 let params = vec![
1544 EgValue::from(self.editor().authtoken()),
1545 self.copy()["barcode"].clone(),
1546 ];
1547
1548 let result = self.editor().client_mut().send_recv_one(
1549 "open-ils.booking",
1550 "open-ils.booking.reservations.could_capture",
1551 params,
1552 )?;
1553
1554 if let Some(resp) = result {
1555 if let Some(evt) = EgEvent::parse(&resp) {
1556 self.exit_err_on_event(evt)?;
1557 } else {
1558 return Ok(Some(resp));
1559 }
1560 }
1561
1562 Ok(None)
1563 }
1564
1565 fn try_to_transit(&mut self) -> EgResult<()> {
1568 let mut dest_lib = self.copy_circ_lib();
1569
1570 let mut has_remote_hold = false;
1571 if let Some(hold) = self.options.get("remote_hold") {
1572 has_remote_hold = true;
1573 if let Ok(pl) = hold["pickup_lib"].int() {
1574 dest_lib = pl;
1575 }
1576 }
1577
1578 let suppress_transit = self.should_suppress_transit(dest_lib, false)?;
1579 let hold_as_transit = self.get_option_bool("hold_as_transit");
1580
1581 if suppress_transit || (dest_lib == self.circ_lib && !(has_remote_hold && hold_as_transit))
1582 {
1583 return self.checkin_handle_precat();
1585 }
1586
1587 let can_float = self.get_option_bool("can_float");
1588 let manual_float =
1589 self.get_option_bool("manual_float") || self.copy()["floating"]["manual"].boolish();
1590
1591 if can_float && manual_float && !has_remote_hold {
1592 self.update_copy(eg::hash! {"circ_lib": self.circ_lib})?;
1594 return Ok(());
1595 }
1596
1597 self.checkin_build_copy_transit(dest_lib)?;
1599 let mut evt = EgEvent::new("ROUTE_ITEM");
1600 evt.set_org(dest_lib);
1601 self.add_event(evt);
1602
1603 Ok(())
1604 }
1605
1606 fn checkin_handle_precat(&mut self) -> EgResult<()> {
1609 if !self.is_precat_copy() {
1610 return Ok(());
1611 }
1612
1613 if self.copy_status() != C::COPY_STATUS_CATALOGING {
1614 return Ok(());
1615 }
1616
1617 self.add_event_code("ITEM_NOT_CATALOGED");
1618
1619 self.update_copy(eg::hash! {"status": C::COPY_STATUS_CATALOGING})
1620 .map(|_| ())
1621 }
1622
1623 fn checkin_build_copy_transit(&mut self, dest_lib: i64) -> EgResult<()> {
1625 let mut transit = eg::hash! {
1626 "source": self.circ_lib,
1627 "dest": dest_lib,
1628 "target_copy": self.copy_id,
1629 "source_send_time": "now",
1630 "copy_status": self.copy_status(),
1631 };
1632
1633 let maybe_remote_hold = self.options.get("remote_hold");
1636 let has_remote_hold = maybe_remote_hold.is_some();
1637
1638 if let Some(hold) = maybe_remote_hold.as_ref() {
1639 transit["hold"] = hold["id"].clone();
1640
1641 if !hold["current_shelf_lib"].is_null() || !hold["shelf_time"].is_null() {
1643 let mut h = (*hold).clone();
1644 h["current_shelf_lib"].take();
1645 h["shelf_time"].take();
1646 self.editor().update(h)?;
1647 }
1648 }
1649
1650 log::info!("{self} transiting copy to {dest_lib}");
1651
1652 if has_remote_hold {
1653 let t = EgValue::create("ahtc", transit)?;
1654 let t = self.editor().create(t)?;
1655 self.hold_transit = self.editor().retrieve("ahtc", t["id"].clone())?;
1656 } else {
1657 let t = EgValue::create("atc", transit)?;
1658 let t = self.editor().create(t)?;
1659 self.transit = self.editor().retrieve("ahtc", t["id"].clone())?;
1660 }
1661
1662 self.update_copy(eg::hash! {"status": C::COPY_STATUS_IN_TRANSIT})?;
1663 Ok(())
1664 }
1665
1666 fn finish_fines_and_voiding(&mut self) -> EgResult<()> {
1669 let void_overdues = self.get_option_bool("void_overdues");
1670 let mut backdate_maybe = match self.options.get("backate") {
1671 Some(bd) => bd.as_str().map(|d| d.to_string()),
1672 None => None,
1673 };
1674
1675 let circ_id = match self.circ.as_ref() {
1676 Some(c) => c.id()?,
1677 None => return Ok(()),
1678 };
1679
1680 if !void_overdues && backdate_maybe.is_none() {
1681 return Ok(());
1682 }
1683
1684 let mut note_maybe = None;
1685
1686 if void_overdues {
1687 note_maybe = Some("System: Amnesty Checkin");
1688 backdate_maybe = None;
1689 }
1690
1691 billing::void_or_zero_overdues(
1692 self.editor(),
1693 circ_id,
1694 backdate_maybe.as_deref(),
1695 note_maybe,
1696 false,
1697 false,
1698 )?;
1699
1700 billing::check_open_xact(self.editor(), circ_id)
1701 }
1702
1703 fn flesh_checkin_events(&mut self) -> EgResult<()> {
1706 let mut copy = self.copy.take().unwrap().take(); let copy_id = self.copy_id;
1708 let record_id = copy["call_number"]["record"].int()?;
1709
1710 let volume = copy["call_number"].take();
1712 copy["call_number"] = volume["id"].clone();
1713
1714 copy.deflesh()?;
1716
1717 let mut payload = eg::hash! {
1718 "copy": copy,
1719 "volume": volume,
1720 };
1721
1722 if !self.is_precat_copy() {
1723 if let Some(rec) = self.editor().retrieve("rmsr", record_id)? {
1724 payload["title"] = rec;
1725 }
1726 }
1727
1728 if let Some(mut hold) = self.hold.take() {
1729 if hold["cancel_time"].is_null() {
1730 hold["notes"] = EgValue::from(
1731 self.editor()
1732 .search("ahrn", eg::hash! {hold: hold["id"].clone()})?,
1733 );
1734 payload["hold"] = hold;
1735 }
1736 }
1737
1738 if let Some(circ) = self.circ.as_ref() {
1739 let flesh = eg::hash! {
1740 "flesh": 1,
1741 "flesh_fields": {
1742 "circ": ["billable_transaction"],
1743 "mbt": ["summary"]
1744 }
1745 };
1746
1747 let circ_id = circ["id"].clone();
1748
1749 if let Some(fcirc) = self.editor().retrieve_with_ops("circ", circ_id, flesh)? {
1750 payload["circ"] = fcirc;
1751 }
1752 }
1753
1754 if let Some(patron) = self.patron.as_ref() {
1755 let flesh = eg::hash! {
1756 "flesh": 1,
1757 "flesh_fields": {
1758 "au": ["card", "billing_address", "mailing_address"]
1759 }
1760 };
1761
1762 let patron_id = patron["id"].clone();
1763
1764 if let Some(fpatron) = self.editor().retrieve_with_ops("au", patron_id, flesh)? {
1765 payload["patron"] = fpatron;
1766 }
1767 }
1768
1769 if let Some(reservation) = self.reservation.take() {
1770 payload["reservation"] = reservation;
1771 }
1772
1773 if let Some(transit) = self.hold_transit.take().or(self.transit.take()) {
1774 payload["transit"] = transit;
1775 }
1776
1777 let query = eg::hash! {"copy": copy_id};
1778 let flesh = eg::hash! {
1779 "flesh": 1,
1780 "flesh_fields": {
1781 "alci": ["inventory_workstation"]
1782 }
1783 };
1784
1785 if let Some(inventory) = self.editor().search_with_ops("alci", query, flesh)?.pop() {
1786 payload["copy"]["latest_inventory"] = inventory;
1787 }
1788
1789 if self.events.is_empty() {
1791 self.events.push(EgEvent::new("NO_CHANGE"));
1792 }
1793
1794 for (idx, evt) in self.events.iter_mut().enumerate() {
1796 if idx > 0 {
1797 evt.set_payload(payload.clone());
1798 }
1799 }
1800
1801 self.events[0].set_payload(payload);
1804
1805 Ok(())
1806 }
1807
1808 pub fn hold_revert_sanity_checks(&mut self) -> EgResult<()> {
1811 if !self.get_option_bool("revert_hold_fulfillment") {
1812 return Ok(());
1813 }
1814
1815 if self.circ.is_some()
1816 && self.copy.is_some()
1817 && self.copy_status() == C::COPY_STATUS_CHECKED_OUT
1818 && self.patron.is_some()
1819 && !self.is_renewal()
1820 {
1821 return Ok(());
1822 }
1823
1824 log::warn!("{self} hold-revert requested but makes no sense");
1825
1826 Err(EgEvent::new("NO_CHANGE").into())
1829 }
1830
1831 fn revert_hold_fulfillment(&mut self) -> EgResult<bool> {
1833 if !self.get_option_bool("revert_hold_fulfillment") {
1834 return Ok(false);
1835 }
1836
1837 let query = eg::hash! {
1838 "usr": self.patron.as_ref().unwrap()["id"].clone(),
1839 "cancel_time": EgValue::Null,
1840 "fulfillment_time": {"!=": EgValue::Null},
1841 "current_copy": self.copy()["id"].clone(),
1842 };
1843
1844 let ops = eg::hash! {
1845 "order_by": {
1846 "ahr": "fulfillment_time desc"
1847 },
1848 "limit": 1
1849 };
1850
1851 let mut hold = match self.editor().search_with_ops("ahr", query, ops)?.pop() {
1852 Some(h) => h,
1853 None => return Ok(false),
1854 };
1855
1856 let xact_date =
1859 date::parse_datetime(self.circ.as_ref().unwrap()["xact_start"].as_str().unwrap())?;
1860
1861 let ff_date = date::parse_datetime(
1862 self.hold.as_ref().unwrap()["fulfillment_time"]
1863 .as_str()
1864 .unwrap(),
1865 )?;
1866
1867 if xact_date.timestamp() != ff_date.timestamp() {
1871 return Ok(false);
1872 }
1873
1874 log::info!("{self} undoing fulfillment for hold {}", hold["id"]);
1875
1876 hold["fulfillment_time"].take();
1877 hold["fulfillment_staff"].take();
1878 hold["fulfillment_lib"].take();
1879
1880 self.editor().update(hold)?;
1881
1882 self.update_copy(eg::hash! {"status": C::COPY_STATUS_ON_HOLDS_SHELF})?;
1883
1884 Ok(true)
1885 }
1886}