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