]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/src/perlmods/OpenILS/Application/Booking.pm
Patch from Lebbeous Fogle-Weekley implementing pickup and return interfaces for booki...
[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 OpenILS::Utils::CStoreEditor qw/:funcs/;
11 use OpenILS::Utils::Fieldmapper;
12 use OpenILS::Application::AppUtils;
13 my $U = "OpenILS::Application::AppUtils";
14
15 use OpenSRF::Utils::Logger qw/$logger/;
16
17 sub prepare_new_brt {
18     my ($record_id, $owning_lib, $mvr) = @_;
19
20     my $brt = new Fieldmapper::booking::resource_type;
21     $brt->isnew(1);
22     $brt->name($mvr->title);
23     $brt->record($record_id);
24     $brt->catalog_item('t');
25     $brt->transferable('t');
26     $brt->owner($owning_lib);
27
28     return $brt;
29 }
30
31 sub get_existing_brt {
32     my ($e, $record_id, $owning_lib, $mvr) = @_;
33     my $results = $e->search_booking_resource_type(
34         {name => $mvr->title, owner => $owning_lib, record => $record_id}
35     );
36
37     return $results->[0] if scalar(@$results) > 0;
38     return undef;
39 }
40
41 sub get_mvr {
42     return $U->simplereq(
43         'open-ils.search',
44         'open-ils.search.biblio.record.mods_slim.retrieve.authoritative',
45         shift # record id
46     );
47 }
48
49 sub get_unique_owning_libs {
50     my %hash = ();
51     $hash{$_->call_number->owning_lib} = 1 foreach (@_);    # @_ are copies
52     return keys %hash;
53 }
54
55 sub fetch_copies_by_ids {
56     my ($e, $copy_ids) = @_;
57     my $results = $e->search_asset_copy([
58         {id => $copy_ids},
59         {flesh => 1, flesh_fields => {acp => ['call_number']}}
60     ]);
61     return $results if ref($results) eq 'ARRAY';
62     return [];
63 }
64
65 sub get_single_record_id {
66     my $record_id = undef;
67     foreach (@_) {  # @_ are copies
68         return undef if
69             (defined $record_id && $record_id != $_->call_number->record);
70         $record_id = $_->call_number->record;
71     }
72     return $record_id;
73 }
74
75 # This function generates the correct json_query clause for determining
76 # whether two given ranges overlap.  Each range is composed of a start
77 # and an end point.  All four points should be the same type (could be int,
78 # date, time, timestamp, or perhaps other types).
79 #
80 # The first range (or the first two points) should be specified as
81 # literal values.  The second range (or the last two points) should be
82 # specified as the names of columns, the values of which in a given row
83 # will constitute the second range in the comparison.
84 #
85 # ALSO: PostgreSQL includes an OVERLAPS operator which provides the same
86 # functionality in a much more concise way, but json_query does not (yet).
87 sub json_query_ranges_overlap {
88     +{ '-or' => [
89         { '-and' => [{$_[2] => {'>=', $_[0]}}, {$_[2] => {'<',  $_[1]}}]},
90         { '-and' => [{$_[3] => {'>',  $_[0]}}, {$_[3] => {'<',  $_[1]}}]},
91         { '-and' => { $_[3] => {'>',  $_[0]},   $_[2] => {'<=', $_[0]}}},
92         { '-and' => { $_[3] => {'>',  $_[1]},   $_[2] => {'<',  $_[1]}}},
93     ]};
94 }
95
96 sub create_brt_and_brsrc {
97     my ($self, $conn, $authtoken, $copy_ids) = @_;
98     my (@created_brt, @created_brsrc);
99     my %brt_table = ();
100
101     my $e = new_editor(xact => 1, authtoken => $authtoken);
102     return $e->die_event unless $e->checkauth;
103
104     my @copies = @{fetch_copies_by_ids($e, $copy_ids)};
105     my $record_id = get_single_record_id(@copies) or return $e->die_event;
106     my $mvr = get_mvr($record_id) or return $e->die_event;
107
108     foreach (get_unique_owning_libs(@copies)) {
109         $brt_table{$_} = get_existing_brt($e, $record_id, $_, $mvr) ||
110             prepare_new_brt($record_id, $_, $mvr);
111     }
112
113     while (my ($owning_lib, $brt) = each %brt_table) {
114         my $pre_existing = 1;
115         if ($brt->isnew) {
116             if ($e->allowed('ADMIN_BOOKING_RESOURCE_TYPE', $owning_lib)) {
117                 $pre_existing = 0;
118                 return $e->die_event unless (
119                     #    v-- Important: assignment modifies original hash
120                     $brt = $e->create_booking_resource_type($brt)
121                 );
122             }
123         }
124         push @created_brt, [$brt->id, $brt->record, $pre_existing];
125     }
126
127     foreach (@copies) {
128         if ($e->allowed(
129             'ADMIN_BOOKING_RESOURCE', $_->call_number->owning_lib
130         )) {
131             # This block needs to disregard any cstore failures and just
132             # return what results it can.
133             my $brsrc = new Fieldmapper::booking::resource;
134             $brsrc->isnew(1);
135             $brsrc->type($brt_table{$_->call_number->owning_lib}->id);
136             $brsrc->owner($_->call_number->owning_lib);
137             $brsrc->barcode($_->barcode);
138
139             $e->set_savepoint("alpha");
140             my $pre_existing = 0;
141             my $usable_result = undef;
142             if (!($usable_result = $e->create_booking_resource($brsrc))) {
143                 $e->rollback_savepoint("alpha");
144                 if (($usable_result = $e->search_booking_resource(
145                     +{ map { ($_, $brsrc->$_()) } qw/type owner barcode/ }
146                 ))) {
147                     $usable_result = $usable_result->[0];
148                     $pre_existing = 1;
149                 } else {
150                     # So we failed to create a booking resource for this copy.
151                     # For now, let's just keep going.  If the calling app wants
152                     # to consider this an error, it can notice the absence
153                     # of a booking resource for the copy in the returned
154                     # results.
155                     $logger->warn(
156                         "Couldn't create or find brsrc for acp #" .  $_->id
157                     );
158                 }
159             } else {
160                 $e->release_savepoint("alpha");
161             }
162
163             if ($usable_result) {
164                 push @created_brsrc,
165                     [$usable_result->id, $_->id, $pre_existing];
166             }
167         }
168     }
169
170     $e->commit and
171         return {brt => \@created_brt, brsrc => \@created_brsrc} or
172         return $e->die_event;
173 }
174 __PACKAGE__->register_method(
175     method   => "create_brt_and_brsrc",
176     api_name => "open-ils.booking.resources.create_from_copies",
177     signature => {
178         params => [
179             {type => 'string', desc => 'Authentication token'},
180             {type => 'array', desc => 'Copy IDs'},
181         ],
182         return => { desc => "A two-element hash. The 'brt' element " .
183             "is a list of created booking resource types described by " .
184             "3-tuples (id, copy id, was pre-existing).  The 'brsrc' " .
185             "element is a similar list of created booking resources " .
186             "described by (id, record id, was pre-existing) 3-tuples."}
187     }
188 );
189
190
191 sub create_bresv {
192     my ($self, $client, $authtoken,
193         $target_user_barcode, $datetime_range, $pickup_lib,
194         $brt, $brsrc_list, $attr_values) = @_;
195
196     $brsrc_list = [ undef ] if not defined $brsrc_list;
197     return undef if scalar(@$brsrc_list) < 1; # Empty list not ok.
198
199     my $e = new_editor(xact => 1, authtoken => $authtoken);
200     return $e->die_event unless (
201         $e->checkauth and
202         $e->allowed("ADMIN_BOOKING_RESERVATION") and
203         $e->allowed("ADMIN_BOOKING_RESERVATION_ATTR_MAP")
204     );
205
206     my $usr = $U->fetch_user_by_barcode($target_user_barcode);
207     return $usr if ref($usr) eq 'HASH' and exists($usr->{"ilsevent"});
208
209     my $results = [];
210     foreach my $brsrc (@$brsrc_list) {
211         my $bresv = new Fieldmapper::booking::reservation;
212         $bresv->usr($usr->id);
213         $bresv->request_lib($e->requestor->ws_ou);
214         $bresv->pickup_lib($pickup_lib);
215         $bresv->start_time($datetime_range->[0]);
216         $bresv->end_time($datetime_range->[1]);
217
218         # A little sanity checking: don't agree to put a reservation on a
219         # brsrc and a brt when they don't match.  In fact, bomb out of
220         # this transaction entirely.
221         if ($brsrc) {
222             my $brsrc_itself = $e->retrieve_booking_resource([
223                 $brsrc, {
224                     "flesh" => 1,
225                     "flesh_fields" => {"brsrc" => ["type"]}
226                 }
227             ]);
228
229             if (not $brsrc_itself) {
230                 my $ev = new OpenILS::Event(
231                     "RESERVATION_BAD_PARAMS",
232                     desc => "brsrc $brsrc doesn't exist"
233                 );
234                 $e->disconnect;
235                 return $ev;
236             }
237             elsif ($brsrc_itself->type->id != $brt) {
238                 my $ev = new OpenILS::Event(
239                     "RESERVATION_BAD_PARAMS",
240                     desc => "brsrc $brsrc doesn't match given brt $brt"
241                 );
242                 $e->disconnect;
243                 return $ev;
244             }
245
246             # Also bail if the user is trying to create a reservation at
247             # a pickup lib to which our resource won't go.
248             if (
249                 $brsrc_itself->owner != $pickup_lib and
250                     not $brsrc_itself->type->transferable
251             ) {
252                 my $ev = new OpenILS::Event(
253                     "RESERVATION_BAD_PARAMS",
254                     desc => "brsrc $brsrc doesn't belong to $pickup_lib and " .
255                         "is not transferable"
256                 );
257                 $e->disconnect;
258                 return $ev;
259             }
260         }
261         $bresv->target_resource($brsrc);    # undef is ok here
262         $bresv->target_resource_type($brt);
263
264         ($bresv = $e->create_booking_reservation($bresv)) or
265             return $e->die_event;
266
267         # We could/should do some sanity checking on this too: namely, on
268         # whether the attribute values given actually apply to the relevant
269         # brt.  Not seeing any grievous side effects of not checking, though.
270         my @bravm = ();
271         foreach my $value (@$attr_values) {
272             my $bravm = new Fieldmapper::booking::reservation_attr_value_map;
273             $bravm->reservation($bresv->id);
274             $bravm->attr_value($value);
275             $bravm = $e->create_booking_reservation_attr_value_map($bravm) or
276                 return $e->die_event;
277             push @bravm, $bravm;
278         }
279         push @$results, {
280             "bresv" => $bresv->id,
281             "bravm" => \@bravm,
282         };
283     }
284
285     $e->commit or return $e->die_event;
286
287     # Targeting must be tacked on _after_ committing the transaction where the
288     # reservations are actually created.
289     foreach (@$results) {
290         $_->{"targeting"} = $U->storagereq(
291             "open-ils.storage.booking.reservation.resource_targeter",
292             $_->{"bresv"}
293         )->[0];
294     }
295     return $results;
296 }
297 __PACKAGE__->register_method(
298     method   => "create_bresv",
299     api_name => "open-ils.booking.reservations.create",
300     signature => {
301         params => [
302             {type => 'string', desc => 'Authentication token'},
303             {type => 'string', desc => 'Barcode of user for whom to reserve'},
304             {type => 'array', desc => 'Two elements: start and end timestamp'},
305             {type => 'int', desc => 'Desired reservation pickup lib'},
306             {type => 'int', desc => 'Booking resource type'},
307             {type => 'list', desc => 'Booking resource (undef ok; empty not ok)'},
308             {type => 'array', desc => 'Attribute values selected'},
309         ],
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 => ["id"]},
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                         {"current_resource" => {"=" => {"+brsrc" => "id"}}}
380                     ]},
381                 }}
382             ]
383         };
384     }
385     if ($filters->{booked}) {
386         # If only one timestamp has been provided, make it into a range.
387         if (!ref($filters->{booked})) {
388             $filters->{booked} = [($filters->{booked}) x 2];
389         }
390
391         push @{$query->{where}->{"-and"}}, {
392             "-exists" => {
393                 "select" => {"bresv" => ["id"]},
394                 "from" => "bresv",
395                 "where" => {"-and" => [
396                     json_query_ranges_overlap(
397                         $filters->{booked}->[0],
398                         $filters->{booked}->[1],
399                         "start_time",
400                         "end_time"
401                     ),
402                     {"cancel_time" => undef},
403                     {"current_resource" => { "=" => {"+brsrc" => "id"}}}
404                 ]},
405             }
406         };
407         # I think that the "booked" case could be done with a JOIN instead of
408         # an EXISTS, but I'm leaving it this way for symmetry with the
409         # "available" case for now.  The available case cannot be done with a
410         # join.
411     }
412
413     my $cstore = OpenSRF::AppSession->connect('open-ils.cstore');
414     my $rows = $cstore->request( 'open-ils.cstore.json_query.atomic', $query )->gather(1);
415     $cstore->disconnect;
416
417     return @$rows ? [map { $_->{id} } @$rows] : [];
418 }
419 __PACKAGE__->register_method(
420     method   => "resource_list_by_attrs",
421     api_name => "open-ils.booking.resources.filtered_id_list",
422     argc     => 3,
423     signature=> {
424         params => [
425             {type => 'string', desc => 'Authentication token (unused for now,' .
426                ' but at least pass undef here)'},
427             {type => 'object', desc => 'Filter object: see notes for details'},
428             {type => 'bool', desc => 'Return whole objects instead of IDs?'}
429         ],
430         return => { desc => "An array of brsrc ids matching the requested filters." },
431     },
432     notes    => <<'NOTES'
433
434 The filter object parameter can contain the following keys:
435  * type             => The id of a booking resource type (brt)
436  * attribute_values => The ids of booking resource type attribute values that the resource must have assigned to it (brav)
437  * available        => Either:
438                         A timestamp during which the resources are not reserved.  If the resource is overbookable, this is ignored.
439                         A range of two timestamps which do not overlap any reservations for the resources.  If the resource is overbookable, this is ignored.
440  * booked           => Either:
441                         A timestamp during which the resources are reserved.
442                         A range of two timestamps which overlap a reservation of the resources.
443
444 Note that at least one of 'type' or 'attribute_values' is required.
445
446 NOTES
447 );
448
449
450 sub reservation_list_by_filters {
451     my $self = shift;
452     my $client = shift;
453     my $auth = shift;
454     my $filters = shift;
455     my $whole_obj = shift;
456
457     return undef unless ($filters->{user} || $filters->{user_barcode} || $filters->{resource} || $filters->{type} || $filters->{attribute_values});
458
459     my $e = new_editor(authtoken=>$auth);
460     return $e->event unless $e->checkauth;
461     return $e->event unless $e->allowed('VIEW_TRANSACTION');
462
463     my $query = {
464         'select'   => { bresv => [ 'id', 'start_time' ] },
465         'from'     => { bresv => {} },
466         'where'    => {},
467         'order_by' => [{ class => bresv => field => start_time => direction => 'asc' }],
468         'distinct' => 1
469     };
470
471     if ($filters->{fields}) {
472         $query->{where} = $filters->{fields};
473     }
474
475
476     if ($filters->{user}) {
477         $query->{where}->{usr} = $filters->{user};
478     }
479     elsif ($filters->{user_barcode}) {  # just one of user and user_barcode
480         my $usr = $U->fetch_user_by_barcode($filters->{user_barcode});
481         return $usr if ref($usr) eq 'HASH' and exists($usr->{"ilsevent"});
482         $query->{where}->{usr} = $usr->id;
483     }
484
485
486     if ($filters->{type}) {
487         $query->{where}->{target_resource_type} = $filters->{type};
488     }
489
490     if ($filters->{resource}) {
491         $query->{where}->{target_resource} = $filters->{resource};
492     }
493
494     if ($filters->{attribute_values}) {
495
496         $query->{from}->{bresv}->{bravm} = { field => 'reservation' };
497
498         $filters->{attribute_values} = [$filters->{attribute_values}]
499             if (!ref($filters->{attribute_values}));
500
501         $query->{having}->{'+bravm'}->{attr_value}->{'@>'} = {
502             transform => 'array_accum',
503             value => '$_' . $$ . '${' .
504                 join(',', @{$filters->{attribute_values}}) .
505                 '}$_' . $$ . '$'
506         };
507     }
508
509     if ($filters->{search_start} || $filters->{search_end}) {
510         $query->{where}->{'-or'} = {};
511
512         $query->{where}->{'-or'}->{start_time} = { 'between' => [ $filters->{search_start}, $filters->{search_end} ] }
513                 if ($filters->{search_start});
514
515         $query->{where}->{'-or'}->{end_time} = { 'between' => [ $filters->{search_start}, $filters->{search_end} ] }
516                 if ($filters->{search_end});
517     }
518
519     my $cstore = OpenSRF::AppSession->connect('open-ils.cstore');
520     my $ids = [ map { $_->{id} } @{
521         $cstore->request(
522             'open-ils.cstore.json_query.atomic', $query
523         )->gather(1)
524     } ];
525     $cstore->disconnect;
526
527     if (not $whole_obj or @$ids < 1) {
528         $e->disconnect;
529         return $ids;
530     }
531
532     my $bresv_list = $e->search_booking_reservation([
533         {"id" => $ids},
534         {"flesh" => 1,
535             "flesh_fields" => {
536                 "bresv" =>
537                     [qw/target_resource current_resource target_resource_type/]
538             }
539         }]
540     );
541     $e->disconnect;
542     return $bresv_list ? $bresv_list : [];
543 }
544 __PACKAGE__->register_method(
545     method   => "reservation_list_by_filters",
546     api_name => "open-ils.booking.reservations.filtered_id_list",
547     argc     => 2,
548     signature=> {
549         params => [
550             {type => 'string', desc => 'Authentication token'},
551             {type => 'object', desc => 'Filter object -- see notes for details'}
552         ],
553         return => { desc => "An array of bresv ids matching the requested filters." },
554     },
555     notes    => <<'NOTES'
556
557 The filter object parameter can contain the following keys:
558  * user             => The id of a user that has requested a bookable item -- filters on bresv.usr
559  * barcode          => The barcode of a user that has requested a bookable item
560  * type             => The id of a booking resource type (brt) -- filters on bresv.target_resource_type
561  * resource         => The id of a booking resource (brsrc) -- filters on bresv.target_resource
562  * attribute_values => The ids of booking resource type attribute values that the resource must have assigned to it (brav)
563  * search_start     => If search_end is not specified, booking interval (start_time to end_time) must contain this timestamp.
564  * search_end       => If search_start is not specified, booking interval (start_time to end_time) must contain this timestamp.
565  * fields           => An object containing any combination of bresv search filters in standard cstore/pcrud search format.
566
567 Note that at least one of 'user', 'type', 'resource' or 'attribute_values' is required.  If both search_start and search_end are specified,
568 then the result includes any reservations that overlap with that time range.  Any filter fields supplied in 'fields' are overridden
569 by the top-level filters ('user', 'type', 'resource').
570
571 NOTES
572 );
573
574
575 sub naive_ts_string { strftime("%F %T", localtime(shift)); }
576 sub naive_start_of_day { strftime("%F", localtime(shift)) . " 00:00:00"; }
577
578 # Return a list of bresv or an ilsevent on failure.
579 sub get_uncaptured_bresv_for_brsrc {
580     my ($e, $o) = @_; # o's keys (all optional): owning_lib, barcode, range
581
582     my $from_clause = {
583         "bresv" => {
584             "brsrc" => {"field" => "id", "fkey" => "current_resource"}
585         }
586     };
587
588     my $query = {
589         "select" => {
590             "bresv" => [
591                 "current_resource",
592                 {
593                     "column" => "start_time",
594                     "transform" => "min",
595                     "aggregate" => 1
596                 }
597             ]
598         },
599         "from" => $from_clause,
600         "where" => {
601             "-and" => [
602                 {"current_resource" => {"!=" => undef}},
603                 {"capture_time" => undef},
604                 {"cancel_time" => undef},
605                 {"return_time" => undef},
606                 {"pickup_time" => undef}
607             ]
608         }
609     };
610     if ($o->{"owning_lib"}) {
611         push @{$query->{"where"}->{"-and"}},
612             {"+brsrc" => {"owner" => $o->{"owning_lib"}}};
613     }
614     if ($o->{"range"}) {
615         push @{$query->{"where"}->{"-and"}},
616             json_query_ranges_overlap(
617                 $o->{"range"}->[0], $o->{"range"}->[1],
618                 "start_time", "end_time"
619             );
620     }
621     if ($o->{"barcode"}) {
622         push @{$query->{"where"}->{"-and"}},
623             {"+brsrc" => {"barcode" => $o->{"barcode"}}};
624     }
625
626     my $rows = $e->json_query($query);
627     my $current_resource_bresv_map = {};
628     if (@$rows) {
629         my $id_query = {
630             "select" => {"bresv" => ["id"]},
631             "from" => $from_clause,
632             "where" => {
633                 "-and" => [
634                     {"current_resource" => "PLACEHOLDER"},
635                     {"start_time" => "PLACEHOLDER"},
636                 ]
637             }
638         };
639         if ($o->{"owning_lib"}) {
640             push @{$id_query->{"where"}->{"-and"}},
641                 {"+brsrc" => {"owner" => $o->{"owning_lib"}}};
642         }
643
644         foreach (@$rows) {
645             $id_query->{"where"}->{"-and"}->[0]->{"current_resource"} =
646                 $_->{"current_resource"};
647             $id_query->{"where"}->{"-and"}->[1]->{"start_time"} =
648                 $_->{"start_time"};
649
650             my $results = $e->json_query($id_query);
651             if ($results && @$results) {
652                 $current_resource_bresv_map->{$_->{"current_resource"}} =
653                     [map { $_->{"id"} } @$results];
654             }
655         }
656     }
657     return $current_resource_bresv_map;
658 }
659
660 sub get_pull_list {
661     my ($self, $client, $auth, $range, $interval_secs, $owning_lib) = @_;
662
663     my $e = new_editor(xact => 1, authtoken => $auth);
664     return $e->die_event unless $e->checkauth;
665     return $e->die_event unless $e->allowed("RETRIEVE_RESERVATION_PULL_LIST");
666     return $e->die_event unless (
667         ref($range) eq "ARRAY" or
668         ($interval_secs = int($interval_secs)) > 0
669     );
670
671     $owning_lib = $e->requestor->ws_ou if not $owning_lib;
672     $range = [ naive_ts_string(time), naive_ts_string(time + $interval_secs) ]
673         if not $range;
674
675     my $uncaptured = get_uncaptured_bresv_for_brsrc(
676         $e, {"range" => $range, "owning_lib" => $owning_lib}
677     );
678
679     if (keys(%$uncaptured)) {
680         my @all_bresv_ids = map { @{$_} } values %$uncaptured;
681         my %bresv_lookup = (
682             map { $_->id => $_ } @{
683                 $e->search_booking_reservation([{"id" => [@all_bresv_ids]}, {
684                     flesh => 1,
685                     flesh_fields => { bresv => [
686                         "usr", "target_resource_type", "current_resource"
687                     ]}
688                 }])
689             }
690         );
691         $e->disconnect;
692         return [ map {
693             my $key = $_;
694             my $one = $bresv_lookup{$uncaptured->{$key}->[0]};
695             my $result = {
696                 "current_resource" => $one->current_resource,
697                 "target_resource_type" => $one->target_resource_type,
698                 "reservations" => [
699                     map { $bresv_lookup{$_} } @{$uncaptured->{$key}}
700                 ]
701             };
702             foreach (@{$result->{"reservations"}}) {    # deflesh
703                 $_->current_resource($_->current_resource->id);
704                 $_->target_resource_type($_->target_resource_type->id);
705             }
706             $result;
707         } keys %$uncaptured ];
708     } else {
709         $e->disconnect;
710         return [];
711     }
712 }
713 __PACKAGE__->register_method(
714     method   => "get_pull_list",
715     api_name => "open-ils.booking.reservations.get_pull_list",
716     argc     => 4,
717     signature=> {
718         params => [
719             {type => "string", desc => "Authentication token"},
720             {type => "array", desc =>
721                 "range: Date/time range for reservations (opt)"},
722             {type => "int", desc =>
723                 "interval: Seconds from now (instead of range)"},
724             {type => "number", desc => "(Optional) Owning library"}
725         ],
726         return => { desc => "An array of hashes, each containing key/value " .
727             "pairs describing resource, resource type, and a list of " .
728             "reservations that claim the given resource." }
729     }
730 );
731
732
733 sub get_copy_fleshed_just_right {
734     my ($self, $client, $auth, $barcode) = @_;
735
736     return undef if not defined $barcode;
737     return {} if ref($barcode) eq "ARRAY" and not @$barcode;
738
739     my $e = new_editor(authtoken => $auth);
740     my $results = $e->search_asset_copy([
741         {"barcode" => $barcode},
742         {
743             "flesh" => 1,
744             "flesh_fields" => {"acp" => [qw/call_number location/]}
745         }
746     ]);
747
748     if (ref($results) eq "ARRAY") {
749         $e->disconnect;
750         return $results->[0] unless ref $barcode;
751         return +{ map { $_->barcode => $_ } @$results };
752     } else {
753         return $e->die_event;
754     }
755 }
756 __PACKAGE__->register_method(
757     method   => "get_copy_fleshed_just_right",
758     api_name => "open-ils.booking.asset.get_copy_fleshed_just_right",
759     argc     => 2,
760     signature=> {
761         params => [
762             {type => "string", desc => "Authentication token"},
763             {type => "mixed", desc => "One barcode or an array of them"},
764         ],
765         return => { desc =>
766             "A copy, or a hash of copies keyed by barcode if an array of " .
767             "barcodes was given"
768         }
769     }
770 );
771
772
773 sub best_bresv_candidate {
774     my ($e, $id_list) = @_;
775
776     # This will almost always be the case.
777     return $id_list->[0] if @$id_list == 1;
778
779     my @here = ();
780     my $this_ou = $e->requestor->ws_ou;
781     my $results = $e->json_query({
782         "select" => {"brsrc" => ["pickup_lib"], "bresv" => ["id"]},
783         "from" => {
784             "bresv" => {
785                 "brsrc" => {"field" => "id", "fkey" => "current_resource"}
786             }
787         },
788         "where" => {
789             {"+bresv" => {"id" => $id_list}}
790         }
791     });
792
793     foreach (@$results) {
794         push @here, $_->{"id"} if $_->{"pickup_lib"} == $this_ou;
795     }
796
797     if (@here > 0) {
798         return pop @here if @here == 1;
799         return (sort @here)[0];
800     } else {
801         return (sort @$id_list)[0];
802     }
803 }
804
805
806 sub capture_resource_for_reservation {
807     my ($self, $client, $auth, $barcode) = @_;
808
809     my $e = new_editor(xact => 1, authtoken => $auth);
810     return $e->die_event unless $e->checkauth;
811     return $e->die_event unless $e->allowed("CAPTURE_RESERVATION");
812
813     my $uncaptured = get_uncaptured_bresv_for_brsrc(
814         $e, {"barcode" => $barcode}
815     );
816     $e->disconnect;
817
818     if (keys %$uncaptured) {
819         # Note this will only capture one reservation at a time, even in
820         # cases with overbooking (multiple "soonest" bresv's on a resource).
821         my $key = (sort(keys %$uncaptured))[0];
822         return capture_reservation(
823             $self, $client, $auth, best_bresv_candidate($e, $uncaptured->{$key})
824         );
825     } else {
826         return new OpenILS::Event(
827             "RESERVATION_NOT_FOUND",
828             desc => "No capturable reservation found pertaining " .
829                 "to a resource with barcode $barcode",
830             payload => {fail_cause => 'no-reservation', captured => 0}
831         );
832     }
833 }
834 __PACKAGE__->register_method(
835     method   => "capture_resource_for_reservation",
836     api_name => "open-ils.booking.resources.capture_for_reservation",
837     argc     => 3,
838     signature=> {
839         params => [
840             {type => "string", desc => "Authentication token"},
841             {type => "string", desc => "Barcode of booked & targeted resource"},
842             {type => "int", desc => "Pickup library (default to client ws_ou)"},
843         ],
844         return => { desc => "An OpenILS event describing the capture outcome" }
845     }
846 );
847
848
849 sub capture_reservation {
850     my ($self, $client, $auth, $res_id) = @_;
851
852     my $e = new_editor(xact => 1, authtoken => $auth);
853     return $e->event unless $e->checkauth;
854     return $e->event unless $e->allowed('CAPTURE_RESERVATION');
855     my $here = $e->requestor->ws_ou;
856
857     my $reservation = $e->retrieve_booking_reservation([
858         $res_id, {
859             flesh => 2,
860             flesh_fields => {"bresv" => ["usr"], "au" => ["card"]}
861         }
862     ]);
863     return OpenILS::Event->new('RESERVATION_NOT_FOUND') unless $reservation;
864
865     return OpenILS::Event->new('RESERVATION_CAPTURE_FAILED', payload => { captured => 0, fail_cause => 'no-resource' })
866         if (!$reservation->current_resource); # no resource
867
868     return OpenILS::Event->new('RESERVATION_CAPTURE_FAILED', payload => { captured => 0, fail_cause => 'cancelled' })
869         if ($reservation->cancel_time); # canceled
870
871     my $resource = $e->retrieve_booking_resource( $reservation->current_resource );
872     my $type = $e->retrieve_booking_resource_type( $resource->type );
873
874     $reservation->capture_staff( $e->requestor->id );
875     $reservation->capture_time( 'now' );
876
877     my $reservation_id = undef;
878     return $e->event unless ( $e->update_booking_reservation( $reservation ) and $reservation_id = $e->data );
879
880     $reservation->id($reservation_id);
881
882     my $ret = { captured => 1, reservation => $reservation };
883
884     if ($here != $reservation->pickup_lib) {
885         return OpenILS::Event->new('RESERVATION_CAPTURE_FAILED', payload => { captured => 0, fail_cause => 'not-transferable' })
886             if (!$U->is_true($type->transferable)); # non-transferable resource
887
888         # need to transit the item ... is it already in transit?
889         my $transit = $e->search_action_reservation_transit_copy( { reservation => $res_id, dest_recv_time => undef } )->[0];
890
891         if (!$transit) { # not yet in transit
892             $transit = new Fieldmapper::action::reservation_transit_copy;
893
894             $transit->target_copy($resource->id);
895             $transit->copy_status(15);
896             $transit->source_send_time('now');
897             $transit->source($here);
898             $transit->dest($reservation->pickup_lib);
899
900             $e->create_action_reservation_transit_copy( $transit );
901
902             if ($U->is_true($type->catalog_item)) {
903                 my $copy = $e->search_asset_copy( { barcode => $resource->barcode, deleted => 'f' } )->[0];
904
905                 if ($copy) {
906                     return new OpenILS::Event(
907                         "OPEN_CIRCULATION_EXISTS",
908                         payload => { captured => 0, copy => $copy }
909                     ) if $copy->status == 1;
910                     $copy->status(6);
911                     $e->update_asset_copy( $copy );
912                     $$ret{catalog_item} = $copy; # $e->data is just id (int)
913                 }
914             }
915         }
916
917         $$ret{transit} = $transit;
918     } elsif ($U->is_true($type->catalog_item)) {
919         my $copy = $e->search_asset_copy( { barcode => $resource->barcode, deleted => 'f' } )->[0];
920
921         if ($copy) {
922             return OpenILS::Event->new('OPEN_CIRCULATION_EXISTS', payload => { captured => 0, copy => $copy }) if ($copy->status == 1);
923             $copy->status(15);
924             $e->update_asset_copy( $copy );
925             $$ret{catalog_item} = $copy; # $e->data is just id (int)
926         }
927     }
928
929     $e->commit;
930
931     return OpenILS::Event->new('SUCCESS', payload => $ret);
932 }
933 __PACKAGE__->register_method(
934     method   => "capture_reservation",
935     api_name => "open-ils.booking.reservations.capture",
936     argc     => 2,
937     signature=> {
938         params => [
939             {type => 'string', desc => 'Authentication token'},
940             {type => 'mixed', desc =>
941                 'Reservation ID (number) or array of resource barcodes'}
942         ],
943         return => { desc => "An OpenILS Event object describing the outcome of the capture, with relevant payload." },
944     }
945 );
946
947
948 sub cancel_reservation {
949     my ($self, $client, $auth, $id_list) = @_;
950
951     my $e = new_editor(xact => 1, authtoken => $auth);
952     return $e->die_event unless $e->checkauth;
953     # Should the following permission really be checked as relates to each
954     # individual reservation's request_lib?  Hrmm...
955     return $e->die_event unless $e->allowed("ADMIN_BOOKING_RESERVATION");
956
957     my $bresv_list = $e->search_booking_reservation([
958         {"id" => $id_list},
959         {"flesh" => 1, "flesh_fields" => {"bresv" => [
960             "current_resource", "target_resource_type"
961         ]}}
962     ]);
963     return $e->die_event if not $bresv_list;
964
965     my $circ = OpenSRF::AppSession->connect("open-ils.circ") or
966         return $e->die_event;
967     my @results = ();
968     foreach my $bresv (@$bresv_list) {
969         if (
970             $bresv->target_resource_type->catalog_item == "t" &&
971             $bresv->current_resource
972         ) {
973             $logger->info("result of no-op checkin (upon cxl bresv) is " .
974                 $circ->request(
975                     "open-ils.circ.checkin", $auth,
976                     {"barcode" => $bresv->current_resource->barcode,
977                         "noop" => 1}
978                 )->gather(1)->{"textcode"});
979         }
980         $bresv->cancel_time("now");
981         $e->update_booking_reservation($bresv) or do {
982             $circ->disconnect;
983             return $e->die_event;
984         };
985
986         push @results, $bresv->id;
987     }
988
989     $e->commit;
990     $circ->disconnect;
991
992     return \@results;
993 }
994 __PACKAGE__->register_method(
995     method   => "cancel_reservation",
996     api_name => "open-ils.booking.reservations.cancel",
997     argc     => 2,
998     signature=> {
999         params => [
1000             {type => "string", desc => "Authentication token"},
1001             {type => "array", desc => "List of reservation IDs"}
1002         ],
1003         return => { desc => "A list of canceled reservation IDs" },
1004     }
1005 );
1006
1007
1008 sub get_captured_reservations {
1009     my ($self, $client, $auth, $barcode, $which) = @_;
1010
1011     my $e = new_editor(xact => 1, authtoken => $auth);
1012     return $e->die_event unless $e->checkauth;
1013     return $e->die_event unless $e->allowed("VIEW_USER");
1014     return $e->die_event unless $e->allowed("ADMIN_BOOKING_RESERVATION");
1015
1016     # fetch the patron for our uses in any case...
1017     my $patron = $U->fetch_user_by_barcode($barcode);
1018     return $patron if ref($patron) eq "HASH" and exists $patron->{"ilsevent"};
1019
1020     my $bresv_flesh = {
1021         "flesh" => 1,
1022         "flesh_fields" => {"bresv" => [
1023             qw/target_resource_type current_resource/
1024         ]}
1025     };
1026
1027     my $dispatch = {
1028         "patron" => sub {
1029             return $patron;
1030         },
1031         "ready" => sub {
1032             return $e->search_booking_reservation([
1033                 {
1034                     "usr" => $patron->id,
1035                     "capture_time" => {"!=" => undef},
1036                     "pickup_time" => undef,
1037                     "cancel_time" => undef
1038                 },
1039                 $bresv_flesh
1040             ]) or $e->die_event;
1041         },
1042         "out" => sub {
1043             return $e->search_booking_reservation([
1044                 {
1045                     "usr" => $patron->id,
1046                     "pickup_time" => {"!=" => undef},
1047                     "return_time" => undef,
1048                     "cancel_time" => undef
1049                 },
1050                 $bresv_flesh
1051             ]) or $e->die_event;
1052         },
1053         "in" => sub {
1054             return $e->search_booking_reservation([
1055                 {
1056                     "usr" => $patron->id,
1057                     "return_time" => {">=" => naive_start_of_day()},
1058                     "cancel_time" => undef
1059                 },
1060                 $bresv_flesh
1061             ]) or $e->die_event;
1062         }
1063     };
1064
1065     my $result = {};
1066     foreach (@$which) {
1067         my $f = $dispatch->{$_};
1068         if ($f) {
1069             my $r = &{$f}();
1070             return $r if (ref($r) eq "HASH" and exists $r->{"ilsevent"});
1071             $result->{$_} = $r;
1072         }
1073     }
1074
1075     return $result;
1076 }
1077 __PACKAGE__->register_method(
1078     method   => "get_captured_reservations",
1079     api_name => "open-ils.booking.reservations.get_captured",
1080     argc     => 3,
1081     signature=> {
1082         params => [
1083             {type => "string", desc => "Authentication token"},
1084             {type => "string", desc => "Patron barcode"},
1085             {type => "array", desc => "Parts wanted (patron, ready, out, in?)"}
1086         ],
1087         return => { desc => "A hash of parts." } # XXX describe more fully
1088     }
1089 );
1090
1091
1092 sub get_bresv_by_returnable_resource_barcode {
1093     my ($self, $client, $auth, $barcode) = @_;
1094
1095     my $e = new_editor(xact => 1, authtoken => $auth);
1096     return $e->die_event unless $e->checkauth;
1097     return $e->die_event unless $e->allowed("VIEW_USER");
1098     return $e->die_event unless $e->allowed("ADMIN_BOOKING_RESERVATION");
1099
1100     my $rows = $e->json_query({
1101         "select" => {"bresv" => ["id"]},
1102         "from" => {
1103             "bresv" => {
1104                 "brsrc" => {"field" => "id", "fkey" => "current_resource"}
1105             }
1106         },
1107         "where" => {
1108             "+brsrc" => {"barcode" => $barcode},
1109             "-and" => {
1110                 "pickup_time" => {"!=" => undef},
1111                 "cancel_time" => undef,
1112                 "return_time" => undef
1113             }
1114         }
1115     }) or return $e->die_event;
1116
1117     if (@$rows < 1) {
1118         return $rows;
1119     } else {
1120         # More than one result might be possible, but we don't want to return
1121         # more than one at this time.
1122         my $id = $rows->[0]->{"id"};
1123         return $e->retrieve_booking_reservation([
1124             $id, {
1125                 "flesh" => 2,
1126                 "flesh_fields" => {
1127                     "bresv" => [qw/usr target_resource_type current_resource/],
1128                     "au" => ["card"]
1129                 }
1130             }
1131         ]) or $e->die_event;
1132     }
1133 }
1134
1135 __PACKAGE__->register_method(
1136     method   => "get_bresv_by_returnable_resource_barcode",
1137     api_name => "open-ils.booking.reservations.by_returnable_resource_barcode",
1138     argc     => 2,
1139     signature=> {
1140         params => [
1141             {type => "string", desc => "Authentication token"},
1142             {type => "string", desc => "Resource barcode"},
1143         ],
1144         return => { desc => "A fleshed bresv or an ilsevent on error" }
1145     }
1146 );
1147
1148
1149 1;