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