--- /dev/null
+package OpenILS::Application::Curbside;
+
+use strict;
+use warnings;
+
+use POSIX qw/strftime/;
+use OpenSRF::AppSession;
+use OpenILS::Application;
+use base qw/OpenILS::Application/;
+
+use OpenILS::Utils::DateTime qw/:datetime/;
+use OpenILS::Utils::CStoreEditor qw/:funcs/;
+use OpenILS::Utils::Fieldmapper;
+use OpenILS::Application::AppUtils;
+my $U = "OpenILS::Application::AppUtils";
+
+use DateTime;
+use DateTime::Format::ISO8601;
+
+my $date_parser = DateTime::Format::ISO8601->new;
+
+use OpenSRF::Utils::Logger qw/$logger/;
+
+sub fetch_delivered { # returns appointments delivered TODAY
+ my ($self, $conn, $authtoken, $org) = @_;
+
+ my $e = new_editor(xact => 1, authtoken => $authtoken);
+ return $e->die_event unless $e->checkauth;
+
+ $org ||= $e->requestor->ws_ou;
+
+ return new OpenILS::Event("CURBSIDE_NOT_ALLOWED") unless ($U->is_true(
+ $U->ou_ancestor_setting_value($org, 'circ.curbside')
+ ));
+
+ my $slots = $e->search_action_curbside([{
+ org => $org,
+ arrival => { '!=' => undef},
+ delivered => { '>' => 'today'},
+ },{
+ flesh => 1, flesh_fields => {acsp => 'patron'},
+ order_by => { acsp => {delivered => {direction => 'desc'}} }
+ }]);
+
+ $conn->respond($_) for @$slots;
+ return undef;
+}
+__PACKAGE__->register_method(
+ method => "fetch_delivered",
+ api_name => "open-ils.curbside.fetch_delivered",
+ signature => {
+ params => [
+ {type => 'string', desc => 'Authentication token'},
+ {type => 'number', desc => 'Library ID'},
+ ],
+ return => { desc => 'A stream of appointments that were delivered today'}
+ }
+);
+
+sub fetch_latest_delivered { # returns appointments delivered TODAY
+ my ($self, $conn, $authtoken, $org) = @_;
+
+ my $e = new_editor(xact => 1, authtoken => $authtoken);
+ return $e->die_event unless $e->checkauth;
+
+ $org ||= $e->requestor->ws_ou;
+
+ return new OpenILS::Event("CURBSIDE_NOT_ALLOWED") unless ($U->is_true(
+ $U->ou_ancestor_setting_value($org, 'circ.curbside')
+ ));
+
+ my $slots = $e->search_action_curbside([{
+ org => $org,
+ arrival => { '!=' => undef},
+ delivered => { '>' => 'today'},
+ },{
+ limit => 1,
+ order_by => { acsp => {delivered => {direction => 'desc'}} }
+ }]);
+
+ return @$slots ? $$slots[0]->delivered : undef;
+}
+__PACKAGE__->register_method(
+ method => "fetch_latest_delivered",
+ api_name => "open-ils.curbside.fetch_delivered.latest",
+ signature => {
+ params => [
+ {type => 'string', desc => 'Authentication token'},
+ {type => 'number', desc => 'Library ID'},
+ ],
+ return => { desc => 'Most recent delivery timestamp from today, or error event'}
+ }
+);
+
+sub fetch_arrived {
+ my ($self, $conn, $authtoken, $org) = @_;
+
+ my $e = new_editor(xact => 1, authtoken => $authtoken);
+ return $e->die_event unless $e->checkauth;
+
+ $org ||= $e->requestor->ws_ou;
+
+ return new OpenILS::Event("CURBSIDE_NOT_ALLOWED") unless ($U->is_true(
+ $U->ou_ancestor_setting_value($org, 'circ.curbside')
+ ));
+
+ my $slots = $e->search_action_curbside([{
+ org => $org,
+ arrival => { '!=' => undef},
+ delivered => undef,
+ },{
+ flesh => 1, flesh_fields => {acsp => 'patron'},
+ order_by => { acsp => 'arrival' }
+ }]);
+
+ for my $s (@$slots) {
+ my $holds = $e->search_action_hold_request({
+ usr => $s->patron->id,
+ current_shelf_lib => $s->org,
+ pickup_lib => $s->org,
+ shelf_time => {'!=' => undef},
+ cancel_time => undef,
+ fulfillment_time => undef
+ });
+
+ $conn->respond({slot => $s, holds => $holds});
+ }
+
+ return undef;
+}
+__PACKAGE__->register_method(
+ method => "fetch_arrived",
+ api_name => "open-ils.curbside.fetch_arrived",
+ signature => {
+ params => [
+ {type => 'string', desc => 'Authentication token'},
+ {type => 'number', desc => 'Library ID'},
+ ],
+ return => { desc => 'A stream of appointments for patrons that have arrived but are not delivered'}
+ }
+);
+
+sub fetch_latest_arrived {
+ my ($self, $conn, $authtoken, $org) = @_;
+
+ my $e = new_editor(xact => 1, authtoken => $authtoken);
+ return $e->die_event unless $e->checkauth;
+
+ $org ||= $e->requestor->ws_ou;
+
+ return new OpenILS::Event("CURBSIDE_NOT_ALLOWED") unless ($U->is_true(
+ $U->ou_ancestor_setting_value($org, 'circ.curbside')
+ ));
+
+ my $slots = $e->search_action_curbside([{
+ org => $org,
+ arrival => { '!=' => undef},
+ delivered => undef,
+ },{
+ limit => 1, order_by => { acsp => { arrival => { direction => 'desc' } } }
+ }]);
+
+ return @$slots ? $$slots[0]->arrived : undef;
+}
+__PACKAGE__->register_method(
+ method => "fetch_latest_arrived",
+ api_name => "open-ils.curbside.fetch_arrived.latest",
+ signature => {
+ params => [
+ {type => 'string', desc => 'Authentication token'},
+ {type => 'number', desc => 'Library ID'},
+ ],
+ return => { desc => 'Most recent arrival time on undelivered appointments'}
+ }
+);
+
+sub fetch_staged {
+ my ($self, $conn, $authtoken, $org) = @_;
+
+ my $e = new_editor(xact => 1, authtoken => $authtoken);
+ return $e->die_event unless $e->checkauth;
+
+ $org ||= $e->requestor->ws_ou;
+
+ return new OpenILS::Event("CURBSIDE_NOT_ALLOWED") unless ($U->is_true(
+ $U->ou_ancestor_setting_value($org, 'circ.curbside')
+ ));
+
+ my $slots = $e->search_action_curbside([{
+ org => $org,
+ staged => { '!=' => undef},
+ arrived => undef
+ },{
+ flesh => 1, flesh_fields => {acsp => 'patron'},
+ order_by => { acsp => 'slot' }
+ }]);
+
+ for my $s (@$slots) {
+ my $holds = $e->search_action_hold_request({
+ usr => $s->patron->id,
+ current_shelf_lib => $s->org,
+ pickup_lib => $s->org,
+ shelf_time => {'!=' => undef},
+ cancel_time => undef,
+ fulfillment_time => undef
+ });
+
+ $conn->respond({slot => $s, holds => $holds});
+ }
+
+ return undef;
+}
+__PACKAGE__->register_method(
+ method => "fetch_staged",
+ api_name => "open-ils.curbside.fetch_staged",
+ signature => {
+ params => [
+ {type => 'string', desc => 'Authentication token'},
+ {type => 'number', desc => 'Library ID'},
+ ],
+ return => { desc => 'A stream of appointments that are staged but patrons have not yet arrived'}
+ }
+);
+
+sub fetch_latest_staged {
+ my ($self, $conn, $authtoken, $org) = @_;
+
+ my $e = new_editor(xact => 1, authtoken => $authtoken);
+ return $e->die_event unless $e->checkauth;
+
+ $org ||= $e->requestor->ws_ou;
+
+ return new OpenILS::Event("CURBSIDE_NOT_ALLOWED") unless ($U->is_true(
+ $U->ou_ancestor_setting_value($org, 'circ.curbside')
+ ));
+
+ my $slots = $e->search_action_curbside([{
+ org => $org,
+ staged => { '!=' => undef},
+ arrived => undef
+ },{
+ limit => 1, order_by => { acsp => { slot => { direction => 'desc' } } }
+ }]);
+
+ return @$slots ? $$slots[0]->staged : undef;
+}
+__PACKAGE__->register_method(
+ method => "fetch_latest_staged",
+ api_name => "open-ils.curbside.fetch_staged.latest",
+ signature => {
+ params => [
+ {type => 'string', desc => 'Authentication token'},
+ {type => 'number', desc => 'Library ID'},
+ ],
+ return => { desc => 'Time of most recently staged appointment'}
+ }
+);
+
+sub fetch_to_be_staged {
+ my ($self, $conn, $authtoken, $org) = @_;
+
+ my $e = new_editor(xact => 1, authtoken => $authtoken);
+ return $e->die_event unless $e->checkauth;
+
+ $org ||= $e->requestor->ws_ou;
+
+ return new OpenILS::Event("CURBSIDE_NOT_ALLOWED") unless ($U->is_true(
+ $U->ou_ancestor_setting_value($org, 'circ.curbside')
+ ));
+
+ my $gran = $U->ou_ancestor_setting_value($org, 'circ.curbside.granularity') || '15 minutes';
+ my $gran_seconds = interval_to_seconds($gran);
+ my $horizon = new DateTime;
+ $horizon->add(seconds => $gran_seconds * 2);
+
+ my $slots = $e->search_action_curbside([{
+ org => $org,
+ staged => undef,
+ slot => { '<=' => $horizon->strftime('%FT%T%z') },
+ },{
+ flesh => 1, flesh_fields => {acsp => 'patron'},
+ order_by => { acsp => 'slot' }
+ }]);
+
+ for my $s (@$slots) {
+ my $holds = $e->search_action_hold_request({
+ usr => $s->patron->id,
+ current_shelf_lib => $s->org,
+ pickup_lib => $s->org,
+ shelf_time => {'!=' => undef},
+ cancel_time => undef,
+ fulfillment_time => undef
+ });
+
+ $conn->respond({slot => $s, holds => $holds});
+ }
+
+ return undef;
+}
+__PACKAGE__->register_method(
+ method => "fetch_to_be_staged",
+ api_name => "open-ils.curbside.fetch_to_be_staged",
+ signature => {
+ params => [
+ {type => 'string', desc => 'Authentication token'},
+ {type => 'number', desc => 'Library ID'},
+ ],
+ return => { desc => 'A stream of appointments that need to be staged'}
+ }
+);
+
+sub fetch_latest_to_be_staged {
+ my ($self, $conn, $authtoken, $org) = @_;
+
+ my $e = new_editor(xact => 1, authtoken => $authtoken);
+ return $e->die_event unless $e->checkauth;
+
+ $org ||= $e->requestor->ws_ou;
+
+ return new OpenILS::Event("CURBSIDE_NOT_ALLOWED") unless ($U->is_true(
+ $U->ou_ancestor_setting_value($org, 'circ.curbside')
+ ));
+
+ my $gran = $U->ou_ancestor_setting_value($org, 'circ.curbside.granularity') || '15 minutes';
+ my $gran_seconds = interval_to_seconds($gran);
+ my $horizon = new DateTime;
+ $horizon->add(seconds => $gran_seconds * 2);
+
+ my $slots = $e->search_action_curbside([{
+ org => $org,
+ staged => undef,
+ slot => { '<=' => $horizon->strftime('%FT%T%z') },
+ },{
+ limit => 1, order_by => { acsp => { slot => { direction => 'desc' } } }
+ }]);
+
+ return @$slots ? $$slots[0]->slot : undef;
+}
+__PACKAGE__->register_method(
+ method => "fetch_latest_to_be_staged",
+ api_name => "open-ils.curbside.fetch_to_be_staged.latest",
+ signature => {
+ params => [
+ {type => 'string', desc => 'Authentication token'},
+ {type => 'number', desc => 'Library ID'},
+ ],
+ return => { desc => 'Appointment time of the latest slot that needs to be staged'}
+ }
+);
+
+sub times_for_date {
+ my ($self, $conn, $authtoken, $date, $org) = @_;
+
+ my $e = new_editor(xact => 1, authtoken => $authtoken);
+ return $e->die_event unless $e->checkauth;
+
+ $org ||= $e->requestor->ws_ou;
+
+ return new OpenILS::Event("CURBSIDE_NOT_ALLOWED") unless ($U->is_true(
+ $U->ou_ancestor_setting_value($org, 'circ.curbside')
+ ));
+
+ my $start_obj = $date_parser->parse_datetime($date);
+ return $conn->respond_complete unless ($start_obj);
+
+ my $gran = $U->ou_ancestor_setting_value($org, 'circ.curbside.granularity') || '15 minutes';
+ my $gran_seconds = interval_to_seconds($gran);
+
+ my $max = $U->ou_ancestor_setting_value($org, 'circ.curbside.max_concurrent') || 10;
+
+ my $hoo = $e->retrieve_actor_org_unit_hours_of_operation($org);
+ return undef unless ($hoo);
+
+ my $dow = $start_obj->day_of_week_0;
+
+ my $open_method = "dow_${dow}_open";
+ my $close_method = "dow_${dow}_close";
+
+ my $open_time = $hoo->$open_method;
+ my $close_time = $hoo->$close_method;
+ return $conn->respond_complete if ($open_time eq $close_time); # location closed that day
+
+ $start_obj = $date_parser->parse_datetime($date.'T'.$open_time); # reset this to opening time
+ my $end_obj = $date_parser->parse_datetime($date.'T'.$close_time);
+
+ my $now_obj = DateTime->now;
+ my $step_obj = $start_obj->clone;
+ while (DateTime->compare($step_obj,$end_obj) <= 0) { # inside HOO
+ if (DateTime->compare($step_obj,$now_obj) >= 0) { # don't offer times in the past
+ my $step_ts = $step_obj->strftime('%FT%T%z');
+ my $other_slots = $e->search_action_curbside({org => $org, slot => $step_ts}, {idlist => 1});
+ my $available = $max - scalar(@$other_slots);
+ $conn->respond([$step_obj->strftime('%T'), $available]);
+ }
+ $step_obj->add(seconds => $gran_seconds);
+ }
+
+ $e->disconnect;
+ return undef;
+}
+__PACKAGE__->register_method(
+ method => "times_for_date",
+ api_name => "open-ils.curbside.times_for_date",
+ argc => 2,
+ streaming=> 1,
+ signature=> {
+ params => [
+ {type => "string", desc => "Authentication token"},
+ {type => "string", desc => "Date to find times for"},
+ {type => "number", desc => "Library ID (default ws_ou)"},
+ ],
+ return => {desc => 'A stream of array refs, structure: ["hh:mm:ss",$available_count]; event on error.'}
+ },
+ notes => 'Restricted to logged in users to avoid spamming induced load'
+);
+
+sub create_update_appointment {
+ my ($self, $conn, $authtoken, $patron, $date, $time, $org) = @_;
+ my $mode = 'create';
+ $mode = 'update' if ($self->api_name =~ /update/);
+
+ my $e = new_editor(xact => 1, authtoken => $authtoken);
+ return $e->die_event unless $e->checkauth;
+
+ $org ||= $e->requestor->ws_ou;
+
+ return new OpenILS::Event("CURBSIDE_NOT_ALLOWED") unless ($U->is_true(
+ $U->ou_ancestor_setting_value($org, 'circ.curbside')
+ ));
+
+ unless ($patron == $e->requestor->id) {
+ return $e->die_event unless $e->allowed("STAFF_LOGIN");
+ }
+
+ my $date_obj = $date_parser->parse_datetime($date);
+ return undef unless ($date_obj);
+
+ my $slot;
+
+ # do they already have an open slot?
+ # NOTE: once arrived is set, it's past the point of editing.
+ my $old_slot = $e->search_action_curbside({
+ patron => $patron,
+ org => $org,
+ slot => { '!=' => undef },
+ arrived=> undef
+ })->[0];
+ if ($old_slot) {
+ if ($mode eq 'create') {
+ my $ev = new OpenILS::Event("CURBSIDE_EXISTS");
+ $e->disconnect;
+ return $ev;
+ } else {
+ $slot = $old_slot;
+ }
+ }
+
+
+ my $gran = $U->ou_ancestor_setting_value($org, 'circ.curbside.granularity') || '15 minutes';
+ my $max = $U->ou_ancestor_setting_value($org, 'circ.curbside.max_concurrent') || 10;
+
+ # some sanity checking
+ my $hoo = $e->retrieve_actor_org_unit_hours_of_operation($org);
+ return undef unless ($hoo);
+
+ my $dow = $date_obj->day_of_week_0;
+
+ my $open_method = "dow_${dow}_open";
+ my $close_method = "dow_${dow}_close";
+
+ my $open_time = $hoo->$open_method;
+ my $close_time = $hoo->$close_method;
+ return undef if ($open_time eq $close_time); # location closed that day
+
+ my $open_seconds = interval_to_seconds($open_time);
+ my $close_seconds = interval_to_seconds($close_time);
+
+ my $time_seconds = interval_to_seconds($time);
+ my $gran_seconds = interval_to_seconds($gran);
+
+ return undef if ($time_seconds < $open_seconds); # too early
+ return undef if ($time_seconds > $close_seconds + 1); # too late (/at/ closing allowed)
+
+ $date_obj = $date_parser->parse_datetime($date.'T'.$open_time);
+
+ my $time_into_open_second = $time - $open_time;
+ if (my $extra_time = $time_into_open_second % $gran) { # a remainder means we got a time we shouldn't have
+ $time_into_open_second -= $extra_time; # just back it off to have staff gather earlier
+ }
+
+ my $slot_ts = $date_obj->add(seconds => $time_into_open_second)->strftime('%FT%T%z');
+
+ # finally, confirm that there aren't too many already
+ my $other_slots = $e->search_action_curbside({org => $org, slot => $slot_ts}, {idlist => 1});
+ if (scalar(@$other_slots) >= $max) { # oops... return error
+ my $ev = new OpenILS::Event("CURBSIDE_MAX_FOR_TIME");
+ $e->disconnect;
+ return $ev;
+ }
+
+ my $method = 'update_action_curbside';
+ if ($mode eq 'create' or !$slot) {
+ $slot = $e->search_action_curbside({
+ patron => $patron,
+ org => $org,
+ slot => undef,
+ arrived=> undef,
+ })->[0];
+ }
+
+ if (!$slot) { # just in case the hold-ready reactor isn't in place
+ $slot = Fieldmapper::action::curbside->new;
+ $slot->patron($patron);
+ $slot->org($org);
+ $method = 'create_action_curbside';
+ }
+
+ $slot->slot($slot_ts);
+ $slot = $e->$method($slot);
+
+ $e->commit;
+ $conn->respond_complete($slot);
+
+ OpenSRF::AppSession
+ ->create('open-ils.trigger')
+ ->request(
+ 'open-ils.trigger.event.autocreate',
+ 'hold.confirm_curbside',
+ $slot, $slot->org);
+
+ return undef;
+}
+__PACKAGE__->register_method(
+ method => "create_update_appointment",
+ api_name => "open-ils.curbside.update_appointment",
+ signature => {
+ params => [
+ {type => 'string', desc => 'Authentication token'},
+ {type => 'number', desc => 'Patron ID'},
+ {type => 'string', desc => 'New Date'},
+ {type => 'string', desc => 'New Time'},
+ {type => 'number', desc => 'Library ID (default ws_ou)'},
+ ],
+ return => { desc => 'An action::curbside record on success, '.
+ 'an ILS Event on config, permission, or '.
+ 'recoverable errors, or nothing on bad '.
+ 'or silly data'}
+ }
+);
+
+__PACKAGE__->register_method(
+ method => "create_update_appointment",
+ api_name => "open-ils.curbside.create_appointment",
+ signature => {
+ params => [
+ {type => 'string', desc => 'Authentication token'},
+ {type => 'number', desc => 'Patron ID'},
+ {type => 'string', desc => 'Date'},
+ {type => 'string', desc => 'Time'},
+ {type => 'number', desc => 'Library ID (default ws_ou)'},
+ ],
+ return => { desc => 'An action::curbside record on success, '.
+ 'an ILS Event on config, permission, or '.
+ 'recoverable errors, or nothing on bad '.
+ 'or silly data'}
+ }
+);
+
+sub delete_appointment {
+ my ($self, $conn, $authtoken, $appointment) = @_;
+ my $e = new_editor(xact => 1, authtoken => $authtoken);
+ return $e->die_event unless $e->checkauth;
+
+ my $slot = $e->retrieve_action_curbside($appointment);
+ return undef unless ($slot);
+
+ unless ($appointment->patron == $e->requestor->id) {
+ return $e->die_event unless $e->allowed("STAFF_LOGIN");
+ }
+
+ $e->delete_action_curbside($slot) or return $e->die_event;
+ $e->commit;
+
+ return undef;
+}
+__PACKAGE__->register_method(
+ method => "delete_appointment",
+ api_name => "open-ils.curbside.delete_appointment",
+ signature => {
+ params => [
+ {type => 'string', desc => 'Authentication token'},
+ {type => 'number', desc => 'Patron'},
+ {type => 'string', desc => 'Date'},
+ {type => 'string', desc => 'Time'},
+ ],
+ return => { desc => 'Nothing on success or no appointment found'.
+ 'an ILS Event on permission error'}
+ }
+);
+
+sub mark_staged {
+ my ($self, $conn, $authtoken, $appointment) = @_;
+ my $e = new_editor(xact => 1, authtoken => $authtoken);
+ return $e->die_event unless $e->checkauth;
+ return $e->die_event unless $e->allowed("STAFF_LOGIN");
+
+ my $slot = $e->retrieve_action_curbside($appointment);
+ return undef unless ($slot);
+
+ $slot->staged('now');
+ $slot->stage_staff($e->requestor->id);
+ $slot = $e->update_action_curbside($slot) or return $e->die_event;
+ $e->commit;
+
+ return $slot;
+}
+__PACKAGE__->register_method(
+ method => "mark_staged",
+ api_name => "open-ils.curbside.mark_staged",
+ signature => {
+ params => [
+ {type => 'string', desc => 'Authentication token'},
+ {type => 'number', desc => 'Appointment ID'},
+ ],
+ return => { desc => 'Appointment on success, nothing when no appointment found, '.
+ 'an ILS Event on permission error'}
+ }
+);
+
+sub mark_arrived {
+ my ($self, $conn, $authtoken, $appointment) = @_;
+ my $e = new_editor(xact => 1, authtoken => $authtoken);
+ return $e->die_event unless $e->checkauth;
+
+ my $slot = $e->retrieve_action_curbside($appointment);
+ return undef unless ($slot);
+
+ unless ($appointment->patron == $e->requestor->id) {
+ return $e->die_event unless $e->allowed("STAFF_LOGIN");
+ }
+
+ $slot->arrival('now');
+
+ $slot = $e->update_action_curbside($slot) or return $e->die_event;
+ $e->commit;
+
+ return $slot;
+}
+__PACKAGE__->register_method(
+ method => "mark_arrived",
+ api_name => "open-ils.curbside.mark_arrived",
+ signature => {
+ params => [
+ {type => 'string', desc => 'Authentication token'},
+ {type => 'number', desc => 'Appointment ID'},
+ ],
+ return => { desc => 'Nothing on success or no appointment found'.
+ 'an ILS Event on permission error'}
+ }
+);
+
+sub mark_delivered {
+ my ($self, $conn, $authtoken, $appointment) = @_;
+ my $e = new_editor(xact => 1, authtoken => $authtoken);
+ return $e->die_event unless $e->checkauth;
+ return $e->die_event unless $e->allowed("STAFF_LOGIN");
+
+ my $slot = $e->retrieve_action_curbside($appointment);
+ return undef unless ($slot);
+
+ if (!$slot->staged) {
+ $slot->staged('now');
+ $slot->stage_staff($e->requestor->id);
+ }
+
+ if (!$slot->arrival) {
+ $slot->arrival('now');
+ }
+
+ $slot->delivered('now');
+ $slot->delivery_staff($e->requestor->id);
+
+ $slot = $e->update_action_curbside($slot) or return $e->die_event;
+ $e->commit;
+
+ my $holds = $e->search_action_hold_request({
+ usr => $slot->patron,
+ current_shelf_lib => $slot->org,
+ pickup_lib => $slot->org,
+ shelf_time => {'!=' => undef},
+ cancel_time => undef,
+ fulfillment_time => undef
+ });
+
+ my $circ_sess = OpenSRF::AppSession->connect('open-ils.circ');
+ my @requests = map {
+ $circ_sess->request( # Just try as hard as possible to check out everything
+ 'open-ils.circ.checkout.full.override',
+ $authtoken, { patron => $slot->patron, copyid => $_->current_copy }
+ )
+ } @$holds;
+
+ $conn->respond($_->gather(1)) for @requests;
+ $circ_sess->disconnect;
+
+ return $slot;
+}
+__PACKAGE__->register_method(
+ method => "mark_delivered",
+ api_name => "open-ils.curbside.mark_delivered",
+ signature => {
+ params => [
+ {type => 'string', desc => 'Authentication token'},
+ {type => 'number', desc => 'Appointment ID'},
+ ],
+ return => { desc => 'Nothing for no appointment found, '.
+ 'a stream of open-ils.circ.checkout.full.override '.
+ 'responses followed by the finalized slot, '.
+ 'or an ILS Event on permission error'}
+ }
+);
+
+1;