1 package OpenILS::Application::Booking;
6 use OpenILS::Application;
7 use base qw/OpenILS::Application/;
9 use OpenILS::Utils::CStoreEditor qw/:funcs/;
10 use OpenILS::Utils::Fieldmapper;
11 use OpenILS::Application::AppUtils;
12 my $U = "OpenILS::Application::AppUtils";
14 use OpenSRF::Utils::Logger qw/$logger/;
17 my ($record_id, $owning_lib, $mvr) = @_;
19 my $brt = new Fieldmapper::booking::resource_type;
21 $brt->name($mvr->title);
22 $brt->record($record_id);
23 $brt->catalog_item('t');
24 $brt->owner($owning_lib);
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}
35 return $results->[0] if scalar(@$results) > 0;
42 'open-ils.search.biblio.record.mods_slim.retrieve.authoritative',
47 sub get_unique_owning_libs {
49 $hash{$_->call_number->owning_lib} = 1 foreach (@_); # @_ are copies
53 sub fetch_copies_by_ids {
54 my ($e, $copy_ids) = @_;
55 my $results = $e->search_asset_copy([
57 {flesh => 1, flesh_fields => {acp => ['call_number']}}
59 return $results if ref($results) eq 'ARRAY';
63 sub get_single_record_id {
64 my $record_id = undef;
65 foreach (@_) { # @_ are copies
67 (defined $record_id && $record_id != $_->call_number->record);
68 $record_id = $_->call_number->record;
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).
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.
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 {
87 { '-and' => [{$_[2] => {'>=', $_[0]}}, {$_[2] => {'<', $_[1]}}]},
88 { '-and' => [{$_[3] => {'>', $_[0]}}, {$_[3] => {'<', $_[1]}}]},
89 { '-and' => { $_[3] => {'>', $_[0]}, $_[2] => {'<=', $_[0]}}},
90 { '-and' => { $_[3] => {'>', $_[1]}, $_[2] => {'<', $_[1]}}},
94 sub create_brt_and_brsrc {
95 my ($self, $conn, $authtoken, $copy_ids) = @_;
96 my (@created_brt, @created_brsrc);
99 my $e = new_editor(xact => 1, authtoken => $authtoken);
100 return $e->die_event unless $e->checkauth;
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;
106 foreach (get_unique_owning_libs(@copies)) {
107 $brt_table{$_} = get_existing_brt($e, $record_id, $_, $mvr) ||
108 prepare_new_brt($record_id, $_, $mvr);
111 while (my ($owning_lib, $brt) = each %brt_table) {
112 my $pre_existing = 1;
114 if ($e->allowed('ADMIN_BOOKING_RESOURCE_TYPE', $owning_lib)) {
116 return $e->die_event unless (
117 # v-- Important: assignment modifies original hash
118 $brt = $e->create_booking_resource_type($brt)
122 push @created_brt, [$brt->id, $brt->record, $pre_existing];
127 'ADMIN_BOOKING_RESOURCE', $_->call_number->owning_lib
129 # This block needs to disregard any cstore failures and just
130 # return what results it can.
131 my $brsrc = new Fieldmapper::booking::resource;
133 $brsrc->type($brt_table{$_->call_number->owning_lib}->id);
134 $brsrc->owner($_->call_number->owning_lib);
135 $brsrc->barcode($_->barcode);
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/ }
145 $usable_result = $usable_result->[0];
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
154 "Couldn't create or find brsrc for acp #" . $_->id
158 $e->release_savepoint("alpha");
161 if ($usable_result) {
163 [$usable_result->id, $_->id, $pre_existing];
169 return {brt => \@created_brt, brsrc => \@created_brsrc} or
170 return $e->die_event;
172 __PACKAGE__->register_method(
173 method => "create_brt_and_brsrc",
174 api_name => "open-ils.booking.resources.create_from_copies",
177 {type => 'string', desc => 'Authentication token'},
178 {type => 'array', desc => 'Copy IDs'},
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."}
190 my ($self, $client, $authtoken,
191 $target_user_barcode, $datetime_range,
192 $brt, $brsrc_list, $attr_values) = @_;
194 $brsrc_list = [ undef ] if not defined $brsrc_list;
195 return undef if scalar(@$brsrc_list) < 1; # Empty list not ok.
197 my $e = new_editor(xact => 1, authtoken => $authtoken);
198 return $e->die_event unless (
200 $e->allowed("ADMIN_BOOKING_RESERVATION") and
201 $e->allowed("ADMIN_BOOKING_RESERVATION_ATTR_MAP")
204 my $usr = $U->fetch_user_by_barcode($target_user_barcode);
205 return $usr if ref($usr) eq 'HASH' and exists($usr->{"ilsevent"});
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]);
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.
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);
224 $bresv->target_resource($brsrc); # undef is ok here
225 $bresv->target_resource_type($brt);
227 ($bresv = $e->create_booking_reservation($bresv)) or
228 return $e->die_event;
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.
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;
243 "bresv" => $bresv->id,
248 $e->commit or return $e->die_event;
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",
260 __PACKAGE__->register_method(
261 method => "create_bresv",
262 api_name => "open-ils.booking.reservations.create",
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'},
272 return => { desc => "A hash containing the new bresv and a list " .
278 sub resource_list_by_attrs {
281 my $auth = shift; # Keep as argument, though not used just now.
284 return undef unless ($filters->{type} || $filters->{attribute_values});
287 'select' => { brsrc => [ 'id' ] },
288 'from' => { brsrc => {} },
293 $query->{where} = {"-and" => []};
294 if ($filters->{type}) {
295 push @{$query->{where}->{"-and"}}, {"type" => $filters->{type}};
298 if ($filters->{attribute_values}) {
300 $query->{from}->{brsrc}->{bram} = { field => 'resource' };
302 $filters->{attribute_values} = [$filters->{attribute_values}]
303 if (!ref($filters->{attribute_values}));
305 $query->{having}->{'+bram'}->{value}->{'@>'} = {
306 transform => 'array_accum',
307 value => '$_' . $$ . '${' .
308 join(',', @{$filters->{attribute_values}}) .
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];
319 push @{$query->{where}->{"-and"}}, {
323 "select" => {"bresv" => ["id"]},
325 "where" => {"-and" => [
326 json_query_ranges_overlap(
327 $filters->{available}->[0],
328 $filters->{available}->[1],
332 {"cancel_time" => undef},
333 {"current_resource" => {"=" => {"+brsrc" => "id"}}}
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];
345 push @{$query->{where}->{"-and"}}, {
347 "select" => {"bresv" => ["id"]},
349 "where" => {"-and" => [
350 json_query_ranges_overlap(
351 $filters->{booked}->[0],
352 $filters->{booked}->[1],
356 {"cancel_time" => undef},
357 {"current_resource" => { "=" => {"+brsrc" => "id"}}}
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
367 my $cstore = OpenSRF::AppSession->connect('open-ils.cstore');
368 my $rows = $cstore->request( 'open-ils.cstore.json_query.atomic', $query )->gather(1);
371 return @$rows ? [map { $_->{id} } @$rows] : [];
373 __PACKAGE__->register_method(
374 method => "resource_list_by_attrs",
375 api_name => "open-ils.booking.resources.filtered_id_list",
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?'}
384 return => { desc => "An array of brsrc ids matching the requested filters." },
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.
395 A timestamp during which the resources are reserved.
396 A range of two timestamps which overlap a reservation of the resources.
398 Note that at least one of 'type' or 'attribute_values' is required.
404 sub reservation_list_by_filters {
409 my $whole_obj = shift;
411 return undef unless ($filters->{user} || $filters->{user_barcode} || $filters->{resource} || $filters->{type} || $filters->{attribute_values});
413 my $e = new_editor(authtoken=>$auth);
414 return $e->event unless $e->checkauth;
415 return $e->event unless $e->allowed('VIEW_TRANSACTION');
418 'select' => { bresv => [ 'id', 'start_time' ] },
419 'from' => { bresv => {} },
421 'order_by' => [{ class => bresv => field => start_time => direction => 'asc' }],
425 if ($filters->{fields}) {
426 $query->{where} = $filters->{fields};
430 if ($filters->{user}) {
431 $query->{where}->{usr} = $filters->{user};
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;
440 if ($filters->{type}) {
441 $query->{where}->{target_resource_type} = $filters->{type};
444 if ($filters->{resource}) {
445 $query->{where}->{target_resource} = $filters->{resource};
448 if ($filters->{attribute_values}) {
450 $query->{from}->{bresv}->{bravm} = { field => 'reservation' };
452 $filters->{attribute_values} = [$filters->{attribute_values}]
453 if (!ref($filters->{attribute_values}));
455 $query->{having}->{'+bravm'}->{attr_value}->{'@>'} = {
456 transform => 'array_accum',
457 value => '$_' . $$ . '${' .
458 join(',', @{$filters->{attribute_values}}) .
463 if ($filters->{search_start} || $filters->{search_end}) {
464 $query->{where}->{'-or'} = {};
466 $query->{where}->{'-or'}->{start_time} = { 'between' => [ $filters->{search_start}, $filters->{search_end} ] }
467 if ($filters->{search_start});
469 $query->{where}->{'-or'}->{end_time} = { 'between' => [ $filters->{search_start}, $filters->{search_end} ] }
470 if ($filters->{search_end});
473 my $cstore = OpenSRF::AppSession->connect('open-ils.cstore');
474 my $ids = [ map { $_->{id} } @{
476 'open-ils.cstore.json_query.atomic', $query
481 return $ids if not $whole_obj;
483 my $bresv_list = $e->search_booking_reservation([
488 [qw/target_resource current_resource target_resource_type/]
492 return $bresv_list ? $bresv_list : [];
494 __PACKAGE__->register_method(
495 method => "reservation_list_by_filters",
496 api_name => "open-ils.booking.reservations.filtered_id_list",
500 {type => 'string', desc => 'Authentication token'},
501 {type => 'object', desc => 'Filter object -- see notes for details'}
503 return => { desc => "An array of bresv ids matching the requested filters." },
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.
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').