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