1 package OpenILS::Application::Booking;
6 use POSIX qw/strftime/;
7 use OpenILS::Application;
8 use base qw/OpenILS::Application/;
10 use OpenILS::Utils::CStoreEditor qw/:funcs/;
11 use OpenILS::Utils::Fieldmapper;
12 use OpenILS::Application::AppUtils;
13 my $U = "OpenILS::Application::AppUtils";
15 use OpenSRF::Utils::Logger qw/$logger/;
18 my ($record_id, $owning_lib, $mvr) = @_;
20 my $brt = new Fieldmapper::booking::resource_type;
22 $brt->name($mvr->title);
23 $brt->record($record_id);
24 $brt->catalog_item('t');
25 $brt->transferable('t');
26 $brt->owner($owning_lib);
31 sub get_existing_brt {
32 my ($e, $record_id, $owning_lib, $mvr) = @_;
33 my $results = $e->search_booking_resource_type(
34 {name => $mvr->title, owner => $owning_lib, record => $record_id}
37 return $results->[0] if scalar(@$results) > 0;
44 'open-ils.search.biblio.record.mods_slim.retrieve.authoritative',
49 sub get_unique_owning_libs {
51 $hash{$_->call_number->owning_lib} = 1 foreach (@_); # @_ are copies
55 sub fetch_copies_by_ids {
56 my ($e, $copy_ids) = @_;
57 my $results = $e->search_asset_copy([
59 {flesh => 1, flesh_fields => {acp => ['call_number']}}
61 return $results if ref($results) eq 'ARRAY';
65 sub get_single_record_id {
66 my $record_id = undef;
67 foreach (@_) { # @_ are copies
69 (defined $record_id && $record_id != $_->call_number->record);
70 $record_id = $_->call_number->record;
75 # This function generates the correct json_query clause for determining
76 # whether two given ranges overlap. Each range is composed of a start
77 # and an end point. All four points should be the same type (could be int,
78 # date, time, timestamp, or perhaps other types).
80 # The first range (or the first two points) should be specified as
81 # literal values. The second range (or the last two points) should be
82 # specified as the names of columns, the values of which in a given row
83 # will constitute the second range in the comparison.
85 # ALSO: PostgreSQL includes an OVERLAPS operator which provides the same
86 # functionality in a much more concise way, but json_query does not (yet).
87 sub json_query_ranges_overlap {
89 { '-and' => [{$_[2] => {'>=', $_[0]}}, {$_[2] => {'<', $_[1]}}]},
90 { '-and' => [{$_[3] => {'>', $_[0]}}, {$_[3] => {'<', $_[1]}}]},
91 { '-and' => { $_[3] => {'>', $_[0]}, $_[2] => {'<=', $_[0]}}},
92 { '-and' => { $_[3] => {'>', $_[1]}, $_[2] => {'<', $_[1]}}},
96 sub create_brt_and_brsrc {
97 my ($self, $conn, $authtoken, $copy_ids) = @_;
98 my (@created_brt, @created_brsrc);
101 my $e = new_editor(xact => 1, authtoken => $authtoken);
102 return $e->die_event unless $e->checkauth;
104 my @copies = @{fetch_copies_by_ids($e, $copy_ids)};
105 my $record_id = get_single_record_id(@copies) or return $e->die_event;
106 my $mvr = get_mvr($record_id) or return $e->die_event;
108 foreach (get_unique_owning_libs(@copies)) {
109 $brt_table{$_} = get_existing_brt($e, $record_id, $_, $mvr) ||
110 prepare_new_brt($record_id, $_, $mvr);
113 while (my ($owning_lib, $brt) = each %brt_table) {
114 my $pre_existing = 1;
116 if ($e->allowed('ADMIN_BOOKING_RESOURCE_TYPE', $owning_lib)) {
118 return $e->die_event unless (
119 # v-- Important: assignment modifies original hash
120 $brt = $e->create_booking_resource_type($brt)
124 push @created_brt, [$brt->id, $brt->record, $pre_existing];
129 'ADMIN_BOOKING_RESOURCE', $_->call_number->owning_lib
131 # This block needs to disregard any cstore failures and just
132 # return what results it can.
133 my $brsrc = new Fieldmapper::booking::resource;
135 $brsrc->type($brt_table{$_->call_number->owning_lib}->id);
136 $brsrc->owner($_->call_number->owning_lib);
137 $brsrc->barcode($_->barcode);
139 $e->set_savepoint("alpha");
140 my $pre_existing = 0;
141 my $usable_result = undef;
142 if (!($usable_result = $e->create_booking_resource($brsrc))) {
143 $e->rollback_savepoint("alpha");
144 if (($usable_result = $e->search_booking_resource(
145 +{ map { ($_, $brsrc->$_()) } qw/type owner barcode/ }
147 $usable_result = $usable_result->[0];
150 # So we failed to create a booking resource for this copy.
151 # For now, let's just keep going. If the calling app wants
152 # to consider this an error, it can notice the absence
153 # of a booking resource for the copy in the returned
156 "Couldn't create or find brsrc for acp #" . $_->id
160 $e->release_savepoint("alpha");
163 if ($usable_result) {
165 [$usable_result->id, $_->id, $pre_existing];
171 return {brt => \@created_brt, brsrc => \@created_brsrc} or
172 return $e->die_event;
174 __PACKAGE__->register_method(
175 method => "create_brt_and_brsrc",
176 api_name => "open-ils.booking.resources.create_from_copies",
179 {type => 'string', desc => 'Authentication token'},
180 {type => 'array', desc => 'Copy IDs'},
182 return => { desc => "A two-element hash. The 'brt' element " .
183 "is a list of created booking resource types described by " .
184 "3-tuples (id, copy id, was pre-existing). The 'brsrc' " .
185 "element is a similar list of created booking resources " .
186 "described by (id, record id, was pre-existing) 3-tuples."}
192 my ($self, $client, $authtoken,
193 $target_user_barcode, $datetime_range, $pickup_lib,
194 $brt, $brsrc_list, $attr_values) = @_;
196 $brsrc_list = [ undef ] if not defined $brsrc_list;
197 return undef if scalar(@$brsrc_list) < 1; # Empty list not ok.
199 my $e = new_editor(xact => 1, authtoken => $authtoken);
200 return $e->die_event unless $e->checkauth;
201 return $e->die_event unless $e->allowed("ADMIN_BOOKING_RESERVATION");
203 my $usr = $U->fetch_user_by_barcode($target_user_barcode);
204 return $usr if ref($usr) eq 'HASH' and exists($usr->{"ilsevent"});
207 foreach my $brsrc (@$brsrc_list) {
208 my $bresv = new Fieldmapper::booking::reservation;
209 $bresv->usr($usr->id);
210 $bresv->request_lib($e->requestor->ws_ou);
211 $bresv->pickup_lib($pickup_lib);
212 $bresv->start_time($datetime_range->[0]);
213 $bresv->end_time($datetime_range->[1]);
215 # A little sanity checking: don't agree to put a reservation on a
216 # brsrc and a brt when they don't match. In fact, bomb out of
217 # this transaction entirely.
219 my $brsrc_itself = $e->retrieve_booking_resource([
222 "flesh_fields" => {"brsrc" => ["type"]}
226 if (not $brsrc_itself) {
227 my $ev = new OpenILS::Event(
228 "RESERVATION_BAD_PARAMS",
229 desc => "brsrc $brsrc doesn't exist"
234 elsif ($brsrc_itself->type->id != $brt) {
235 my $ev = new OpenILS::Event(
236 "RESERVATION_BAD_PARAMS",
237 desc => "brsrc $brsrc doesn't match given brt $brt"
243 # Also bail if the user is trying to create a reservation at
244 # a pickup lib to which our resource won't go.
246 $brsrc_itself->owner != $pickup_lib and
247 not $brsrc_itself->type->transferable
249 my $ev = new OpenILS::Event(
250 "RESERVATION_BAD_PARAMS",
251 desc => "brsrc $brsrc doesn't belong to $pickup_lib and " .
252 "is not transferable"
258 $bresv->target_resource($brsrc); # undef is ok here
259 $bresv->target_resource_type($brt);
261 ($bresv = $e->create_booking_reservation($bresv)) or
262 return $e->die_event;
264 # We could/should do some sanity checking on this too: namely, on
265 # whether the attribute values given actually apply to the relevant
266 # brt. Not seeing any grievous side effects of not checking, though.
268 foreach my $value (@$attr_values) {
269 my $bravm = new Fieldmapper::booking::reservation_attr_value_map;
270 $bravm->reservation($bresv->id);
271 $bravm->attr_value($value);
272 $bravm = $e->create_booking_reservation_attr_value_map($bravm) or
273 return $e->die_event;
277 "bresv" => $bresv->id,
282 $e->commit or return $e->die_event;
284 # Targeting must be tacked on _after_ committing the transaction where the
285 # reservations are actually created.
286 foreach (@$results) {
287 $_->{"targeting"} = $U->storagereq(
288 "open-ils.storage.booking.reservation.resource_targeter",
294 __PACKAGE__->register_method(
295 method => "create_bresv",
296 api_name => "open-ils.booking.reservations.create",
299 {type => 'string', desc => 'Authentication token'},
300 {type => 'string', desc => 'Barcode of user for whom to reserve'},
301 {type => 'array', desc => 'Two elements: start and end timestamp'},
302 {type => 'int', desc => 'Desired reservation pickup lib'},
303 {type => 'int', desc => 'Booking resource type'},
304 {type => 'list', desc => 'Booking resource (undef ok; empty not ok)'},
305 {type => 'array', desc => 'Attribute values selected'},
307 return => { desc => "A hash containing the new bresv and a list " .
313 sub resource_list_by_attrs {
316 my $auth = shift; # Keep as argument, though not used just now.
319 return undef unless ($filters->{type} || $filters->{attribute_values});
322 "select" => {brsrc => ["id"]},
323 "from" => {brsrc => {"brt" => {}}},
328 $query->{where} = {"-and" => []};
329 if ($filters->{type}) {
330 push @{$query->{where}->{"-and"}}, {"type" => $filters->{type}};
333 if ($filters->{pickup_lib}) {
334 push @{$query->{where}->{"-and"}},
336 {"owner" => $filters->{pickup_lib}},
337 {"+brt" => {"transferable" => "t"}}
341 if ($filters->{attribute_values}) {
343 $query->{from}->{brsrc}->{bram} = { field => 'resource' };
345 $filters->{attribute_values} = [$filters->{attribute_values}]
346 if (!ref($filters->{attribute_values}));
348 $query->{having}->{'+bram'}->{value}->{'@>'} = {
349 transform => 'array_accum',
350 value => '$_' . $$ . '${' .
351 join(',', @{$filters->{attribute_values}}) .
356 if ($filters->{available}) {
357 # If only one timestamp has been provided, make it into a range.
358 if (!ref($filters->{available})) {
359 $filters->{available} = [($filters->{available}) x 2];
362 push @{$query->{where}->{"-and"}}, {
366 "select" => {"bresv" => ["id"]},
368 "where" => {"-and" => [
369 json_query_ranges_overlap(
370 $filters->{available}->[0],
371 $filters->{available}->[1],
375 {"cancel_time" => undef},
376 {"current_resource" => {"=" => {"+brsrc" => "id"}}}
382 if ($filters->{booked}) {
383 # If only one timestamp has been provided, make it into a range.
384 if (!ref($filters->{booked})) {
385 $filters->{booked} = [($filters->{booked}) x 2];
388 push @{$query->{where}->{"-and"}}, {
390 "select" => {"bresv" => ["id"]},
392 "where" => {"-and" => [
393 json_query_ranges_overlap(
394 $filters->{booked}->[0],
395 $filters->{booked}->[1],
399 {"cancel_time" => undef},
400 {"current_resource" => { "=" => {"+brsrc" => "id"}}}
404 # I think that the "booked" case could be done with a JOIN instead of
405 # an EXISTS, but I'm leaving it this way for symmetry with the
406 # "available" case for now. The available case cannot be done with a
410 my $cstore = OpenSRF::AppSession->connect('open-ils.cstore');
411 my $rows = $cstore->request( 'open-ils.cstore.json_query.atomic', $query )->gather(1);
414 return @$rows ? [map { $_->{id} } @$rows] : [];
416 __PACKAGE__->register_method(
417 method => "resource_list_by_attrs",
418 api_name => "open-ils.booking.resources.filtered_id_list",
422 {type => 'string', desc => 'Authentication token (unused for now,' .
423 ' but at least pass undef here)'},
424 {type => 'object', desc => 'Filter object: see notes for details'},
425 {type => 'bool', desc => 'Return whole objects instead of IDs?'}
427 return => { desc => "An array of brsrc ids matching the requested filters." },
431 The filter object parameter can contain the following keys:
432 * type => The id of a booking resource type (brt)
433 * attribute_values => The ids of booking resource type attribute values that the resource must have assigned to it (brav)
434 * available => Either:
435 A timestamp during which the resources are not reserved. If the resource is overbookable, this is ignored.
436 A range of two timestamps which do not overlap any reservations for the resources. If the resource is overbookable, this is ignored.
438 A timestamp during which the resources are reserved.
439 A range of two timestamps which overlap a reservation of the resources.
441 Note that at least one of 'type' or 'attribute_values' is required.
447 sub reservation_list_by_filters {
452 my $whole_obj = shift;
454 return undef unless ($filters->{user} || $filters->{user_barcode} || $filters->{resource} || $filters->{type} || $filters->{attribute_values});
456 my $e = new_editor(authtoken=>$auth);
457 return $e->event unless $e->checkauth;
458 return $e->event unless $e->allowed('VIEW_TRANSACTION');
461 'select' => { bresv => [ 'id', 'start_time' ] },
462 'from' => { bresv => {} },
464 'order_by' => [{ class => bresv => field => start_time => direction => 'asc' }],
468 if ($filters->{fields}) {
469 $query->{where} = $filters->{fields};
473 if ($filters->{user}) {
474 $query->{where}->{usr} = $filters->{user};
476 elsif ($filters->{user_barcode}) { # just one of user and user_barcode
477 my $usr = $U->fetch_user_by_barcode($filters->{user_barcode});
478 return $usr if ref($usr) eq 'HASH' and exists($usr->{"ilsevent"});
479 $query->{where}->{usr} = $usr->id;
483 if ($filters->{type}) {
484 $query->{where}->{target_resource_type} = $filters->{type};
487 if ($filters->{resource}) {
488 $query->{where}->{target_resource} = $filters->{resource};
491 if ($filters->{attribute_values}) {
493 $query->{from}->{bresv}->{bravm} = { field => 'reservation' };
495 $filters->{attribute_values} = [$filters->{attribute_values}]
496 if (!ref($filters->{attribute_values}));
498 $query->{having}->{'+bravm'}->{attr_value}->{'@>'} = {
499 transform => 'array_accum',
500 value => '$_' . $$ . '${' .
501 join(',', @{$filters->{attribute_values}}) .
506 if ($filters->{search_start} || $filters->{search_end}) {
507 $query->{where}->{'-or'} = {};
509 $query->{where}->{'-or'}->{start_time} = { 'between' => [ $filters->{search_start}, $filters->{search_end} ] }
510 if ($filters->{search_start});
512 $query->{where}->{'-or'}->{end_time} = { 'between' => [ $filters->{search_start}, $filters->{search_end} ] }
513 if ($filters->{search_end});
516 my $cstore = OpenSRF::AppSession->connect('open-ils.cstore');
517 my $ids = [ map { $_->{id} } @{
519 'open-ils.cstore.json_query.atomic', $query
524 if (not $whole_obj or @$ids < 1) {
529 my $bresv_list = $e->search_booking_reservation([
534 [qw/target_resource current_resource target_resource_type/]
539 return $bresv_list ? $bresv_list : [];
541 __PACKAGE__->register_method(
542 method => "reservation_list_by_filters",
543 api_name => "open-ils.booking.reservations.filtered_id_list",
547 {type => 'string', desc => 'Authentication token'},
548 {type => 'object', desc => 'Filter object -- see notes for details'}
550 return => { desc => "An array of bresv ids matching the requested filters." },
554 The filter object parameter can contain the following keys:
555 * user => The id of a user that has requested a bookable item -- filters on bresv.usr
556 * barcode => The barcode of a user that has requested a bookable item
557 * type => The id of a booking resource type (brt) -- filters on bresv.target_resource_type
558 * resource => The id of a booking resource (brsrc) -- filters on bresv.target_resource
559 * attribute_values => The ids of booking resource type attribute values that the resource must have assigned to it (brav)
560 * search_start => If search_end is not specified, booking interval (start_time to end_time) must contain this timestamp.
561 * search_end => If search_start is not specified, booking interval (start_time to end_time) must contain this timestamp.
562 * fields => An object containing any combination of bresv search filters in standard cstore/pcrud search format.
564 Note that at least one of 'user', 'type', 'resource' or 'attribute_values' is required. If both search_start and search_end are specified,
565 then the result includes any reservations that overlap with that time range. Any filter fields supplied in 'fields' are overridden
566 by the top-level filters ('user', 'type', 'resource').
572 sub naive_ts_string {strftime("%F %T", localtime($_[0] || time));}
573 sub naive_start_of_day {strftime("%F", localtime($_[0] || time))." 00:00:00";}
575 # Return a list of bresv or an ilsevent on failure.
576 sub get_uncaptured_bresv_for_brsrc {
577 my ($e, $o) = @_; # o's keys (all optional): owning_lib, barcode, range
581 "brsrc" => {"field" => "id", "fkey" => "current_resource"}
590 "column" => "start_time",
591 "transform" => "min",
596 "from" => $from_clause,
599 {"current_resource" => {"!=" => undef}},
600 {"capture_time" => undef},
601 {"cancel_time" => undef},
602 {"return_time" => undef},
603 {"pickup_time" => undef}
607 if ($o->{"owning_lib"}) {
608 push @{$query->{"where"}->{"-and"}},
609 {"+brsrc" => {"owner" => $o->{"owning_lib"}}};
612 push @{$query->{"where"}->{"-and"}},
613 json_query_ranges_overlap(
614 $o->{"range"}->[0], $o->{"range"}->[1],
615 "start_time", "end_time"
618 if ($o->{"barcode"}) {
619 push @{$query->{"where"}->{"-and"}},
620 {"+brsrc" => {"barcode" => $o->{"barcode"}}};
623 my $rows = $e->json_query($query);
624 my $current_resource_bresv_map = {};
627 "select" => {"bresv" => ["id"]},
628 "from" => $from_clause,
631 {"current_resource" => "PLACEHOLDER"},
632 {"start_time" => "PLACEHOLDER"},
636 if ($o->{"owning_lib"}) {
637 push @{$id_query->{"where"}->{"-and"}},
638 {"+brsrc" => {"owner" => $o->{"owning_lib"}}};
642 $id_query->{"where"}->{"-and"}->[0]->{"current_resource"} =
643 $_->{"current_resource"};
644 $id_query->{"where"}->{"-and"}->[1]->{"start_time"} =
647 my $results = $e->json_query($id_query);
648 if ($results && @$results) {
649 $current_resource_bresv_map->{$_->{"current_resource"}} =
650 [map { $_->{"id"} } @$results];
654 return $current_resource_bresv_map;
658 my ($self, $client, $auth, $range, $interval_secs, $owning_lib) = @_;
660 my $e = new_editor(xact => 1, authtoken => $auth);
661 return $e->die_event unless $e->checkauth;
662 return $e->die_event unless $e->allowed("RETRIEVE_RESERVATION_PULL_LIST");
663 return $e->die_event unless (
664 ref($range) eq "ARRAY" or
665 ($interval_secs = int($interval_secs)) > 0
668 $owning_lib = $e->requestor->ws_ou if not $owning_lib;
669 $range = [ naive_ts_string(time), naive_ts_string(time + $interval_secs) ]
672 my $uncaptured = get_uncaptured_bresv_for_brsrc(
673 $e, {"range" => $range, "owning_lib" => $owning_lib}
676 if (keys(%$uncaptured)) {
677 my @all_bresv_ids = map { @{$_} } values %$uncaptured;
679 map { $_->id => $_ } @{
680 $e->search_booking_reservation([{"id" => [@all_bresv_ids]}, {
682 flesh_fields => { bresv => [
683 "usr", "target_resource_type", "current_resource"
691 my $one = $bresv_lookup{$uncaptured->{$key}->[0]};
693 "current_resource" => $one->current_resource,
694 "target_resource_type" => $one->target_resource_type,
696 map { $bresv_lookup{$_} } @{$uncaptured->{$key}}
699 foreach (@{$result->{"reservations"}}) { # deflesh
700 $_->current_resource($_->current_resource->id);
701 $_->target_resource_type($_->target_resource_type->id);
704 } keys %$uncaptured ];
710 __PACKAGE__->register_method(
711 method => "get_pull_list",
712 api_name => "open-ils.booking.reservations.get_pull_list",
716 {type => "string", desc => "Authentication token"},
717 {type => "array", desc =>
718 "range: Date/time range for reservations (opt)"},
719 {type => "int", desc =>
720 "interval: Seconds from now (instead of range)"},
721 {type => "number", desc => "(Optional) Owning library"}
723 return => { desc => "An array of hashes, each containing key/value " .
724 "pairs describing resource, resource type, and a list of " .
725 "reservations that claim the given resource." }
730 sub get_copy_fleshed_just_right {
731 my ($self, $client, $auth, $barcode) = @_;
733 return undef if not defined $barcode;
734 return {} if ref($barcode) eq "ARRAY" and not @$barcode;
736 my $e = new_editor(authtoken => $auth);
737 my $results = $e->search_asset_copy([
738 {"barcode" => $barcode},
741 "flesh_fields" => {"acp" => [qw/call_number location/]}
745 if (ref($results) eq "ARRAY") {
747 return $results->[0] unless ref $barcode;
748 return +{ map { $_->barcode => $_ } @$results };
750 return $e->die_event;
753 __PACKAGE__->register_method(
754 method => "get_copy_fleshed_just_right",
755 api_name => "open-ils.booking.asset.get_copy_fleshed_just_right",
759 {type => "string", desc => "Authentication token"},
760 {type => "mixed", desc => "One barcode or an array of them"},
763 "A copy, or a hash of copies keyed by barcode if an array of " .
770 sub best_bresv_candidate {
771 my ($e, $id_list) = @_;
773 # This will almost always be the case.
774 return $id_list->[0] if @$id_list == 1;
777 my $this_ou = $e->requestor->ws_ou;
778 my $results = $e->json_query({
779 "select" => {"brsrc" => ["pickup_lib"], "bresv" => ["id"]},
782 "brsrc" => {"field" => "id", "fkey" => "current_resource"}
786 {"+bresv" => {"id" => $id_list}}
790 foreach (@$results) {
791 push @here, $_->{"id"} if $_->{"pickup_lib"} == $this_ou;
795 return pop @here if @here == 1;
796 return (sort @here)[0];
798 return (sort @$id_list)[0];
803 sub capture_resource_for_reservation {
804 my ($self, $client, $auth, $barcode) = @_;
806 my $e = new_editor(xact => 1, authtoken => $auth);
807 return $e->die_event unless $e->checkauth;
808 return $e->die_event unless $e->allowed("CAPTURE_RESERVATION");
810 my $uncaptured = get_uncaptured_bresv_for_brsrc(
811 $e, {"barcode" => $barcode}
815 if (keys %$uncaptured) {
816 # Note this will only capture one reservation at a time, even in
817 # cases with overbooking (multiple "soonest" bresv's on a resource).
818 my $key = (sort(keys %$uncaptured))[0];
819 return capture_reservation(
820 $self, $client, $auth, best_bresv_candidate($e, $uncaptured->{$key})
823 return new OpenILS::Event(
824 "RESERVATION_NOT_FOUND",
825 desc => "No capturable reservation found pertaining " .
826 "to a resource with barcode $barcode",
827 payload => {fail_cause => 'no-reservation', captured => 0}
831 __PACKAGE__->register_method(
832 method => "capture_resource_for_reservation",
833 api_name => "open-ils.booking.resources.capture_for_reservation",
837 {type => "string", desc => "Authentication token"},
838 {type => "string", desc => "Barcode of booked & targeted resource"},
839 {type => "int", desc => "Pickup library (default to client ws_ou)"},
841 return => { desc => "An OpenILS event describing the capture outcome" }
846 sub capture_reservation {
847 my ($self, $client, $auth, $res_id) = @_;
849 my $e = new_editor(xact => 1, authtoken => $auth);
850 return $e->event unless $e->checkauth;
851 return $e->event unless $e->allowed('CAPTURE_RESERVATION');
852 my $here = $e->requestor->ws_ou;
854 my $reservation = $e->retrieve_booking_reservation([
857 flesh_fields => {"bresv" => ["usr"], "au" => ["card"]}
860 return OpenILS::Event->new('RESERVATION_NOT_FOUND') unless $reservation;
862 return OpenILS::Event->new('RESERVATION_CAPTURE_FAILED', payload => { captured => 0, fail_cause => 'no-resource' })
863 if (!$reservation->current_resource); # no resource
865 return OpenILS::Event->new('RESERVATION_CAPTURE_FAILED', payload => { captured => 0, fail_cause => 'cancelled' })
866 if ($reservation->cancel_time); # canceled
868 my $resource = $e->retrieve_booking_resource( $reservation->current_resource );
869 my $type = $e->retrieve_booking_resource_type( $resource->type );
871 $reservation->capture_staff( $e->requestor->id );
872 $reservation->capture_time( 'now' );
874 my $reservation_id = undef;
875 return $e->event unless ( $e->update_booking_reservation( $reservation ) and $reservation_id = $e->data );
877 $reservation->id($reservation_id);
879 my $ret = { captured => 1, reservation => $reservation };
881 if ($here != $reservation->pickup_lib) {
882 return OpenILS::Event->new('RESERVATION_CAPTURE_FAILED', payload => { captured => 0, fail_cause => 'not-transferable' })
883 if (!$U->is_true($type->transferable)); # non-transferable resource
885 # need to transit the item ... is it already in transit?
886 my $transit = $e->search_action_reservation_transit_copy( { reservation => $res_id, dest_recv_time => undef } )->[0];
888 if (!$transit) { # not yet in transit
889 $transit = new Fieldmapper::action::reservation_transit_copy;
891 $transit->reservation($reservation->id);
892 $transit->target_copy($resource->id);
893 $transit->copy_status(15);
894 $transit->source_send_time('now');
895 $transit->source($here);
896 $transit->dest($reservation->pickup_lib);
898 $e->create_action_reservation_transit_copy( $transit );
900 if ($U->is_true($type->catalog_item)) {
901 my $copy = $e->search_asset_copy( { barcode => $resource->barcode, deleted => 'f' } )->[0];
904 return new OpenILS::Event(
905 "OPEN_CIRCULATION_EXISTS",
906 payload => { captured => 0, copy => $copy }
907 ) if $copy->status == 1;
909 $e->update_asset_copy( $copy );
910 $$ret{catalog_item} = $copy; # $e->data is just id (int)
915 $$ret{transit} = $transit;
916 } elsif ($U->is_true($type->catalog_item)) {
917 my $copy = $e->search_asset_copy( { barcode => $resource->barcode, deleted => 'f' } )->[0];
920 return OpenILS::Event->new('OPEN_CIRCULATION_EXISTS', payload => { captured => 0, copy => $copy }) if ($copy->status == 1);
922 $e->update_asset_copy( $copy );
923 $$ret{catalog_item} = $copy; # $e->data is just id (int)
929 return OpenILS::Event->new('SUCCESS', payload => $ret);
931 __PACKAGE__->register_method(
932 method => "capture_reservation",
933 api_name => "open-ils.booking.reservations.capture",
937 {type => 'string', desc => 'Authentication token'},
938 {type => 'mixed', desc =>
939 'Reservation ID (number) or array of resource barcodes'}
941 return => { desc => "An OpenILS Event object describing the outcome of the capture, with relevant payload." },
946 sub cancel_reservation {
947 my ($self, $client, $auth, $id_list) = @_;
949 my $e = new_editor(xact => 1, authtoken => $auth);
950 return $e->die_event unless $e->checkauth;
951 # Should the following permission really be checked as relates to each
952 # individual reservation's request_lib? Hrmm...
953 return $e->die_event unless $e->allowed("ADMIN_BOOKING_RESERVATION");
955 my $bresv_list = $e->search_booking_reservation([
957 {"flesh" => 1, "flesh_fields" => {"bresv" => [
958 "current_resource", "target_resource_type"
961 return $e->die_event if not $bresv_list;
963 my $circ = OpenSRF::AppSession->connect("open-ils.circ") or
964 return $e->die_event;
966 foreach my $bresv (@$bresv_list) {
968 $bresv->target_resource_type->catalog_item == "t" &&
969 $bresv->current_resource
971 $logger->info("result of no-op checkin (upon cxl bresv) is " .
973 "open-ils.circ.checkin", $auth,
974 {"barcode" => $bresv->current_resource->barcode,
976 )->gather(1)->{"textcode"});
978 $bresv->cancel_time("now");
979 $e->update_booking_reservation($bresv) or do {
981 return $e->die_event;
984 push @results, $bresv->id;
992 __PACKAGE__->register_method(
993 method => "cancel_reservation",
994 api_name => "open-ils.booking.reservations.cancel",
998 {type => "string", desc => "Authentication token"},
999 {type => "array", desc => "List of reservation IDs"}
1001 return => { desc => "A list of canceled reservation IDs" },
1006 sub get_captured_reservations {
1007 my ($self, $client, $auth, $barcode, $which) = @_;
1009 my $e = new_editor(xact => 1, authtoken => $auth);
1010 return $e->die_event unless $e->checkauth;
1011 return $e->die_event unless $e->allowed("VIEW_USER");
1012 return $e->die_event unless $e->allowed("ADMIN_BOOKING_RESERVATION");
1014 # fetch the patron for our uses in any case...
1015 my $patron = $U->fetch_user_by_barcode($barcode);
1016 return $patron if ref($patron) eq "HASH" and exists $patron->{"ilsevent"};
1020 "flesh_fields" => {"bresv" => [
1021 qw/target_resource_type current_resource/
1030 return $e->search_booking_reservation([
1032 "usr" => $patron->id,
1033 "capture_time" => {"!=" => undef},
1034 "pickup_time" => undef,
1035 "start_time" => {">=" => naive_start_of_day()},
1036 "cancel_time" => undef
1039 ]) or $e->die_event;
1042 return $e->search_booking_reservation([
1044 "usr" => $patron->id,
1045 "pickup_time" => {"!=" => undef},
1046 "return_time" => undef,
1047 "cancel_time" => undef
1050 ]) or $e->die_event;
1053 return $e->search_booking_reservation([
1055 "usr" => $patron->id,
1056 "return_time" => {">=" => naive_start_of_day()},
1057 "cancel_time" => undef
1060 ]) or $e->die_event;
1066 my $f = $dispatch->{$_};
1069 return $r if (ref($r) eq "HASH" and exists $r->{"ilsevent"});
1076 __PACKAGE__->register_method(
1077 method => "get_captured_reservations",
1078 api_name => "open-ils.booking.reservations.get_captured",
1082 {type => "string", desc => "Authentication token"},
1083 {type => "string", desc => "Patron barcode"},
1084 {type => "array", desc => "Parts wanted (patron, ready, out, in?)"}
1086 return => { desc => "A hash of parts." } # XXX describe more fully
1091 sub get_bresv_by_returnable_resource_barcode {
1092 my ($self, $client, $auth, $barcode) = @_;
1094 my $e = new_editor(xact => 1, authtoken => $auth);
1095 return $e->die_event unless $e->checkauth;
1096 return $e->die_event unless $e->allowed("VIEW_USER");
1097 return $e->die_event unless $e->allowed("ADMIN_BOOKING_RESERVATION");
1099 my $rows = $e->json_query({
1100 "select" => {"bresv" => ["id"]},
1103 "brsrc" => {"field" => "id", "fkey" => "current_resource"}
1107 "+brsrc" => {"barcode" => $barcode},
1109 "pickup_time" => {"!=" => undef},
1110 "cancel_time" => undef,
1111 "return_time" => undef
1114 }) or return $e->die_event;
1119 # More than one result might be possible, but we don't want to return
1120 # more than one at this time.
1121 my $id = $rows->[0]->{"id"};
1122 return $e->retrieve_booking_reservation([
1126 "bresv" => [qw/usr target_resource_type current_resource/],
1130 ]) or $e->die_event;
1134 __PACKAGE__->register_method(
1135 method => "get_bresv_by_returnable_resource_barcode",
1136 api_name => "open-ils.booking.reservations.by_returnable_resource_barcode",
1140 {type => "string", desc => "Authentication token"},
1141 {type => "string", desc => "Resource barcode"},
1143 return => { desc => "A fleshed bresv or an ilsevent on error" }