LP1816475: Booking module refresh
[working/Evergreen.git] / Open-ILS / src / perlmods / lib / OpenILS / Application / Booking.pm
1 package OpenILS::Application::Booking;
2
3 use strict;
4 use warnings;
5
6 use POSIX qw/strftime/;
7 use OpenILS::Application;
8 use base qw/OpenILS::Application/;
9
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";
15
16 use OpenSRF::Utils::Logger qw/$logger/;
17
18 sub prepare_new_brt {
19     my ($record_id, $owning_lib, $mvr) = @_;
20
21     my $brt = new Fieldmapper::booking::resource_type;
22     $brt->isnew(1);
23     $brt->name($mvr->title);
24     $brt->record($record_id);
25     $brt->catalog_item('t');
26     $brt->transferable('t');
27     $brt->owner($owning_lib);
28
29     return $brt;
30 }
31
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}
36     );
37
38     return $results->[0] if scalar(@$results) > 0;
39     return undef;
40 }
41
42 sub get_mvr {
43     return $U->simplereq(
44         'open-ils.search',
45         'open-ils.search.biblio.record.mods_slim.retrieve.authoritative',
46         shift # record id
47     );
48 }
49
50 sub get_unique_owning_libs {
51     my %hash = ();
52     $hash{$_->call_number->owning_lib} = 1 foreach (@_);    # @_ are copies
53     return keys %hash;
54 }
55
56 sub fetch_copies_by_ids {
57     my ($e, $copy_ids) = @_;
58     my $results = $e->search_asset_copy([
59         {id => $copy_ids},
60         {flesh => 1, flesh_fields => {acp => ['call_number']}}
61     ]);
62     return $results if ref($results) eq 'ARRAY';
63     return [];
64 }
65
66 sub get_single_record_id {
67     my $record_id = undef;
68     foreach (@_) {  # @_ are copies
69         return undef if
70             (defined $record_id && $record_id != $_->call_number->record);
71         $record_id = $_->call_number->record;
72     }
73     return $record_id;
74 }
75
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).
80 #
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.
85 #
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 {
89     +{ '-or' => [
90         { '-and' => [{$_[2] => {'>=', $_[0]}}, {$_[2] => {'<',  $_[1]}}]},
91         { '-and' => [{$_[3] => {'>',  $_[0]}}, {$_[3] => {'<',  $_[1]}}]},
92         { '-and' => { $_[3] => {'>',  $_[0]},   $_[2] => {'<=', $_[0]}}},
93         { '-and' => { $_[3] => {'>',  $_[1]},   $_[2] => {'<',  $_[1]}}},
94     ]};
95 }
96
97 sub create_brt_and_brsrc {
98     my ($self, $conn, $authtoken, $copy_ids) = @_;
99     my (@created_brt, @created_brsrc);
100     my %brt_table = ();
101
102     my $e = new_editor(xact => 1, authtoken => $authtoken);
103     return $e->die_event unless $e->checkauth;
104
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;
108
109     foreach (get_unique_owning_libs(@copies)) {
110         $brt_table{$_} = get_existing_brt($e, $record_id, $_, $mvr) ||
111             prepare_new_brt($record_id, $_, $mvr);
112     }
113
114     while (my ($owning_lib, $brt) = each %brt_table) {
115         my $pre_existing = 1;
116         if ($brt->isnew) {
117             if ($e->allowed('ADMIN_BOOKING_RESOURCE_TYPE', $owning_lib)) {
118                 $pre_existing = 0;
119                 return $e->die_event unless (
120                     #    v-- Important: assignment modifies original hash
121                     $brt = $e->create_booking_resource_type($brt)
122                 );
123             }
124         }
125         push @created_brt, [$brt->id, $brt->record, $pre_existing];
126     }
127
128     foreach (@copies) {
129         if ($e->allowed(
130             'ADMIN_BOOKING_RESOURCE', $_->call_number->owning_lib
131         )) {
132             # This block needs to disregard any cstore failures and just
133             # return what results it can.
134             my $brsrc = new Fieldmapper::booking::resource;
135             $brsrc->isnew(1);
136             $brsrc->type($brt_table{$_->call_number->owning_lib}->id);
137             $brsrc->owner($_->call_number->owning_lib);
138             $brsrc->barcode($_->barcode);
139
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/ }
147                 ))) {
148                     $usable_result = $usable_result->[0];
149                     $pre_existing = 1;
150                 } else {
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
155                     # results.
156                     $logger->warn(
157                         "Couldn't create or find brsrc for acp #" .  $_->id
158                     );
159                 }
160             } else {
161                 $e->release_savepoint("alpha");
162             }
163
164             if ($usable_result) {
165                 push @created_brsrc,
166                     [$usable_result->id, $_->id, $pre_existing];
167             }
168         }
169     }
170
171     $e->commit and
172         return {brt => \@created_brt, brsrc => \@created_brsrc} or
173         return $e->die_event;
174 }
175 __PACKAGE__->register_method(
176     method   => "create_brt_and_brsrc",
177     api_name => "open-ils.booking.resources.create_from_copies",
178     signature => {
179         params => [
180             {type => 'string', desc => 'Authentication token'},
181             {type => 'array', desc => 'Copy IDs'},
182         ],
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."}
188     }
189 );
190
191
192 sub create_bresv {
193     my ($self, $client, $authtoken,
194         $target_user_barcode, $datetime_range, $pickup_lib,
195         $brt, $brsrc_list, $attr_values, $email_notify, $note) = @_;
196
197     $brsrc_list = [ undef ] if not defined $brsrc_list;
198     return undef if scalar(@$brsrc_list) < 1; # Empty list not ok.
199
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");
203
204     my $usr = $U->fetch_user_by_barcode($target_user_barcode);
205     return $usr if ref($usr) eq 'HASH' and exists($usr->{"ilsevent"});
206
207     my $results = [];
208     foreach my $brsrc (@$brsrc_list) {
209         my $bresv = new Fieldmapper::booking::reservation;
210         $bresv->usr($usr->id);
211         $bresv->request_lib($e->requestor->ws_ou);
212         $bresv->pickup_lib($pickup_lib);
213         $bresv->start_time($datetime_range->[0]);
214         $bresv->end_time($datetime_range->[1]);
215         $bresv->email_notify(1) if $email_notify;
216         $bresv->note($note) if $note;
217
218         # A little sanity checking: don't agree to put a reservation on a
219         # brsrc and a brt when they don't match.  In fact, bomb out of
220         # this transaction entirely.
221         if ($brsrc) {
222             my $brsrc_itself = $e->retrieve_booking_resource([
223                 $brsrc, {
224                     "flesh" => 1,
225                     "flesh_fields" => {"brsrc" => ["type"]}
226                 }
227             ]);
228
229             if (not $brsrc_itself) {
230                 my $ev = new OpenILS::Event(
231                     "RESERVATION_BAD_PARAMS",
232                     desc => "brsrc $brsrc doesn't exist"
233                 );
234                 $e->disconnect;
235                 return $ev;
236             }
237             elsif ($brsrc_itself->type->id != $brt) {
238                 my $ev = new OpenILS::Event(
239                     "RESERVATION_BAD_PARAMS",
240                     desc => "brsrc $brsrc doesn't match given brt $brt"
241                 );
242                 $e->disconnect;
243                 return $ev;
244             }
245
246             # Also bail if the user is trying to create a reservation at
247             # a pickup lib to which our resource won't go.
248             if (
249                 $brsrc_itself->owner != $pickup_lib and
250                     not $brsrc_itself->type->transferable
251             ) {
252                 my $ev = new OpenILS::Event(
253                     "RESERVATION_BAD_PARAMS",
254                     desc => "brsrc $brsrc doesn't belong to $pickup_lib and " .
255                         "is not transferable"
256                 );
257                 $e->disconnect;
258                 return $ev;
259             }
260         }
261         $bresv->target_resource($brsrc);    # undef is ok here
262         $bresv->target_resource_type($brt);
263
264         ($bresv = $e->create_booking_reservation($bresv)) or
265             return $e->die_event;
266
267         # We could/should do some sanity checking on this too: namely, on
268         # whether the attribute values given actually apply to the relevant
269         # brt.  Not seeing any grievous side effects of not checking, though.
270         my @bravm = ();
271         foreach my $value (@$attr_values) {
272             my $bravm = new Fieldmapper::booking::reservation_attr_value_map;
273             $bravm->reservation($bresv->id);
274             $bravm->attr_value($value);
275             $bravm = $e->create_booking_reservation_attr_value_map($bravm) or
276                 return $e->die_event;
277             push @bravm, $bravm;
278         }
279         push @$results, {
280             "bresv" => $bresv->id,
281             "bravm" => \@bravm,
282         };
283     }
284
285     $e->commit or return $e->die_event;
286
287     # Targeting must be tacked on _after_ committing the transaction where the
288     # reservations are actually created.
289     foreach (@$results) {
290         $_->{"targeting"} = $U->storagereq(
291             "open-ils.storage.booking.reservation.resource_targeter",
292             $_->{"bresv"}
293         )->[0];
294     }
295     return $results;
296 }
297 __PACKAGE__->register_method(
298     method   => "create_bresv",
299     api_name => "open-ils.booking.reservations.create",
300     signature => {
301         params => [
302             {type => 'string', desc => 'Authentication token'},
303             {type => 'string', desc => 'Barcode of user for whom to reserve'},
304             {type => 'array', desc => 'Two elements: start and end timestamp'},
305             {type => 'int', desc => 'Desired reservation pickup lib'},
306             {type => 'int', desc => 'Booking resource type'},
307             {type => 'list', desc => 'Booking resource (undef ok; empty not ok)'},
308             {type => 'array', desc => 'Attribute values selected'},
309             {type => 'bool', desc => 'Email notification?'},
310             {type => 'string', desc => 'Optional note'},
311         ],
312         return => { desc => "A hash containing the new bresv and a list " .
313             "of new bravm"}
314     }
315 );
316
317
318 sub resource_list_by_attrs {
319     my $self = shift;
320     my $client = shift;
321     my $auth = shift; # Keep as argument, though not used just now.
322     my $filters = shift;
323
324     return undef unless ($filters->{type} || $filters->{attribute_values});
325
326     my $query = {
327         "select"   => {brsrc => [qw/id owner/], brt => ["elbow_room"]},
328         "from"     => {brsrc => {"brt" => {}}},
329         "where"    => {},
330         "distinct" => 1
331     };
332
333     $query->{where} = {"-and" => []};
334     if ($filters->{type}) {
335         push @{$query->{where}->{"-and"}}, {"type" => $filters->{type}};
336     }
337
338     if ($filters->{pickup_lib}) {
339         push @{$query->{where}->{"-and"}},
340             {"-or" => [
341                 {"owner" => $filters->{pickup_lib}},
342                 {"+brt" => {"transferable" => "t"}}
343             ]};
344     }
345
346     if ($filters->{attribute_values}) {
347
348         $query->{from}->{brsrc}->{bram} = { field => 'resource' };
349
350         $filters->{attribute_values} = [$filters->{attribute_values}]
351             if (!ref($filters->{attribute_values}));
352
353         $query->{having}->{'+bram'}->{value}->{'@>'} = {
354             transform => 'array_accum',
355             value => '$_' . $$ . '${' .
356                 join(',', @{$filters->{attribute_values}}) .
357                 '}$_' . $$ . '$'
358         };
359     }
360
361     if ($filters->{available}) {
362         # If only one timestamp has been provided, make it into a range.
363         if (!ref($filters->{available})) {
364             $filters->{available} = [($filters->{available}) x 2];
365         }
366
367         push @{$query->{where}->{"-and"}}, {
368             "-or" => [
369                 {"overbook" => "t"},
370                 {"-not-exists" => {
371                     "select" => {"bresv" => ["id"]},
372                     "from" => "bresv",
373                     "where" => {"-and" => [
374                         json_query_ranges_overlap(
375                             $filters->{available}->[0],
376                             $filters->{available}->[1],
377                             "start_time",
378                             "end_time"
379                         ),
380                         {"cancel_time" => undef},
381                         {"return_time" => undef},
382                         {"current_resource" => {"=" => {"+brsrc" => "id"}}}
383                     ]},
384                 }}
385             ]
386         };
387     }
388     if ($filters->{booked}) {
389         # If only one timestamp has been provided, make it into a range.
390         if (!ref($filters->{booked})) {
391             $filters->{booked} = [($filters->{booked}) x 2];
392         }
393
394         push @{$query->{where}->{"-and"}}, {
395             "-exists" => {
396                 "select" => {"bresv" => ["id"]},
397                 "from" => "bresv",
398                 "where" => {"-and" => [
399                     json_query_ranges_overlap(
400                         $filters->{booked}->[0],
401                         $filters->{booked}->[1],
402                         "start_time",
403                         "end_time"
404                     ),
405                     {"cancel_time" => undef},
406                     {"current_resource" => { "=" => {"+brsrc" => "id"}}}
407                 ]},
408             }
409         };
410         # I think that the "booked" case could be done with a JOIN instead of
411         # an EXISTS, but I'm leaving it this way for symmetry with the
412         # "available" case for now.  The available case cannot be done with a
413         # join.
414     }
415
416     my $cstore = OpenSRF::AppSession->connect('open-ils.cstore');
417     my $rows = $cstore->request(
418         "open-ils.cstore.json_query.atomic", $query
419     )->gather(1);
420     $cstore->disconnect;
421
422     return [] if not @$rows;
423
424     if ($filters->{"pickup_lib"} && $filters->{"available"}) {
425         my @new_rows = ();
426         my $general_elbow_room = $U->ou_ancestor_setting_value(
427             $filters->{"pickup_lib"},
428             "circ.booking_reservation.default_elbow_room"
429         ) || '0 seconds';
430         my $would_start = $filters->{"available"}->[0];
431         my $dt_parser = new DateTime::Format::ISO8601;
432
433         $logger->info(
434             "general_elbow_room: '$general_elbow_room', " .
435             "would_start: '$would_start'"
436         );
437
438         # Here, elbow_room will double as required transit time padding.
439         foreach (@$rows) {
440             my $elbow_room = $_->{"elbow_room"} || $general_elbow_room;
441             if ($_->{"owner"} != $filters->{"pickup_lib"}) {
442                 (my $ws = $would_start) =~ s/ /T/;
443                 push @new_rows, $_ if DateTime->compare(
444                     $dt_parser->parse_datetime($ws),
445                     DateTime->now(
446                         "time_zone" => DateTime::TimeZone->new(
447                             "name" => "local"
448                         )
449                     )->add(seconds => interval_to_seconds($elbow_room))
450                 ) >= 0;
451             } else {
452                 push @new_rows, $_;
453             }
454         }
455         return [map { $_->{id} } @new_rows];
456     } else {
457         return [map { $_->{id} } @$rows];
458     }
459 }
460
461
462 __PACKAGE__->register_method(
463     method   => "resource_list_by_attrs",
464     api_name => "open-ils.booking.resources.filtered_id_list",
465     argc     => 2,
466     signature=> {
467         params => [
468             {type => 'string', desc => 'Authentication token (unused for now,' .
469                ' but at least pass undef here)'},
470             {type => 'object', desc => 'Filter object: see notes for details'},
471         ],
472         return => { desc => "An array of brsrc ids matching the requested filters." },
473     },
474     notes    => <<'NOTES'
475
476 The filter object parameter can contain the following keys:
477  * type             => The id of a booking resource type (brt)
478  * attribute_values => The ids of booking resource type attribute values that the resource must have assigned to it (brav)
479  * available        => Either:
480                         A timestamp during which the resources are not reserved.  If the resource is overbookable, this is ignored.
481                         A range of two timestamps which do not overlap any reservations for the resources.  If the resource is overbookable, this is ignored.
482  * booked           => Either:
483                         A timestamp during which the resources are reserved.
484                         A range of two timestamps which overlap a reservation of the resources.
485
486 Note that at least one of 'type' or 'attribute_values' is required.
487
488 NOTES
489 );
490
491
492 sub reservation_list_by_filters {
493     my $self = shift;
494     my $client = shift;
495     my $auth = shift;
496     my $filters = shift;
497     my $whole_obj = shift;
498
499     return undef unless (
500            $filters->{user}
501         || $filters->{user_barcode}
502         || $filters->{resource}
503         || $filters->{type}
504         || $filters->{attribute_values}
505     );
506
507     my $e = new_editor(authtoken=>$auth);
508     return $e->event unless $e->checkauth;
509     return $e->event unless $e->allowed('VIEW_TRANSACTION');
510
511     my $query = {
512         'select'   => { bresv => [ 'id', 'start_time' ] },
513         'from'     => { bresv => {} },
514         'where'    => {},
515         'order_by' => [{ class => bresv => field => start_time => direction => 'asc' }],
516         'distinct' => 1
517     };
518
519     if ($filters->{fields}) {
520         $query->{where} = $filters->{fields};
521     }
522
523
524     if ($filters->{user}) {
525         $query->{where}->{usr} = $filters->{user};
526     }
527     elsif ($filters->{user_barcode}) {  # just one of user and user_barcode
528         my $usr = $U->fetch_user_by_barcode($filters->{user_barcode});
529         return $usr if ref($usr) eq 'HASH' and exists($usr->{"ilsevent"});
530         $query->{where}->{usr} = $usr->id;
531     }
532
533
534     if ($filters->{type}) {
535         $query->{where}->{target_resource_type} = $filters->{type};
536     }
537
538     $query->{where}->{"-and"} = [];
539     if ($filters->{resource}) {
540 #       $query->{where}->{target_resource} = $filters->{resource};
541         push @{$query->{where}->{"-and"}}, {
542             "-or" => {
543                 "target_resource" => $filters->{resource},
544                 "current_resource" => $filters->{resource}
545             }
546         };
547     }
548
549     if ($filters->{attribute_values}) {
550
551         $query->{from}->{bresv}->{bravm} = { field => 'reservation' };
552
553         $filters->{attribute_values} = [$filters->{attribute_values}]
554             if (!ref($filters->{attribute_values}));
555
556         $query->{having}->{'+bravm'}->{attr_value}->{'@>'} = {
557             transform => 'array_accum',
558             value => '$_' . $$ . '${' .
559                 join(',', @{$filters->{attribute_values}}) .
560                 '}$_' . $$ . '$'
561         };
562     }
563
564     if ($filters->{search_start} || $filters->{search_end}) {
565         my $or = {};
566
567         $or->{start_time} =
568             {'between' => [ $filters->{search_start}, $filters->{search_end}]}
569                 if $filters->{search_start};
570
571         $or->{end_time} =
572             {'between' =>[$filters->{search_start}, $filters->{search_end}]}
573                 if $filters->{search_end};
574
575         push @{$query->{where}->{"-and"}}, {"-or" => $or};
576     }
577
578     if (not scalar @{$query->{"where"}->{"-and"}}) {
579         delete $query->{"where"}->{"-and"};
580     }
581
582     my $cstore = OpenSRF::AppSession->connect('open-ils.cstore');
583     my $ids = [ map { $_->{id} } @{
584         $cstore->request(
585             'open-ils.cstore.json_query.atomic', $query
586         )->gather(1)
587     } ];
588     $cstore->disconnect;
589
590     if (not $whole_obj or @$ids < 1) {
591         $e->disconnect;
592         return $ids;
593     }
594
595     my $bresv_list = $e->search_booking_reservation([
596         {"id" => $ids},
597         {"flesh" => 1,
598             "flesh_fields" => {
599                 "bresv" =>
600                     [qw/target_resource current_resource target_resource_type/]
601             }
602         }]
603     );
604     $e->disconnect;
605     return $bresv_list ? $bresv_list : [];
606 }
607
608 __PACKAGE__->register_method(
609     method   => "upcoming_reservation_list_by_user",
610     api_name => "open-ils.booking.reservations.upcoming_reservation_list_by_user",
611     argc     => 2,
612     signature=> {
613         params => [
614             {type => 'string', desc => 'Authentication token'},
615             {type => 'User ID', type => 'number', desc => 'User ID'},
616         ],
617         return => { desc => "Information about all reservations for a user that haven't yet ended." },
618     },
619     notes    => "You can send undef/NULL as the User ID to get reservations for the logged in user."
620 );
621
622 sub upcoming_reservation_list_by_user {
623     my ($self, $conn, $auth, $user_id) = @_;
624     my $e = new_editor(authtoken => $auth);
625     return $e->event unless $e->checkauth;
626
627     $user_id = $e->requestor->id unless defined $user_id;
628     
629     unless($e->requestor->id == $user_id) {
630         my $user = $e->retrieve_actor_user($user_id) or return $e->event;
631         return $e->event unless $e->allowed('VIEW_TRANSACTION');
632     }
633
634     my $select = { 'bresv' => [qw/start_time end_time cancel_time capture_time pickup_time pickup_lib/],
635         'brsrc' => [ 'barcode' ],
636         'brt' => [{'column' => 'name', 'alias' => 'resource_type_name'}],
637         'aou' => ['shortname', {'column' => 'name', 'alias' => 'pickup_name'}] };
638
639     my $from = { 'bresv' => {'brsrc' => {'field' => 'id', 'fkey' => 'current_resource'},
640         'brt' => {'field' => 'id', 'fkey' => 'target_resource_type'},
641         'aou' => {'field' => 'id', 'fkey' => 'pickup_lib'}} };
642
643     my $query = {
644         'select'   => $select,
645         'from'     => $from,
646         'where'    => { 'usr' => $user_id, 'return_time' => undef, 'end_time' => {'>' => gmtime_ISO8601() }},
647         'order_by' => [{ class => bresv => field => start_time => direction => 'asc' }]
648     };
649
650     my $cstore = OpenSRF::AppSession->connect('open-ils.cstore');
651     my $rows = $cstore->request(
652         'open-ils.cstore.json_query.atomic', $query
653     )->gather(1);
654     $cstore->disconnect;
655     $e->disconnect;
656     return [] if not @$rows;
657     return $rows;
658 }
659
660 __PACKAGE__->register_method(
661     method   => "reservation_list_by_filters",
662     api_name => "open-ils.booking.reservations.filtered_id_list",
663     argc     => 3,
664     signature=> {
665         params => [
666             {type => 'string', desc => 'Authentication token'},
667             {type => "object", desc => "Filter object: see notes for details"},
668             {type => "bool", desc => "Return whole object instead of ID? (default false)"}
669         ],
670         return => { desc => "An array of bresv ids matching the requested filters." },
671     },
672     notes    => <<'NOTES'
673
674 The filter object parameter can contain the following keys:
675  * user             => The id of a user that has requested a bookable item -- filters on bresv.usr
676  * barcode          => The barcode of a user that has requested a bookable item
677  * type             => The id of a booking resource type (brt) -- filters on bresv.target_resource_type
678  * resource         => The id of a booking resource (brsrc) -- filters on bresv.target_resource
679  * attribute_values => The ids of booking resource type attribute values that the resource must have assigned to it (brav)
680  * search_start     => If search_end is not specified, booking interval (start_time to end_time) must contain this timestamp.
681  * search_end       => If search_start is not specified, booking interval (start_time to end_time) must contain this timestamp.
682  * fields           => An object containing any combination of bresv search filters in standard cstore/pcrud search format.
683
684 Note that at least one of 'user', 'type', 'resource' or 'attribute_values' is required.  If both search_start and search_end are specified,
685 then the result includes any reservations that overlap with that time range.  Any filter fields supplied in 'fields' are overridden
686 by the top-level filters ('user', 'type', 'resource').
687
688 NOTES
689 );
690
691
692 sub naive_ts_string {strftime("%F %T", localtime($_[0] || time));}
693
694 # Return a map of bresv or an ilsevent on failure.
695 sub get_uncaptured_bresv_for_brsrc {
696     my ($e, $o) = @_; # o's keys (all optional): owning_lib, barcode, range
697
698     my $from_clause = {
699         "bresv" => {
700             "brsrc" => {"field" => "id", "fkey" => "current_resource"}
701         }
702     };
703
704     my $query = {
705         "select" => {
706             "bresv" => [
707                 "current_resource",
708                 {
709                     "column" => "start_time",
710                     "transform" => "min",
711                     "aggregate" => 1
712                 }
713             ]
714         },
715         "from" => $from_clause,
716         "where" => {
717             "-and" => [
718                 {"current_resource" => {"!=" => undef}},
719                 {"capture_time" => undef},
720                 {"cancel_time" => undef},
721                 {"return_time" => undef},
722                 {"pickup_time" => undef}
723             ]
724         }
725     };
726     if ($o->{"owning_lib"}) {
727         push @{$query->{"where"}->{"-and"}},
728             {"+brsrc" => {"owner" => $o->{"owning_lib"}}};
729     }
730     if ($o->{"range"}) {
731         push @{$query->{"where"}->{"-and"}},
732             json_query_ranges_overlap(
733                 $o->{"range"}->[0], $o->{"range"}->[1],
734                 "start_time", "end_time"
735             );
736     }
737     if ($o->{"barcode"}) {
738         push @{$query->{"where"}->{"-and"}},
739             {"+brsrc" => {"barcode" => $o->{"barcode"}}};
740     }
741
742     my $rows = $e->json_query($query);
743     my $current_resource_bresv_map = {};
744     if (@$rows) {
745         my $id_query = {
746             "select" => {"bresv" => ["id"]},
747             "from" => $from_clause,
748             "where" => {
749                 "-and" => [
750                     {"current_resource" => "PLACEHOLDER"},
751                     {"start_time" => "PLACEHOLDER"},
752                     {"capture_time" => undef},
753                     {"cancel_time" => undef},
754                     {"return_time" => undef},
755                     {"pickup_time" => undef}
756                 ]
757             }
758         };
759         if ($o->{"owning_lib"}) {
760             push @{$id_query->{"where"}->{"-and"}},
761                 {"+brsrc" => {"owner" => $o->{"owning_lib"}}};
762         }
763
764         foreach (@$rows) {
765             $id_query->{"where"}->{"-and"}->[0]->{"current_resource"} =
766                 $_->{"current_resource"};
767             $id_query->{"where"}->{"-and"}->[1]->{"start_time"} =
768                 $_->{"start_time"};
769
770             my $results = $e->json_query($id_query);
771             if ($results && @$results) {
772                 $current_resource_bresv_map->{$_->{"current_resource"}} =
773                     [map { $_->{"id"} } @$results];
774             }
775         }
776     }
777     return $current_resource_bresv_map;
778 }
779
780 sub get_pull_list {
781     my ($self, $client, $auth, $range, $interval_secs, $owning_lib) = @_;
782
783     my $e = new_editor(xact => 1, authtoken => $auth);
784     return $e->die_event unless $e->checkauth;
785     return $e->die_event unless $e->allowed("RETRIEVE_RESERVATION_PULL_LIST");
786     return $e->die_event unless (
787         ref($range) eq "ARRAY" or
788         ($interval_secs = int($interval_secs)) > 0
789     );
790
791     $owning_lib = $e->requestor->ws_ou if not $owning_lib;
792     $range = [ naive_ts_string(time), naive_ts_string(time + $interval_secs) ]
793         if not $range;
794
795     my $uncaptured = get_uncaptured_bresv_for_brsrc(
796         $e, {"range" => $range, "owning_lib" => $owning_lib}
797     );
798
799     if (keys(%$uncaptured)) {
800         my @all_bresv_ids = map { @{$_} } values %$uncaptured;
801         my %bresv_lookup = (
802             map { $_->id => $_ } @{
803                 $e->search_booking_reservation([{"id" => [@all_bresv_ids]}, {
804                     flesh => 1,
805                     flesh_fields => { bresv => [
806                         "usr", "target_resource_type", "current_resource"
807                     ]}
808                 }])
809             }
810         );
811         $e->disconnect;
812         return [ map {
813             my $key = $_;
814             my $one = $bresv_lookup{$uncaptured->{$key}->[0]};
815             my $result = {
816                 "current_resource" => $one->current_resource,
817                 "target_resource_type" => $one->target_resource_type,
818                 "reservations" => [
819                     map { $bresv_lookup{$_} } @{$uncaptured->{$key}}
820                 ]
821             };
822             foreach (@{$result->{"reservations"}}) {    # deflesh
823                 $_->current_resource($_->current_resource->id);
824                 $_->target_resource_type($_->target_resource_type->id);
825             }
826             $result;
827         } keys %$uncaptured ];
828     } else {
829         $e->disconnect;
830         return [];
831     }
832 }
833 __PACKAGE__->register_method(
834     method   => "get_pull_list",
835     api_name => "open-ils.booking.reservations.get_pull_list",
836     argc     => 4,
837     signature=> {
838         params => [
839             {type => "string", desc => "Authentication token"},
840             {type => "array", desc =>
841                 "range: Date/time range for reservations (opt)"},
842             {type => "int", desc =>
843                 "interval: Seconds from now (instead of range)"},
844             {type => "number", desc => "(Optional) Owning library"}
845         ],
846         return => { desc => "An array of hashes, each containing key/value " .
847             "pairs describing resource, resource type, and a list of " .
848             "reservations that claim the given resource." }
849     }
850 );
851
852
853 sub could_capture {
854     my ($self, $client, $auth, $barcode) = @_;
855
856     my $e = new_editor("authtoken" => $auth);
857     return $e->die_event unless $e->checkauth;
858     return $e->die_event unless $e->allowed("COPY_CHECKIN");
859
860     my $dt_parser = new DateTime::Format::ISO8601;
861     my $now = now DateTime; # sic
862     my $res = get_uncaptured_bresv_for_brsrc($e, {"barcode" => $barcode});
863
864     if ($res and keys %$res) {
865         my $id;
866         while ((undef, $id) = each %$res) {
867             my $bresv = $e->retrieve_booking_reservation([
868                 $id, {
869                     "flesh" => 1, "flesh_fields" => {
870                         "bresv" => [qw(
871                             usr target_resource_type
872                             target_resource current_resource
873                         )]
874                     }
875                 }
876             ]);
877             my $elbow_room = interval_to_seconds(
878                 $bresv->target_resource_type->elbow_room ||
879                 $U->ou_ancestor_setting_value(
880                     $bresv->pickup_lib,
881                     "circ.booking_reservation.default_elbow_room"
882                 ) ||
883                 "0 seconds"
884             );
885
886             unless ($elbow_room) {
887                 $client->respond($bresv);
888             } else {
889                 my $start_time = $dt_parser->parse_datetime(
890                     clean_ISO8601($bresv->start_time)
891                 );
892
893                 if ($now >= $start_time->subtract("seconds" => $elbow_room)) {
894                     $client->respond($bresv);
895                 } else {
896                     $logger->info(
897                         "not within elbow room: $elbow_room, " .
898                         "else would have returned bresv " . $bresv->id
899                     );
900                 }
901             }
902         }
903     }
904     $e->disconnect;
905     undef;
906 }
907 __PACKAGE__->register_method(
908     method   => "could_capture",
909     api_name => "open-ils.booking.reservations.could_capture",
910     argc     => 2,
911     streaming=> 1,
912     signature=> {
913         params => [
914             {type => "string", desc => "Authentication token"},
915             {type => "string", desc => "Resource barcode"}
916         ],
917         return => {desc => "One or zero reservations; event on error."}
918     }
919 );
920
921
922 sub get_copy_fleshed_just_right {
923     my ($self, $client, $auth, $barcode) = @_;
924
925     return undef if not defined $barcode;
926     return {} if ref($barcode) eq "ARRAY" and not @$barcode;
927
928     my $e = new_editor(authtoken => $auth);
929     my $results = $e->search_asset_copy([
930         {"barcode" => $barcode},
931         {
932             "flesh" => 1,
933             "flesh_fields" => {"acp" => [qw/call_number location/]}
934         }
935     ]);
936
937     if (ref($results) eq "ARRAY") {
938         $e->disconnect;
939         return $results->[0] unless ref $barcode;
940         return +{ map { $_->barcode => $_ } @$results };
941     } else {
942         return $e->die_event;
943     }
944 }
945 __PACKAGE__->register_method(
946     method   => "get_copy_fleshed_just_right",
947     api_name => "open-ils.booking.asset.get_copy_fleshed_just_right",
948     argc     => 2,
949     signature=> {
950         params => [
951             {type => "string", desc => "Authentication token"},
952             {type => "mixed", desc => "One barcode or an array of them"},
953         ],
954         return => { desc =>
955             "A copy, or a hash of copies keyed by barcode if an array of " .
956             "barcodes was given"
957         }
958     }
959 );
960
961
962 sub best_bresv_candidate {
963     my ($e, $id_list) = @_;
964
965     # This will almost always be the case.
966     if (@$id_list == 1) {
967         $logger->info("best_bresv_candidate (only) " . $id_list->[0]);
968         return $id_list->[0];
969     }
970
971     my @here = ();
972     my $this_ou = $e->requestor->ws_ou;
973     my $results = $e->json_query({
974         "select" => {"brsrc" => ["pickup_lib"], "bresv" => ["id"]},
975         "from" => {
976             "bresv" => {
977                 "brsrc" => {"field" => "id", "fkey" => "current_resource"}
978             }
979         },
980         "where" => {
981             {"+bresv" => {"id" => $id_list}}
982         }
983     });
984
985     foreach (@$results) {
986         push @here, $_->{"id"} if $_->{"pickup_lib"} == $this_ou;
987     }
988
989     my $result;
990     if (@here > 0) {
991         $result = @here == 1 ? pop @here : (sort @here)[0];
992     } else {
993         $result = (sort @$id_list)[0];
994     }
995     $logger->info(
996         "best_bresv_candidate from " . join(",", @$id_list) . ": $result"
997     );
998     return $result;
999 }
1000
1001
1002 sub capture_resource_for_reservation {
1003     my ($self, $client, $auth, $barcode, $no_update_copy) = @_;
1004
1005     my $e = new_editor(authtoken => $auth);
1006     return $e->die_event unless $e->checkauth;
1007     return $e->die_event unless $e->allowed("COPY_CHECKIN");
1008
1009     my $uncaptured = get_uncaptured_bresv_for_brsrc(
1010         $e, {"barcode" => $barcode}
1011     );
1012
1013     if (keys %$uncaptured) {
1014         # Note this will only capture one reservation at a time, even in
1015         # cases with overbooking (multiple "soonest" bresv's on a resource).
1016         my $bresv = best_bresv_candidate(
1017             $e, $uncaptured->{
1018                 (sort(keys %$uncaptured))[0]
1019             }
1020         );
1021         $e->disconnect;
1022         return capture_reservation(
1023             $self, $client, $auth, $bresv, $no_update_copy
1024         );
1025     } else {
1026         return new OpenILS::Event(
1027             "RESERVATION_NOT_FOUND",
1028             "desc" => "No capturable reservation found pertaining " .
1029                 "to a resource with barcode $barcode",
1030             "payload" => {"fail_cause" => "no-reservation", "captured" => 0}
1031         );
1032     }
1033 }
1034 __PACKAGE__->register_method(
1035     method   => "capture_resource_for_reservation",
1036     api_name => "open-ils.booking.resources.capture_for_reservation",
1037     argc     => 3,
1038     signature=> {
1039         params => [
1040             {type => "string", desc => "Authentication token"},
1041             {type => "string", desc => "Barcode of booked & targeted resource"},
1042             {type => "number", desc => "(optional) 1 to not update copy"}
1043         ],
1044         return => { desc => "An OpenILS event describing the capture outcome" }
1045     }
1046 );
1047
1048
1049 sub capture_reservation {
1050     my ($self, $client, $auth, $res_id, $no_update_copy) = @_;
1051
1052     my $e = new_editor("xact" => 1, "authtoken" => $auth);
1053     return $e->die_event unless $e->checkauth;
1054     return $e->die_event unless $e->allowed("COPY_CHECKIN");
1055     my $here = $e->requestor->ws_ou;
1056
1057     my $reservation = $e->retrieve_booking_reservation([
1058         $res_id, {
1059             "flesh" => 2, "flesh_fields" => {
1060                 "bresv" => [qw/usr current_resource type/],
1061                 "au" => ["card"],
1062                 "brsrc" => ["type"]
1063             }
1064         }
1065     ]);
1066
1067     return new OpenILS::Event("RESERVATION_NOT_FOUND") unless $reservation;
1068     return new OpenILS::Event(
1069         "RESERVATION_CAPTURE_FAILED",
1070         payload => {"captured" => 0, "fail_cause" => "no-resource"}
1071     ) unless $reservation->current_resource;
1072
1073     return new OpenILS::Event(
1074         "RESERVATION_CAPTURE_FAILED",
1075         "payload" => {"captured" => 0, "fail_cause" => "cancelled"}
1076     ) if $reservation->cancel_time;
1077
1078     $reservation->capture_staff($e->requestor->id);
1079     $reservation->capture_time("now");
1080
1081     $e->update_booking_reservation($reservation) or return $e->die_event;
1082
1083     my $ret = {"captured" => 1, "reservation" => $reservation};
1084
1085     my $search_acp_like_this = [
1086         {
1087             "barcode" => $reservation->current_resource->barcode,
1088             "deleted" => "f"
1089         },
1090         {"flesh" => 1, "flesh_fields" => {"acp" => ["call_number"]}}
1091     ];
1092
1093     if ($here != $reservation->pickup_lib) {
1094         $logger->info("resource isn't at the reservation's pickup lib...");
1095         return new OpenILS::Event(
1096             "RESERVATION_CAPTURE_FAILED",
1097             "payload" => {"captured" => 0, "fail_cause" => "not-transferable"}
1098         ) unless $U->is_true(
1099             $reservation->current_resource->type->transferable
1100         );
1101
1102         # need to transit the item ... is it already in transit?
1103         my $transit = $e->search_action_reservation_transit_copy(
1104             {"reservation" => $res_id, "dest_recv_time" => undef, cancel_time => undef}
1105         )->[0];
1106
1107         if (!$transit) { # not yet in transit
1108             $transit = new Fieldmapper::action::reservation_transit_copy;
1109
1110             $transit->reservation($reservation->id);
1111             $transit->target_copy($reservation->current_resource->id);
1112             $transit->copy_status(15);
1113             $transit->source_send_time("now");
1114             $transit->source($here);
1115             $transit->dest($reservation->pickup_lib);
1116
1117             $e->create_action_reservation_transit_copy($transit);
1118
1119             if ($U->is_true(
1120                 $reservation->current_resource->type->catalog_item
1121             )) {
1122                 my $copy = $e->search_asset_copy($search_acp_like_this)->[0];
1123
1124                 if ($copy) {
1125                     return new OpenILS::Event(
1126                         "OPEN_CIRCULATION_EXISTS",
1127                         "payload" => {"captured" => 0, "copy" => $copy}
1128                     ) if $copy->status == 1 and not $no_update_copy;
1129
1130                     $ret->{"mvr"} = get_mvr($copy->call_number->record);
1131                     if ($no_update_copy) {
1132                         $ret->{"new_copy_status"} = 6;
1133                     } else {
1134                         $copy->status(6);
1135                         $e->update_asset_copy($copy) or return $e->die_event;
1136                     }
1137                 }
1138             }
1139         }
1140
1141         $ret->{"transit"} = $transit;
1142     } elsif ($U->is_true($reservation->current_resource->type->catalog_item)) {
1143         $logger->info("resource is a catalog item...");
1144         my $copy = $e->search_asset_copy($search_acp_like_this)->[0];
1145
1146         if ($copy) {
1147             return new OpenILS::Event(
1148                 "OPEN_CIRCULATION_EXISTS",
1149                 "payload" => {"captured" => 0, "copy" => $copy}
1150             ) if $copy->status == 1 and not $no_update_copy;
1151
1152             $ret->{"mvr"} = get_mvr($copy->call_number->record);
1153             if ($no_update_copy) {
1154                 $ret->{"new_copy_status"} = 15;
1155             } else {
1156                 $copy->status(15);
1157                 $e->update_asset_copy($copy) or return $e->die_event;
1158             }
1159         }
1160     }
1161
1162     $e->commit or return $e->die_event;
1163
1164     # create action trigger event to notify that reservation is available
1165     if ($reservation->email_notify) {
1166         my $ses = OpenSRF::AppSession->create('open-ils.trigger');
1167         $ses->request('open-ils.trigger.event.autocreate', 'reservation.available', $reservation, $reservation->pickup_lib);
1168     }
1169
1170     # XXX I'm not sure whether these last two elements of the payload
1171     # actually get used anywhere.
1172     $ret->{"resource"} = $reservation->current_resource;
1173     $ret->{"type"} = $reservation->current_resource->type;
1174     return new OpenILS::Event("SUCCESS", "payload" => $ret);
1175 }
1176 __PACKAGE__->register_method(
1177     method   => "capture_reservation",
1178     api_name => "open-ils.booking.reservations.capture",
1179     argc     => 2,
1180     signature=> {
1181         params => [
1182             {type => 'string', desc => 'Authentication token'},
1183             {type => 'mixed', desc =>
1184                 'Reservation ID (number) or array of resource barcodes'}
1185         ],
1186         return => { desc => "An OpenILS Event object describing the outcome of the capture, with relevant payload." },
1187     }
1188 );
1189
1190
1191 sub cancel_reservation {
1192     my ($self, $client, $auth, $id_list) = @_;
1193
1194     my $e = new_editor(xact => 1, authtoken => $auth);
1195     return $e->die_event unless $e->checkauth;
1196     # Should the following permission really be checked as relates to each
1197     # individual reservation's request_lib?  Hrmm...
1198     return $e->die_event unless $e->allowed("ADMIN_BOOKING_RESERVATION");
1199
1200     my $bresv_list = $e->search_booking_reservation([
1201         {"id" => $id_list},
1202         {"flesh" => 1, "flesh_fields" => {"bresv" => [
1203             "current_resource", "target_resource_type"
1204         ]}}
1205     ]);
1206     return $e->die_event if not $bresv_list;
1207
1208     my @results = ();
1209     my $circ = OpenSRF::AppSession->connect("open-ils.circ") or
1210         return $e->die_event;
1211     foreach my $bresv (@$bresv_list) {
1212         $bresv->cancel_time("now");
1213         $e->update_booking_reservation($bresv) or do {
1214             $circ->disconnect;
1215             return $e->die_event;
1216         };
1217         $e->xact_commit;
1218         $e->xact_begin;
1219
1220         if (
1221             $bresv->target_resource_type->catalog_item == "t" &&
1222             $bresv->current_resource
1223         ) {
1224             $logger->info("result of no-op checkin (upon cxl bresv) is " .
1225                 $circ->request(
1226                     "open-ils.circ.checkin", $auth,
1227                     {"barcode" => $bresv->current_resource->barcode,
1228                         "noop" => 1}
1229                 )->gather(1)->{"textcode"});
1230         }
1231         push @results, $bresv->id;
1232     }
1233
1234     $e->disconnect;
1235     $circ->disconnect;
1236
1237     return \@results;
1238 }
1239 __PACKAGE__->register_method(
1240     method   => "cancel_reservation",
1241     api_name => "open-ils.booking.reservations.cancel",
1242     argc     => 2,
1243     signature=> {
1244         params => [
1245             {type => "string", desc => "Authentication token"},
1246             {type => "array", desc => "List of reservation IDs"}
1247         ],
1248         return => { desc => "A list of canceled reservation IDs" },
1249     }
1250 );
1251
1252
1253 sub get_captured_reservations {
1254     my ($self, $client, $auth, $barcode, $which) = @_;
1255
1256     my $e = new_editor(xact => 1, authtoken => $auth);
1257     return $e->die_event unless $e->checkauth;
1258     return $e->die_event unless $e->allowed("VIEW_USER");
1259     return $e->die_event unless $e->allowed("ADMIN_BOOKING_RESERVATION");
1260
1261     # fetch the patron for our uses in any case...
1262     my $patron = $U->fetch_user_by_barcode($barcode);
1263     return $patron if ref($patron) eq "HASH" and exists $patron->{"ilsevent"};
1264
1265     my $bresv_flesh = {
1266         "flesh" => 1,
1267         "flesh_fields" => {"bresv" => [
1268             qw/target_resource_type current_resource/
1269         ]}
1270     };
1271
1272     my $dispatch = {
1273         "patron" => sub {
1274             return $patron;
1275         },
1276         "ready" => sub {
1277             return $e->search_booking_reservation([
1278                 {
1279                     "usr" => $patron->id,
1280                     "capture_time" => {"!=" => undef},
1281                     "pickup_time" => undef,
1282                     "start_time" => {"!=" => undef},
1283                     "cancel_time" => undef
1284                 },
1285                 $bresv_flesh
1286             ]) or $e->die_event;
1287         },
1288         "out" => sub {
1289             return $e->search_booking_reservation([
1290                 {
1291                     "usr" => $patron->id,
1292                     "pickup_time" => {"!=" => undef},
1293                     "return_time" => undef,
1294                     "cancel_time" => undef
1295                 },
1296                 $bresv_flesh
1297             ]) or $e->die_event;
1298         },
1299         "in" => sub {
1300             return $e->search_booking_reservation([
1301                 {
1302                     "usr" => $patron->id,
1303                     "return_time" => {">=" => "today"},
1304                     "cancel_time" => undef
1305                 },
1306                 $bresv_flesh
1307             ]) or $e->die_event;
1308         }
1309     };
1310
1311     my $result = {};
1312     foreach (@$which) {
1313         my $f = $dispatch->{$_};
1314         if ($f) {
1315             my $r = &{$f}();
1316             return $r if (ref($r) eq "HASH" and exists $r->{"ilsevent"});
1317             $result->{$_} = $r;
1318         }
1319     }
1320
1321     return $result;
1322 }
1323 __PACKAGE__->register_method(
1324     method   => "get_captured_reservations",
1325     api_name => "open-ils.booking.reservations.get_captured",
1326     argc     => 3,
1327     signature=> {
1328         params => [
1329             {type => "string", desc => "Authentication token"},
1330             {type => "string", desc => "Patron barcode"},
1331             {type => "array", desc => "Parts wanted (patron, ready, out, in?)"}
1332         ],
1333         return => { desc => "A hash of parts." } # XXX describe more fully
1334     }
1335 );
1336
1337
1338 sub get_bresv_by_returnable_resource_barcode {
1339     my ($self, $client, $auth, $barcode) = @_;
1340
1341     my $e = new_editor(xact => 1, authtoken => $auth);
1342     return $e->die_event unless $e->checkauth;
1343     return $e->die_event unless $e->allowed("VIEW_USER");
1344 #    return $e->die_event unless $e->allowed("ADMIN_BOOKING_RESERVATION");
1345
1346     my $rows = $e->json_query({
1347         "select" => {"bresv" => ["id"]},
1348         "from" => {
1349             "bresv" => {
1350                 "brsrc" => {"field" => "id", "fkey" => "current_resource"}
1351             }
1352         },
1353         "where" => {
1354             "+brsrc" => {"barcode" => $barcode},
1355             "-and" => {
1356                 "pickup_time" => {"!=" => undef},
1357                 "cancel_time" => undef,
1358                 "return_time" => undef
1359             }
1360         }
1361     }) or return $e->die_event;
1362
1363     if (@$rows < 1) {
1364         $e->rollback;
1365         return $rows;
1366     } else {
1367         # More than one result might be possible, but we don't want to return
1368         # more than one at this time.
1369         my $id = $rows->[0]->{"id"};
1370         my $resp =$e->retrieve_booking_reservation([
1371             $id, {
1372                 "flesh" => 2,
1373                 "flesh_fields" => {
1374                     "bresv" => [qw/usr target_resource_type current_resource/],
1375                     "au" => ["card"]
1376                 }
1377             }
1378         ]) or $e->die_event;
1379         $e->rollback;
1380         return $resp;
1381     }
1382 }
1383
1384 __PACKAGE__->register_method(
1385     method   => "get_bresv_by_returnable_resource_barcode",
1386     api_name => "open-ils.booking.reservations.by_returnable_resource_barcode",
1387     argc     => 2,
1388     signature=> {
1389         params => [
1390             {type => "string", desc => "Authentication token"},
1391             {type => "string", desc => "Resource barcode"},
1392         ],
1393         return => { desc => "A fleshed bresv or an ilsevent on error" }
1394     }
1395 );
1396
1397
1398 1;