]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/src/perlmods/OpenILS/Application/Booking.pm
Patch from Lebbeous Fogle-Weekley to add booking reservation interfaces, supporting...
[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 OpenILS::Application;
7 use base qw/OpenILS::Application/;
8
9 use OpenILS::Utils::CStoreEditor qw/:funcs/;
10 use OpenILS::Utils::Fieldmapper;
11 use OpenILS::Application::AppUtils;
12 my $U = "OpenILS::Application::AppUtils";
13
14 use OpenSRF::Utils::Logger qw/$logger/;
15
16 sub prepare_new_brt {
17     my ($record_id, $owning_lib, $mvr) = @_;
18
19     my $brt = new Fieldmapper::booking::resource_type;
20     $brt->isnew(1);
21     $brt->name($mvr->title);
22     $brt->record($record_id);
23     $brt->catalog_item('t');
24     $brt->owner($owning_lib);
25
26     return $brt;
27 }
28
29 sub get_existing_brt {
30     my ($e, $record_id, $owning_lib, $mvr) = @_;
31     my $results = $e->search_booking_resource_type(
32         {name => $mvr->title, owner => $owning_lib, record => $record_id}
33     );
34
35     return $results->[0] if scalar(@$results) > 0;
36     return undef;
37 }
38
39 sub get_mvr {
40     return $U->simplereq(
41         'open-ils.search',
42         'open-ils.search.biblio.record.mods_slim.retrieve.authoritative',
43         shift # record id
44     );
45 }
46
47 sub get_unique_owning_libs {
48     my %hash = ();
49     $hash{$_->call_number->owning_lib} = 1 foreach (@_);    # @_ are copies
50     return keys %hash;
51 }
52
53 sub fetch_copies_by_ids {
54     my ($e, $copy_ids) = @_;
55     my $results = $e->search_asset_copy([
56         {id => $copy_ids},
57         {flesh => 1, flesh_fields => {acp => ['call_number']}}
58     ]);
59     return $results if ref($results) eq 'ARRAY';
60     return [];
61 }
62
63 sub get_single_record_id {
64     my $record_id = undef;
65     foreach (@_) {  # @_ are copies
66         return undef if
67             (defined $record_id && $record_id != $_->call_number->record);
68         $record_id = $_->call_number->record;
69     }
70     return $record_id;
71 }
72
73 # This function generates the correct json_query clause for determining
74 # whether two given ranges overlap.  Each range is composed of a start
75 # and an end point.  All four points should be the same type (could be int,
76 # date, time, timestamp, or perhaps other types).
77 #
78 # The first range (or the first two points) should be specified as
79 # literal values.  The second range (or the last two points) should be
80 # specified as the names of columns, the values of which in a given row
81 # will constitute the second range in the comparison.
82 #
83 # ALSO: PostgreSQL includes an OVERLAPS operator which provides the same
84 # functionality in a much more concise way, but json_query does not (yet).
85 sub json_query_ranges_overlap {
86     +{ '-or' => [
87         { '-and' => [{$_[2] => {'>=', $_[0]}}, {$_[2] => {'<',  $_[1]}}]},
88         { '-and' => [{$_[3] => {'>',  $_[0]}}, {$_[3] => {'<',  $_[1]}}]},
89         { '-and' => { $_[3] => {'>',  $_[0]},   $_[2] => {'<=', $_[0]}}},
90         { '-and' => { $_[3] => {'>',  $_[1]},   $_[2] => {'<',  $_[1]}}},
91     ]};
92 }
93
94 sub create_brt_and_brsrc {
95     my ($self, $conn, $authtoken, $copy_ids) = @_;
96     my (@created_brt, @created_brsrc);
97     my %brt_table = ();
98
99     my $e = new_editor(xact => 1, authtoken => $authtoken);
100     return $e->die_event unless $e->checkauth;
101
102     my @copies = @{fetch_copies_by_ids($e, $copy_ids)};
103     my $record_id = get_single_record_id(@copies) or return $e->die_event;
104     my $mvr = get_mvr($record_id) or return $e->die_event;
105
106     foreach (get_unique_owning_libs(@copies)) {
107         $brt_table{$_} = get_existing_brt($e, $record_id, $_, $mvr) ||
108             prepare_new_brt($record_id, $_, $mvr);
109     }
110
111     while (my ($owning_lib, $brt) = each %brt_table) {
112         my $pre_existing = 1;
113         if ($brt->isnew) {
114             if ($e->allowed('ADMIN_BOOKING_RESOURCE_TYPE', $owning_lib)) {
115                 $pre_existing = 0;
116                 return $e->die_event unless (
117                     #    v-- Important: assignment modifies original hash
118                     $brt = $e->create_booking_resource_type($brt)
119                 );
120             }
121         }
122         push @created_brt, [$brt->id, $brt->record, $pre_existing];
123     }
124
125     foreach (@copies) {
126         if ($e->allowed(
127             'ADMIN_BOOKING_RESOURCE', $_->call_number->owning_lib
128         )) {
129             # This block needs to disregard any cstore failures and just
130             # return what results it can.
131             my $brsrc = new Fieldmapper::booking::resource;
132             $brsrc->isnew(1);
133             $brsrc->type($brt_table{$_->call_number->owning_lib}->id);
134             $brsrc->owner($_->call_number->owning_lib);
135             $brsrc->barcode($_->barcode);
136
137             $e->set_savepoint("alpha");
138             my $pre_existing = 0;
139             my $usable_result = undef;
140             if (!($usable_result = $e->create_booking_resource($brsrc))) {
141                 $e->rollback_savepoint("alpha");
142                 if (($usable_result = $e->search_booking_resource(
143                     +{ map { ($_, $brsrc->$_()) } qw/type owner barcode/ }
144                 ))) {
145                     $usable_result = $usable_result->[0];
146                     $pre_existing = 1;
147                 } else {
148                     # So we failed to create a booking resource for this copy.
149                     # For now, let's just keep going.  If the calling app wants
150                     # to consider this an error, it can notice the absence
151                     # of a booking resource for the copy in the returned
152                     # results.
153                     $logger->warn(
154                         "Couldn't create or find brsrc for acp #" .  $_->id
155                     );
156                 }
157             } else {
158                 $e->release_savepoint("alpha");
159             }
160
161             if ($usable_result) {
162                 push @created_brsrc,
163                     [$usable_result->id, $_->id, $pre_existing];
164             }
165         }
166     }
167
168     $e->commit and
169         return {brt => \@created_brt, brsrc => \@created_brsrc} or
170         return $e->die_event;
171 }
172 __PACKAGE__->register_method(
173     method   => "create_brt_and_brsrc",
174     api_name => "open-ils.booking.resources.create_from_copies",
175     signature => {
176         params => [
177             {type => 'string', desc => 'Authentication token'},
178             {type => 'array', desc => 'Copy IDs'},
179         ],
180         return => { desc => "A two-element hash. The 'brt' element " .
181             "is a list of created booking resource types described by " .
182             "3-tuples (id, copy id, was pre-existing).  The 'brsrc' " .
183             "element is a similar list of created booking resources " .
184             "described by (id, record id, was pre-existing) 3-tuples."}
185     }
186 );
187
188
189 sub create_bresv {
190     my ($self, $client, $authtoken,
191         $target_user_barcode, $datetime_range,
192         $brt, $brsrc_list, $attr_values) = @_;
193
194     $brsrc_list = [ undef ] if not defined $brsrc_list;
195     return undef if scalar(@$brsrc_list) < 1; # Empty list not ok.
196
197     my $e = new_editor(xact => 1, authtoken => $authtoken);
198     return $e->die_event unless (
199         $e->checkauth and
200         $e->allowed("ADMIN_BOOKING_RESERVATION") and
201         $e->allowed("ADMIN_BOOKING_RESERVATION_ATTR_MAP")
202     );
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($e->requestor->ws_ou);
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($brsrc) or
221                 return $e->die_event;
222             return $e->die_event if ($brsrc_itself->type != $brt);
223         }
224         $bresv->target_resource($brsrc);    # undef is ok here
225         $bresv->target_resource_type($brt);
226
227         ($bresv = $e->create_booking_reservation($bresv)) or
228             return $e->die_event;
229
230         # We could/should do some sanity checking on this too: namely, on
231         # whether the attribute values given actually apply to the relevant
232         # brt.  Not seeing any grievous side effects of not checking, though.
233         my @bravm = ();
234         foreach my $value (@$attr_values) {
235             my $bravm = new Fieldmapper::booking::reservation_attr_value_map;
236             $bravm->reservation($bresv->id);
237             $bravm->attr_value($value);
238             $bravm = $e->create_booking_reservation_attr_value_map($bravm) or
239                 return $e->die_event;
240             push @bravm, $bravm;
241         }
242         push @$results, {
243             "bresv" => $bresv->id,
244             "bravm" => \@bravm,
245         };
246     }
247
248     $e->commit or return $e->die_event;
249
250     # Targeting must be tacked on _after_ committing the transaction where the
251     # reservations are actually created.
252     foreach (@$results) {
253         $_->{"targeting"} = $U->storagereq(
254             "open-ils.storage.booking.reservation.resource_targeter",
255             $_->{"bresv"}
256         )->[0];
257     }
258     return $results;
259 }
260 __PACKAGE__->register_method(
261     method   => "create_bresv",
262     api_name => "open-ils.booking.reservations.create",
263     signature => {
264         params => [
265             {type => 'string', desc => 'Authentication token'},
266             {type => 'string', desc => 'Barcode of user for whom to reserve'},
267             {type => 'array', desc => 'Two elements: start and end timestamp'},
268             {type => 'int', desc => 'Booking resource type'},
269             {type => 'list', desc => 'Booking resource (undef ok; empty not ok)'},
270             {type => 'array', desc => 'Attribute values selected'},
271         ],
272         return => { desc => "A hash containing the new bresv and a list " .
273             "of new bravm"}
274     }
275 );
276
277
278 sub resource_list_by_attrs {
279     my $self = shift;
280     my $client = shift;
281     my $auth = shift; # Keep as argument, though not used just now.
282     my $filters = shift;
283
284     return undef unless ($filters->{type} || $filters->{attribute_values});
285
286     my $query = {
287         'select'   => { brsrc => [ 'id' ] },
288         'from'     => { brsrc => {} },
289         'where'    => {},
290         'distinct' => 1
291     };
292
293     $query->{where} = {"-and" => []};
294     if ($filters->{type}) {
295         push @{$query->{where}->{"-and"}}, {"type" => $filters->{type}};
296     }
297
298     if ($filters->{attribute_values}) {
299
300         $query->{from}->{brsrc}->{bram} = { field => 'resource' };
301
302         $filters->{attribute_values} = [$filters->{attribute_values}]
303             if (!ref($filters->{attribute_values}));
304
305         $query->{having}->{'+bram'}->{value}->{'@>'} = {
306             transform => 'array_accum',
307             value => '$_' . $$ . '${' .
308                 join(',', @{$filters->{attribute_values}}) .
309                 '}$_' . $$ . '$'
310         };
311     }
312
313     if ($filters->{available}) {
314         # If only one timestamp has been provided, make it into a range.
315         if (!ref($filters->{available})) {
316             $filters->{available} = [($filters->{available}) x 2];
317         }
318
319         push @{$query->{where}->{"-and"}}, {
320             "-or" => [
321                 {"overbook" => "t"},
322                 {"-not-exists" => {
323                     "select" => {"bresv" => ["id"]},
324                     "from" => "bresv",
325                     "where" => {"-and" => [
326                         json_query_ranges_overlap(
327                             $filters->{available}->[0],
328                             $filters->{available}->[1],
329                             "start_time",
330                             "end_time"
331                         ),
332                         {"cancel_time" => undef},
333                         {"current_resource" => {"=" => {"+brsrc" => "id"}}}
334                     ]},
335                 }}
336             ]
337         };
338     }
339     if ($filters->{booked}) {
340         # If only one timestamp has been provided, make it into a range.
341         if (!ref($filters->{booked})) {
342             $filters->{booked} = [($filters->{booked}) x 2];
343         }
344
345         push @{$query->{where}->{"-and"}}, {
346             "-exists" => {
347                 "select" => {"bresv" => ["id"]},
348                 "from" => "bresv",
349                 "where" => {"-and" => [
350                     json_query_ranges_overlap(
351                         $filters->{booked}->[0],
352                         $filters->{booked}->[1],
353                         "start_time",
354                         "end_time"
355                     ),
356                     {"cancel_time" => undef},
357                     {"current_resource" => { "=" => {"+brsrc" => "id"}}}
358                 ]},
359             }
360         };
361         # I think that the "booked" case could be done with a JOIN instead of
362         # an EXISTS, but I'm leaving it this way for symmetry with the
363         # "available" case for now.  The available case cannot be done with a
364         # join.
365     }
366
367     my $cstore = OpenSRF::AppSession->connect('open-ils.cstore');
368     my $rows = $cstore->request( 'open-ils.cstore.json_query.atomic', $query )->gather(1);
369     $cstore->disconnect;
370
371     return @$rows ? [map { $_->{id} } @$rows] : [];
372 }
373 __PACKAGE__->register_method(
374     method   => "resource_list_by_attrs",
375     api_name => "open-ils.booking.resources.filtered_id_list",
376     argc     => 3,
377     signature=> {
378         params => [
379             {type => 'string', desc => 'Authentication token (unused for now,' .
380                ' but at least pass undef here)'},
381             {type => 'object', desc => 'Filter object: see notes for details'},
382             {type => 'bool', desc => 'Return whole objects instead of IDs?'}
383         ],
384         return => { desc => "An array of brsrc ids matching the requested filters." },
385     },
386     notes    => <<'NOTES'
387
388 The filter object parameter can contain the following keys:
389  * type             => The id of a booking resource type (brt)
390  * attribute_values => The ids of booking resource type attribute values that the resource must have assigned to it (brav)
391  * available        => Either:
392                         A timestamp during which the resources are not reserved.  If the resource is overbookable, this is ignored.
393                         A range of two timestamps which do not overlap any reservations for the resources.  If the resource is overbookable, this is ignored.
394  * booked           => Either:
395                         A timestamp during which the resources are reserved.
396                         A range of two timestamps which overlap a reservation of the resources.
397
398 Note that at least one of 'type' or 'attribute_values' is required.
399
400 NOTES
401 );
402
403
404 sub reservation_list_by_filters {
405     my $self = shift;
406     my $client = shift;
407     my $auth = shift;
408     my $filters = shift;
409     my $whole_obj = shift;
410
411     return undef unless ($filters->{user} || $filters->{user_barcode} || $filters->{resource} || $filters->{type} || $filters->{attribute_values});
412
413     my $e = new_editor(authtoken=>$auth);
414     return $e->event unless $e->checkauth;
415     return $e->event unless $e->allowed('VIEW_TRANSACTION');
416
417     my $query = {
418         'select'   => { bresv => [ 'id', 'start_time' ] },
419         'from'     => { bresv => {} },
420         'where'    => {},
421         'order_by' => [{ class => bresv => field => start_time => direction => 'asc' }],
422         'distinct' => 1
423     };
424
425     if ($filters->{fields}) {
426         $query->{where} = $filters->{fields};
427     }
428
429
430     if ($filters->{user}) {
431         $query->{where}->{usr} = $filters->{user};
432     }
433     elsif ($filters->{user_barcode}) {  # just one of user and user_barcode
434         my $usr = $U->fetch_user_by_barcode($filters->{user_barcode});
435         return $usr if ref($usr) eq 'HASH' and exists($usr->{"ilsevent"});
436         $query->{where}->{usr} = $usr->id;
437     }
438
439
440     if ($filters->{type}) {
441         $query->{where}->{target_resource_type} = $filters->{type};
442     }
443
444     if ($filters->{resource}) {
445         $query->{where}->{target_resource} = $filters->{resource};
446     }
447
448     if ($filters->{attribute_values}) {
449
450         $query->{from}->{bresv}->{bravm} = { field => 'reservation' };
451
452         $filters->{attribute_values} = [$filters->{attribute_values}]
453             if (!ref($filters->{attribute_values}));
454
455         $query->{having}->{'+bravm'}->{attr_value}->{'@>'} = {
456             transform => 'array_accum',
457             value => '$_' . $$ . '${' .
458                 join(',', @{$filters->{attribute_values}}) .
459                 '}$_' . $$ . '$'
460         };
461     }
462
463     if ($filters->{search_start} || $filters->{search_end}) {
464         $query->{where}->{'-or'} = {};
465
466         $query->{where}->{'-or'}->{start_time} = { 'between' => [ $filters->{search_start}, $filters->{search_end} ] }
467                 if ($filters->{search_start});
468
469         $query->{where}->{'-or'}->{end_time} = { 'between' => [ $filters->{search_start}, $filters->{search_end} ] }
470                 if ($filters->{search_end});
471     }
472
473     my $cstore = OpenSRF::AppSession->connect('open-ils.cstore');
474     my $ids = [ map { $_->{id} } @{
475         $cstore->request(
476             'open-ils.cstore.json_query.atomic', $query
477         )->gather(1)
478     } ];
479     $cstore->disconnect;
480
481     return $ids if not $whole_obj;
482
483     my $bresv_list = $e->search_booking_reservation([
484         {"id" => $ids},
485         {"flesh" => 1,
486             "flesh_fields" => {
487                 "bresv" =>
488                     [qw/target_resource current_resource target_resource_type/]
489             }
490         }]
491     );
492     return $bresv_list ? $bresv_list : [];
493 }
494 __PACKAGE__->register_method(
495     method   => "reservation_list_by_filters",
496     api_name => "open-ils.booking.reservations.filtered_id_list",
497     argc     => 2,
498     signature=> {
499         params => [
500             {type => 'string', desc => 'Authentication token'},
501             {type => 'object', desc => 'Filter object -- see notes for details'}
502         ],
503         return => { desc => "An array of bresv ids matching the requested filters." },
504     },
505     notes    => <<'NOTES'
506
507 The filter object parameter can contain the following keys:
508  * user             => The id of a user that has requested a bookable item -- filters on bresv.usr
509  * barcode          => The barcode of a user that has requested a bookable item
510  * type             => The id of a booking resource type (brt) -- filters on bresv.target_resource_type
511  * resource         => The id of a booking resource (brsrc) -- filters on bresv.target_resource
512  * attribute_values => The ids of booking resource type attribute values that the resource must have assigned to it (brav)
513  * search_start     => If search_end is not specified, booking interval (start_time to end_time) must contain this timestamp.
514  * search_end       => If search_start is not specified, booking interval (start_time to end_time) must contain this timestamp.
515  * fields           => An object containing any combination of bresv search filters in standard cstore/pcrud search format.
516
517 Note that at least one of 'user', 'type', 'resource' or 'attribute_values' is required.  If both search_start and search_end are specified,
518 then the result includes any reservations that overlap with that time range.  Any filter fields supplied in 'fields' are overridden
519 by the top-level filters ('user', 'type', 'resource').
520
521 NOTES
522 );
523
524 1;