1 package OpenILS::Application::Curbside;
6 use POSIX qw/strftime/;
7 use OpenSRF::AppSession;
8 use OpenILS::Application;
9 use base qw/OpenILS::Application/;
11 use OpenILS::Utils::DateTime qw/:datetime/;
12 use OpenILS::Utils::CStoreEditor qw/:funcs/;
13 use OpenILS::Utils::Fieldmapper;
14 use OpenILS::Application::AppUtils;
15 my $U = "OpenILS::Application::AppUtils";
17 use Digest::MD5 qw(md5_hex);
20 use DateTime::Format::ISO8601;
22 my $date_parser = DateTime::Format::ISO8601->new;
24 use OpenSRF::Utils::Logger qw/$logger/;
26 sub fetch_mine { # returns appointments owned by $authtoken user, optional $org filter
27 my ($self, $conn, $authtoken, $org, $limit, $offset) = @_;
29 my $e = new_editor(xact => 1, authtoken => $authtoken);
30 return $e->die_event unless $e->checkauth;
32 # NOTE: not checking if curbside is enabled here
33 # because the pickup lib might be anything
35 my $slots = $e->search_action_curbside([{
36 patron => $e->requestor->id,
37 delivered => { '=' => undef },
38 ( $org ? (org => $org) : () )
40 ($limit ? (limit => $limit) : ()),
41 ($offset ? (offset => $offset) : ()),
42 flesh => 2, flesh_fields => {acsp => ['patron'], au => ['card']},
43 order_by => { acsp => {slot => {direction => 'asc'}} }
46 $conn->respond($_) for @$slots;
49 __PACKAGE__->register_method(
50 method => "fetch_mine",
51 api_name => "open-ils.curbside.fetch_mine",
55 {type => 'string', desc => 'Authentication token'},
56 {type => 'number', desc => 'Optional Library ID to filter further'},
57 {type => 'number', desc => 'Fetch limit'},
58 {type => 'number', desc => 'Fetch offset'},
60 return => { desc => 'A stream of appointments that the authenticated user owns'}
64 sub fetch_appointments { # returns appointment for user at location
65 my ($self, $conn, $authtoken, $usr, $org) = @_;
67 my $e = new_editor(xact => 1, authtoken => $authtoken);
68 return $e->die_event unless $e->checkauth;
70 $org ||= $e->requestor->ws_ou;
72 return new OpenILS::Event("CURBSIDE_NOT_ALLOWED") unless ($U->is_true(
73 $U->ou_ancestor_setting_value($org, 'circ.curbside')
76 return new OpenILS::Event("BAD_PARAMS", "desc" => "No user ID supplied") unless $usr;
78 unless ($usr == $e->requestor->id) {
79 return $e->die_event unless $e->allowed("STAFF_LOGIN");
82 my $slots = $e->search_action_curbside([{
84 delivered => { '=' => undef },
87 order_by => { acsp => {slot => {direction => 'asc'}} }
90 $conn->respond($_) for @$slots;
93 __PACKAGE__->register_method(
94 method => "fetch_appointments",
95 api_name => "open-ils.curbside.open_user_appointments_at_lib",
99 {type => 'string', desc => 'Authentication token'},
100 {type => 'number', desc => 'Patron ID'},
101 {type => 'number', desc => 'Optional pickup library. If not supplied, taken from WS of session.'},
103 return => { desc => 'A stream of appointments that the authenticated user owns'}
107 sub fetch_holds_for_patron_at_pickup_lib {
108 my ($self, $conn, $authtoken, $usr, $org) = @_;
110 my $e = new_editor(xact => 1, authtoken => $authtoken);
111 return $e->die_event unless $e->checkauth;
113 $org ||= $e->requestor->ws_ou;
115 return new OpenILS::Event("CURBSIDE_NOT_ALLOWED") unless ($U->is_true(
116 $U->ou_ancestor_setting_value($org, 'circ.curbside')
119 return new OpenILS::Event("BAD_PARAMS", "desc" => "No user ID supplied") unless $usr;
121 my $holds = $e->search_action_hold_request({
123 current_shelf_lib => $org,
125 shelf_time => {'!=' => undef},
126 cancel_time => undef,
127 fulfillment_time => undef
130 return scalar(@$holds);
133 __PACKAGE__->register_method(
134 method => "fetch_holds_for_patron_at_pickup_lib",
135 api_name => "open-ils.curbside.patron.ready_holds_at_lib.count",
138 {type => 'string', desc => 'Authentication token'},
139 {type => 'number', desc => 'Patron ID'},
140 {type => 'number', desc => 'Optional pickup library. If not supplied, taken from WS of session.'},
142 return => { desc => 'Number of holds on the shelf for the patron at the specified library'}
146 sub _flesh_and_emit_slots {
147 my ($conn, $e, $slots) = @_;
149 for my $s (@$slots) {
153 $start_time = DateTime::Format::ISO8601->new->parse_datetime(clean_ISO8601($s->delivered));
154 $end_time = $start_time->clone->add(seconds => 90); # 90 seconds is arbitrary
156 my $holds = $e->search_action_hold_request([{
157 usr => $s->patron->id,
158 current_shelf_lib => $s->org,
159 pickup_lib => $s->org,
160 shelf_time => {'!=' => undef},
161 cancel_time => undef,
164 '-and' => [ { fulfillment_time => {'>=' => $start_time->strftime('%FT%T%z') } },
165 { fulfillment_time => {'<=' => $end_time->strftime('%FT%T%z') } } ],
167 (fulfillment_time => undef),
169 flesh => 1, flesh_fields => {ahr => ['current_copy']},
172 my $rhrr_list = $e->search_reporter_hold_request_record(
173 {id => [ map { $_->id } @$holds ]}
177 ($_->id => $e->retrieve_metabib_wide_display_entry( $_->bib_record))
180 $conn->respond({slot_id => $s->id, slot => $s, holds => $holds, bib_data_by_hold => \%bib_data});
184 sub fetch_delivered { # returns appointments delivered TODAY
185 my ($self, $conn, $authtoken, $org, $limit, $offset) = @_;
187 my $e = new_editor(xact => 1, authtoken => $authtoken);
188 return $e->die_event unless $e->checkauth;
190 $org ||= $e->requestor->ws_ou;
192 return new OpenILS::Event("CURBSIDE_NOT_ALLOWED") unless ($U->is_true(
193 $U->ou_ancestor_setting_value($org, 'circ.curbside')
196 my $slots = $e->search_action_curbside([{
198 arrival => { '!=' => undef},
199 delivered => { '>' => 'today'},
201 ($limit ? (limit => $limit) : ()),
202 ($offset ? (offset => $offset) : ()),
203 flesh => 2, flesh_fields => {acsp => ['patron'], au => ['card']},
204 order_by => { acsp => {delivered => {direction => 'desc'}} }
207 _flesh_and_emit_slots($conn, $e, $slots);
211 __PACKAGE__->register_method(
212 method => "fetch_delivered",
213 api_name => "open-ils.curbside.fetch_delivered",
217 {type => 'string', desc => 'Authentication token'},
218 {type => 'number', desc => 'Library ID'},
219 {type => 'number', desc => 'Fetch limit'},
220 {type => 'number', desc => 'Fetch offset'},
222 return => { desc => 'A stream of appointments that were delivered today'}
226 sub fetch_latest_delivered { # returns appointments delivered TODAY
227 my ($self, $conn, $authtoken, $org) = @_;
229 my $e = new_editor(xact => 1, authtoken => $authtoken);
230 return $e->die_event unless $e->checkauth;
232 $org ||= $e->requestor->ws_ou;
234 return new OpenILS::Event("CURBSIDE_NOT_ALLOWED") unless ($U->is_true(
235 $U->ou_ancestor_setting_value($org, 'circ.curbside')
238 my $slots = $e->search_action_curbside([{
240 arrival => { '!=' => undef},
241 delivered => { '>' => 'today'},
243 order_by => { acsp => {delivered => {direction => 'desc'}} }
246 return md5_hex( join(',', @$slots) );
248 __PACKAGE__->register_method(
249 method => "fetch_latest_delivered",
250 api_name => "open-ils.curbside.fetch_delivered.latest",
253 {type => 'string', desc => 'Authentication token'},
254 {type => 'number', desc => 'Library ID'},
256 return => { desc => 'Hash of appointment IDs delivered today, or error event'}
261 my ($self, $conn, $authtoken, $org, $limit, $offset) = @_;
263 my $e = new_editor(xact => 1, authtoken => $authtoken);
264 return $e->die_event unless $e->checkauth;
266 $org ||= $e->requestor->ws_ou;
268 return new OpenILS::Event("CURBSIDE_NOT_ALLOWED") unless ($U->is_true(
269 $U->ou_ancestor_setting_value($org, 'circ.curbside')
272 my $slots = $e->search_action_curbside([{
274 arrival => { '!=' => undef},
277 ($limit ? (limit => $limit) : ()),
278 ($offset ? (offset => $offset) : ()),
279 flesh => 3, flesh_fields => {acsp => ['patron'], au => ['card','standing_penalties'], ausp => ['standing_penalty']},
280 order_by => { acsp => 'arrival' }
284 _flesh_and_emit_slots($conn, $e, $slots);
288 __PACKAGE__->register_method(
289 method => "fetch_arrived",
290 api_name => "open-ils.curbside.fetch_arrived",
294 {type => 'string', desc => 'Authentication token'},
295 {type => 'number', desc => 'Library ID'},
296 {type => 'number', desc => 'Fetch limit'},
297 {type => 'number', desc => 'Fetch offset'},
299 return => { desc => 'A stream of appointments for patrons that have arrived but are not delivered'}
303 sub fetch_latest_arrived {
304 my ($self, $conn, $authtoken, $org) = @_;
306 my $e = new_editor(xact => 1, authtoken => $authtoken);
307 return $e->die_event unless $e->checkauth;
309 $org ||= $e->requestor->ws_ou;
311 return new OpenILS::Event("CURBSIDE_NOT_ALLOWED") unless ($U->is_true(
312 $U->ou_ancestor_setting_value($org, 'circ.curbside')
315 my $slots = $e->search_action_curbside([{
317 arrival => { '!=' => undef},
320 order_by => { acsp => { arrival => { direction => 'desc' } } }
323 return md5_hex( join(',', @$slots) );
325 __PACKAGE__->register_method(
326 method => "fetch_latest_arrived",
327 api_name => "open-ils.curbside.fetch_arrived.latest",
330 {type => 'string', desc => 'Authentication token'},
331 {type => 'number', desc => 'Library ID'},
333 return => { desc => 'Hash of appointment IDs for undelivered appointments'}
338 my ($self, $conn, $authtoken, $org, $limit, $offset) = @_;
340 my $e = new_editor(xact => 1, authtoken => $authtoken);
341 return $e->die_event unless $e->checkauth;
343 $org ||= $e->requestor->ws_ou;
345 return new OpenILS::Event("CURBSIDE_NOT_ALLOWED") unless ($U->is_true(
346 $U->ou_ancestor_setting_value($org, 'circ.curbside')
349 my $slots = $e->search_action_curbside([{
351 staged => { '!=' => undef},
354 ($limit ? (limit => $limit) : ()),
355 ($offset ? (offset => $offset) : ()),
356 flesh => 3, flesh_fields => {acsp => ['patron'], au => ['card','standing_penalties'], ausp => ['standing_penalty']},
357 order_by => { acsp => 'slot' }
360 _flesh_and_emit_slots($conn, $e, $slots);
364 __PACKAGE__->register_method(
365 method => "fetch_staged",
366 api_name => "open-ils.curbside.fetch_staged",
370 {type => 'string', desc => 'Authentication token'},
371 {type => 'number', desc => 'Library ID'},
372 {type => 'number', desc => 'Fetch limit'},
373 {type => 'number', desc => 'Fetch offset'},
375 return => { desc => 'A stream of appointments that are staged but patrons have not yet arrived'}
379 sub fetch_latest_staged {
380 my ($self, $conn, $authtoken, $org) = @_;
382 my $e = new_editor(xact => 1, authtoken => $authtoken);
383 return $e->die_event unless $e->checkauth;
385 $org ||= $e->requestor->ws_ou;
387 return new OpenILS::Event("CURBSIDE_NOT_ALLOWED") unless ($U->is_true(
388 $U->ou_ancestor_setting_value($org, 'circ.curbside')
391 my $slots = $e->search_action_curbside([{
393 staged => { '!=' => undef},
397 { class => acsp => field => slot => direction => 'desc' },
398 { class => acsp => field => id => direction => 'desc' }
402 return md5_hex( join(',', @$slots) );
404 __PACKAGE__->register_method(
405 method => "fetch_latest_staged",
406 api_name => "open-ils.curbside.fetch_staged.latest",
409 {type => 'string', desc => 'Authentication token'},
410 {type => 'number', desc => 'Library ID'},
412 return => { desc => 'Hash of appointment IDs for staged appointment'}
416 sub fetch_to_be_staged {
417 my ($self, $conn, $authtoken, $org, $limit, $offset) = @_;
419 my $e = new_editor(xact => 1, authtoken => $authtoken);
420 return $e->die_event unless $e->checkauth;
422 $org ||= $e->requestor->ws_ou;
424 return new OpenILS::Event("CURBSIDE_NOT_ALLOWED") unless ($U->is_true(
425 $U->ou_ancestor_setting_value($org, 'circ.curbside')
428 my $gran = $U->ou_ancestor_setting_value($org, 'circ.curbside.granularity') || '15 minutes';
429 my $gran_seconds = interval_to_seconds($gran);
430 my $horizon = DateTime->now; # NOTE: does not need timezone set because it gets UTC, not floating, so we can math with it
431 $horizon->add(seconds => $gran_seconds * 2);
433 my $slots = $e->search_action_curbside([{
436 slot => { '<=' => $horizon->strftime('%FT%T%z') },
438 ($limit ? (limit => $limit) : ()),
439 ($offset ? (offset => $offset) : ()),
440 flesh => 3, flesh_fields => {acsp => ['patron','stage_staff'], au => ['card','standing_penalties'], ausp => ['standing_penalty']},
441 order_by => { acsp => 'slot' }
444 _flesh_and_emit_slots($conn, $e, $slots);
448 __PACKAGE__->register_method(
449 method => "fetch_to_be_staged",
450 api_name => "open-ils.curbside.fetch_to_be_staged",
454 {type => 'string', desc => 'Authentication token'},
455 {type => 'number', desc => 'Library ID'},
456 {type => 'number', desc => 'Fetch limit'},
457 {type => 'number', desc => 'Fetch offset'},
459 return => { desc => 'A stream of appointments that need to be staged'}
463 sub fetch_latest_to_be_staged {
464 my ($self, $conn, $authtoken, $org) = @_;
466 my $e = new_editor(xact => 1, authtoken => $authtoken);
467 return $e->die_event unless $e->checkauth;
469 $org ||= $e->requestor->ws_ou;
471 return new OpenILS::Event("CURBSIDE_NOT_ALLOWED") unless ($U->is_true(
472 $U->ou_ancestor_setting_value($org, 'circ.curbside')
475 my $gran = $U->ou_ancestor_setting_value($org, 'circ.curbside.granularity') || '15 minutes';
476 my $gran_seconds = interval_to_seconds($gran);
477 my $horizon = DateTime->now; # NOTE: does not need timezone set because it gets UTC, not floating, so we can math with it
478 $horizon->add(seconds => $gran_seconds * 2);
480 my $slots = $e->search_action_curbside([{
483 slot => { '<=' => $horizon->strftime('%FT%T%z') },
486 { class => acsp => field => slot => direction => 'desc' },
487 { class => acsp => field => id => direction => 'desc' }
491 return md5_hex( join(',', map { join('-', $_->id(), $_->stage_staff() // '', $_->arrival() // '') } @$slots) );
493 __PACKAGE__->register_method(
494 method => "fetch_latest_to_be_staged",
495 api_name => "open-ils.curbside.fetch_to_be_staged.latest",
498 {type => 'string', desc => 'Authentication token'},
499 {type => 'number', desc => 'Library ID'},
501 return => { desc => 'Hash of appointment IDs that needs to be staged'}
506 my ($self, $conn, $authtoken, $date, $org) = @_;
508 my $e = new_editor(xact => 1, authtoken => $authtoken);
509 return $e->die_event unless $e->checkauth;
511 $org ||= $e->requestor->ws_ou;
513 return new OpenILS::Event("CURBSIDE_NOT_ALLOWED") unless ($U->is_true(
514 $U->ou_ancestor_setting_value($org, 'circ.curbside')
517 my $start_obj = $date_parser->parse_datetime($date);
518 return $conn->respond_complete unless ($start_obj);
520 my $gran = $U->ou_ancestor_setting_value($org, 'circ.curbside.granularity') || '15 minutes';
521 $gran .= ' minutes' if ($gran =~ /^\s*\d+\s*$/); # Assume minutes for bare numbers (maybe surrounded by spaces)
523 my $gran_seconds = interval_to_seconds($gran);
524 $gran_seconds = 600 if ($gran_seconds < 600); # No smaller than 10 minute intervals
526 my $max = $U->ou_ancestor_setting_value($org, 'circ.curbside.max_concurrent') || 10;
528 my $hoo = $e->retrieve_actor_org_unit_hours_of_operation($org);
529 return undef unless ($hoo);
531 my $dow = $start_obj->day_of_week_0;
533 my $open_method = "dow_${dow}_open";
534 my $close_method = "dow_${dow}_close";
536 my $open_time = $hoo->$open_method;
537 my $close_time = $hoo->$close_method;
538 return $conn->respond_complete if ($open_time eq $close_time); # location closed that day
540 my $tz = $U->ou_ancestor_setting_value($org, 'lib.timezone') || 'local';
541 $start_obj = $date_parser->parse_datetime($date.'T'.$open_time)->set_time_zone($tz); # reset this to opening time
542 my $end_obj = $date_parser->parse_datetime($date.'T'.$close_time)->set_time_zone($tz);
544 my $now_obj = DateTime->now; # NOTE: does not need timezone set because it gets UTC, not floating, so we can math with it
545 # Add two step intervals to avoid having an appointment be scheduled
546 # sooner than the library could stage the items. Setting the earliest
547 # available time to be no earlier than two intervals from now
548 # is arbitrary and could be made configurable in the future, though
549 # it does follow the hard-coding of the horizon in fetch_to_be_staged().
550 $now_obj->add(seconds => 2 * $gran_seconds);
553 my $step_obj = $start_obj->clone;
554 while (DateTime->compare($step_obj,$end_obj) < 0) { # inside HOO
555 if (DateTime->compare($step_obj,$now_obj) >= 0) { # only offer times in the future
556 my $step_ts = $step_obj->strftime('%FT%T%z');
558 if (!@$closings) { # Look for closings that include this slot time.
559 $closings = $e->search_actor_org_unit_closed_date(
560 {org_unit => $org, close_start => {'<=' => $step_ts }, close_end => {'>=' => $step_ts }}
565 for my $closing (@$closings) {
566 # If we have closings, we check that we're still inside at least one of them.
567 # If we /are/ inside one then we just move on. Otherwise, we'll forget
568 # them and check for closings with the next slot time.
569 if (DateTime->compare($step_obj,$date_parser->parse_datetime(clean_ISO8601($closing->close_end))->set_time_zone($tz)) < 0) {
570 $step_obj->add(seconds => $gran_seconds);
578 my $other_slots = $e->search_action_curbside({org => $org, slot => $step_ts}, {idlist => 1});
579 my $available = $max - scalar(@$other_slots);
580 $available = $available < 0 ? 0 : $available; # so truthiness testing is always easy in the client
582 $conn->respond([$step_obj->strftime('%T'), $available]);
584 $step_obj->add(seconds => $gran_seconds);
590 __PACKAGE__->register_method(
591 method => "times_for_date",
592 api_name => "open-ils.curbside.times_for_date",
597 {type => "string", desc => "Authentication token"},
598 {type => "string", desc => "Date to find times for"},
599 {type => "number", desc => "Library ID (default ws_ou)"},
601 return => {desc => 'A stream of array refs, structure: ["hh:mm:ss",$available_count]; event on error.'}
603 notes => 'Restricted to logged in users to avoid spamming induced load'
606 sub create_update_appointment {
607 my ($self, $conn, $authtoken, $patron, $date, $time, $org, $notes) = @_;
609 $mode = 'update' if ($self->api_name =~ /update/);
611 my $e = new_editor(xact => 1, authtoken => $authtoken);
612 return $e->die_event unless $e->checkauth;
614 $org ||= $e->requestor->ws_ou;
616 return new OpenILS::Event("CURBSIDE_NOT_ALLOWED") unless ($U->is_true(
617 $U->ou_ancestor_setting_value($org, 'circ.curbside')
620 unless ($patron == $e->requestor->id) {
621 return $e->die_event unless $e->allowed("STAFF_LOGIN");
624 my $date_obj = $date_parser->parse_datetime($date); # no TZ necessary, just using it to test the input and get DOW
625 return undef unless ($date_obj);
627 if ($time =~ /^\d\d:\d\d$/) {
628 $time .= ":00"; # tack on seconds if needed to keep
629 # interval_to_seconds happy
634 # do they already have an open slot?
635 # NOTE: once arrival is set, it's past the point of editing.
636 my $old_slot = $e->search_action_curbside({
639 slot => { '!=' => undef },
643 if ($mode eq 'create') {
644 my $ev = new OpenILS::Event("CURBSIDE_EXISTS");
652 my $gran = $U->ou_ancestor_setting_value($org, 'circ.curbside.granularity') || '15 minutes';
653 my $max = $U->ou_ancestor_setting_value($org, 'circ.curbside.max_concurrent') || 10;
655 # some sanity checking
656 my $hoo = $e->retrieve_actor_org_unit_hours_of_operation($org);
657 return undef unless ($hoo);
659 my $dow = $date_obj->day_of_week_0;
661 my $open_method = "dow_${dow}_open";
662 my $close_method = "dow_${dow}_close";
664 my $open_time = $hoo->$open_method;
665 my $close_time = $hoo->$close_method;
666 return undef if ($open_time eq $close_time); # location closed that day
668 my $open_seconds = interval_to_seconds($open_time);
669 my $close_seconds = interval_to_seconds($close_time);
671 my $time_seconds = interval_to_seconds($time);
672 my $gran_seconds = interval_to_seconds($gran);
674 return undef if ($time_seconds < $open_seconds); # too early
675 return undef if ($time_seconds > $close_seconds + 1); # too late (/at/ closing allowed)
677 my $time_into_open_second = $time_seconds - $open_seconds;
678 if (my $extra_time = $time_into_open_second % $gran) { # a remainder means we got a time we shouldn't have
679 $time_into_open_second -= $extra_time; # just back it off to have staff gather earlier
682 my $tz = $U->ou_ancestor_setting_value($org, 'lib.timezone') || 'local';
683 $date_obj = $date_parser->parse_datetime($date.'T'.$open_time)->set_time_zone($tz);
685 my $slot_ts = $date_obj->add(seconds => $time_into_open_second)->strftime('%FT%T%z');
687 # finally, confirm that there aren't too many already
688 my $other_slots = $e->search_action_curbside(
691 ( $slot ? (id => { '<>' => $slot->id }) : () ) # exclude our own slot from the count
695 if (scalar(@$other_slots) >= $max) { # oops... return error
696 my $ev = new OpenILS::Event("CURBSIDE_MAX_FOR_TIME");
701 my $method = 'update_action_curbside';
702 if ($mode eq 'create' or !$slot) {
703 $slot = $e->search_action_curbside({
711 if (!$slot) { # just in case the hold-ready reactor isn't in place
712 $slot = Fieldmapper::action::curbside->new;
714 $slot->patron($patron);
716 $slot->notes($notes) if ($notes);
717 $method = 'create_action_curbside';
719 $slot->notes($notes) if ($notes);
721 $method = 'update_action_curbside';
724 $slot->slot($slot_ts);
725 $e->$method($slot) or return $e->die_event;
728 $conn->respond_complete($e->retrieve_action_curbside($slot->id));
731 ->create('open-ils.trigger')
733 'open-ils.trigger.event.autocreate',
734 'hold.confirm_curbside',
739 __PACKAGE__->register_method(
740 method => "create_update_appointment",
741 api_name => "open-ils.curbside.update_appointment",
744 {type => 'string', desc => 'Authentication token'},
745 {type => 'number', desc => 'Patron ID'},
746 {type => 'string', desc => 'New Date'},
747 {type => 'string', desc => 'New Time'},
748 {type => 'number', desc => 'Library ID (default ws_ou)'},
750 return => { desc => 'An action::curbside record on success, '.
751 'an ILS Event on config, permission, or '.
752 'recoverable errors, or nothing on bad '.
757 __PACKAGE__->register_method(
758 method => "create_update_appointment",
759 api_name => "open-ils.curbside.create_appointment",
762 {type => 'string', desc => 'Authentication token'},
763 {type => 'number', desc => 'Patron ID'},
764 {type => 'string', desc => 'Date'},
765 {type => 'string', desc => 'Time'},
766 {type => 'number', desc => 'Library ID (default ws_ou)'},
768 return => { desc => 'An action::curbside record on success, '.
769 'an ILS Event on config, permission, or '.
770 'recoverable errors, or nothing on bad '.
775 sub delete_appointment {
776 my ($self, $conn, $authtoken, $appointment) = @_;
777 my $e = new_editor(xact => 1, authtoken => $authtoken);
778 return $e->die_event unless $e->checkauth;
780 my $slot = $e->retrieve_action_curbside($appointment);
781 return undef unless ($slot);
783 unless ($slot->patron == $e->requestor->id) {
784 return $e->die_event unless $e->allowed("STAFF_LOGIN");
787 $e->delete_action_curbside($slot) or return $e->die_event;
792 __PACKAGE__->register_method(
793 method => "delete_appointment",
794 api_name => "open-ils.curbside.delete_appointment",
797 {type => 'string', desc => 'Authentication token'},
798 {type => 'number', desc => 'Appointment ID'},
800 return => { desc => '-1 on success, nothing when no appointment found, '.
801 'or an ILS Event on permission error'}
805 sub manage_staging_claim {
806 my ($self, $conn, $authtoken, $appointment) = @_;
807 my $e = new_editor(xact => 1, authtoken => $authtoken);
808 return $e->die_event unless $e->checkauth;
809 return $e->die_event unless $e->allowed("STAFF_LOGIN");
811 my $slot = $e->retrieve_action_curbside($appointment);
812 return undef unless ($slot);
814 if ($self->api_name =~ /unclaim/) {
815 $slot->clear_stage_staff();
817 $slot->stage_staff($e->requestor->id);
820 $e->update_action_curbside($slot) or return $e->die_event;
823 return $e->retrieve_action_curbside([
826 flesh_fields => {acsp => ['patron','stage_staff'], au => ['card','standing_penalties'], ausp => ['standing_penalty']},
830 __PACKAGE__->register_method(
831 method => "manage_staging_claim",
832 api_name => "open-ils.curbside.claim_staging",
835 {type => 'string', desc => 'Authentication token'},
836 {type => 'number', desc => 'Appointment ID'},
838 return => { desc => 'Appointment on success, nothing when no appointment found, '.
839 'an ILS Event on permission error'}
842 __PACKAGE__->register_method(
843 method => "manage_staging_claim",
844 api_name => "open-ils.curbside.unclaim_staging",
847 {type => 'string', desc => 'Authentication token'},
848 {type => 'number', desc => 'Appointment ID'},
850 return => { desc => 'Appointment on success, nothing when no appointment found, '.
851 'an ILS Event on permission error'}
856 my ($self, $conn, $authtoken, $appointment) = @_;
857 my $e = new_editor(xact => 1, authtoken => $authtoken);
858 return $e->die_event unless $e->checkauth;
859 return $e->die_event unless $e->allowed("STAFF_LOGIN");
861 my $slot = $e->retrieve_action_curbside($appointment);
862 return undef unless ($slot);
864 $slot->staged('now');
865 $slot->stage_staff($e->requestor->id);
866 $e->update_action_curbside($slot) or return $e->die_event;
869 return $e->retrieve_action_curbside($slot->id);
871 __PACKAGE__->register_method(
872 method => "mark_staged",
873 api_name => "open-ils.curbside.mark_staged",
876 {type => 'string', desc => 'Authentication token'},
877 {type => 'number', desc => 'Appointment ID'},
879 return => { desc => 'Appointment on success, nothing when no appointment found, '.
880 'an ILS Event on permission error'}
885 my ($self, $conn, $authtoken, $appointment) = @_;
886 my $e = new_editor(xact => 1, authtoken => $authtoken);
887 return $e->die_event unless $e->checkauth;
888 return $e->die_event unless $e->allowed("STAFF_LOGIN");
890 my $slot = $e->retrieve_action_curbside($appointment);
891 return undef unless ($slot);
893 $slot->clear_staged();
894 $slot->clear_stage_staff();
895 $e->update_action_curbside($slot) or return $e->die_event;
898 return $e->retrieve_action_curbside($slot->id);
900 __PACKAGE__->register_method(
901 method => "mark_unstaged",
902 api_name => "open-ils.curbside.mark_unstaged",
905 {type => 'string', desc => 'Authentication token'},
906 {type => 'number', desc => 'Appointment ID'},
908 return => { desc => 'Appointment on success, nothing when no appointment found, '.
909 'an ILS Event on permission error'}
914 my ($self, $conn, $authtoken, $appointment) = @_;
915 my $e = new_editor(xact => 1, authtoken => $authtoken);
916 return $e->die_event unless $e->checkauth;
918 my $slot = $e->retrieve_action_curbside($appointment);
919 return undef unless ($slot);
921 unless ($slot->patron == $e->requestor->id) {
922 return $e->die_event unless $e->allowed("STAFF_LOGIN");
925 $slot->arrival('now');
927 $e->update_action_curbside($slot) or return $e->die_event;
930 return $e->retrieve_action_curbside($slot->id);
932 __PACKAGE__->register_method(
933 method => "mark_arrived",
934 api_name => "open-ils.curbside.mark_arrived",
937 {type => 'string', desc => 'Authentication token'},
938 {type => 'number', desc => 'Appointment ID'},
940 return => { desc => 'Appointment on success, nothing when no appointment found, '.
941 'or an ILS Event on permission error'}
946 my ($self, $conn, $authtoken, $appointment) = @_;
947 my $e = new_editor(xact => 1, authtoken => $authtoken);
948 return $e->die_event unless $e->checkauth;
949 return $e->die_event unless $e->allowed("STAFF_LOGIN");
951 my $slot = $e->retrieve_action_curbside($appointment);
952 return undef unless ($slot);
954 if (!$slot->staged) {
955 $slot->staged('now');
956 $slot->stage_staff($e->requestor->id);
959 if (!$slot->arrival) {
960 $slot->arrival('now');
963 $slot->delivered('now');
964 $slot->delivery_staff($e->requestor->id);
966 $e->update_action_curbside($slot) or return $e->die_event;
969 my $holds = $e->search_action_hold_request({
970 usr => $slot->patron,
971 current_shelf_lib => $slot->org,
972 pickup_lib => $slot->org,
973 shelf_time => {'!=' => undef},
974 cancel_time => undef,
975 fulfillment_time => undef
978 my $circ_sess = OpenSRF::AppSession->connect('open-ils.circ');
980 $circ_sess->request( # Just try as hard as possible to check out everything
981 'open-ils.circ.checkout.full.override',
982 $authtoken, { patron => $slot->patron, copyid => $_->current_copy }
986 my @successful_checkouts;
987 my $successful_patron;
988 for my $r (@requests) {
989 my $co_res = $r->gather(1);
990 $conn->respond($co_res);
991 next if (ref($co_res) eq 'ARRAY'); # success is always singular
993 if ($co_res->{textcode} eq 'SUCCESS') { # that's great news...
994 push @successful_checkouts, $co_res->{payload}->{circ}->id;
995 $successful_patron = $co_res->{payload}->{circ}->usr;
999 $conn->respond_complete($e->retrieve_action_curbside($slot->id));
1001 $circ_sess->request(
1002 'open-ils.circ.checkout.batch_notify.session.atomic',
1005 \@successful_checkouts
1006 ) if (@successful_checkouts);
1008 $circ_sess->disconnect;
1011 __PACKAGE__->register_method(
1012 method => "mark_delivered",
1013 api_name => "open-ils.curbside.mark_delivered",
1017 {type => 'string', desc => 'Authentication token'},
1018 {type => 'number', desc => 'Appointment ID'},
1020 return => { desc => 'Nothing for no appointment found, '.
1021 'a stream of open-ils.circ.checkout.full.override '.
1022 'responses followed by the finalized slot, '.
1023 'or an ILS Event on permission error'}