]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/perlmods/lib/OpenILS/Application/Booking.pm
LP2061136 - Stamping 1405 DB upgrade script
[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 $filters = { type => $brt, available => $datetime_range, pickup_lib => $pickup_lib };
208     if ( defined $brsrc_list->[0] ) {
209         $filters->{'resources'} = $brsrc_list;
210     }
211     my $available_resources = resource_list_by_attrs($self, $client, $authtoken, $filters);
212     unless (scalar @{$available_resources}) {
213         my $ev = OpenILS::Event->new(
214             'RESOURCE_IN_USE',
215             desc => 'Resource is in use at this time'
216         );
217         return $ev;
218     }
219
220     my $results = [];
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;
230
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.
234         if ($brsrc) {
235             my $brsrc_itself = $e->retrieve_booking_resource([
236                 $brsrc, {
237                     "flesh" => 1,
238                     "flesh_fields" => {"brsrc" => ["type"]}
239                 }
240             ]);
241
242             if (not $brsrc_itself) {
243                 my $ev = new OpenILS::Event(
244                     "RESERVATION_BAD_PARAMS",
245                     desc => "brsrc $brsrc doesn't exist"
246                 );
247                 $e->disconnect;
248                 return $ev;
249             }
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"
254                 );
255                 $e->disconnect;
256                 return $ev;
257             }
258
259             # Also bail if the user is trying to create a reservation at
260             # a pickup lib to which our resource won't go.
261             if (
262                 $brsrc_itself->owner != $pickup_lib and
263                     not $brsrc_itself->type->transferable
264             ) {
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"
269                 );
270                 $e->disconnect;
271                 return $ev;
272             }
273         }
274         $bresv->target_resource($brsrc);    # undef is ok here
275         $bresv->target_resource_type($brt);
276
277         ($bresv = $e->create_booking_reservation($bresv)) or
278             return $e->die_event;
279
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.
283         my @bravm = ();
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;
290             push @bravm, $bravm;
291         }
292         push @$results, {
293             "bresv" => $bresv->id,
294             "bravm" => \@bravm,
295         };
296     }
297
298     $e->commit or return $e->die_event;
299
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",
305             $_->{"bresv"}
306         )->[0];
307     }
308     return $results;
309 }
310 __PACKAGE__->register_method(
311     method   => "create_bresv",
312     api_name => "open-ils.booking.reservations.create",
313     signature => {
314         params => [
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'},
324         ],
325         return => { desc => "A hash containing the new bresv and a list " .
326             "of new bravm"}
327     }
328 );
329
330
331 sub resource_list_by_attrs {
332     my $self = shift;
333     my $client = shift;
334     my $auth = shift; # Keep as argument, though not used just now.
335     my $filters = shift;
336
337     return undef unless ($filters->{type} || $filters->{attribute_values});
338
339     my $query = {
340         "select"   => {brsrc => [qw/id owner/], brt => ["elbow_room"]},
341         "from"     => {brsrc => {"brt" => {}}},
342         "where"    => {},
343         "distinct" => 1
344     };
345
346     $query->{where} = {"-and" => []};
347     if ($filters->{type}) {
348         push @{$query->{where}->{"-and"}}, {"type" => $filters->{type}};
349     }
350
351     if ($filters->{resources}) {
352         push @{$query->{where}->{'-and'}}, {'id' => $filters->{resources}};
353     }
354
355     if ($filters->{pickup_lib}) {
356         push @{$query->{where}->{"-and"}},
357             {"-or" => [
358                 {"owner" => $filters->{pickup_lib}},
359                 {"+brt" => {"transferable" => "t"}}
360             ]};
361     }
362
363     if ($filters->{attribute_values}) {
364
365         $query->{from}->{brsrc}->{bram} = { field => 'resource' };
366
367         $filters->{attribute_values} = [$filters->{attribute_values}]
368             if (!ref($filters->{attribute_values}));
369
370         $query->{having}->{'+bram'}->{value}->{'@>'} = {
371             transform => 'array_agg',
372             value => '$_' . $$ . '${' .
373                 join(',', @{$filters->{attribute_values}}) .
374                 '}$_' . $$ . '$'
375         };
376     }
377
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];
382         }
383
384         push @{$query->{where}->{"-and"}}, {
385             "-or" => [
386                 {"overbook" => "t"},
387                 {"-not-exists" => {
388                     "select" => {"bresv" => ["id"]},
389                     "from" => "bresv",
390                     "where" => {"-and" => [
391                         json_query_ranges_overlap(
392                             $filters->{available}->[0],
393                             $filters->{available}->[1],
394                             "start_time",
395                             "end_time"
396                         ),
397                         {"cancel_time" => undef},
398                         {"return_time" => undef},
399                         {"current_resource" => {"=" => {"+brsrc" => "id"}}}
400                     ]},
401                 }}
402             ]
403         };
404     }
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];
409         }
410
411         push @{$query->{where}->{"-and"}}, {
412             "-exists" => {
413                 "select" => {"bresv" => ["id"]},
414                 "from" => "bresv",
415                 "where" => {"-and" => [
416                     json_query_ranges_overlap(
417                         $filters->{booked}->[0],
418                         $filters->{booked}->[1],
419                         "start_time",
420                         "end_time"
421                     ),
422                     {"cancel_time" => undef},
423                     {"current_resource" => { "=" => {"+brsrc" => "id"}}}
424                 ]},
425             }
426         };
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
430         # join.
431     }
432
433     my $cstore = OpenSRF::AppSession->connect('open-ils.cstore');
434     my $rows = $cstore->request(
435         "open-ils.cstore.json_query.atomic", $query
436     )->gather(1);
437     $cstore->disconnect;
438
439     return [] if not @$rows;
440
441     if ($filters->{"pickup_lib"} && $filters->{"available"}) {
442         my @new_rows = ();
443         my $general_elbow_room = $U->ou_ancestor_setting_value(
444             $filters->{"pickup_lib"},
445             "circ.booking_reservation.default_elbow_room"
446         ) || '0 seconds';
447         my $would_start = $filters->{"available"}->[0];
448         my $dt_parser = new DateTime::Format::ISO8601;
449
450         $logger->info(
451             "general_elbow_room: '$general_elbow_room', " .
452             "would_start: '$would_start'"
453         );
454
455         # Here, elbow_room will double as required transit time padding.
456         foreach (@$rows) {
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),
462                     DateTime->now(
463                         "time_zone" => DateTime::TimeZone->new(
464                             "name" => "local"
465                         )
466                     )->add(seconds => interval_to_seconds($elbow_room))
467                 ) >= 0;
468             } else {
469                 push @new_rows, $_;
470             }
471         }
472         return [map { $_->{id} } @new_rows];
473     } else {
474         return [map { $_->{id} } @$rows];
475     }
476 }
477
478
479 __PACKAGE__->register_method(
480     method   => "resource_list_by_attrs",
481     api_name => "open-ils.booking.resources.filtered_id_list",
482     argc     => 2,
483     signature=> {
484         params => [
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'},
488         ],
489         return => { desc => "An array of brsrc ids matching the requested filters." },
490     },
491     notes    => <<'NOTES'
492
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.
503  * booked           => Either:
504                         A timestamp during which the resources are reserved.
505                         A range of two timestamps which overlap a reservation of the resources.
506
507 Note that at least one of 'type' or 'attribute_values' is required.
508
509 NOTES
510 );
511
512
513 sub reservation_list_by_filters {
514     my $self = shift;
515     my $client = shift;
516     my $auth = shift;
517     my $filters = shift;
518     my $whole_obj = shift;
519
520     return undef unless (
521            $filters->{user}
522         || $filters->{user_barcode}
523         || $filters->{resource}
524         || $filters->{type}
525         || $filters->{attribute_values}
526     );
527
528     my $e = new_editor(authtoken=>$auth);
529     return $e->event unless $e->checkauth;
530     return $e->event unless $e->allowed('VIEW_TRANSACTION');
531
532     my $query = {
533         'select'   => { bresv => [ 'id', 'start_time' ] },
534         'from'     => { bresv => {} },
535         'where'    => {},
536         'order_by' => [{ class => bresv => field => start_time => direction => 'asc' }],
537         'distinct' => 1
538     };
539
540     if ($filters->{fields}) {
541         $query->{where} = $filters->{fields};
542     }
543
544
545     if ($filters->{user}) {
546         $query->{where}->{usr} = $filters->{user};
547     }
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;
552     }
553
554
555     if ($filters->{type}) {
556         $query->{where}->{target_resource_type} = $filters->{type};
557     }
558
559     $query->{where}->{"-and"} = [];
560     if ($filters->{resource}) {
561 #       $query->{where}->{target_resource} = $filters->{resource};
562         push @{$query->{where}->{"-and"}}, {
563             "-or" => {
564                 "target_resource" => $filters->{resource},
565                 "current_resource" => $filters->{resource}
566             }
567         };
568     }
569
570     if ($filters->{attribute_values}) {
571
572         $query->{from}->{bresv}->{bravm} = { field => 'reservation' };
573
574         $filters->{attribute_values} = [$filters->{attribute_values}]
575             if (!ref($filters->{attribute_values}));
576
577         $query->{having}->{'+bravm'}->{attr_value}->{'@>'} = {
578             transform => 'array_agg',
579             value => '$_' . $$ . '${' .
580                 join(',', @{$filters->{attribute_values}}) .
581                 '}$_' . $$ . '$'
582         };
583     }
584
585     if ($filters->{search_start} || $filters->{search_end}) {
586         my $or = {};
587
588         $or->{start_time} =
589             {'between' => [ $filters->{search_start}, $filters->{search_end}]}
590                 if $filters->{search_start};
591
592         $or->{end_time} =
593             {'between' =>[$filters->{search_start}, $filters->{search_end}]}
594                 if $filters->{search_end};
595
596         push @{$query->{where}->{"-and"}}, {"-or" => $or};
597     }
598
599     if (not scalar @{$query->{"where"}->{"-and"}}) {
600         delete $query->{"where"}->{"-and"};
601     }
602
603     my $cstore = OpenSRF::AppSession->connect('open-ils.cstore');
604     my $ids = [ map { $_->{id} } @{
605         $cstore->request(
606             'open-ils.cstore.json_query.atomic', $query
607         )->gather(1)
608     } ];
609     $cstore->disconnect;
610
611     if (not $whole_obj or @$ids < 1) {
612         $e->disconnect;
613         return $ids;
614     }
615
616     my $bresv_list = $e->search_booking_reservation([
617         {"id" => $ids},
618         {"flesh" => 1,
619             "flesh_fields" => {
620                 "bresv" =>
621                     [qw/target_resource current_resource target_resource_type/]
622             }
623         }]
624     );
625     $e->disconnect;
626     return $bresv_list ? $bresv_list : [];
627 }
628
629 __PACKAGE__->register_method(
630     method   => "upcoming_reservation_list_by_user",
631     api_name => "open-ils.booking.reservations.upcoming_reservation_list_by_user",
632     argc     => 2,
633     signature=> {
634         params => [
635             {type => 'string', desc => 'Authentication token'},
636             {type => 'User ID', type => 'number', desc => 'User ID'},
637         ],
638         return => { desc => "Information about all reservations for a user that haven't yet ended." },
639     },
640     notes    => "You can send undef/NULL as the User ID to get reservations for the logged in user."
641 );
642
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;
647
648     $user_id = $e->requestor->id unless defined $user_id;
649     
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');
653     }
654
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'}] };
659
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'}} };
663
664     my $query = {
665         'select'   => $select,
666         'from'     => $from,
667         'where'    => { 'usr' => $user_id, 'return_time' => undef, 'end_time' => {'>' => gmtime_ISO8601() }},
668         'order_by' => [{ class => bresv => field => start_time => direction => 'asc' }]
669     };
670
671     my $cstore = OpenSRF::AppSession->connect('open-ils.cstore');
672     my $rows = $cstore->request(
673         'open-ils.cstore.json_query.atomic', $query
674     )->gather(1);
675     $cstore->disconnect;
676     $e->disconnect;
677     return [] if not @$rows;
678     return $rows;
679 }
680
681 __PACKAGE__->register_method(
682     method   => "reservation_list_by_filters",
683     api_name => "open-ils.booking.reservations.filtered_id_list",
684     argc     => 3,
685     signature=> {
686         params => [
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)"}
690         ],
691         return => { desc => "An array of bresv ids matching the requested filters." },
692     },
693     notes    => <<'NOTES'
694
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.
704
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').
708
709 NOTES
710 );
711
712
713 sub naive_ts_string {strftime("%F %T", localtime($_[0] || time));}
714
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
718
719     my $from_clause = {
720         "bresv" => {
721             "brsrc" => {"field" => "id", "fkey" => "current_resource"}
722         }
723     };
724
725     my $query = {
726         "select" => {
727             "bresv" => [
728                 "current_resource",
729                 {
730                     "column" => "start_time",
731                     "transform" => "min",
732                     "aggregate" => 1
733                 }
734             ]
735         },
736         "from" => $from_clause,
737         "where" => {
738             "-and" => [
739                 {"current_resource" => {"!=" => undef}},
740                 {"capture_time" => undef},
741                 {"cancel_time" => undef},
742                 {"return_time" => undef},
743                 {"pickup_time" => undef}
744             ]
745         }
746     };
747     if ($o->{"owning_lib"}) {
748         push @{$query->{"where"}->{"-and"}},
749             {"+brsrc" => {"owner" => $o->{"owning_lib"}}};
750     }
751     if ($o->{"range"}) {
752         push @{$query->{"where"}->{"-and"}},
753             json_query_ranges_overlap(
754                 $o->{"range"}->[0], $o->{"range"}->[1],
755                 "start_time", "end_time"
756             );
757     }
758     if ($o->{"barcode"}) {
759         push @{$query->{"where"}->{"-and"}},
760             {"+brsrc" => {"barcode" => $o->{"barcode"}}};
761     }
762
763     my $rows = $e->json_query($query);
764     my $current_resource_bresv_map = {};
765     if (@$rows) {
766         my $id_query = {
767             "select" => {"bresv" => ["id"]},
768             "from" => $from_clause,
769             "where" => {
770                 "-and" => [
771                     {"current_resource" => "PLACEHOLDER"},
772                     {"start_time" => "PLACEHOLDER"},
773                     {"capture_time" => undef},
774                     {"cancel_time" => undef},
775                     {"return_time" => undef},
776                     {"pickup_time" => undef}
777                 ]
778             }
779         };
780         if ($o->{"owning_lib"}) {
781             push @{$id_query->{"where"}->{"-and"}},
782                 {"+brsrc" => {"owner" => $o->{"owning_lib"}}};
783         }
784
785         foreach (@$rows) {
786             $id_query->{"where"}->{"-and"}->[0]->{"current_resource"} =
787                 $_->{"current_resource"};
788             $id_query->{"where"}->{"-and"}->[1]->{"start_time"} =
789                 $_->{"start_time"};
790
791             my $results = $e->json_query($id_query);
792             if ($results && @$results) {
793                 $current_resource_bresv_map->{$_->{"current_resource"}} =
794                     [map { $_->{"id"} } @$results];
795             }
796         }
797     }
798     return $current_resource_bresv_map;
799 }
800
801 sub get_pull_list {
802     my ($self, $client, $auth, $range, $interval_secs, $owning_lib) = @_;
803
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
810     );
811
812     $owning_lib = $e->requestor->ws_ou if not $owning_lib;
813     $range = [ naive_ts_string(time), naive_ts_string(time + $interval_secs) ]
814         if not $range;
815
816     my $uncaptured = get_uncaptured_bresv_for_brsrc(
817         $e, {"range" => $range, "owning_lib" => $owning_lib}
818     );
819
820     if (keys(%$uncaptured)) {
821         my @all_bresv_ids = map { @{$_} } values %$uncaptured;
822         my %bresv_lookup = (
823             map { $_->id => $_ } @{
824                 $e->search_booking_reservation([{"id" => [@all_bresv_ids]}, {
825                     flesh => 1,
826                     flesh_fields => { bresv => [
827                         "usr", "target_resource_type", "current_resource"
828                     ]}
829                 }])
830             }
831         );
832         $e->disconnect;
833         return [ map {
834             my $key = $_;
835             my $one = $bresv_lookup{$uncaptured->{$key}->[0]};
836             my $result = {
837                 "current_resource" => $one->current_resource,
838                 "target_resource_type" => $one->target_resource_type,
839                 "reservations" => [
840                     map { $bresv_lookup{$_} } @{$uncaptured->{$key}}
841                 ]
842             };
843             foreach (@{$result->{"reservations"}}) {    # deflesh
844                 $_->current_resource($_->current_resource->id);
845                 $_->target_resource_type($_->target_resource_type->id);
846             }
847             $result;
848         } keys %$uncaptured ];
849     } else {
850         $e->disconnect;
851         return [];
852     }
853 }
854 __PACKAGE__->register_method(
855     method   => "get_pull_list",
856     api_name => "open-ils.booking.reservations.get_pull_list",
857     argc     => 4,
858     signature=> {
859         params => [
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"}
866         ],
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." }
870     }
871 );
872
873
874 sub could_capture {
875     my ($self, $client, $auth, $barcode) = @_;
876
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");
880
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});
884
885     if ($res and keys %$res) {
886         my $id;
887         while ((undef, $id) = each %$res) {
888             my $bresv = $e->retrieve_booking_reservation([
889                 $id, {
890                     "flesh" => 1, "flesh_fields" => {
891                         "bresv" => [qw(
892                             usr target_resource_type
893                             target_resource current_resource
894                         )]
895                     }
896                 }
897             ]);
898             my $elbow_room = interval_to_seconds(
899                 $bresv->target_resource_type->elbow_room ||
900                 $U->ou_ancestor_setting_value(
901                     $bresv->pickup_lib,
902                     "circ.booking_reservation.default_elbow_room"
903                 ) ||
904                 "0 seconds"
905             );
906
907             unless ($elbow_room) {
908                 $client->respond($bresv);
909             } else {
910                 my $start_time = $dt_parser->parse_datetime(
911                     clean_ISO8601($bresv->start_time)
912                 );
913
914                 if ($now >= $start_time->subtract("seconds" => $elbow_room)) {
915                     $client->respond($bresv);
916                 } else {
917                     $logger->info(
918                         "not within elbow room: $elbow_room, " .
919                         "else would have returned bresv " . $bresv->id
920                     );
921                 }
922             }
923         }
924     }
925     $e->disconnect;
926     undef;
927 }
928 __PACKAGE__->register_method(
929     method   => "could_capture",
930     api_name => "open-ils.booking.reservations.could_capture",
931     argc     => 2,
932     streaming=> 1,
933     signature=> {
934         params => [
935             {type => "string", desc => "Authentication token"},
936             {type => "string", desc => "Resource barcode"}
937         ],
938         return => {desc => "One or zero reservations; event on error."}
939     }
940 );
941
942
943 sub get_copy_fleshed_just_right {
944     my ($self, $client, $auth, $barcode) = @_;
945
946     return undef if not defined $barcode;
947     return {} if ref($barcode) eq "ARRAY" and not @$barcode;
948
949     my $e = new_editor(authtoken => $auth);
950     my $results = $e->search_asset_copy([
951         {"barcode" => $barcode},
952         {
953             "flesh" => 1,
954             "flesh_fields" => {"acp" => [qw/call_number location/]}
955         }
956     ]);
957
958     if (ref($results) eq "ARRAY") {
959         $e->disconnect;
960         return $results->[0] unless ref $barcode;
961         return +{ map { $_->barcode => $_ } @$results };
962     } else {
963         return $e->die_event;
964     }
965 }
966 __PACKAGE__->register_method(
967     method   => "get_copy_fleshed_just_right",
968     api_name => "open-ils.booking.asset.get_copy_fleshed_just_right",
969     argc     => 2,
970     signature=> {
971         params => [
972             {type => "string", desc => "Authentication token"},
973             {type => "mixed", desc => "One barcode or an array of them"},
974         ],
975         return => { desc =>
976             "A copy, or a hash of copies keyed by barcode if an array of " .
977             "barcodes was given"
978         }
979     }
980 );
981
982
983 sub best_bresv_candidate {
984     my ($e, $id_list) = @_;
985
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];
990     }
991
992     my @here = ();
993     my $this_ou = $e->requestor->ws_ou;
994     my $results = $e->json_query({
995         "select" => {"brsrc" => ["pickup_lib"], "bresv" => ["id"]},
996         "from" => {
997             "bresv" => {
998                 "brsrc" => {"field" => "id", "fkey" => "current_resource"}
999             }
1000         },
1001         "where" => {
1002             {"+bresv" => {"id" => $id_list}}
1003         }
1004     });
1005
1006     foreach (@$results) {
1007         push @here, $_->{"id"} if $_->{"pickup_lib"} == $this_ou;
1008     }
1009
1010     my $result;
1011     if (@here > 0) {
1012         $result = @here == 1 ? pop @here : (sort @here)[0];
1013     } else {
1014         $result = (sort @$id_list)[0];
1015     }
1016     $logger->info(
1017         "best_bresv_candidate from " . join(",", @$id_list) . ": $result"
1018     );
1019     return $result;
1020 }
1021
1022
1023 sub capture_resource_for_reservation {
1024     my ($self, $client, $auth, $barcode, $no_update_copy) = @_;
1025
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");
1029
1030     my $uncaptured = get_uncaptured_bresv_for_brsrc(
1031         $e, {"barcode" => $barcode}
1032     );
1033
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(
1038             $e, $uncaptured->{
1039                 (sort(keys %$uncaptured))[0]
1040             }
1041         );
1042         $e->disconnect;
1043         return capture_reservation(
1044             $self, $client, $auth, $bresv, $no_update_copy
1045         );
1046     } else {
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}
1052         );
1053     }
1054 }
1055 __PACKAGE__->register_method(
1056     method   => "capture_resource_for_reservation",
1057     api_name => "open-ils.booking.resources.capture_for_reservation",
1058     argc     => 3,
1059     signature=> {
1060         params => [
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"}
1064         ],
1065         return => { desc => "An OpenILS event describing the capture outcome" }
1066     }
1067 );
1068
1069
1070 sub capture_reservation {
1071     my ($self, $client, $auth, $res_id, $no_update_copy) = @_;
1072
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;
1077
1078     my $reservation = $e->retrieve_booking_reservation([
1079         $res_id, {
1080             "flesh" => 2, "flesh_fields" => {
1081                 "bresv" => [qw/usr current_resource type/],
1082                 "au" => ["card"],
1083                 "brsrc" => ["type"]
1084             }
1085         }
1086     ]);
1087
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;
1093
1094     return new OpenILS::Event(
1095         "RESERVATION_CAPTURE_FAILED",
1096         "payload" => {"captured" => 0, "fail_cause" => "cancelled"}
1097     ) if $reservation->cancel_time;
1098
1099     $reservation->capture_staff($e->requestor->id);
1100     $reservation->capture_time("now");
1101
1102     $e->update_booking_reservation($reservation) or return $e->die_event;
1103
1104     my $ret = {"captured" => 1, "reservation" => $reservation};
1105
1106     my $search_acp_like_this = [
1107         {
1108             "barcode" => $reservation->current_resource->barcode,
1109             "deleted" => "f"
1110         },
1111         {"flesh" => 1, "flesh_fields" => {"acp" => ["call_number"]}}
1112     ];
1113
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
1121         );
1122
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}
1126         )->[0];
1127
1128         if (!$transit) { # not yet in transit
1129             $transit = new Fieldmapper::action::reservation_transit_copy;
1130
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);
1137
1138             $e->create_action_reservation_transit_copy($transit);
1139
1140             if ($U->is_true(
1141                 $reservation->current_resource->type->catalog_item
1142             )) {
1143                 my $copy = $e->search_asset_copy($search_acp_like_this)->[0];
1144
1145                 if ($copy) {
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;
1150
1151                     $ret->{"mvr"} = get_mvr($copy->call_number->record);
1152                     if ($no_update_copy) {
1153                         $ret->{"new_copy_status"} = 6;
1154                     } else {
1155                         $copy->status(6);
1156                         $e->update_asset_copy($copy) or return $e->die_event;
1157                     }
1158                 }
1159             }
1160         }
1161
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];
1166
1167         if ($copy) {
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;
1172
1173             $ret->{"mvr"} = get_mvr($copy->call_number->record);
1174             if ($no_update_copy) {
1175                 $ret->{"new_copy_status"} = 15;
1176             } else {
1177                 $copy->status(15);
1178                 $e->update_asset_copy($copy) or return $e->die_event;
1179             }
1180         }
1181     }
1182
1183     $e->commit or return $e->die_event;
1184
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);
1189     }
1190
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);
1196 }
1197 __PACKAGE__->register_method(
1198     method   => "capture_reservation",
1199     api_name => "open-ils.booking.reservations.capture",
1200     argc     => 2,
1201     signature=> {
1202         params => [
1203             {type => 'string', desc => 'Authentication token'},
1204             {type => 'mixed', desc =>
1205                 'Reservation ID (number) or array of resource barcodes'}
1206         ],
1207         return => { desc => "An OpenILS Event object describing the outcome of the capture, with relevant payload." },
1208     }
1209 );
1210
1211
1212 sub cancel_reservation {
1213     my ($self, $client, $auth, $id_list) = @_;
1214
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");
1220
1221     my $bresv_list = $e->search_booking_reservation([
1222         {"id" => $id_list},
1223         {"flesh" => 1, "flesh_fields" => {"bresv" => [
1224             "current_resource", "target_resource_type"
1225         ]}}
1226     ]);
1227     return $e->die_event if not $bresv_list;
1228
1229     my @results = ();
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 {
1235             $circ->disconnect;
1236             return $e->die_event;
1237         };
1238         $e->xact_commit;
1239         $e->xact_begin;
1240
1241         if (
1242             $bresv->target_resource_type->catalog_item eq "t" &&
1243             $bresv->current_resource
1244         ) {
1245             $logger->info("result of no-op checkin (upon cxl bresv) is " .
1246                 $circ->request(
1247                     "open-ils.circ.checkin", $auth,
1248                     {"barcode" => $bresv->current_resource->barcode,
1249                         "noop" => 1}
1250                 )->gather(1)->{"textcode"});
1251         }
1252         push @results, $bresv->id;
1253     }
1254
1255     $e->disconnect;
1256     $circ->disconnect;
1257
1258     return \@results;
1259 }
1260 __PACKAGE__->register_method(
1261     method   => "cancel_reservation",
1262     api_name => "open-ils.booking.reservations.cancel",
1263     argc     => 2,
1264     signature=> {
1265         params => [
1266             {type => "string", desc => "Authentication token"},
1267             {type => "array", desc => "List of reservation IDs"}
1268         ],
1269         return => { desc => "A list of canceled reservation IDs" },
1270     }
1271 );
1272
1273
1274 sub get_captured_reservations {
1275     my ($self, $client, $auth, $barcode, $which) = @_;
1276
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");
1281
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"};
1285
1286     my $bresv_flesh = {
1287         "flesh" => 1,
1288         "flesh_fields" => {"bresv" => [
1289             qw/target_resource_type current_resource/
1290         ]}
1291     };
1292
1293     my $dispatch = {
1294         "patron" => sub {
1295             return $patron;
1296         },
1297         "ready" => sub {
1298             return ($e->search_booking_reservation([
1299                 {
1300                     "usr" => $patron->id,
1301                     "capture_time" => {"!=" => undef},
1302                     "pickup_time" => undef,
1303                     "start_time" => {"!=" => undef},
1304                     "cancel_time" => undef
1305                 },
1306                 $bresv_flesh
1307             ]) || $e->die_event);
1308         },
1309         "out" => sub {
1310             return ($e->search_booking_reservation([
1311                 {
1312                     "usr" => $patron->id,
1313                     "pickup_time" => {"!=" => undef},
1314                     "return_time" => undef,
1315                     "cancel_time" => undef
1316                 },
1317                 $bresv_flesh
1318             ]) || $e->die_event);
1319         },
1320         "in" => sub {
1321             return ($e->search_booking_reservation([
1322                 {
1323                     "usr" => $patron->id,
1324                     "return_time" => {">=" => "today"},
1325                     "cancel_time" => undef
1326                 },
1327                 $bresv_flesh
1328             ]) || $e->die_event);
1329         }
1330     };
1331
1332     my $result = {};
1333     foreach (@$which) {
1334         my $f = $dispatch->{$_};
1335         if ($f) {
1336             my $r = &{$f}();
1337             return $r if (ref($r) eq "HASH" and exists $r->{"ilsevent"});
1338             $result->{$_} = $r;
1339         }
1340     }
1341
1342     return $result;
1343 }
1344 __PACKAGE__->register_method(
1345     method   => "get_captured_reservations",
1346     api_name => "open-ils.booking.reservations.get_captured",
1347     argc     => 3,
1348     signature=> {
1349         params => [
1350             {type => "string", desc => "Authentication token"},
1351             {type => "string", desc => "Patron barcode"},
1352             {type => "array", desc => "Parts wanted (patron, ready, out, in?)"}
1353         ],
1354         return => { desc => "A hash of parts." } # XXX describe more fully
1355     }
1356 );
1357
1358
1359 sub get_bresv_by_returnable_resource_barcode {
1360     my ($self, $client, $auth, $barcode) = @_;
1361
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");
1366
1367     my $rows = $e->json_query({
1368         "select" => {"bresv" => ["id"]},
1369         "from" => {
1370             "bresv" => {
1371                 "brsrc" => {"field" => "id", "fkey" => "current_resource"}
1372             }
1373         },
1374         "where" => {
1375             "+brsrc" => {"barcode" => $barcode},
1376             "-and" => {
1377                 "pickup_time" => {"!=" => undef},
1378                 "cancel_time" => undef,
1379                 "return_time" => undef
1380             }
1381         }
1382     }) or return $e->die_event;
1383
1384     if (@$rows < 1) {
1385         $e->rollback;
1386         return $rows;
1387     } else {
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([
1392             $id, {
1393                 "flesh" => 2,
1394                 "flesh_fields" => {
1395                     "bresv" => [qw/usr target_resource_type current_resource/],
1396                     "au" => ["card"]
1397                 }
1398             }
1399         ]) or $e->die_event;
1400         $e->rollback;
1401         return $resp;
1402     }
1403 }
1404
1405 __PACKAGE__->register_method(
1406     method   => "get_bresv_by_returnable_resource_barcode",
1407     api_name => "open-ils.booking.reservations.by_returnable_resource_barcode",
1408     argc     => 2,
1409     signature=> {
1410         params => [
1411             {type => "string", desc => "Authentication token"},
1412             {type => "string", desc => "Resource barcode"},
1413         ],
1414         return => { desc => "A fleshed bresv or an ilsevent on error" }
1415     }
1416 );
1417
1418
1419 1;