]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/perlmods/OpenILS/Application/Booking.pm
Patch from Lebbeous Fogle-Weekley adding a pull list interface for booking reservations
[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->owner($owning_lib);
26
27     return $brt;
28 }
29
30 sub get_existing_brt {
31     my ($e, $record_id, $owning_lib, $mvr) = @_;
32     my $results = $e->search_booking_resource_type(
33         {name => $mvr->title, owner => $owning_lib, record => $record_id}
34     );
35
36     return $results->[0] if scalar(@$results) > 0;
37     return undef;
38 }
39
40 sub get_mvr {
41     return $U->simplereq(
42         'open-ils.search',
43         'open-ils.search.biblio.record.mods_slim.retrieve.authoritative',
44         shift # record id
45     );
46 }
47
48 sub get_unique_owning_libs {
49     my %hash = ();
50     $hash{$_->call_number->owning_lib} = 1 foreach (@_);    # @_ are copies
51     return keys %hash;
52 }
53
54 sub fetch_copies_by_ids {
55     my ($e, $copy_ids) = @_;
56     my $results = $e->search_asset_copy([
57         {id => $copy_ids},
58         {flesh => 1, flesh_fields => {acp => ['call_number']}}
59     ]);
60     return $results if ref($results) eq 'ARRAY';
61     return [];
62 }
63
64 sub get_single_record_id {
65     my $record_id = undef;
66     foreach (@_) {  # @_ are copies
67         return undef if
68             (defined $record_id && $record_id != $_->call_number->record);
69         $record_id = $_->call_number->record;
70     }
71     return $record_id;
72 }
73
74 # This function generates the correct json_query clause for determining
75 # whether two given ranges overlap.  Each range is composed of a start
76 # and an end point.  All four points should be the same type (could be int,
77 # date, time, timestamp, or perhaps other types).
78 #
79 # The first range (or the first two points) should be specified as
80 # literal values.  The second range (or the last two points) should be
81 # specified as the names of columns, the values of which in a given row
82 # will constitute the second range in the comparison.
83 #
84 # ALSO: PostgreSQL includes an OVERLAPS operator which provides the same
85 # functionality in a much more concise way, but json_query does not (yet).
86 sub json_query_ranges_overlap {
87     +{ '-or' => [
88         { '-and' => [{$_[2] => {'>=', $_[0]}}, {$_[2] => {'<',  $_[1]}}]},
89         { '-and' => [{$_[3] => {'>',  $_[0]}}, {$_[3] => {'<',  $_[1]}}]},
90         { '-and' => { $_[3] => {'>',  $_[0]},   $_[2] => {'<=', $_[0]}}},
91         { '-and' => { $_[3] => {'>',  $_[1]},   $_[2] => {'<',  $_[1]}}},
92     ]};
93 }
94
95 sub create_brt_and_brsrc {
96     my ($self, $conn, $authtoken, $copy_ids) = @_;
97     my (@created_brt, @created_brsrc);
98     my %brt_table = ();
99
100     my $e = new_editor(xact => 1, authtoken => $authtoken);
101     return $e->die_event unless $e->checkauth;
102
103     my @copies = @{fetch_copies_by_ids($e, $copy_ids)};
104     my $record_id = get_single_record_id(@copies) or return $e->die_event;
105     my $mvr = get_mvr($record_id) or return $e->die_event;
106
107     foreach (get_unique_owning_libs(@copies)) {
108         $brt_table{$_} = get_existing_brt($e, $record_id, $_, $mvr) ||
109             prepare_new_brt($record_id, $_, $mvr);
110     }
111
112     while (my ($owning_lib, $brt) = each %brt_table) {
113         my $pre_existing = 1;
114         if ($brt->isnew) {
115             if ($e->allowed('ADMIN_BOOKING_RESOURCE_TYPE', $owning_lib)) {
116                 $pre_existing = 0;
117                 return $e->die_event unless (
118                     #    v-- Important: assignment modifies original hash
119                     $brt = $e->create_booking_resource_type($brt)
120                 );
121             }
122         }
123         push @created_brt, [$brt->id, $brt->record, $pre_existing];
124     }
125
126     foreach (@copies) {
127         if ($e->allowed(
128             'ADMIN_BOOKING_RESOURCE', $_->call_number->owning_lib
129         )) {
130             # This block needs to disregard any cstore failures and just
131             # return what results it can.
132             my $brsrc = new Fieldmapper::booking::resource;
133             $brsrc->isnew(1);
134             $brsrc->type($brt_table{$_->call_number->owning_lib}->id);
135             $brsrc->owner($_->call_number->owning_lib);
136             $brsrc->barcode($_->barcode);
137
138             $e->set_savepoint("alpha");
139             my $pre_existing = 0;
140             my $usable_result = undef;
141             if (!($usable_result = $e->create_booking_resource($brsrc))) {
142                 $e->rollback_savepoint("alpha");
143                 if (($usable_result = $e->search_booking_resource(
144                     +{ map { ($_, $brsrc->$_()) } qw/type owner barcode/ }
145                 ))) {
146                     $usable_result = $usable_result->[0];
147                     $pre_existing = 1;
148                 } else {
149                     # So we failed to create a booking resource for this copy.
150                     # For now, let's just keep going.  If the calling app wants
151                     # to consider this an error, it can notice the absence
152                     # of a booking resource for the copy in the returned
153                     # results.
154                     $logger->warn(
155                         "Couldn't create or find brsrc for acp #" .  $_->id
156                     );
157                 }
158             } else {
159                 $e->release_savepoint("alpha");
160             }
161
162             if ($usable_result) {
163                 push @created_brsrc,
164                     [$usable_result->id, $_->id, $pre_existing];
165             }
166         }
167     }
168
169     $e->commit and
170         return {brt => \@created_brt, brsrc => \@created_brsrc} or
171         return $e->die_event;
172 }
173 __PACKAGE__->register_method(
174     method   => "create_brt_and_brsrc",
175     api_name => "open-ils.booking.resources.create_from_copies",
176     signature => {
177         params => [
178             {type => 'string', desc => 'Authentication token'},
179             {type => 'array', desc => 'Copy IDs'},
180         ],
181         return => { desc => "A two-element hash. The 'brt' element " .
182             "is a list of created booking resource types described by " .
183             "3-tuples (id, copy id, was pre-existing).  The 'brsrc' " .
184             "element is a similar list of created booking resources " .
185             "described by (id, record id, was pre-existing) 3-tuples."}
186     }
187 );
188
189
190 sub create_bresv {
191     my ($self, $client, $authtoken,
192         $target_user_barcode, $datetime_range,
193         $brt, $brsrc_list, $attr_values) = @_;
194
195     $brsrc_list = [ undef ] if not defined $brsrc_list;
196     return undef if scalar(@$brsrc_list) < 1; # Empty list not ok.
197
198     my $e = new_editor(xact => 1, authtoken => $authtoken);
199     return $e->die_event unless (
200         $e->checkauth and
201         $e->allowed("ADMIN_BOOKING_RESERVATION") and
202         $e->allowed("ADMIN_BOOKING_RESERVATION_ATTR_MAP")
203     );
204
205     my $usr = $U->fetch_user_by_barcode($target_user_barcode);
206     return $usr if ref($usr) eq 'HASH' and exists($usr->{"ilsevent"});
207
208     my $results = [];
209     foreach my $brsrc (@$brsrc_list) {
210         my $bresv = new Fieldmapper::booking::reservation;
211         $bresv->usr($usr->id);
212         $bresv->request_lib($e->requestor->ws_ou);
213         $bresv->pickup_lib($e->requestor->ws_ou);
214         $bresv->start_time($datetime_range->[0]);
215         $bresv->end_time($datetime_range->[1]);
216
217         # A little sanity checking: don't agree to put a reservation on a
218         # brsrc and a brt when they don't match.  In fact, bomb out of
219         # this transaction entirely.
220         if ($brsrc) {
221             my $brsrc_itself = $e->retrieve_booking_resource($brsrc) or
222                 return $e->die_event;
223             return $e->die_event if ($brsrc_itself->type != $brt);
224         }
225         $bresv->target_resource($brsrc);    # undef is ok here
226         $bresv->target_resource_type($brt);
227
228         ($bresv = $e->create_booking_reservation($bresv)) or
229             return $e->die_event;
230
231         # We could/should do some sanity checking on this too: namely, on
232         # whether the attribute values given actually apply to the relevant
233         # brt.  Not seeing any grievous side effects of not checking, though.
234         my @bravm = ();
235         foreach my $value (@$attr_values) {
236             my $bravm = new Fieldmapper::booking::reservation_attr_value_map;
237             $bravm->reservation($bresv->id);
238             $bravm->attr_value($value);
239             $bravm = $e->create_booking_reservation_attr_value_map($bravm) or
240                 return $e->die_event;
241             push @bravm, $bravm;
242         }
243         push @$results, {
244             "bresv" => $bresv->id,
245             "bravm" => \@bravm,
246         };
247     }
248
249     $e->commit or return $e->die_event;
250
251     # Targeting must be tacked on _after_ committing the transaction where the
252     # reservations are actually created.
253     foreach (@$results) {
254         $_->{"targeting"} = $U->storagereq(
255             "open-ils.storage.booking.reservation.resource_targeter",
256             $_->{"bresv"}
257         )->[0];
258     }
259     return $results;
260 }
261 __PACKAGE__->register_method(
262     method   => "create_bresv",
263     api_name => "open-ils.booking.reservations.create",
264     signature => {
265         params => [
266             {type => 'string', desc => 'Authentication token'},
267             {type => 'string', desc => 'Barcode of user for whom to reserve'},
268             {type => 'array', desc => 'Two elements: start and end timestamp'},
269             {type => 'int', desc => 'Booking resource type'},
270             {type => 'list', desc => 'Booking resource (undef ok; empty not ok)'},
271             {type => 'array', desc => 'Attribute values selected'},
272         ],
273         return => { desc => "A hash containing the new bresv and a list " .
274             "of new bravm"}
275     }
276 );
277
278
279 sub resource_list_by_attrs {
280     my $self = shift;
281     my $client = shift;
282     my $auth = shift; # Keep as argument, though not used just now.
283     my $filters = shift;
284
285     return undef unless ($filters->{type} || $filters->{attribute_values});
286
287     my $query = {
288         'select'   => { brsrc => [ 'id' ] },
289         'from'     => { brsrc => {} },
290         'where'    => {},
291         'distinct' => 1
292     };
293
294     $query->{where} = {"-and" => []};
295     if ($filters->{type}) {
296         push @{$query->{where}->{"-and"}}, {"type" => $filters->{type}};
297     }
298
299     if ($filters->{attribute_values}) {
300
301         $query->{from}->{brsrc}->{bram} = { field => 'resource' };
302
303         $filters->{attribute_values} = [$filters->{attribute_values}]
304             if (!ref($filters->{attribute_values}));
305
306         $query->{having}->{'+bram'}->{value}->{'@>'} = {
307             transform => 'array_accum',
308             value => '$_' . $$ . '${' .
309                 join(',', @{$filters->{attribute_values}}) .
310                 '}$_' . $$ . '$'
311         };
312     }
313
314     if ($filters->{available}) {
315         # If only one timestamp has been provided, make it into a range.
316         if (!ref($filters->{available})) {
317             $filters->{available} = [($filters->{available}) x 2];
318         }
319
320         push @{$query->{where}->{"-and"}}, {
321             "-or" => [
322                 {"overbook" => "t"},
323                 {"-not-exists" => {
324                     "select" => {"bresv" => ["id"]},
325                     "from" => "bresv",
326                     "where" => {"-and" => [
327                         json_query_ranges_overlap(
328                             $filters->{available}->[0],
329                             $filters->{available}->[1],
330                             "start_time",
331                             "end_time"
332                         ),
333                         {"cancel_time" => undef},
334                         {"current_resource" => {"=" => {"+brsrc" => "id"}}}
335                     ]},
336                 }}
337             ]
338         };
339     }
340     if ($filters->{booked}) {
341         # If only one timestamp has been provided, make it into a range.
342         if (!ref($filters->{booked})) {
343             $filters->{booked} = [($filters->{booked}) x 2];
344         }
345
346         push @{$query->{where}->{"-and"}}, {
347             "-exists" => {
348                 "select" => {"bresv" => ["id"]},
349                 "from" => "bresv",
350                 "where" => {"-and" => [
351                     json_query_ranges_overlap(
352                         $filters->{booked}->[0],
353                         $filters->{booked}->[1],
354                         "start_time",
355                         "end_time"
356                     ),
357                     {"cancel_time" => undef},
358                     {"current_resource" => { "=" => {"+brsrc" => "id"}}}
359                 ]},
360             }
361         };
362         # I think that the "booked" case could be done with a JOIN instead of
363         # an EXISTS, but I'm leaving it this way for symmetry with the
364         # "available" case for now.  The available case cannot be done with a
365         # join.
366     }
367
368     my $cstore = OpenSRF::AppSession->connect('open-ils.cstore');
369     my $rows = $cstore->request( 'open-ils.cstore.json_query.atomic', $query )->gather(1);
370     $cstore->disconnect;
371
372     return @$rows ? [map { $_->{id} } @$rows] : [];
373 }
374 __PACKAGE__->register_method(
375     method   => "resource_list_by_attrs",
376     api_name => "open-ils.booking.resources.filtered_id_list",
377     argc     => 3,
378     signature=> {
379         params => [
380             {type => 'string', desc => 'Authentication token (unused for now,' .
381                ' but at least pass undef here)'},
382             {type => 'object', desc => 'Filter object: see notes for details'},
383             {type => 'bool', desc => 'Return whole objects instead of IDs?'}
384         ],
385         return => { desc => "An array of brsrc ids matching the requested filters." },
386     },
387     notes    => <<'NOTES'
388
389 The filter object parameter can contain the following keys:
390  * type             => The id of a booking resource type (brt)
391  * attribute_values => The ids of booking resource type attribute values that the resource must have assigned to it (brav)
392  * available        => Either:
393                         A timestamp during which the resources are not reserved.  If the resource is overbookable, this is ignored.
394                         A range of two timestamps which do not overlap any reservations for the resources.  If the resource is overbookable, this is ignored.
395  * booked           => Either:
396                         A timestamp during which the resources are reserved.
397                         A range of two timestamps which overlap a reservation of the resources.
398
399 Note that at least one of 'type' or 'attribute_values' is required.
400
401 NOTES
402 );
403
404
405 sub reservation_list_by_filters {
406     my $self = shift;
407     my $client = shift;
408     my $auth = shift;
409     my $filters = shift;
410     my $whole_obj = shift;
411
412     return undef unless ($filters->{user} || $filters->{user_barcode} || $filters->{resource} || $filters->{type} || $filters->{attribute_values});
413
414     my $e = new_editor(authtoken=>$auth);
415     return $e->event unless $e->checkauth;
416     return $e->event unless $e->allowed('VIEW_TRANSACTION');
417
418     my $query = {
419         'select'   => { bresv => [ 'id', 'start_time' ] },
420         'from'     => { bresv => {} },
421         'where'    => {},
422         'order_by' => [{ class => bresv => field => start_time => direction => 'asc' }],
423         'distinct' => 1
424     };
425
426     if ($filters->{fields}) {
427         $query->{where} = $filters->{fields};
428     }
429
430
431     if ($filters->{user}) {
432         $query->{where}->{usr} = $filters->{user};
433     }
434     elsif ($filters->{user_barcode}) {  # just one of user and user_barcode
435         my $usr = $U->fetch_user_by_barcode($filters->{user_barcode});
436         return $usr if ref($usr) eq 'HASH' and exists($usr->{"ilsevent"});
437         $query->{where}->{usr} = $usr->id;
438     }
439
440
441     if ($filters->{type}) {
442         $query->{where}->{target_resource_type} = $filters->{type};
443     }
444
445     if ($filters->{resource}) {
446         $query->{where}->{target_resource} = $filters->{resource};
447     }
448
449     if ($filters->{attribute_values}) {
450
451         $query->{from}->{bresv}->{bravm} = { field => 'reservation' };
452
453         $filters->{attribute_values} = [$filters->{attribute_values}]
454             if (!ref($filters->{attribute_values}));
455
456         $query->{having}->{'+bravm'}->{attr_value}->{'@>'} = {
457             transform => 'array_accum',
458             value => '$_' . $$ . '${' .
459                 join(',', @{$filters->{attribute_values}}) .
460                 '}$_' . $$ . '$'
461         };
462     }
463
464     if ($filters->{search_start} || $filters->{search_end}) {
465         $query->{where}->{'-or'} = {};
466
467         $query->{where}->{'-or'}->{start_time} = { 'between' => [ $filters->{search_start}, $filters->{search_end} ] }
468                 if ($filters->{search_start});
469
470         $query->{where}->{'-or'}->{end_time} = { 'between' => [ $filters->{search_start}, $filters->{search_end} ] }
471                 if ($filters->{search_end});
472     }
473
474     my $cstore = OpenSRF::AppSession->connect('open-ils.cstore');
475     my $ids = [ map { $_->{id} } @{
476         $cstore->request(
477             'open-ils.cstore.json_query.atomic', $query
478         )->gather(1)
479     } ];
480     $cstore->disconnect;
481
482     if (not $whole_obj) {
483         $e->disconnect;
484         return $ids;
485     }
486
487     my $bresv_list = $e->search_booking_reservation([
488         {"id" => $ids},
489         {"flesh" => 1,
490             "flesh_fields" => {
491                 "bresv" =>
492                     [qw/target_resource current_resource target_resource_type/]
493             }
494         }]
495     );
496     $e->disconnect;
497     return $bresv_list ? $bresv_list : [];
498 }
499 __PACKAGE__->register_method(
500     method   => "reservation_list_by_filters",
501     api_name => "open-ils.booking.reservations.filtered_id_list",
502     argc     => 2,
503     signature=> {
504         params => [
505             {type => 'string', desc => 'Authentication token'},
506             {type => 'object', desc => 'Filter object -- see notes for details'}
507         ],
508         return => { desc => "An array of bresv ids matching the requested filters." },
509     },
510     notes    => <<'NOTES'
511
512 The filter object parameter can contain the following keys:
513  * user             => The id of a user that has requested a bookable item -- filters on bresv.usr
514  * barcode          => The barcode of a user that has requested a bookable item
515  * type             => The id of a booking resource type (brt) -- filters on bresv.target_resource_type
516  * resource         => The id of a booking resource (brsrc) -- filters on bresv.target_resource
517  * attribute_values => The ids of booking resource type attribute values that the resource must have assigned to it (brav)
518  * search_start     => If search_end is not specified, booking interval (start_time to end_time) must contain this timestamp.
519  * search_end       => If search_start is not specified, booking interval (start_time to end_time) must contain this timestamp.
520  * fields           => An object containing any combination of bresv search filters in standard cstore/pcrud search format.
521
522 Note that at least one of 'user', 'type', 'resource' or 'attribute_values' is required.  If both search_start and search_end are specified,
523 then the result includes any reservations that overlap with that time range.  Any filter fields supplied in 'fields' are overridden
524 by the top-level filters ('user', 'type', 'resource').
525
526 NOTES
527 );
528
529
530 sub naive_ts_string { strftime("%F %T", localtime(shift)); }
531
532 sub get_pull_list {
533     my ($self, $client, $auth, $range, $interval_secs, $pickup_lib) = @_;
534
535     my $e = new_editor(xact => 1, authtoken => $auth);
536     return $e->die_event unless $e->checkauth;
537     return $e->die_event unless $e->allowed('RETRIEVE_RESERVATION_PULL_LIST');
538     return $e->die_event unless (
539         ref($range) eq 'ARRAY' or
540         ($interval_secs = int($interval_secs)) > 0
541     );
542
543     $range = [ naive_ts_string(time), naive_ts_string(time + $interval_secs) ]
544         if not $range;
545
546     my @fundamental_constraints = (
547         {"current_resource" => {"!=" => undef}},
548         {"capture_time" => undef},
549         {"cancel_time" => undef},
550         {"return_time" => undef},
551         {"pickup_time" => undef}
552     );
553
554     my $query = {
555         "select" => {
556             "bresv" => [
557                 "current_resource",
558                 {
559                     "column" => "start_time",
560                     "transform" => "min",
561                     "aggregate" => 1
562                 }
563             ]
564         },
565         "from" => "bresv",
566         "where" => {
567             "-and" => [
568                 json_query_ranges_overlap(
569                     $range->[0], $range->[1], "start_time", "end_time"
570                 ),
571                 @fundamental_constraints
572             ],
573         }
574     };
575     if ($pickup_lib) {
576         push @{$query->{"where"}->{"-and"}}, {"pickup_lib" => $pickup_lib};
577     }
578
579     my $rows = $e->json_query($query);
580     my %resource_id_map = ();
581     my @all_ids = ();
582     if (@$rows) {
583         my $id_query = {
584             "select" => {"bresv" => ["id"]},
585             "from" => "bresv",
586             "where" => {
587                 "-and" => [
588                     {"current_resource" => "PLACEHOLDER"},
589                     {"start_time" => "PLACEHOLDER"},
590                 ]
591             }
592         };
593         if ($pickup_lib) {
594             push @{$id_query->{"where"}->{"-and"}},
595                 {"pickup_lib" => $pickup_lib};
596         }
597
598         foreach (@$rows) {
599             $id_query->{"where"}->{"-and"}->[0]->{"current_resource"} =
600                 $_->{"current_resource"};
601             $id_query->{"where"}->{"-and"}->[1]->{"start_time"} =
602                 $_->{"start_time"};
603
604             my $results = $e->json_query($id_query);
605             if (@$results) {
606                 my @these_ids = map { $_->{"id"} } @$results;
607                 push @all_ids, @these_ids;
608
609                 $resource_id_map{$_->{"current_resource"}} = [@these_ids];
610             }
611         }
612     }
613     if (@all_ids) {
614         my %bresv_lookup = (
615             map { $_->id => $_ } @{
616                 $e->search_booking_reservation([{"id" => [@all_ids]}, {
617                     flesh => 1,
618                     flesh_fields => { bresv => [
619                             "usr",
620                             "target_resource_type",
621                             "current_resource"
622                     ]}
623                 }])
624             }
625         );
626         $e->disconnect;
627         return [ map {
628             my $key = $_;
629             my $one = $bresv_lookup{$resource_id_map{$key}->[0]};
630             my $result = {
631                 "current_resource" => $one->current_resource,
632                 "target_resource_type" => $one->target_resource_type,
633                 "reservations" => [
634                     map { $bresv_lookup{$_} } @{$resource_id_map{$key}}
635                 ]
636             };
637             foreach (@{$result->{"reservations"}}) {    # deflesh
638                 $_->current_resource($_->current_resource->id);
639                 $_->target_resource_type($_->target_resource_type->id);
640             }
641             $result;
642         } keys %resource_id_map ];
643     } else {
644         $e->disconnect;
645         return [];
646     }
647 }
648 __PACKAGE__->register_method(
649     method   => "get_pull_list",
650     api_name => "open-ils.booking.reservations.get_pull_list",
651     argc     => 4,
652     signature=> {
653         params => [
654             {type => "string", desc => "Authentication token"},
655             {type => "array", desc =>
656                 "range: Date/time range for reservations (opt)"},
657             {type => "int", desc =>
658                 "interval: Seconds from now (instead of range)"},
659             {type => "number", desc => "(Optional) Pickup library"}
660         ],
661         return => { desc => "An array of hashes, each containing key/value " .
662             "pairs describing resource, resource type, and a list of " .
663             "reservations that claim the given resource." }
664     }
665 );
666
667
668 sub get_copy_fleshed_just_right {
669     my ($self, $client, $auth, $barcode) = @_;
670
671     my $e = new_editor(authtoken => $auth);
672     my $results = $e->search_asset_copy([
673         {"barcode" => $barcode},
674         {
675             "flesh" => 1,
676             "flesh_fields" => {"acp" => [qw/call_number location/]}
677         }
678     ]);
679
680     if (ref($results) eq 'ARRAY') {
681         $e->disconnect;
682         return $results->[0] unless ref $barcode;
683         return +{ map { $_->barcode => $_ } @$results };
684     } else {
685         return $e->die_event;
686     }
687 }
688 __PACKAGE__->register_method(
689     method   => "get_copy_fleshed_just_right",
690     api_name => "open-ils.booking.asset.get_copy_fleshed_just_right",
691     argc     => 2,
692     signature=> {
693         params => [
694             {type => "string", desc => "Authentication token"},
695             {type => "mixed", desc => "One barcode or an array of them"},
696         ],
697         return => { desc =>
698             "A copy, or a hash of copies keyed by barcode if an array of " .
699             "barcodes was given"
700         }
701     }
702 );
703
704
705 sub capture_reservation {
706     my $self = shift;
707     my $client = shift;
708     my $auth = shift;
709     my $res_id = shift;
710
711     my $e = new_editor(xact => 1, authtoken => $auth);
712     return $e->event unless $e->checkauth;
713     return $e->event unless $e->allowed('CAPTURE_RESERVATION');
714     my $here = $e->requestor->ws_ou;
715
716     my $reservation = $e->retrieve_booking_reservation( $res_id );
717     return OpenILS::Event->new('RESERVATION_NOT_FOUND') unless $reservation;
718
719     return OpenILS::Event->new('RESERVATION_CAPTURE_FAILED', payload => { captured => 0, fail_cause => 'no-resource' })
720         if (!$reservation->current_resource); # no resource
721
722     return OpenILS::Event->new('RESERVATION_CAPTURE_FAILED', payload => { captured => 0, fail_cause => 'cancelled' })
723         if ($reservation->cancel_time); # canceled
724
725     my $resource = $e->retrieve_booking_resource( $reservation->current_resource );
726     my $type = $e->retrieve_booking_resource( $resource->type );
727
728     $reservation->capture_staff( $e->requestor->id );
729     $reservation->capture_time( 'now' );
730
731     return $e->event unless ( $e->update_booking_reservation( $reservation ) and $reservation = $e->data );
732
733     my $ret = { captured => 1, reservation => $reservation };
734
735     if ($here != $reservation->pickup_lib) {
736         return OpenILS::Event->new('RESERVATION_CAPTURE_FAILED', payload => { captured => 0, fail_cause => 'not-transferable' })
737             if (!$U->is_true($type->transferable)); # non-transferable resource
738
739         # need to transit the item ... is it already in transit?
740         my $transit = $e->search_action_reservation_transit_copy( { reservation => $res_id, dest_recv_time => undef } )->[0];
741
742         if (!$transit) { # not yet in transit
743             $transit = new Fieldmapper::action::reservation_transit_copy ();
744
745             $transit->copy($resource->id);
746             $transit->copy_status(15);
747             $transit->source_send_time('now');
748             $transit->source($here);
749             $transit->dest($reservation->pickup_lib);
750
751             $e->create_action_reservation_transit_copy( $transit );
752
753             if ($U->is_true($type->catalog_item)) {
754                 my $copy = $e->search_asset_copy( { barcode => $resource->barcode, deleted => 'f' } )->[0];
755
756                 if ($copy) {
757                     return OpenILS::Event->new('OPEN_CIRCULATION_EXISTS', payload => $copy) if ($copy->status == 1);
758                     $copy->status(6);
759                     $e->update_asset_copy( $copy );
760                     $$ret{catalog_item} = $e->data;
761                 }
762             }
763         }
764
765         $$ret{transit} = $transit;
766     } elsif ($U->is_true($type->catalog_item)) {
767         my $copy = $e->search_asset_copy( { barcode => $resource->barcode, deleted => 'f' } )->[0];
768
769         if ($copy) {
770             return OpenILS::Event->new('OPEN_CIRCULATION_EXISTS', payload => { captured => 0, copy => $copy }) if ($copy->status == 1);
771             $copy->status(15);
772             $e->update_asset_copy( $copy );
773             $$ret{catalog_item} = $e->data;
774         }
775     }
776
777     $e->commit;
778
779     return OpenILS::Event->new('SUCCESS', payload => $ret);
780 }
781 __PACKAGE__->register_method(
782     method   => "capture_reservation",
783     api_name => "open-ils.booking.reservations.capture",
784     argc     => 2,
785     signature=> {
786         params => [
787             {type => 'string', desc => 'Authentication token'},
788             {type => 'number', desc => 'Reservation ID'}
789         ],
790         return => { desc => "An OpenILS Event object describing the outcome of the capture, with relevant payload." },
791     }
792 );
793
794
795 1;