1 package OpenILS::Application::Booking;
6 use POSIX qw/strftime/;
7 use OpenILS::Application;
8 use base qw/OpenILS::Application/;
10 use OpenILS::Utils::DateTime qw/:datetime/;
11 use OpenILS::Utils::CStoreEditor qw/:funcs/;
12 use OpenILS::Utils::Fieldmapper;
13 use OpenILS::Application::AppUtils;
14 my $U = "OpenILS::Application::AppUtils";
16 use OpenSRF::Utils::Logger qw/$logger/;
19 my ($record_id, $owning_lib, $mvr) = @_;
21 my $brt = new Fieldmapper::booking::resource_type;
23 $brt->name($mvr->title);
24 $brt->record($record_id);
25 $brt->catalog_item('t');
26 $brt->transferable('t');
27 $brt->owner($owning_lib);
32 sub get_existing_brt {
33 my ($e, $record_id, $owning_lib, $mvr) = @_;
34 my $results = $e->search_booking_resource_type(
35 {name => $mvr->title, owner => $owning_lib, record => $record_id}
38 return $results->[0] if scalar(@$results) > 0;
45 'open-ils.search.biblio.record.mods_slim.retrieve.authoritative',
50 sub get_unique_owning_libs {
52 $hash{$_->call_number->owning_lib} = 1 foreach (@_); # @_ are copies
56 sub fetch_copies_by_ids {
57 my ($e, $copy_ids) = @_;
58 my $results = $e->search_asset_copy([
60 {flesh => 1, flesh_fields => {acp => ['call_number']}}
62 return $results if ref($results) eq 'ARRAY';
66 sub get_single_record_id {
67 my $record_id = undef;
68 foreach (@_) { # @_ are copies
70 (defined $record_id && $record_id != $_->call_number->record);
71 $record_id = $_->call_number->record;
76 # This function generates the correct json_query clause for determining
77 # whether two given ranges overlap. Each range is composed of a start
78 # and an end point. All four points should be the same type (could be int,
79 # date, time, timestamp, or perhaps other types).
81 # The first range (or the first two points) should be specified as
82 # literal values. The second range (or the last two points) should be
83 # specified as the names of columns, the values of which in a given row
84 # will constitute the second range in the comparison.
86 # ALSO: PostgreSQL includes an OVERLAPS operator which provides the same
87 # functionality in a much more concise way, but json_query does not (yet).
88 sub json_query_ranges_overlap {
90 { '-and' => [{$_[2] => {'>=', $_[0]}}, {$_[2] => {'<', $_[1]}}]},
91 { '-and' => [{$_[3] => {'>', $_[0]}}, {$_[3] => {'<', $_[1]}}]},
92 { '-and' => { $_[3] => {'>', $_[0]}, $_[2] => {'<=', $_[0]}}},
93 { '-and' => { $_[3] => {'>', $_[1]}, $_[2] => {'<', $_[1]}}},
97 sub create_brt_and_brsrc {
98 my ($self, $conn, $authtoken, $copy_ids) = @_;
99 my (@created_brt, @created_brsrc);
102 my $e = new_editor(xact => 1, authtoken => $authtoken);
103 return $e->die_event unless $e->checkauth;
105 my @copies = @{fetch_copies_by_ids($e, $copy_ids)};
106 my $record_id = get_single_record_id(@copies) or return $e->die_event;
107 my $mvr = get_mvr($record_id) or return $e->die_event;
109 foreach (get_unique_owning_libs(@copies)) {
110 $brt_table{$_} = get_existing_brt($e, $record_id, $_, $mvr) ||
111 prepare_new_brt($record_id, $_, $mvr);
114 while (my ($owning_lib, $brt) = each %brt_table) {
115 my $pre_existing = 1;
117 if ($e->allowed('ADMIN_BOOKING_RESOURCE_TYPE', $owning_lib)) {
119 return $e->die_event unless (
120 # v-- Important: assignment modifies original hash
121 $brt = $e->create_booking_resource_type($brt)
125 push @created_brt, [$brt->id, $brt->record, $pre_existing];
130 'ADMIN_BOOKING_RESOURCE', $_->call_number->owning_lib
132 # This block needs to disregard any cstore failures and just
133 # return what results it can.
134 my $brsrc = new Fieldmapper::booking::resource;
136 $brsrc->type($brt_table{$_->call_number->owning_lib}->id);
137 $brsrc->owner($_->call_number->owning_lib);
138 $brsrc->barcode($_->barcode);
140 $e->set_savepoint("alpha");
141 my $pre_existing = 0;
142 my $usable_result = undef;
143 if (!($usable_result = $e->create_booking_resource($brsrc))) {
144 $e->rollback_savepoint("alpha");
145 if (($usable_result = $e->search_booking_resource(
146 +{ map { ($_, $brsrc->$_()) } qw/type owner barcode/ }
148 $usable_result = $usable_result->[0];
151 # So we failed to create a booking resource for this copy.
152 # For now, let's just keep going. If the calling app wants
153 # to consider this an error, it can notice the absence
154 # of a booking resource for the copy in the returned
157 "Couldn't create or find brsrc for acp #" . $_->id
161 $e->release_savepoint("alpha");
164 if ($usable_result) {
166 [$usable_result->id, $_->id, $pre_existing];
172 return {brt => \@created_brt, brsrc => \@created_brsrc} or
173 return $e->die_event;
175 __PACKAGE__->register_method(
176 method => "create_brt_and_brsrc",
177 api_name => "open-ils.booking.resources.create_from_copies",
180 {type => 'string', desc => 'Authentication token'},
181 {type => 'array', desc => 'Copy IDs'},
183 return => { desc => "A two-element hash. The 'brt' element " .
184 "is a list of created booking resource types described by " .
185 "3-tuples (id, copy id, was pre-existing). The 'brsrc' " .
186 "element is a similar list of created booking resources " .
187 "described by (id, record id, was pre-existing) 3-tuples."}
193 my ($self, $client, $authtoken,
194 $target_user_barcode, $datetime_range, $pickup_lib,
195 $brt, $brsrc_list, $attr_values, $email_notify, $note) = @_;
197 $brsrc_list = [ undef ] if not defined $brsrc_list;
198 return undef if scalar(@$brsrc_list) < 1; # Empty list not ok.
200 my $e = new_editor(xact => 1, authtoken => $authtoken);
201 return $e->die_event unless $e->checkauth;
202 return $e->die_event unless $e->allowed("ADMIN_BOOKING_RESERVATION");
204 my $usr = $U->fetch_user_by_barcode($target_user_barcode);
205 return $usr if ref($usr) eq 'HASH' and exists($usr->{"ilsevent"});
207 my $filters = { type => $brt, available => $datetime_range, pickup_lib => $pickup_lib };
208 if ( defined $brsrc_list->[0] ) {
209 $filters->{'resources'} = $brsrc_list;
211 my $available_resources = resource_list_by_attrs($self, $client, $authtoken, $filters);
212 unless (scalar @{$available_resources}) {
213 my $ev = OpenILS::Event->new(
215 desc => 'Resource is in use at this time'
221 foreach my $brsrc (@$brsrc_list) {
222 my $bresv = new Fieldmapper::booking::reservation;
223 $bresv->usr($usr->id);
224 $bresv->request_lib($e->requestor->ws_ou);
225 $bresv->pickup_lib($pickup_lib);
226 $bresv->start_time($datetime_range->[0]);
227 $bresv->end_time($datetime_range->[1]);
228 $bresv->email_notify(1) if $email_notify;
229 $bresv->note($note) if $note;
231 # A little sanity checking: don't agree to put a reservation on a
232 # brsrc and a brt when they don't match. In fact, bomb out of
233 # this transaction entirely.
235 my $brsrc_itself = $e->retrieve_booking_resource([
238 "flesh_fields" => {"brsrc" => ["type"]}
242 if (not $brsrc_itself) {
243 my $ev = new OpenILS::Event(
244 "RESERVATION_BAD_PARAMS",
245 desc => "brsrc $brsrc doesn't exist"
250 elsif ($brsrc_itself->type->id != $brt) {
251 my $ev = new OpenILS::Event(
252 "RESERVATION_BAD_PARAMS",
253 desc => "brsrc $brsrc doesn't match given brt $brt"
259 # Also bail if the user is trying to create a reservation at
260 # a pickup lib to which our resource won't go.
262 $brsrc_itself->owner != $pickup_lib and
263 not $brsrc_itself->type->transferable
265 my $ev = new OpenILS::Event(
266 "RESERVATION_BAD_PARAMS",
267 desc => "brsrc $brsrc doesn't belong to $pickup_lib and " .
268 "is not transferable"
274 $bresv->target_resource($brsrc); # undef is ok here
275 $bresv->target_resource_type($brt);
277 ($bresv = $e->create_booking_reservation($bresv)) or
278 return $e->die_event;
280 # We could/should do some sanity checking on this too: namely, on
281 # whether the attribute values given actually apply to the relevant
282 # brt. Not seeing any grievous side effects of not checking, though.
284 foreach my $value (@$attr_values) {
285 my $bravm = new Fieldmapper::booking::reservation_attr_value_map;
286 $bravm->reservation($bresv->id);
287 $bravm->attr_value($value);
288 $bravm = $e->create_booking_reservation_attr_value_map($bravm) or
289 return $e->die_event;
293 "bresv" => $bresv->id,
298 $e->commit or return $e->die_event;
300 # Targeting must be tacked on _after_ committing the transaction where the
301 # reservations are actually created.
302 foreach (@$results) {
303 $_->{"targeting"} = $U->storagereq(
304 "open-ils.storage.booking.reservation.resource_targeter",
310 __PACKAGE__->register_method(
311 method => "create_bresv",
312 api_name => "open-ils.booking.reservations.create",
315 {type => 'string', desc => 'Authentication token'},
316 {type => 'string', desc => 'Barcode of user for whom to reserve'},
317 {type => 'array', desc => 'Two elements: start and end timestamp'},
318 {type => 'int', desc => 'Desired reservation pickup lib'},
319 {type => 'int', desc => 'Booking resource type'},
320 {type => 'list', desc => 'Booking resource (undef ok; empty not ok)'},
321 {type => 'array', desc => 'Attribute values selected'},
322 {type => 'bool', desc => 'Email notification?'},
323 {type => 'string', desc => 'Optional note'},
325 return => { desc => "A hash containing the new bresv and a list " .
331 sub resource_list_by_attrs {
334 my $auth = shift; # Keep as argument, though not used just now.
337 return undef unless ($filters->{type} || $filters->{attribute_values});
340 "select" => {brsrc => [qw/id owner/], brt => ["elbow_room"]},
341 "from" => {brsrc => {"brt" => {}}},
346 $query->{where} = {"-and" => []};
347 if ($filters->{type}) {
348 push @{$query->{where}->{"-and"}}, {"type" => $filters->{type}};
351 if ($filters->{resources}) {
352 push @{$query->{where}->{'-and'}}, {'id' => $filters->{resources}};
355 if ($filters->{pickup_lib}) {
356 push @{$query->{where}->{"-and"}},
358 {"owner" => $filters->{pickup_lib}},
359 {"+brt" => {"transferable" => "t"}}
363 if ($filters->{attribute_values}) {
365 $query->{from}->{brsrc}->{bram} = { field => 'resource' };
367 $filters->{attribute_values} = [$filters->{attribute_values}]
368 if (!ref($filters->{attribute_values}));
370 $query->{having}->{'+bram'}->{value}->{'@>'} = {
371 transform => 'array_agg',
372 value => '$_' . $$ . '${' .
373 join(',', @{$filters->{attribute_values}}) .
378 if ($filters->{available}) {
379 # If only one timestamp has been provided, make it into a range.
380 if (!ref($filters->{available})) {
381 $filters->{available} = [($filters->{available}) x 2];
384 push @{$query->{where}->{"-and"}}, {
388 "select" => {"bresv" => ["id"]},
390 "where" => {"-and" => [
391 json_query_ranges_overlap(
392 $filters->{available}->[0],
393 $filters->{available}->[1],
397 {"cancel_time" => undef},
398 {"return_time" => undef},
399 {"current_resource" => {"=" => {"+brsrc" => "id"}}}
405 if ($filters->{booked}) {
406 # If only one timestamp has been provided, make it into a range.
407 if (!ref($filters->{booked})) {
408 $filters->{booked} = [($filters->{booked}) x 2];
411 push @{$query->{where}->{"-and"}}, {
413 "select" => {"bresv" => ["id"]},
415 "where" => {"-and" => [
416 json_query_ranges_overlap(
417 $filters->{booked}->[0],
418 $filters->{booked}->[1],
422 {"cancel_time" => undef},
423 {"current_resource" => { "=" => {"+brsrc" => "id"}}}
427 # I think that the "booked" case could be done with a JOIN instead of
428 # an EXISTS, but I'm leaving it this way for symmetry with the
429 # "available" case for now. The available case cannot be done with a
433 my $cstore = OpenSRF::AppSession->connect('open-ils.cstore');
434 my $rows = $cstore->request(
435 "open-ils.cstore.json_query.atomic", $query
439 return [] if not @$rows;
441 if ($filters->{"pickup_lib"} && $filters->{"available"}) {
443 my $general_elbow_room = $U->ou_ancestor_setting_value(
444 $filters->{"pickup_lib"},
445 "circ.booking_reservation.default_elbow_room"
447 my $would_start = $filters->{"available"}->[0];
448 my $dt_parser = new DateTime::Format::ISO8601;
451 "general_elbow_room: '$general_elbow_room', " .
452 "would_start: '$would_start'"
455 # Here, elbow_room will double as required transit time padding.
457 my $elbow_room = $_->{"elbow_room"} || $general_elbow_room;
458 if ($_->{"owner"} != $filters->{"pickup_lib"}) {
459 (my $ws = $would_start) =~ s/ /T/;
460 push @new_rows, $_ if DateTime->compare(
461 $dt_parser->parse_datetime($ws),
463 "time_zone" => DateTime::TimeZone->new(
466 )->add(seconds => interval_to_seconds($elbow_room))
472 return [map { $_->{id} } @new_rows];
474 return [map { $_->{id} } @$rows];
479 __PACKAGE__->register_method(
480 method => "resource_list_by_attrs",
481 api_name => "open-ils.booking.resources.filtered_id_list",
485 {type => 'string', desc => 'Authentication token (unused for now,' .
486 ' but at least pass undef here)'},
487 {type => 'object', desc => 'Filter object: see notes for details'},
489 return => { desc => "An array of brsrc ids matching the requested filters." },
493 The filter object parameter can contain the following keys:
494 * type => The id of a booking resource type (brt)
495 * resources => The ids of specific resources to choose amongst
496 * pickup_lib => The relevant pickup library. When paired with the available filter,
497 it will check if the resource lives at the pickup_lib or if it is
498 allowed to transit to the pickup_lib.
499 * attribute_values => The ids of booking resource type attribute values that the resource must have assigned to it (brav)
500 * available => Either:
501 A timestamp during which the resources are not reserved. If the resource is overbookable, this is ignored.
502 A range of two timestamps which do not overlap any reservations for the resources. If the resource is overbookable, this is ignored.
504 A timestamp during which the resources are reserved.
505 A range of two timestamps which overlap a reservation of the resources.
507 Note that at least one of 'type' or 'attribute_values' is required.
513 sub reservation_list_by_filters {
518 my $whole_obj = shift;
520 return undef unless (
522 || $filters->{user_barcode}
523 || $filters->{resource}
525 || $filters->{attribute_values}
528 my $e = new_editor(authtoken=>$auth);
529 return $e->event unless $e->checkauth;
530 return $e->event unless $e->allowed('VIEW_TRANSACTION');
533 'select' => { bresv => [ 'id', 'start_time' ] },
534 'from' => { bresv => {} },
536 'order_by' => [{ class => bresv => field => start_time => direction => 'asc' }],
540 if ($filters->{fields}) {
541 $query->{where} = $filters->{fields};
545 if ($filters->{user}) {
546 $query->{where}->{usr} = $filters->{user};
548 elsif ($filters->{user_barcode}) { # just one of user and user_barcode
549 my $usr = $U->fetch_user_by_barcode($filters->{user_barcode});
550 return $usr if ref($usr) eq 'HASH' and exists($usr->{"ilsevent"});
551 $query->{where}->{usr} = $usr->id;
555 if ($filters->{type}) {
556 $query->{where}->{target_resource_type} = $filters->{type};
559 $query->{where}->{"-and"} = [];
560 if ($filters->{resource}) {
561 # $query->{where}->{target_resource} = $filters->{resource};
562 push @{$query->{where}->{"-and"}}, {
564 "target_resource" => $filters->{resource},
565 "current_resource" => $filters->{resource}
570 if ($filters->{attribute_values}) {
572 $query->{from}->{bresv}->{bravm} = { field => 'reservation' };
574 $filters->{attribute_values} = [$filters->{attribute_values}]
575 if (!ref($filters->{attribute_values}));
577 $query->{having}->{'+bravm'}->{attr_value}->{'@>'} = {
578 transform => 'array_agg',
579 value => '$_' . $$ . '${' .
580 join(',', @{$filters->{attribute_values}}) .
585 if ($filters->{search_start} || $filters->{search_end}) {
589 {'between' => [ $filters->{search_start}, $filters->{search_end}]}
590 if $filters->{search_start};
593 {'between' =>[$filters->{search_start}, $filters->{search_end}]}
594 if $filters->{search_end};
596 push @{$query->{where}->{"-and"}}, {"-or" => $or};
599 if (not scalar @{$query->{"where"}->{"-and"}}) {
600 delete $query->{"where"}->{"-and"};
603 my $cstore = OpenSRF::AppSession->connect('open-ils.cstore');
604 my $ids = [ map { $_->{id} } @{
606 'open-ils.cstore.json_query.atomic', $query
611 if (not $whole_obj or @$ids < 1) {
616 my $bresv_list = $e->search_booking_reservation([
621 [qw/target_resource current_resource target_resource_type/]
626 return $bresv_list ? $bresv_list : [];
629 __PACKAGE__->register_method(
630 method => "upcoming_reservation_list_by_user",
631 api_name => "open-ils.booking.reservations.upcoming_reservation_list_by_user",
635 {type => 'string', desc => 'Authentication token'},
636 {type => 'User ID', type => 'number', desc => 'User ID'},
638 return => { desc => "Information about all reservations for a user that haven't yet ended." },
640 notes => "You can send undef/NULL as the User ID to get reservations for the logged in user."
643 sub upcoming_reservation_list_by_user {
644 my ($self, $conn, $auth, $user_id) = @_;
645 my $e = new_editor(authtoken => $auth);
646 return $e->event unless $e->checkauth;
648 $user_id = $e->requestor->id unless defined $user_id;
650 unless($e->requestor->id == $user_id) {
651 my $user = $e->retrieve_actor_user($user_id) or return $e->event;
652 return $e->event unless $e->allowed('VIEW_TRANSACTION');
655 my $select = { 'bresv' => [qw/start_time end_time cancel_time capture_time pickup_time pickup_lib/],
656 'brsrc' => [ 'barcode' ],
657 'brt' => [{'column' => 'name', 'alias' => 'resource_type_name'}],
658 'aou' => ['shortname', {'column' => 'name', 'alias' => 'pickup_name'}] };
660 my $from = { 'bresv' => {'brsrc' => {'field' => 'id', 'fkey' => 'current_resource'},
661 'brt' => {'field' => 'id', 'fkey' => 'target_resource_type'},
662 'aou' => {'field' => 'id', 'fkey' => 'pickup_lib'}} };
667 'where' => { 'usr' => $user_id, 'return_time' => undef, 'end_time' => {'>' => gmtime_ISO8601() }},
668 'order_by' => [{ class => bresv => field => start_time => direction => 'asc' }]
671 my $cstore = OpenSRF::AppSession->connect('open-ils.cstore');
672 my $rows = $cstore->request(
673 'open-ils.cstore.json_query.atomic', $query
677 return [] if not @$rows;
681 __PACKAGE__->register_method(
682 method => "reservation_list_by_filters",
683 api_name => "open-ils.booking.reservations.filtered_id_list",
687 {type => 'string', desc => 'Authentication token'},
688 {type => "object", desc => "Filter object: see notes for details"},
689 {type => "bool", desc => "Return whole object instead of ID? (default false)"}
691 return => { desc => "An array of bresv ids matching the requested filters." },
695 The filter object parameter can contain the following keys:
696 * user => The id of a user that has requested a bookable item -- filters on bresv.usr
697 * barcode => The barcode of a user that has requested a bookable item
698 * type => The id of a booking resource type (brt) -- filters on bresv.target_resource_type
699 * resource => The id of a booking resource (brsrc) -- filters on bresv.target_resource
700 * attribute_values => The ids of booking resource type attribute values that the resource must have assigned to it (brav)
701 * search_start => If search_end is not specified, booking interval (start_time to end_time) must contain this timestamp.
702 * search_end => If search_start is not specified, booking interval (start_time to end_time) must contain this timestamp.
703 * fields => An object containing any combination of bresv search filters in standard cstore/pcrud search format.
705 Note that at least one of 'user', 'type', 'resource' or 'attribute_values' is required. If both search_start and search_end are specified,
706 then the result includes any reservations that overlap with that time range. Any filter fields supplied in 'fields' are overridden
707 by the top-level filters ('user', 'type', 'resource').
713 sub naive_ts_string {strftime("%F %T", localtime($_[0] || time));}
715 # Return a map of bresv or an ilsevent on failure.
716 sub get_uncaptured_bresv_for_brsrc {
717 my ($e, $o) = @_; # o's keys (all optional): owning_lib, barcode, range
721 "brsrc" => {"field" => "id", "fkey" => "current_resource"}
730 "column" => "start_time",
731 "transform" => "min",
736 "from" => $from_clause,
739 {"current_resource" => {"!=" => undef}},
740 {"capture_time" => undef},
741 {"cancel_time" => undef},
742 {"return_time" => undef},
743 {"pickup_time" => undef}
747 if ($o->{"owning_lib"}) {
748 push @{$query->{"where"}->{"-and"}},
749 {"+brsrc" => {"owner" => $o->{"owning_lib"}}};
752 push @{$query->{"where"}->{"-and"}},
753 json_query_ranges_overlap(
754 $o->{"range"}->[0], $o->{"range"}->[1],
755 "start_time", "end_time"
758 if ($o->{"barcode"}) {
759 push @{$query->{"where"}->{"-and"}},
760 {"+brsrc" => {"barcode" => $o->{"barcode"}}};
763 my $rows = $e->json_query($query);
764 my $current_resource_bresv_map = {};
767 "select" => {"bresv" => ["id"]},
768 "from" => $from_clause,
771 {"current_resource" => "PLACEHOLDER"},
772 {"start_time" => "PLACEHOLDER"},
773 {"capture_time" => undef},
774 {"cancel_time" => undef},
775 {"return_time" => undef},
776 {"pickup_time" => undef}
780 if ($o->{"owning_lib"}) {
781 push @{$id_query->{"where"}->{"-and"}},
782 {"+brsrc" => {"owner" => $o->{"owning_lib"}}};
786 $id_query->{"where"}->{"-and"}->[0]->{"current_resource"} =
787 $_->{"current_resource"};
788 $id_query->{"where"}->{"-and"}->[1]->{"start_time"} =
791 my $results = $e->json_query($id_query);
792 if ($results && @$results) {
793 $current_resource_bresv_map->{$_->{"current_resource"}} =
794 [map { $_->{"id"} } @$results];
798 return $current_resource_bresv_map;
802 my ($self, $client, $auth, $range, $interval_secs, $owning_lib) = @_;
804 my $e = new_editor(xact => 1, authtoken => $auth);
805 return $e->die_event unless $e->checkauth;
806 return $e->die_event unless $e->allowed("RETRIEVE_RESERVATION_PULL_LIST");
807 return $e->die_event unless (
808 ref($range) eq "ARRAY" or
809 ($interval_secs = int($interval_secs)) > 0
812 $owning_lib = $e->requestor->ws_ou if not $owning_lib;
813 $range = [ naive_ts_string(time), naive_ts_string(time + $interval_secs) ]
816 my $uncaptured = get_uncaptured_bresv_for_brsrc(
817 $e, {"range" => $range, "owning_lib" => $owning_lib}
820 if (keys(%$uncaptured)) {
821 my @all_bresv_ids = map { @{$_} } values %$uncaptured;
823 map { $_->id => $_ } @{
824 $e->search_booking_reservation([{"id" => [@all_bresv_ids]}, {
826 flesh_fields => { bresv => [
827 "usr", "target_resource_type", "current_resource"
835 my $one = $bresv_lookup{$uncaptured->{$key}->[0]};
837 "current_resource" => $one->current_resource,
838 "target_resource_type" => $one->target_resource_type,
840 map { $bresv_lookup{$_} } @{$uncaptured->{$key}}
843 foreach (@{$result->{"reservations"}}) { # deflesh
844 $_->current_resource($_->current_resource->id);
845 $_->target_resource_type($_->target_resource_type->id);
848 } keys %$uncaptured ];
854 __PACKAGE__->register_method(
855 method => "get_pull_list",
856 api_name => "open-ils.booking.reservations.get_pull_list",
860 {type => "string", desc => "Authentication token"},
861 {type => "array", desc =>
862 "range: Date/time range for reservations (opt)"},
863 {type => "int", desc =>
864 "interval: Seconds from now (instead of range)"},
865 {type => "number", desc => "(Optional) Owning library"}
867 return => { desc => "An array of hashes, each containing key/value " .
868 "pairs describing resource, resource type, and a list of " .
869 "reservations that claim the given resource." }
875 my ($self, $client, $auth, $barcode) = @_;
877 my $e = new_editor("authtoken" => $auth);
878 return $e->die_event unless $e->checkauth;
879 return $e->die_event unless $e->allowed("COPY_CHECKIN");
881 my $dt_parser = new DateTime::Format::ISO8601;
882 my $now = now DateTime; # sic
883 my $res = get_uncaptured_bresv_for_brsrc($e, {"barcode" => $barcode});
885 if ($res and keys %$res) {
887 while ((undef, $id) = each %$res) {
888 my $bresv = $e->retrieve_booking_reservation([
890 "flesh" => 1, "flesh_fields" => {
892 usr target_resource_type
893 target_resource current_resource
898 my $elbow_room = interval_to_seconds(
899 $bresv->target_resource_type->elbow_room ||
900 $U->ou_ancestor_setting_value(
902 "circ.booking_reservation.default_elbow_room"
907 unless ($elbow_room) {
908 $client->respond($bresv);
910 my $start_time = $dt_parser->parse_datetime(
911 clean_ISO8601($bresv->start_time)
914 if ($now >= $start_time->subtract("seconds" => $elbow_room)) {
915 $client->respond($bresv);
918 "not within elbow room: $elbow_room, " .
919 "else would have returned bresv " . $bresv->id
928 __PACKAGE__->register_method(
929 method => "could_capture",
930 api_name => "open-ils.booking.reservations.could_capture",
935 {type => "string", desc => "Authentication token"},
936 {type => "string", desc => "Resource barcode"}
938 return => {desc => "One or zero reservations; event on error."}
943 sub get_copy_fleshed_just_right {
944 my ($self, $client, $auth, $barcode) = @_;
946 return undef if not defined $barcode;
947 return {} if ref($barcode) eq "ARRAY" and not @$barcode;
949 my $e = new_editor(authtoken => $auth);
950 my $results = $e->search_asset_copy([
951 {"barcode" => $barcode},
954 "flesh_fields" => {"acp" => [qw/call_number location/]}
958 if (ref($results) eq "ARRAY") {
960 return $results->[0] unless ref $barcode;
961 return +{ map { $_->barcode => $_ } @$results };
963 return $e->die_event;
966 __PACKAGE__->register_method(
967 method => "get_copy_fleshed_just_right",
968 api_name => "open-ils.booking.asset.get_copy_fleshed_just_right",
972 {type => "string", desc => "Authentication token"},
973 {type => "mixed", desc => "One barcode or an array of them"},
976 "A copy, or a hash of copies keyed by barcode if an array of " .
983 sub best_bresv_candidate {
984 my ($e, $id_list) = @_;
986 # This will almost always be the case.
987 if (@$id_list == 1) {
988 $logger->info("best_bresv_candidate (only) " . $id_list->[0]);
989 return $id_list->[0];
993 my $this_ou = $e->requestor->ws_ou;
994 my $results = $e->json_query({
995 "select" => {"brsrc" => ["pickup_lib"], "bresv" => ["id"]},
998 "brsrc" => {"field" => "id", "fkey" => "current_resource"}
1002 {"+bresv" => {"id" => $id_list}}
1006 foreach (@$results) {
1007 push @here, $_->{"id"} if $_->{"pickup_lib"} == $this_ou;
1012 $result = @here == 1 ? pop @here : (sort @here)[0];
1014 $result = (sort @$id_list)[0];
1017 "best_bresv_candidate from " . join(",", @$id_list) . ": $result"
1023 sub capture_resource_for_reservation {
1024 my ($self, $client, $auth, $barcode, $no_update_copy) = @_;
1026 my $e = new_editor(authtoken => $auth);
1027 return $e->die_event unless $e->checkauth;
1028 return $e->die_event unless $e->allowed("COPY_CHECKIN");
1030 my $uncaptured = get_uncaptured_bresv_for_brsrc(
1031 $e, {"barcode" => $barcode}
1034 if (keys %$uncaptured) {
1035 # Note this will only capture one reservation at a time, even in
1036 # cases with overbooking (multiple "soonest" bresv's on a resource).
1037 my $bresv = best_bresv_candidate(
1039 (sort(keys %$uncaptured))[0]
1043 return capture_reservation(
1044 $self, $client, $auth, $bresv, $no_update_copy
1047 return new OpenILS::Event(
1048 "RESERVATION_NOT_FOUND",
1049 "desc" => "No capturable reservation found pertaining " .
1050 "to a resource with barcode $barcode",
1051 "payload" => {"fail_cause" => "no-reservation", "captured" => 0}
1055 __PACKAGE__->register_method(
1056 method => "capture_resource_for_reservation",
1057 api_name => "open-ils.booking.resources.capture_for_reservation",
1061 {type => "string", desc => "Authentication token"},
1062 {type => "string", desc => "Barcode of booked & targeted resource"},
1063 {type => "number", desc => "(optional) 1 to not update copy"}
1065 return => { desc => "An OpenILS event describing the capture outcome" }
1070 sub capture_reservation {
1071 my ($self, $client, $auth, $res_id, $no_update_copy) = @_;
1073 my $e = new_editor("xact" => 1, "authtoken" => $auth);
1074 return $e->die_event unless $e->checkauth;
1075 return $e->die_event unless $e->allowed("COPY_CHECKIN");
1076 my $here = $e->requestor->ws_ou;
1078 my $reservation = $e->retrieve_booking_reservation([
1080 "flesh" => 2, "flesh_fields" => {
1081 "bresv" => [qw/usr current_resource type/],
1088 return new OpenILS::Event("RESERVATION_NOT_FOUND") unless $reservation;
1089 return new OpenILS::Event(
1090 "RESERVATION_CAPTURE_FAILED",
1091 payload => {"captured" => 0, "fail_cause" => "no-resource"}
1092 ) unless $reservation->current_resource;
1094 return new OpenILS::Event(
1095 "RESERVATION_CAPTURE_FAILED",
1096 "payload" => {"captured" => 0, "fail_cause" => "cancelled"}
1097 ) if $reservation->cancel_time;
1099 $reservation->capture_staff($e->requestor->id);
1100 $reservation->capture_time("now");
1102 $e->update_booking_reservation($reservation) or return $e->die_event;
1104 my $ret = {"captured" => 1, "reservation" => $reservation};
1106 my $search_acp_like_this = [
1108 "barcode" => $reservation->current_resource->barcode,
1111 {"flesh" => 1, "flesh_fields" => {"acp" => ["call_number"]}}
1114 if ($here != $reservation->pickup_lib) {
1115 $logger->info("resource isn't at the reservation's pickup lib...");
1116 return new OpenILS::Event(
1117 "RESERVATION_CAPTURE_FAILED",
1118 "payload" => {"captured" => 0, "fail_cause" => "not-transferable"}
1119 ) unless $U->is_true(
1120 $reservation->current_resource->type->transferable
1123 # need to transit the item ... is it already in transit?
1124 my $transit = $e->search_action_reservation_transit_copy(
1125 {"reservation" => $res_id, "dest_recv_time" => undef, cancel_time => undef}
1128 if (!$transit) { # not yet in transit
1129 $transit = new Fieldmapper::action::reservation_transit_copy;
1131 $transit->reservation($reservation->id);
1132 $transit->target_copy($reservation->current_resource->id);
1133 $transit->copy_status(15);
1134 $transit->source_send_time("now");
1135 $transit->source($here);
1136 $transit->dest($reservation->pickup_lib);
1138 $e->create_action_reservation_transit_copy($transit);
1141 $reservation->current_resource->type->catalog_item
1143 my $copy = $e->search_asset_copy($search_acp_like_this)->[0];
1146 return new OpenILS::Event(
1147 "OPEN_CIRCULATION_EXISTS",
1148 "payload" => {"captured" => 0, "copy" => $copy}
1149 ) if $copy->status == 1 and not $no_update_copy;
1151 $ret->{"mvr"} = get_mvr($copy->call_number->record);
1152 if ($no_update_copy) {
1153 $ret->{"new_copy_status"} = 6;
1156 $e->update_asset_copy($copy) or return $e->die_event;
1162 $ret->{"transit"} = $transit;
1163 } elsif ($U->is_true($reservation->current_resource->type->catalog_item)) {
1164 $logger->info("resource is a catalog item...");
1165 my $copy = $e->search_asset_copy($search_acp_like_this)->[0];
1168 return new OpenILS::Event(
1169 "OPEN_CIRCULATION_EXISTS",
1170 "payload" => {"captured" => 0, "copy" => $copy}
1171 ) if $copy->status == 1 and not $no_update_copy;
1173 $ret->{"mvr"} = get_mvr($copy->call_number->record);
1174 if ($no_update_copy) {
1175 $ret->{"new_copy_status"} = 15;
1178 $e->update_asset_copy($copy) or return $e->die_event;
1183 $e->commit or return $e->die_event;
1185 # create action trigger event to notify that reservation is available
1186 if ($reservation->email_notify) {
1187 my $ses = OpenSRF::AppSession->create('open-ils.trigger');
1188 $ses->request('open-ils.trigger.event.autocreate', 'reservation.available', $reservation, $reservation->pickup_lib);
1191 # XXX I'm not sure whether these last two elements of the payload
1192 # actually get used anywhere.
1193 $ret->{"resource"} = $reservation->current_resource;
1194 $ret->{"type"} = $reservation->current_resource->type;
1195 return new OpenILS::Event("SUCCESS", "payload" => $ret);
1197 __PACKAGE__->register_method(
1198 method => "capture_reservation",
1199 api_name => "open-ils.booking.reservations.capture",
1203 {type => 'string', desc => 'Authentication token'},
1204 {type => 'mixed', desc =>
1205 'Reservation ID (number) or array of resource barcodes'}
1207 return => { desc => "An OpenILS Event object describing the outcome of the capture, with relevant payload." },
1212 sub cancel_reservation {
1213 my ($self, $client, $auth, $id_list) = @_;
1215 my $e = new_editor(xact => 1, authtoken => $auth);
1216 return $e->die_event unless $e->checkauth;
1217 # Should the following permission really be checked as relates to each
1218 # individual reservation's request_lib? Hrmm...
1219 return $e->die_event unless $e->allowed("ADMIN_BOOKING_RESERVATION");
1221 my $bresv_list = $e->search_booking_reservation([
1223 {"flesh" => 1, "flesh_fields" => {"bresv" => [
1224 "current_resource", "target_resource_type"
1227 return $e->die_event if not $bresv_list;
1230 my $circ = OpenSRF::AppSession->connect("open-ils.circ") or
1231 return $e->die_event;
1232 foreach my $bresv (@$bresv_list) {
1233 $bresv->cancel_time("now");
1234 $e->update_booking_reservation($bresv) or do {
1236 return $e->die_event;
1242 $bresv->target_resource_type->catalog_item eq "t" &&
1243 $bresv->current_resource
1245 $logger->info("result of no-op checkin (upon cxl bresv) is " .
1247 "open-ils.circ.checkin", $auth,
1248 {"barcode" => $bresv->current_resource->barcode,
1250 )->gather(1)->{"textcode"});
1252 push @results, $bresv->id;
1260 __PACKAGE__->register_method(
1261 method => "cancel_reservation",
1262 api_name => "open-ils.booking.reservations.cancel",
1266 {type => "string", desc => "Authentication token"},
1267 {type => "array", desc => "List of reservation IDs"}
1269 return => { desc => "A list of canceled reservation IDs" },
1274 sub get_captured_reservations {
1275 my ($self, $client, $auth, $barcode, $which) = @_;
1277 my $e = new_editor(xact => 1, authtoken => $auth);
1278 return $e->die_event unless $e->checkauth;
1279 return $e->die_event unless $e->allowed("VIEW_USER");
1280 return $e->die_event unless $e->allowed("ADMIN_BOOKING_RESERVATION");
1282 # fetch the patron for our uses in any case...
1283 my $patron = $U->fetch_user_by_barcode($barcode);
1284 return $patron if ref($patron) eq "HASH" and exists $patron->{"ilsevent"};
1288 "flesh_fields" => {"bresv" => [
1289 qw/target_resource_type current_resource/
1298 return ($e->search_booking_reservation([
1300 "usr" => $patron->id,
1301 "capture_time" => {"!=" => undef},
1302 "pickup_time" => undef,
1303 "start_time" => {"!=" => undef},
1304 "cancel_time" => undef
1307 ]) || $e->die_event);
1310 return ($e->search_booking_reservation([
1312 "usr" => $patron->id,
1313 "pickup_time" => {"!=" => undef},
1314 "return_time" => undef,
1315 "cancel_time" => undef
1318 ]) || $e->die_event);
1321 return ($e->search_booking_reservation([
1323 "usr" => $patron->id,
1324 "return_time" => {">=" => "today"},
1325 "cancel_time" => undef
1328 ]) || $e->die_event);
1334 my $f = $dispatch->{$_};
1337 return $r if (ref($r) eq "HASH" and exists $r->{"ilsevent"});
1344 __PACKAGE__->register_method(
1345 method => "get_captured_reservations",
1346 api_name => "open-ils.booking.reservations.get_captured",
1350 {type => "string", desc => "Authentication token"},
1351 {type => "string", desc => "Patron barcode"},
1352 {type => "array", desc => "Parts wanted (patron, ready, out, in?)"}
1354 return => { desc => "A hash of parts." } # XXX describe more fully
1359 sub get_bresv_by_returnable_resource_barcode {
1360 my ($self, $client, $auth, $barcode) = @_;
1362 my $e = new_editor(xact => 1, authtoken => $auth);
1363 return $e->die_event unless $e->checkauth;
1364 return $e->die_event unless $e->allowed("VIEW_USER");
1365 # return $e->die_event unless $e->allowed("ADMIN_BOOKING_RESERVATION");
1367 my $rows = $e->json_query({
1368 "select" => {"bresv" => ["id"]},
1371 "brsrc" => {"field" => "id", "fkey" => "current_resource"}
1375 "+brsrc" => {"barcode" => $barcode},
1377 "pickup_time" => {"!=" => undef},
1378 "cancel_time" => undef,
1379 "return_time" => undef
1382 }) or return $e->die_event;
1388 # More than one result might be possible, but we don't want to return
1389 # more than one at this time.
1390 my $id = $rows->[0]->{"id"};
1391 my $resp =$e->retrieve_booking_reservation([
1395 "bresv" => [qw/usr target_resource_type current_resource/],
1399 ]) or $e->die_event;
1405 __PACKAGE__->register_method(
1406 method => "get_bresv_by_returnable_resource_barcode",
1407 api_name => "open-ils.booking.reservations.by_returnable_resource_barcode",
1411 {type => "string", desc => "Authentication token"},
1412 {type => "string", desc => "Resource barcode"},
1414 return => { desc => "A fleshed bresv or an ilsevent on error" }