LP#1386347: Clear hold-copy-map efficiently
[Evergreen.git] / Open-ILS / src / perlmods / lib / OpenILS / Application / Circ / Holds.pm
index 584b4c7..1ec6066 100644 (file)
@@ -1,5 +1,5 @@
 # ---------------------------------------------------------------
-# Copyright (C) 2005  Georgia Public Library Service 
+# Copyright (C) 2005  Georgia Public Library Service
 # Bill Erickson <highfalutin@gmail.com>
 
 # This program is free software; you can redistribute it and/or
@@ -36,6 +36,7 @@ use DateTime::Format::ISO8601;
 use OpenSRF::Utils qw/:datetime/;
 use Digest::MD5 qw(md5_hex);
 use OpenSRF::Utils::Cache;
+use OpenSRF::Utils::JSON;
 my $apputils = "OpenILS::Application::AppUtils";
 my $U = $apputils;
 
@@ -67,12 +68,17 @@ __PACKAGE__->register_method(
 
 
 sub test_and_create_hold_batch {
-       my( $self, $conn, $auth, $params, $target_list ) = @_;
+    my( $self, $conn, $auth, $params, $target_list, $oargs ) = @_;
 
-       my $override = 1 if $self->api_name =~ /override/;
+    my $override = 0;
+    if ($self->api_name =~ /override/) {
+        $override = 1;
+        $oargs = { all => 1 } unless defined $oargs;
+        $$params{oargs} = $oargs; # for is_possible checking.
+    }
 
-       my $e = new_editor(authtoken=>$auth);
-       return $e->die_event unless $e->checkauth;
+    my $e = new_editor(authtoken=>$auth);
+    return $e->die_event unless $e->checkauth;
     $$params{'requestor'} = $e->requestor->id;
 
     my $target_field;
@@ -86,20 +92,33 @@ sub test_and_create_hold_batch {
     elsif ($$params{'hold_type'} eq 'P') { $target_field = 'partid'; }
     else { return undef; }
 
+    my $formats_map = delete $$params{holdable_formats_map};
+
     foreach (@$target_list) {
         $$params{$target_field} = $_;
+
+        # copy the requested formats from the target->formats map
+        # into the top-level formats attr for each hold
+        $$params{holdable_formats} = $formats_map->{$_};
+
         my $res;
-        if (! $override) {        
-            ($res) = $self->method_lookup(
-                'open-ils.circ.title_hold.is_possible')->run($auth, $params);
-        }
-        if ($override || $res->{'success'} == 1) {
+        ($res) = $self->method_lookup(
+            'open-ils.circ.title_hold.is_possible')->run($auth, $params, $override ? $oargs : {});
+        if ($res->{'success'} == 1) {
+
+            $params->{'depth'} = $res->{'depth'} if $res->{'depth'};
+
+            # Remove oargs from params so holds can be created.
+            if ($$params{oargs}) {
+                delete $$params{oargs};
+            }
+
             my $ahr = construct_hold_request_object($params);
             my ($res2) = $self->method_lookup(
                 $override
                 ? 'open-ils.circ.holds.create.override'
                 : 'open-ils.circ.holds.create'
-            )->run($auth, $ahr);
+            )->run($auth, $ahr, $oargs);
             $res2 = {
                 'target' => $$params{$target_field},
                 'result' => $res2
@@ -166,10 +185,10 @@ __PACKAGE__->register_method(
 
 
 sub create_hold_batch {
-       my( $self, $conn, $auth, $hold_list ) = @_;
+    my( $self, $conn, $auth, $hold_list, $oargs ) = @_;
     (my $method = $self->api_name) =~ s/\.batch//og;
     foreach (@$hold_list) {
-        my ($res) = $self->method_lookup($method)->run($auth, $_);
+        my ($res) = $self->method_lookup($method)->run($auth, $_, $oargs);
         $conn->respond($res);
     }
     return undef;
@@ -181,7 +200,7 @@ __PACKAGE__->register_method(
     api_name  => "open-ils.circ.holds.create",
     signature => {
         desc => "Create a new hold for an item.  From a permissions perspective, " .
-                "the login session is used as the 'requestor' of the hold.  "      . 
+                "the login session is used as the 'requestor' of the hold.  "      .
                 "The hold recipient is determined by the 'usr' setting within the hold object. " .
                 'First we verify the requestor has holds request permissions.  '         .
                 'Then we verify that the recipient is allowed to make the given hold.  ' .
@@ -223,12 +242,16 @@ __PACKAGE__->register_method(
 );
 
 sub create_hold {
-       my( $self, $conn, $auth, $hold ) = @_;
+    my( $self, $conn, $auth, $hold, $oargs ) = @_;
     return -1 unless $hold;
-       my $e = new_editor(authtoken=>$auth, xact=>1);
-       return $e->die_event unless $e->checkauth;
+    my $e = new_editor(authtoken=>$auth, xact=>1);
+    return $e->die_event unless $e->checkauth;
 
-       my $override = 1 if $self->api_name =~ /override/;
+    my $override = 0;
+    if ($self->api_name =~ /override/) {
+        $override = 1;
+        $oargs = { all => 1 } unless defined $oargs;
+    }
 
     my @events;
 
@@ -236,7 +259,7 @@ sub create_hold {
     my $recipient = $requestor;
 
     if( $requestor->id ne $hold->usr ) {
-        # Make sure the requestor is allowed to place holds for 
+        # Make sure the requestor is allowed to place holds for
         # the recipient if they are not the same people
         $recipient = $e->retrieve_actor_user($hold->usr)  or return $e->die_event;
         $e->allowed('REQUEST_HOLDS', $recipient->home_ou) or return $e->die_event;
@@ -261,16 +284,16 @@ sub create_hold {
 
     # See if a duplicate hold already exists
     my $sargs = {
-        usr                    => $recipient->id, 
-        hold_type      => $t, 
-        fulfillment_time => undef, 
-        target         => $hold->target,
-        cancel_time    => undef,
+        usr              => $recipient->id,
+        hold_type        => $t,
+        fulfillment_time => undef,
+        target           => $hold->target,
+        cancel_time      => undef,
     };
 
     $sargs->{holdable_formats} = $hold->holdable_formats if $t eq 'M';
-        
-    my $existing = $e->search_action_hold_request($sargs); 
+
+    my $existing = $e->search_action_hold_request($sargs);
     push( @events, OpenILS::Event->new('HOLD_EXISTS')) if @$existing;
 
     my $checked_out = hold_item_is_checked_out($e, $recipient->id, $hold->hold_type, $hold->target);
@@ -288,10 +311,14 @@ sub create_hold {
         return $e->die_event unless $e->allowed('ISSUANCE_HOLDS', $porg);
     } elsif ( $t eq OILS_HOLD_TYPE_COPY ) {
         return $e->die_event unless $e->allowed('COPY_HOLDS',   $porg);
-    } elsif ( $t eq OILS_HOLD_TYPE_FORCE ) {
-        return $e->die_event unless $e->allowed('COPY_HOLDS',   $porg);
-    } elsif ( $t eq OILS_HOLD_TYPE_RECALL ) {
-        return $e->die_event unless $e->allowed('COPY_HOLDS',   $porg);
+    } elsif ( $t eq OILS_HOLD_TYPE_FORCE || $t eq OILS_HOLD_TYPE_RECALL ) {
+        my $copy = $e->retrieve_asset_copy($hold->target)
+            or return $e->die_event;
+        if ( $t eq OILS_HOLD_TYPE_FORCE ) {
+            return $e->die_event unless $e->allowed('COPY_HOLDS_FORCE',   $copy->circ_lib);
+        } elsif ( $t eq OILS_HOLD_TYPE_RECALL ) {
+            return $e->die_event unless $e->allowed('COPY_HOLDS_RECALL',   $copy->circ_lib);
+        }
     }
 
     if( @events ) {
@@ -302,63 +329,92 @@ sub create_hold {
         for my $evt (@events) {
             next unless $evt;
             my $name = $evt->{textcode};
-            return $e->die_event unless $e->allowed("$name.override", $porg);
+            if($oargs->{all} || grep { $_ eq $name } @{$oargs->{events}}) {
+                return $e->die_event unless $e->allowed("$name.override", $porg);
+            } else {
+                $e->rollback;
+                return \@events;
+            }
         }
     }
 
+        # Check for hold expiration in the past, and set it to empty string.
+        $hold->expire_time(undef) if ($hold->expire_time && $U->datecmp($hold->expire_time) == -1);
+
     # set the configured expire time
     unless($hold->expire_time) {
-        my $interval = $U->ou_ancestor_setting_value($recipient->home_ou, OILS_SETTING_HOLD_EXPIRE);
-        if($interval) {
-            my $date = DateTime->now->add(seconds => OpenSRF::Utils::interval_to_seconds($interval));
-            $hold->expire_time($U->epoch2ISO8601($date->epoch));
+        $hold->expire_time(calculate_expire_time($recipient->home_ou));
+    }
+
+
+    # if behind-the-desk pickup is supported at the hold pickup lib,
+    # set the value to the patron default, unless a value has already
+    # been applied.  If it's not supported, force the value to false.
+
+    my $bdous = $U->ou_ancestor_setting_value(
+        $hold->pickup_lib, 
+        'circ.holds.behind_desk_pickup_supported', $e);
+
+    if ($bdous) {
+        if (!defined $hold->behind_desk) {
+
+            my $set = $e->search_actor_user_setting({
+                usr => $hold->usr, 
+                name => 'circ.holds_behind_desk'
+            })->[0];
+        
+            $hold->behind_desk('t') if $set and 
+                OpenSRF::Utils::JSON->JSON2perl($set->value);
         }
+    } else {
+        # behind the desk not supported, force it to false
+        $hold->behind_desk('f');
     }
 
-    $hold->requestor($e->requestor->id); 
+    $hold->requestor($e->requestor->id);
     $hold->request_lib($e->requestor->ws_ou);
     $hold->selection_ou($hold->pickup_lib) unless $hold->selection_ou;
     $hold = $e->create_action_hold_request($hold) or return $e->die_event;
 
-       $e->commit;
+    $e->commit;
 
-       $conn->respond_complete($hold->id);
+    $conn->respond_complete($hold->id);
 
     $U->storagereq(
-        'open-ils.storage.action.hold_request.copy_targeter', 
+        'open-ils.storage.action.hold_request.copy_targeter',
         undef, $hold->id ) unless $U->is_true($hold->frozen);
 
-       return undef;
+    return undef;
 }
 
 # makes sure that a user has permission to place the type of requested hold
 # returns the Perm exception if not allowed, returns undef if all is well
 sub _check_holds_perm {
-       my($type, $user_id, $org_id) = @_;
-
-       my $evt;
-       if ($type eq "M") {
-               $evt = $apputils->check_perms($user_id, $org_id, "MR_HOLDS"    );
-       } elsif ($type eq "T") {
-               $evt = $apputils->check_perms($user_id, $org_id, "TITLE_HOLDS" );
-       } elsif($type eq "V") {
-               $evt = $apputils->check_perms($user_id, $org_id, "VOLUME_HOLDS");
-       } elsif($type eq "C") {
-               $evt = $apputils->check_perms($user_id, $org_id, "COPY_HOLDS"  );
-       }
+    my($type, $user_id, $org_id) = @_;
+
+    my $evt;
+    if ($type eq "M") {
+        $evt = $apputils->check_perms($user_id, $org_id, "MR_HOLDS"    );
+    } elsif ($type eq "T") {
+        $evt = $apputils->check_perms($user_id, $org_id, "TITLE_HOLDS" );
+    } elsif($type eq "V") {
+        $evt = $apputils->check_perms($user_id, $org_id, "VOLUME_HOLDS");
+    } elsif($type eq "C") {
+        $evt = $apputils->check_perms($user_id, $org_id, "COPY_HOLDS"  );
+    }
 
     return $evt if $evt;
-       return undef;
+    return undef;
 }
 
 # tests if the given user is allowed to place holds on another's behalf
 sub _check_request_holds_perm {
-       my $user_id = shift;
-       my $org_id  = shift;
-       if (my $evt = $apputils->check_perms(
-               $user_id, $org_id, "REQUEST_HOLDS")) {
-               return $evt;
-       }
+    my $user_id = shift;
+    my $org_id  = shift;
+    if (my $evt = $apputils->check_perms(
+        $user_id, $org_id, "REQUEST_HOLDS")) {
+        return $evt;
+    }
 }
 
 my $ses_is_req_note = 'The login session is the requestor.  If the requestor is different from the user, ' .
@@ -381,25 +437,25 @@ __PACKAGE__->register_method(
 
 
 sub retrieve_holds_by_id {
-       my($self, $client, $auth, $hold_id) = @_;
-       my $e = new_editor(authtoken=>$auth);
-       $e->checkauth or return $e->event;
-       $e->allowed('VIEW_HOLD') or return $e->event;
-
-       my $holds = $e->search_action_hold_request(
-               [
-                       { id =>  $hold_id , fulfillment_time => undef }, 
-                       { 
+    my($self, $client, $auth, $hold_id) = @_;
+    my $e = new_editor(authtoken=>$auth);
+    $e->checkauth or return $e->event;
+    $e->allowed('VIEW_HOLD') or return $e->event;
+
+    my $holds = $e->search_action_hold_request(
+        [
+            { id =>  $hold_id , fulfillment_time => undef },
+            {
                 order_by => { ahr => "request_time" },
                 flesh => 1,
                 flesh_fields => {ahr => ['notes']}
             }
-               ]
-       );
+        ]
+    );
 
-       flesh_hold_transits($holds);
-       flesh_hold_notices($holds, $e);
-       return $holds;
+    flesh_hold_transits($holds);
+    flesh_hold_notices($holds, $e);
+    return $holds;
 }
 
 
@@ -410,7 +466,8 @@ __PACKAGE__->register_method(
         desc   => "Retrieves all the holds, with hold transits attached, for the specified user.  $ses_is_req_note",
         params => [
             { desc => 'Authentication token', type => 'string'  },
-            { desc => 'User ID',              type => 'integer' }
+            { desc => 'User ID',              type => 'integer' },
+            { desc => 'Available Only',       type => 'boolean' }
         ],
         return => {
             desc => 'list of holds, event on error',
@@ -426,7 +483,8 @@ __PACKAGE__->register_method(
         desc   => "Retrieves all the hold IDs, for the specified user.  $ses_is_req_note",
         params => [
             { desc => 'Authentication token', type => 'string'  },
-            { desc => 'User ID',              type => 'integer' }
+            { desc => 'User ID',              type => 'integer' },
+            { desc => 'Available Only',       type => 'boolean' }
         ],
         return => {
             desc => 'list of holds, event on error',
@@ -468,7 +526,7 @@ __PACKAGE__->register_method(
 
 
 sub retrieve_holds {
-    my ($self, $client, $auth, $user_id) = @_;
+    my ($self, $client, $auth, $user_id, $available) = @_;
 
     my $e = new_editor(authtoken=>$auth);
     return $e->event unless $e->checkauth;
@@ -491,7 +549,7 @@ sub retrieve_holds {
 
     my $holds_query = {
         select => {ahr => ['id']},
-        from => 'ahr', 
+        from => 'ahr',
         where => {usr => $user_id, fulfillment_time => undef}
     };
 
@@ -530,8 +588,23 @@ sub retrieve_holds {
     } else {
 
         # order non-cancelled holds by ready-for-pickup, then active, followed by suspended
-        $holds_query->{order_by} = {ahr => ['shelf_time', 'frozen', 'request_time']};
+        # "compare" sorts false values to the front.  testing pickup_lib != current_shelf_lib
+        # will sort by pl = csl > pl != csl > followed by csl is null;
+        $holds_query->{order_by} = [
+            {   class => 'ahr',
+                field => 'pickup_lib',
+                compare => {'!='  => {'+ahr' => 'current_shelf_lib'}}},
+            {class => 'ahr', field => 'shelf_time'},
+            {class => 'ahr', field => 'frozen'},
+            {class => 'ahr', field => 'request_time'}
+
+        ];
         $holds_query->{where}->{cancel_time} = undef;
+        if($available) {
+            $holds_query->{where}->{shelf_time} = {'!=' => undef};
+            # Maybe?
+            $holds_query->{where}->{pickup_lib} = {'=' => {'+ahr' => 'current_shelf_lib'}};
+        }
     }
 
     my $hold_ids = $e->json_query($holds_query);
@@ -591,7 +664,7 @@ sub __user_hold_count {
 __PACKAGE__->register_method(
     method   => "retrieve_holds_by_pickup_lib",
     api_name => "open-ils.circ.holds.retrieve_by_pickup_lib",
-    notes    => 
+    notes    =>
       "Retrieves all the holds, with hold transits attached, for the specified pickup_ou id."
 );
 
@@ -606,18 +679,18 @@ sub retrieve_holds_by_pickup_lib {
 
     #FIXME -- put an appropriate permission check here
     #my( $user, $target, $evt ) = $apputils->checkses_requestor(
-    #  $login_session, $user_id, 'VIEW_HOLD' );
+    #    $login_session, $user_id, 'VIEW_HOLD' );
     #return $evt if $evt;
 
-       my $holds = $apputils->simplereq(
-               'open-ils.cstore',
-               "open-ils.cstore.direct.action.hold_request.search.atomic",
-               { 
-                       pickup_lib =>  $ou_id , 
-                       fulfillment_time => undef,
-                       cancel_time => undef
-               }, 
-               { order_by => { ahr => "request_time" } }
+    my $holds = $apputils->simplereq(
+        'open-ils.cstore',
+        "open-ils.cstore.direct.action.hold_request.search.atomic",
+        {
+            pickup_lib =>  $ou_id ,
+            fulfillment_time => undef,
+            cancel_time => undef
+        },
+        { order_by => { ahr => "request_time" } }
     );
 
     if ( ! $self->api_name =~ /id_list/ ) {
@@ -635,12 +708,12 @@ __PACKAGE__->register_method(
 );
 
 sub uncancel_hold {
-       my($self, $client, $auth, $hold_id) = @_;
-       my $e = new_editor(authtoken=>$auth, xact=>1);
-       return $e->die_event unless $e->checkauth;
+    my($self, $client, $auth, $hold_id) = @_;
+    my $e = new_editor(authtoken=>$auth, xact=>1);
+    return $e->die_event unless $e->checkauth;
 
-       my $hold = $e->retrieve_action_hold_request($hold_id)
-               or return $e->die_event;
+    my $hold = $e->retrieve_action_hold_request($hold_id)
+        or return $e->die_event;
     return $e->die_event unless $e->allowed('CANCEL_HOLDS', $hold->request_lib);
 
     if ($hold->fulfillment_time) {
@@ -657,11 +730,7 @@ sub uncancel_hold {
         $hold->request_lib, 'circ.holds.uncancel.reset_request_time', $e)) {
 
         $hold->request_time('now');
-        my $interval = $U->ou_ancestor_setting_value($hold->request_lib, OILS_SETTING_HOLD_EXPIRE);
-        if($interval) {
-            my $date = DateTime->now->add(seconds => OpenSRF::Utils::interval_to_seconds($interval));
-            $hold->expire_time($U->epoch2ISO8601($date->epoch));
-        }
+        $hold->expire_time(calculate_expire_time($hold->request_lib));
     }
 
     $hold->clear_cancel_time;
@@ -672,6 +741,7 @@ sub uncancel_hold {
     $hold->clear_capture_time;
     $hold->clear_prev_check_time;
     $hold->clear_shelf_expire_time;
+    $hold->clear_current_shelf_lib;
 
     $e->update_action_hold_request($hold) or return $e->die_event;
     $e->commit;
@@ -701,85 +771,77 @@ __PACKAGE__->register_method(
 );
 
 sub cancel_hold {
-       my($self, $client, $auth, $holdid, $cause, $note) = @_;
+    my($self, $client, $auth, $holdid, $cause, $note) = @_;
 
-       my $e = new_editor(authtoken=>$auth, xact=>1);
-       return $e->die_event unless $e->checkauth;
+    my $e = new_editor(authtoken=>$auth, xact=>1);
+    return $e->die_event unless $e->checkauth;
 
-       my $hold = $e->retrieve_action_hold_request($holdid)
-               or return $e->die_event;
+    my $hold = $e->retrieve_action_hold_request($holdid)
+        or return $e->die_event;
 
-       if( $e->requestor->id ne $hold->usr ) {
-               return $e->die_event unless $e->allowed('CANCEL_HOLDS');
-       }
+    if( $e->requestor->id ne $hold->usr ) {
+        return $e->die_event unless $e->allowed('CANCEL_HOLDS');
+    }
 
-       if ($hold->cancel_time) {
+    if ($hold->cancel_time) {
         $e->rollback;
         return 1;
     }
 
-       # If the hold is captured, reset the copy status
-       if( $hold->capture_time and $hold->current_copy ) {
+    # If the hold is captured, reset the copy status
+    if( $hold->capture_time and $hold->current_copy ) {
 
-               my $copy = $e->retrieve_asset_copy($hold->current_copy)
-                       or return $e->die_event;
+        my $copy = $e->retrieve_asset_copy($hold->current_copy)
+            or return $e->die_event;
 
-               if( $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
+        if( $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
          $logger->info("canceling hold $holdid whose item is on the holds shelf");
-#                      $logger->info("setting copy to status 'reshelving' on hold cancel");
-#                      $copy->status(OILS_COPY_STATUS_RESHELVING);
-#                      $copy->editor($e->requestor->id);
-#                      $copy->edit_date('now');
-#                      $e->update_asset_copy($copy) or return $e->event;
-
-               } elsif( $copy->status == OILS_COPY_STATUS_IN_TRANSIT ) {
-
-                       my $hid = $hold->id;
-                       $logger->warn("! canceling hold [$hid] that is in transit");
-                       my $transid = $e->search_action_hold_transit_copy({hold=>$hold->id},{idlist=>1})->[0];
-
-                       if( $transid ) {
-                               my $trans = $e->retrieve_action_transit_copy($transid);
-                               # Leave the transit alive, but  set the copy status to 
-                               # reshelving so it will be properly reshelved when it gets back home
-                               if( $trans ) {
-                                       $trans->copy_status( OILS_COPY_STATUS_RESHELVING );
-                                       $e->update_action_transit_copy($trans) or return $e->die_event;
-                               }
-                       }
-               }
-       }
-
-       $hold->cancel_time('now');
+#            $logger->info("setting copy to status 'reshelving' on hold cancel");
+#            $copy->status(OILS_COPY_STATUS_RESHELVING);
+#            $copy->editor($e->requestor->id);
+#            $copy->edit_date('now');
+#            $e->update_asset_copy($copy) or return $e->event;
+
+        } elsif( $copy->status == OILS_COPY_STATUS_IN_TRANSIT ) {
+
+            my $hid = $hold->id;
+            $logger->warn("! canceling hold [$hid] that is in transit");
+            my $transid = $e->search_action_hold_transit_copy({hold=>$hold->id},{idlist=>1})->[0];
+
+            if( $transid ) {
+                my $trans = $e->retrieve_action_transit_copy($transid);
+                # Leave the transit alive, but  set the copy status to
+                # reshelving so it will be properly reshelved when it gets back home
+                if( $trans ) {
+                    $trans->copy_status( OILS_COPY_STATUS_RESHELVING );
+                    $e->update_action_transit_copy($trans) or return $e->die_event;
+                }
+            }
+        }
+    }
+
+    $hold->cancel_time('now');
     $hold->cancel_cause($cause);
     $hold->cancel_note($note);
-       $e->update_action_hold_request($hold)
-               or return $e->die_event;
-
-       delete_hold_copy_maps($self, $e, $hold->id);
-
-       $e->commit;
+    $e->update_action_hold_request($hold)
+        or return $e->die_event;
 
-    $U->create_events_for_hook('hold_request.cancel.staff', $hold, $hold->pickup_lib)
-        if $e->requestor->id != $hold->usr;
+    $e->commit;
 
-       return 1;
-}
+    # re-fetch the hold to pick up the real cancel_time (not "now") for A/T
+    $e->xact_begin;
+    $hold = $e->retrieve_action_hold_request($hold->id) or return $e->die_event;
+    $e->rollback;
 
-sub delete_hold_copy_maps {
-       my $class  = shift;
-       my $editor = shift;
-       my $holdid = shift;
+    if ($e->requestor->id == $hold->usr) {
+        $U->create_events_for_hook('hold_request.cancel.patron', $hold, $hold->pickup_lib);
+    } else {
+        $U->create_events_for_hook('hold_request.cancel.staff', $hold, $hold->pickup_lib);
+    }
 
-       my $maps = $editor->search_action_hold_copy_map({hold=>$holdid});
-       for(@$maps) {
-               $editor->delete_action_hold_copy_map($_) 
-                       or return $editor->event;
-       }
-       return undef;
+    return 1;
 }
 
-
 my $update_hold_desc = 'The login session is the requestor. '       .
    'If the requestor is different from the usr field on the hold, ' .
    'the requestor must have UPDATE_HOLDS permissions. '             .
@@ -822,7 +884,7 @@ __PACKAGE__->register_method(
 );
 
 sub update_hold {
-       my($self, $client, $auth, $hold, $values) = @_;
+    my($self, $client, $auth, $hold, $values) = @_;
     my $e = new_editor(authtoken=>$auth, xact=>1);
     return $e->die_event unless $e->checkauth;
     my $resp = update_hold_impl($self, $e, $hold, $values);
@@ -835,7 +897,7 @@ sub update_hold {
 }
 
 sub batch_update_hold {
-       my($self, $client, $auth, $hold_list, $values_list) = @_;
+    my($self, $client, $auth, $hold_list, $values_list) = @_;
     my $e = new_editor(authtoken=>$auth);
     return $e->die_event unless $e->checkauth;
 
@@ -859,11 +921,24 @@ sub batch_update_hold {
 
 sub update_hold_impl {
     my($self, $e, $hold, $values) = @_;
+    my $hold_status;
+    my $need_retarget = 0;
 
     unless($hold) {
         $hold = $e->retrieve_action_hold_request($values->{id})
             or return $e->die_event;
         for my $k (keys %$values) {
+            # Outside of pickup_lib (covered by the first regex) I don't know when these would currently change.
+            # But hey, why not cover things that may happen later?
+            if ($k =~ '_(lib|ou)$' || $k eq 'target' || $k eq 'hold_type' || $k eq 'requestor' || $k eq 'selection_depth' || $k eq 'holdable_formats') {
+                if (defined $values->{$k} && defined $hold->$k() && $values->{$k} ne $hold->$k()) {
+                    # Value changed? RETARGET!
+                    $need_retarget = 1;
+                } elsif (defined $hold->$k() != defined $values->{$k}) {
+                    # Value being set or cleared? RETARGET!
+                    $need_retarget = 1;
+                }
+            }
             if (defined $values->{$k}) {
                 $hold->$k($values->{$k});
             } else {
@@ -879,7 +954,7 @@ sub update_hold_impl {
     return OpenILS::Event->new('BAD_PARAMS') if $hold->usr != $orig_hold->usr;
 
     if($hold->usr ne $e->requestor->id) {
-        # if the hold is for a different user, make sure the 
+        # if the hold is for a different user, make sure the
         # requestor has the appropriate permissions
         my $usr = $e->retrieve_actor_user($hold->usr)
             or return $e->die_event;
@@ -894,26 +969,39 @@ sub update_hold_impl {
         return OpenILS::Event->new('BAD_PARAMS') if $hold->fulfillment_time;
         return $e->die_event unless $e->allowed('UPDATE_HOLD_REQUEST_TIME', $hold->pickup_lib);
     }
-    
-       
-       # --------------------------------------------------------------
-       # Code for making sure staff have appropriate permissons for cut_in_line
-       # This, as is, doesn't prevent a user from cutting their own holds in line 
-       # but needs to
-       # --------------------------------------------------------------        
-       if($U->is_true($hold->cut_in_line) ne $U->is_true($orig_hold->cut_in_line)) {
-               return $e->die_event unless $e->allowed('UPDATE_HOLD_REQUEST_TIME', $hold->pickup_lib);
-       }
+
+
+    # --------------------------------------------------------------
+    # Code for making sure staff have appropriate permissons for cut_in_line
+    # This, as is, doesn't prevent a user from cutting their own holds in line
+    # but needs to
+    # --------------------------------------------------------------
+    if($U->is_true($hold->cut_in_line) ne $U->is_true($orig_hold->cut_in_line)) {
+        return $e->die_event unless $e->allowed('UPDATE_HOLD_REQUEST_TIME', $hold->pickup_lib);
+    }
+
+
+    # --------------------------------------------------------------
+    # Disallow hold suspencion if the hold is already captured.
+    # --------------------------------------------------------------
+    if ($U->is_true($hold->frozen) and not $U->is_true($orig_hold->frozen)) {
+        $hold_status = _hold_status($e, $hold);
+        if ($hold_status > 2 && $hold_status != 7) { # hold is captured
+            $logger->info("bypassing hold freeze on captured hold");
+            return OpenILS::Event->new('HOLD_SUSPEND_AFTER_CAPTURE');
+        }
+    }
+
 
     # --------------------------------------------------------------
-    # if the hold is on the holds shelf or in transit and the pickup 
+    # if the hold is on the holds shelf or in transit and the pickup
     # lib changes we need to create a new transit.
     # --------------------------------------------------------------
     if($orig_hold->pickup_lib ne $hold->pickup_lib) {
 
-        my $status = _hold_status($e, $hold);
+        $hold_status = _hold_status($e, $hold) unless $hold_status;
 
-        if($status == 3) { # in transit
+        if($hold_status == 3) { # in transit
 
             return $e->die_event unless $e->allowed('UPDATE_PICKUP_LIB_FROM_TRANSIT', $orig_hold->pickup_lib);
             return $e->die_event unless $e->allowed('UPDATE_PICKUP_LIB_FROM_TRANSIT', $hold->pickup_lib);
@@ -921,39 +1009,118 @@ sub update_hold_impl {
             $logger->info("updating pickup lib for hold ".$hold->id." while already in transit");
 
             # update the transit to reflect the new pickup location
-                       my $transit = $e->search_action_hold_transit_copy(
-                {hold=>$hold->id, dest_recv_time => undef})->[0] 
+            my $transit = $e->search_action_hold_transit_copy(
+                {hold=>$hold->id, dest_recv_time => undef})->[0]
                 or return $e->die_event;
 
             $transit->prev_dest($transit->dest); # mark the previous destination on the transit
             $transit->dest($hold->pickup_lib);
             $e->update_action_hold_transit_copy($transit) or return $e->die_event;
 
-        } elsif($status == 4) { # on holds shelf
+        } elsif($hold_status == 4 or $hold_status == 5 or $hold_status == 8) { # on holds shelf
 
             return $e->die_event unless $e->allowed('UPDATE_PICKUP_LIB_FROM_HOLDS_SHELF', $orig_hold->pickup_lib);
             return $e->die_event unless $e->allowed('UPDATE_PICKUP_LIB_FROM_HOLDS_SHELF', $hold->pickup_lib);
 
             $logger->info("updating pickup lib for hold ".$hold->id." while on holds shelf");
 
-            # create the new transit
-            my $evt = transit_hold($e, $orig_hold, $hold, $e->retrieve_asset_copy($hold->current_copy));
-            return $evt if $evt;
+            if ($hold->pickup_lib eq $orig_hold->current_shelf_lib) {
+                # This can happen if the pickup lib is changed while the hold is
+                # on the shelf, then changed back to the original pickup lib.
+                # Restore the original shelf_expire_time to prevent abuse.
+                set_hold_shelf_expire_time(undef, $hold, $e, $hold->shelf_time);
+
+            } else {
+                # clear to prevent premature shelf expiration
+                $hold->clear_shelf_expire_time;
+            }
         }
-    } 
+    }
+
+    if($U->is_true($hold->frozen)) {
+        $logger->info("clearing current_copy and check_time for frozen hold ".$hold->id);
+        $hold->clear_current_copy;
+        $hold->clear_prev_check_time;
+        # Clear expire_time to prevent frozen holds from expiring.
+        $logger->info("clearing expire_time for frozen hold ".$hold->id);
+        $hold->clear_expire_time;
+    }
+
+    # If the hold_expire_time is in the past && is not equal to the
+    # original expire_time, then reset the expire time to be in the
+    # future.
+    if ($hold->expire_time && $U->datecmp($hold->expire_time) == -1 && $U->datecmp($hold->expire_time, $orig_hold->expire_time) != 0) {
+        $hold->expire_time(calculate_expire_time($hold->request_lib));
+    }
+
+    # If the hold is reactivated, reset the expire_time.
+    if(!$U->is_true($hold->frozen) && $U->is_true($orig_hold->frozen)) {
+        $logger->info("Reset expire_time on activated hold ".$hold->id);
+        $hold->expire_time(calculate_expire_time($hold->request_lib));
+    }
 
-    update_hold_if_frozen($self, $e, $hold, $orig_hold);
     $e->update_action_hold_request($hold) or return $e->die_event;
     $e->commit;
 
+    if(!$U->is_true($hold->frozen) && $U->is_true($orig_hold->frozen)) {
+        $logger->info("Running targeter on activated hold ".$hold->id);
+        $U->storagereq( 'open-ils.storage.action.hold_request.copy_targeter', undef, $hold->id );
+    }
+
     # a change to mint-condition changes the set of potential copies, so retarget the hold;
     if($U->is_true($hold->mint_condition) and !$U->is_true($orig_hold->mint_condition)) {
-        _reset_hold($self, $e->requestor, $hold) 
+        _reset_hold($self, $e->requestor, $hold)
+    } elsif($need_retarget && !defined $hold->capture_time()) { # If needed, retarget the hold due to changes
+        $U->storagereq(
+            'open-ils.storage.action.hold_request.copy_targeter', undef, $hold->id );
     }
 
     return $hold->id;
 }
 
+# this does not update the hold in the DB.  It only
+# sets the shelf_expire_time field on the hold object.
+# start_time is optional and defaults to 'now'
+sub set_hold_shelf_expire_time {
+    my ($class, $hold, $editor, $start_time) = @_;
+
+    my $shelf_expire = $U->ou_ancestor_setting_value(
+        $hold->pickup_lib,
+        'circ.holds.default_shelf_expire_interval',
+        $editor
+    );
+
+    return undef unless $shelf_expire;
+
+    $start_time = ($start_time) ?
+        DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($start_time)) :
+        DateTime->now(time_zone => 'local'); # without time_zone we get UTC ... yuck!
+
+    my $seconds = OpenSRF::Utils->interval_to_seconds($shelf_expire);
+    my $expire_time = $start_time->add(seconds => $seconds);
+
+    # if the shelf expire time overlaps with a pickup lib's
+    # closed date, push it out to the first open date
+    my $dateinfo = $U->storagereq(
+        'open-ils.storage.actor.org_unit.closed_date.overlap',
+        $hold->pickup_lib, $expire_time->strftime('%FT%T%z'));
+
+    if($dateinfo) {
+        my $dt_parser = DateTime::Format::ISO8601->new;
+        $expire_time = $dt_parser->parse_datetime(cleanse_ISO8601($dateinfo->{end}));
+
+        # TODO: enable/disable time bump via setting?
+        $expire_time->set(hour => '23', minute => '59', second => '59');
+
+        $logger->info("circulator: shelf_expire_time overlaps".
+            " with closed date, pushing expire time to $expire_time");
+    }
+
+    $hold->shelf_expire_time($expire_time->strftime('%FT%T%z'));
+    return undef;
+}
+
+
 sub transit_hold {
     my($e, $orig_hold, $hold, $copy) = @_;
     my $src  = $orig_hold->pickup_lib;
@@ -978,8 +1145,8 @@ sub transit_hold {
     return undef;
 }
 
-# if the hold is frozen, this method ensures that the hold is not "targeted", 
-# that is, it clears the current_copy and prev_check_time to essentiallly 
+# if the hold is frozen, this method ensures that the hold is not "targeted",
+# that is, it clears the current_copy and prev_check_time to essentiallly
 # reset the hold.  If it is being activated, it runs the targeter in the background
 sub update_hold_if_frozen {
     my($self, $e, $hold, $orig_hold) = @_;
@@ -1015,7 +1182,7 @@ __PACKAGE__->register_method(
 );
 
 sub hold_note_CUD {
-       my($self, $conn, $auth, $note) = @_;
+    my($self, $conn, $auth, $note) = @_;
 
     my $e = new_editor(authtoken => $auth, xact => 1);
     return $e->die_event unless $e->checkauth;
@@ -1062,50 +1229,58 @@ Returns event on error or:
  4 for 'arrived',
  5 for 'hold-shelf-delay'
  6 for 'canceled'
+ 7 for 'suspended'
+ 8 for 'captured, on wrong hold shelf'
 END_OF_DESC
         }
     }
 );
 
 sub retrieve_hold_status {
-       my($self, $client, $auth, $hold_id) = @_;
+    my($self, $client, $auth, $hold_id) = @_;
 
-       my $e = new_editor(authtoken => $auth);
-       return $e->event unless $e->checkauth;
-       my $hold = $e->retrieve_action_hold_request($hold_id)
-               or return $e->event;
+    my $e = new_editor(authtoken => $auth);
+    return $e->event unless $e->checkauth;
+    my $hold = $e->retrieve_action_hold_request($hold_id)
+        or return $e->event;
 
-       if( $e->requestor->id != $hold->usr ) {
-               return $e->event unless $e->allowed('VIEW_HOLD');
-       }
+    if( $e->requestor->id != $hold->usr ) {
+        return $e->event unless $e->allowed('VIEW_HOLD');
+    }
 
-       return _hold_status($e, $hold);
+    return _hold_status($e, $hold);
 
 }
 
 sub _hold_status {
-       my($e, $hold) = @_;
+    my($e, $hold) = @_;
     if ($hold->cancel_time) {
         return 6;
     }
-       return 1 unless $hold->current_copy;
-       return 2 unless $hold->capture_time;
+    if ($U->is_true($hold->frozen) && !$hold->capture_time) {
+        return 7;
+    }
+    if ($hold->current_shelf_lib and $hold->current_shelf_lib ne $hold->pickup_lib) {
+        return 8;
+    }
+    return 1 unless $hold->current_copy;
+    return 2 unless $hold->capture_time;
 
-       my $copy = $hold->current_copy;
-       unless( ref $copy ) {
-               $copy = $e->retrieve_asset_copy($hold->current_copy)
-                       or return $e->event;
-       }
+    my $copy = $hold->current_copy;
+    unless( ref $copy ) {
+        $copy = $e->retrieve_asset_copy($hold->current_copy)
+            or return $e->event;
+    }
 
-       return 3 if $copy->status == OILS_COPY_STATUS_IN_TRANSIT;
+    return 3 if $copy->status == OILS_COPY_STATUS_IN_TRANSIT;
 
-       if($copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF) {
+    if($copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF) {
 
         my $hs_wait_interval = $U->ou_ancestor_setting_value($hold->pickup_lib, 'circ.hold_shelf_status_delay');
         return 4 unless $hs_wait_interval;
 
-        # if a hold_shelf_status_delay interval is defined and start_time plus 
-        # the interval is greater than now, consider the hold to be in the virtual 
+        # if a hold_shelf_status_delay interval is defined and start_time plus
+        # the interval is greater than now, consider the hold to be in the virtual
         # "on its way to the holds shelf" status. Return 5.
 
         my $transit    = $e->search_action_hold_transit_copy({hold => $hold->id})->[0];
@@ -1132,12 +1307,12 @@ __PACKAGE__->register_method(
             { desc => 'Hold ID', type => 'number'},
         ],
         return => {
-            desc => q/Summary object with keys: 
+            desc => q/Summary object with keys:
                 total_holds : total holds in queue
                 queue_position : current queue position
                 potential_copies : number of potential copies for this hold
                 estimated_wait : estimated wait time in days
-                status : hold status  
+                status : hold status
                      -1 => error or unexpected state,
                      1 => 'waiting for copy to become available',
                      2 => 'waiting for copy capture',
@@ -1152,12 +1327,12 @@ __PACKAGE__->register_method(
 
 sub retrieve_hold_queue_stats {
     my($self, $conn, $auth, $hold_id) = @_;
-       my $e = new_editor(authtoken => $auth);
-       return $e->event unless $e->checkauth;
-       my $hold = $e->retrieve_action_hold_request($hold_id) or return $e->event;
-       if($e->requestor->id != $hold->usr) {
-               return $e->event unless $e->allowed('VIEW_HOLD');
-       }
+    my $e = new_editor(authtoken => $auth);
+    return $e->event unless $e->checkauth;
+    my $hold = $e->retrieve_action_hold_request($hold_id) or return $e->event;
+    if($e->requestor->id != $hold->usr) {
+        return $e->event unless $e->allowed('VIEW_HOLD');
+    }
     return retrieve_hold_queue_status_impl($e, $hold);
 }
 
@@ -1165,7 +1340,7 @@ sub retrieve_hold_queue_status_impl {
     my $e = shift;
     my $hold = shift;
 
-    # The holds queue is defined as the distinct set of holds that share at 
+    # The holds queue is defined as the distinct set of holds that share at
     # least one potential copy with the context hold, plus any holds that
     # share the same hold type and target.  The latter part exists to
     # accomodate holds that currently have no potential copies
@@ -1174,18 +1349,22 @@ sub retrieve_hold_queue_status_impl {
         # fetch cut_in_line and request_time since they're in the order_by
         # and we're asking for distinct values
         select => {ahr => ['id', 'cut_in_line', 'request_time']},
-        from   => {
-            ahr => {
-                'ahcm' => {
-                    join => {
+        from   => 'ahr',
+        where => {
+            id => { in => {
+                select => { ahcm => ['hold'] },
+                from   => {
+                    'ahcm' => {
                         'ahcm2' => {
                             'class' => 'ahcm',
                             'field' => 'target_copy',
                             'fkey'  => 'target_copy'
                         }
                     }
-                }
-            }
+                },
+                where => { '+ahcm2' => { hold => $hold->id } },
+                distinct => 1
+            }}
         },
         order_by => [
             {
@@ -1197,13 +1376,10 @@ sub retrieve_hold_queue_status_impl {
             },
             { "class" => "ahr", "field" => "request_time" }
         ],
-        distinct => 1,
-        where => {
-            '+ahcm2' => { hold => $hold->id }
-        }
+        distinct => 1
     });
 
-    if (!@$q_holds) { # none? maybe we don't have a map ... 
+    if (!@$q_holds) { # none? maybe we don't have a map ...
         $q_holds = $e->json_query({
             select => {ahr => ['id', 'cut_in_line', 'request_time']},
             from   => 'ahr',
@@ -1218,9 +1394,15 @@ sub retrieve_hold_queue_status_impl {
                 { "class" => "ahr", "field" => "request_time" }
             ],
             where    => {
-                hold_type => $hold->hold_type, 
-                target    => $hold->target 
-           } 
+                hold_type => $hold->hold_type,
+                target    => $hold->target,
+                capture_time => undef,
+                cancel_time => undef,
+                '-or' => [
+                    {expire_time => undef },
+                    {expire_time => {'>' => 'now'}}
+                ]
+           }
         });
     }
 
@@ -1235,7 +1417,7 @@ sub retrieve_hold_queue_status_impl {
         select => {
             acp => [ {column => 'id', transform => 'count', aggregate => 1, alias => 'count'} ],
             ccm => [ {column =>'avg_wait_time'} ]
-        }, 
+        },
         from => {
             ahcm => {
                 acp => {
@@ -1244,7 +1426,7 @@ sub retrieve_hold_queue_status_impl {
                     }
                 }
             }
-        }, 
+        },
         where => {'+ahcm' => {hold => $hold->id} }
     });
 
@@ -1255,16 +1437,16 @@ sub retrieve_hold_queue_status_impl {
     $min_wait = OpenSRF::Utils::interval_to_seconds($min_wait || '0 seconds');
     $default_wait ||= '0 seconds';
 
-    # Estimated wait time is the average wait time across the set 
+    # Estimated wait time is the average wait time across the set
     # of potential copies, divided by the number of potential copies
-    # times the queue position.  
+    # times the queue position.
 
     my $combined_secs = 0;
     my $num_potentials = 0;
 
     for my $wait_data (@$hold_data) {
         my $count += $wait_data->{count};
-        $combined_secs += $count * 
+        $combined_secs += $count *
             OpenSRF::Utils::interval_to_seconds($wait_data->{avg_wait_time} || $default_wait);
         $num_potentials += $count;
     }
@@ -1288,23 +1470,23 @@ sub retrieve_hold_queue_status_impl {
 
 
 sub fetch_open_hold_by_current_copy {
-       my $class = shift;
-       my $copyid = shift;
-       my $hold = $apputils->simplereq(
-               'open-ils.cstore', 
-               'open-ils.cstore.direct.action.hold_request.search.atomic',
-               { current_copy =>  $copyid , cancel_time => undef, fulfillment_time => undef });
-       return $hold->[0] if ref($hold);
-       return undef;
+    my $class = shift;
+    my $copyid = shift;
+    my $hold = $apputils->simplereq(
+        'open-ils.cstore',
+        'open-ils.cstore.direct.action.hold_request.search.atomic',
+        { current_copy =>  $copyid , cancel_time => undef, fulfillment_time => undef });
+    return $hold->[0] if ref($hold);
+    return undef;
 }
 
 sub fetch_related_holds {
-       my $class = shift;
-       my $copyid = shift;
-       return $apputils->simplereq(
-               'open-ils.cstore', 
-               'open-ils.cstore.direct.action.hold_request.search.atomic',
-               { current_copy =>  $copyid , cancel_time => undef, fulfillment_time => undef });
+    my $class = shift;
+    my $copyid = shift;
+    return $apputils->simplereq(
+        'open-ils.cstore',
+        'open-ils.cstore.direct.action.hold_request.search.atomic',
+        { current_copy =>  $copyid , cancel_time => undef, fulfillment_time => undef });
 }
 
 
@@ -1357,37 +1539,68 @@ __PACKAGE__->register_method(
     }
 );
 
+__PACKAGE__->register_method(
+    method    => "hold_pull_list",
+    stream => 1,
+    # TODO: tag with api_level 2 once fully supported
+    api_name  => "open-ils.circ.hold_pull_list.fleshed.stream",
+    signature => {
+        desc   => q/Returns a stream of fleshed holds  that need to be 
+                    "pulled" by a given location.  The location is 
+                    determined by the login session.  
+                    This API calls always run in authoritative mode./,
+        params => [
+            { desc => 'Limit (optional)',  type => 'number'},
+            { desc => 'Offset (optional)', type => 'number'},
+        ],
+        return => {
+            desc => 'Stream of holds holds, or event on failure',
+        }
+    }
+);
 
 sub hold_pull_list {
-       my( $self, $conn, $authtoken, $limit, $offset ) = @_;
-       my( $reqr, $evt ) = $U->checkses($authtoken);
-       return $evt if $evt;
+    my( $self, $conn, $authtoken, $limit, $offset ) = @_;
+    my( $reqr, $evt ) = $U->checkses($authtoken);
+    return $evt if $evt;
 
-       my $org = $reqr->ws_ou || $reqr->home_ou;
-       # the perm locaiton shouldn't really matter here since holds
-       # will exist all over and VIEW_HOLDS should be universal
-       $evt = $U->check_perms($reqr->id, $org, 'VIEW_HOLD');
-       return $evt if $evt;
+    my $org = $reqr->ws_ou || $reqr->home_ou;
+    # the perm locaiton shouldn't really matter here since holds
+    # will exist all over and VIEW_HOLDS should be universal
+    $evt = $U->check_perms($reqr->id, $org, 'VIEW_HOLD');
+    return $evt if $evt;
 
     if($self->api_name =~ /count/) {
 
-               my $count = $U->storagereq(
-                       'open-ils.storage.direct.action.hold_request.pull_list.current_copy_circ_lib.status_filtered.count',
-                       $org, $limit, $offset ); 
+        my $count = $U->storagereq(
+            'open-ils.storage.direct.action.hold_request.pull_list.current_copy_circ_lib.status_filtered.count',
+            $org, $limit, $offset );
 
         $logger->info("Grabbing pull list for org unit $org with $count items");
         return $count;
 
     } elsif( $self->api_name =~ /id_list/ ) {
-               return $U->storagereq(
-                       'open-ils.storage.direct.action.hold_request.pull_list.id_list.current_copy_circ_lib.status_filtered.atomic',
-                       $org, $limit, $offset ); 
+        $U->storagereq(
+            'open-ils.storage.direct.action.hold_request.pull_list.id_list.current_copy_circ_lib.status_filtered.atomic',
+            $org, $limit, $offset );
+
+    } elsif ($self->api_name =~ /fleshed/) {
+
+        my $ids = $U->storagereq(
+            'open-ils.storage.direct.action.hold_request.pull_list.id_list.current_copy_circ_lib.status_filtered.atomic',
+            $org, $limit, $offset );
+
+        my $e = new_editor(xact => 1, requestor => $reqr);
+        $conn->respond(uber_hold_impl($e, $_, {flesh_acpl => 1})) for @$ids;
+        $e->rollback;
+        $conn->respond_complete;
+        return;
 
-       } else {
-               return $U->storagereq(
-                       'open-ils.storage.direct.action.hold_request.pull_list.search.current_copy_circ_lib.status_filtered.atomic',
-                       $org, $limit, $offset ); 
-       }
+    } else {
+        $U->storagereq(
+            'open-ils.storage.direct.action.hold_request.pull_list.search.current_copy_circ_lib.status_filtered.atomic',
+            $org, $limit, $offset );
+    }
 }
 
 __PACKAGE__->register_method(
@@ -1501,7 +1714,7 @@ sub print_hold_pull_list_stream {
             "select" => {"ahr" => ["id"]},
             "from" => {
                 "ahr" => {
-                    "acp" => { 
+                    "acp" => {
                         "field" => "id",
                         "fkey" => "current_copy",
                         "filter" => {
@@ -1524,7 +1737,7 @@ sub print_hold_pull_list_stream {
                             },
                             "acplo" => {
                                 "field" => "org",
-                                "fkey" => "circ_lib", 
+                                "fkey" => "circ_lib",
                                 "type" => "left",
                                 "filter" => {
                                     "location" => {"=" => {"+acp" => "location"}}
@@ -1582,7 +1795,7 @@ __PACKAGE__->register_method(
     method        => 'fetch_hold_notify',
     api_name      => 'open-ils.circ.hold_notification.retrieve_by_hold',
     authoritative => 1,
-    signature     => q/ 
+    signature     => q/
 Returns a list of hold notification objects based on hold id.
 @param authtoken The loggin session key
 @param holdid The id of the hold whose notifications we want to retrieve
@@ -1591,21 +1804,21 @@ Returns a list of hold notification objects based on hold id.
 );
 
 sub fetch_hold_notify {
-       my( $self, $conn, $authtoken, $holdid ) = @_;
-       my( $requestor, $evt ) = $U->checkses($authtoken);
-       return $evt if $evt;
-       my ($hold, $patron);
-       ($hold, $evt) = $U->fetch_hold($holdid);
-       return $evt if $evt;
-       ($patron, $evt) = $U->fetch_user($hold->usr);
-       return $evt if $evt;
+    my( $self, $conn, $authtoken, $holdid ) = @_;
+    my( $requestor, $evt ) = $U->checkses($authtoken);
+    return $evt if $evt;
+    my ($hold, $patron);
+    ($hold, $evt) = $U->fetch_hold($holdid);
+    return $evt if $evt;
+    ($patron, $evt) = $U->fetch_user($hold->usr);
+    return $evt if $evt;
 
-       $evt = $U->check_perms($requestor->id, $patron->home_ou, 'VIEW_HOLD_NOTIFICATION');
-       return $evt if $evt;
+    $evt = $U->check_perms($requestor->id, $patron->home_ou, 'VIEW_HOLD_NOTIFICATION');
+    return $evt if $evt;
 
-       $logger->info("User ".$requestor->id." fetching hold notifications for hold $holdid");
-       return $U->cstorereq(
-               'open-ils.cstore.direct.action.hold_notification.search.atomic', {hold => $holdid} );
+    $logger->info("User ".$requestor->id." fetching hold notifications for hold $holdid");
+    return $U->cstorereq(
+        'open-ils.cstore.direct.action.hold_notification.search.atomic', {hold => $holdid} );
 }
 
 
@@ -1627,10 +1840,10 @@ sub create_hold_notify {
 
    my $hold = $e->retrieve_action_hold_request($note->hold)
       or return $e->die_event;
-   my $patron = $e->retrieve_actor_user($hold->usr) 
+   my $patron = $e->retrieve_actor_user($hold->usr)
       or return $e->die_event;
 
-   return $e->die_event unless 
+   return $e->die_event unless
       $e->allowed('CREATE_HOLD_NOTIFICATION', $patron->home_ou);
 
    $note->notify_staff($e->requestor->id);
@@ -1643,11 +1856,11 @@ __PACKAGE__->register_method(
     method    => 'create_hold_note',
     api_name  => 'open-ils.circ.hold_note.create',
     signature => q/
-               Creates a new hold request note object
-               @param authtoken The login session key
-               @param note The hold note object to create
-               @return ID of the new object on success, Event on error
-               /
+        Creates a new hold request note object
+        @param authtoken The login session key
+        @param note The hold note object to create
+        @return ID of the new object on success, Event on error
+        /
 );
 
 sub create_hold_note {
@@ -1657,10 +1870,10 @@ sub create_hold_note {
 
    my $hold = $e->retrieve_action_hold_request($note->hold)
       or return $e->die_event;
-   my $patron = $e->retrieve_actor_user($hold->usr) 
+   my $patron = $e->retrieve_actor_user($hold->usr)
       or return $e->die_event;
 
-   return $e->die_event unless 
+   return $e->die_event unless
       $e->allowed('UPDATE_HOLD', $patron->home_ou); # FIXME: Using permcrud perm listed in fm_IDL.xml for ahrn.  Probably want something more specific
 
    $e->create_action_hold_request_note($note) or return $e->die_event;
@@ -1672,25 +1885,25 @@ __PACKAGE__->register_method(
     method    => 'reset_hold',
     api_name  => 'open-ils.circ.hold.reset',
     signature => q/
-               Un-captures and un-targets a hold, essentially returning
-               it to the state it was in directly after it was placed,
-               then attempts to re-target the hold
-               @param authtoken The login session key
-               @param holdid The id of the hold
-       /
+        Un-captures and un-targets a hold, essentially returning
+        it to the state it was in directly after it was placed,
+        then attempts to re-target the hold
+        @param authtoken The login session key
+        @param holdid The id of the hold
+    /
 );
 
 
 sub reset_hold {
-       my( $self, $conn, $auth, $holdid ) = @_;
-       my $reqr;
-       my ($hold, $evt) = $U->fetch_hold($holdid);
-       return $evt if $evt;
-       ($reqr, $evt) = $U->checksesperm($auth, 'UPDATE_HOLD');
-       return $evt if $evt;
-       $evt = _reset_hold($self, $reqr, $hold);
-       return $evt if $evt;
-       return 1;
+    my( $self, $conn, $auth, $holdid ) = @_;
+    my $reqr;
+    my ($hold, $evt) = $U->fetch_hold($holdid);
+    return $evt if $evt;
+    ($reqr, $evt) = $U->checksesperm($auth, 'UPDATE_HOLD');
+    return $evt if $evt;
+    $evt = _reset_hold($self, $reqr, $hold);
+    return $evt if $evt;
+    return 1;
 }
 
 
@@ -1708,10 +1921,10 @@ sub reset_hold_batch {
     for my $hold_id ($hold_ids) {
 
         my $hold = $e->retrieve_action_hold_request(
-            [$hold_id, {flesh => 1, flesh_fields => {ahr => ['usr']}}]) 
+            [$hold_id, {flesh => 1, flesh_fields => {ahr => ['usr']}}])
             or return $e->event;
 
-           next unless $e->allowed('UPDATE_HOLD', $hold->usr->home_ou);
+        next unless $e->allowed('UPDATE_HOLD', $hold->usr->home_ou);
         _reset_hold($self, $e->requestor, $hold);
     }
 
@@ -1720,60 +1933,61 @@ sub reset_hold_batch {
 
 
 sub _reset_hold {
-       my ($self, $reqr, $hold) = @_;
+    my ($self, $reqr, $hold) = @_;
 
-       my $e = new_editor(xact =>1, requestor => $reqr);
+    my $e = new_editor(xact =>1, requestor => $reqr);
 
-       $logger->info("reseting hold ".$hold->id);
+    $logger->info("reseting hold ".$hold->id);
 
-       my $hid = $hold->id;
+    my $hid = $hold->id;
 
-       if( $hold->capture_time and $hold->current_copy ) {
+    if( $hold->capture_time and $hold->current_copy ) {
 
-               my $copy = $e->retrieve_asset_copy($hold->current_copy)
-                       or return $e->die_event;
+        my $copy = $e->retrieve_asset_copy($hold->current_copy)
+            or return $e->die_event;
 
-               if( $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
-                       $logger->info("setting copy to status 'reshelving' on hold retarget");
-                       $copy->status(OILS_COPY_STATUS_RESHELVING);
-                       $copy->editor($e->requestor->id);
-                       $copy->edit_date('now');
-                       $e->update_asset_copy($copy) or return $e->die_event;
+        if( $copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF ) {
+            $logger->info("setting copy to status 'reshelving' on hold retarget");
+            $copy->status(OILS_COPY_STATUS_RESHELVING);
+            $copy->editor($e->requestor->id);
+            $copy->edit_date('now');
+            $e->update_asset_copy($copy) or return $e->die_event;
 
-               } elsif( $copy->status == OILS_COPY_STATUS_IN_TRANSIT ) {
+        } elsif( $copy->status == OILS_COPY_STATUS_IN_TRANSIT ) {
 
-                       # We don't want the copy to remain "in transit"
-                       $copy->status(OILS_COPY_STATUS_RESHELVING);
-                       $logger->warn("! reseting hold [$hid] that is in transit");
-                       my $transid = $e->search_action_hold_transit_copy({hold=>$hold->id},{idlist=>1})->[0];
+            # We don't want the copy to remain "in transit"
+            $copy->status(OILS_COPY_STATUS_RESHELVING);
+            $logger->warn("! reseting hold [$hid] that is in transit");
+            my $transid = $e->search_action_hold_transit_copy({hold=>$hold->id},{idlist=>1})->[0];
 
-                       if( $transid ) {
-                               my $trans = $e->retrieve_action_transit_copy($transid);
-                               if( $trans ) {
-                                       $logger->info("Aborting transit [$transid] on hold [$hid] reset...");
-                                       my $evt = OpenILS::Application::Circ::Transit::__abort_transit($e, $trans, $copy, 1);
-                                       $logger->info("Transit abort completed with result $evt");
-                                       unless ("$evt" eq 1) {
+            if( $transid ) {
+                my $trans = $e->retrieve_action_transit_copy($transid);
+                if( $trans ) {
+                    $logger->info("Aborting transit [$transid] on hold [$hid] reset...");
+                    my $evt = OpenILS::Application::Circ::Transit::__abort_transit($e, $trans, $copy, 1, 1);
+                    $logger->info("Transit abort completed with result $evt");
+                    unless ("$evt" eq 1) {
                         $e->rollback;
-                                           return $evt;
+                        return $evt;
                     }
-                               }
-                       }
-               }
-       }
+                }
+            }
+        }
+    }
 
-       $hold->clear_capture_time;
-       $hold->clear_current_copy;
-       $hold->clear_shelf_time;
-       $hold->clear_shelf_expire_time;
+    $hold->clear_capture_time;
+    $hold->clear_current_copy;
+    $hold->clear_shelf_time;
+    $hold->clear_shelf_expire_time;
+    $hold->clear_current_shelf_lib;
 
-       $e->update_action_hold_request($hold) or return $e->die_event;
-       $e->commit;
+    $e->update_action_hold_request($hold) or return $e->die_event;
+    $e->commit;
 
-       $U->storagereq(
-               'open-ils.storage.action.hold_request.copy_targeter', undef, $hold->id );
+    $U->storagereq(
+        'open-ils.storage.action.hold_request.copy_targeter', undef, $hold->id );
 
-       return undef;
+    return undef;
 }
 
 
@@ -1781,73 +1995,73 @@ __PACKAGE__->register_method(
     method    => 'fetch_open_title_holds',
     api_name  => 'open-ils.circ.open_holds.retrieve',
     signature => q/
-               Returns a list ids of un-fulfilled holds for a given title id
-               @param authtoken The login session key
-               @param id the id of the item whose holds we want to retrieve
-               @param type The hold type - M, T, I, V, C, F, R
-       /
+        Returns a list ids of un-fulfilled holds for a given title id
+        @param authtoken The login session key
+        @param id the id of the item whose holds we want to retrieve
+        @param type The hold type - M, T, I, V, C, F, R
+    /
 );
 
 sub fetch_open_title_holds {
-       my( $self, $conn, $auth, $id, $type, $org ) = @_;
-       my $e = new_editor( authtoken => $auth );
-       return $e->event unless $e->checkauth;
+    my( $self, $conn, $auth, $id, $type, $org ) = @_;
+    my $e = new_editor( authtoken => $auth );
+    return $e->event unless $e->checkauth;
 
-       $type ||= "T";
-       $org  ||= $e->requestor->ws_ou;
+    $type ||= "T";
+    $org  ||= $e->requestor->ws_ou;
 
-#      return $e->search_action_hold_request(
-#              { target => $id, hold_type => $type, fulfillment_time => undef }, {idlist=>1});
+#    return $e->search_action_hold_request(
+#        { target => $id, hold_type => $type, fulfillment_time => undef }, {idlist=>1});
 
-       # XXX make me return IDs in the future ^--
-       my $holds = $e->search_action_hold_request(
-               { 
-                       target                          => $id, 
-                       cancel_time                     => undef, 
-                       hold_type                       => $type, 
-                       fulfillment_time        => undef 
-               }
-       );
+    # XXX make me return IDs in the future ^--
+    my $holds = $e->search_action_hold_request(
+        {
+            target           => $id,
+            cancel_time      => undef,
+            hold_type        => $type,
+            fulfillment_time => undef
+        }
+    );
 
-       flesh_hold_transits($holds);
-       return $holds;
+    flesh_hold_transits($holds);
+    return $holds;
 }
 
 
 sub flesh_hold_transits {
-       my $holds = shift;
-       for my $hold ( @$holds ) {
-               $hold->transit(
-                       $apputils->simplereq(
-                               'open-ils.cstore',
-                               "open-ils.cstore.direct.action.hold_transit_copy.search.atomic",
-                               { hold => $hold->id },
-                               { order_by => { ahtc => 'id desc' }, limit => 1 }
-                       )->[0]
-               );
-       }
+    my $holds = shift;
+    for my $hold ( @$holds ) {
+        $hold->transit(
+            $apputils->simplereq(
+                'open-ils.cstore',
+                "open-ils.cstore.direct.action.hold_transit_copy.search.atomic",
+                { hold => $hold->id },
+                { order_by => { ahtc => 'id desc' }, limit => 1 }
+            )->[0]
+        );
+    }
 }
 
 sub flesh_hold_notices {
-       my( $holds, $e ) = @_;
-       $e ||= new_editor();
+    my( $holds, $e ) = @_;
+    $e ||= new_editor();
 
-       for my $hold (@$holds) {
-               my $notices = $e->search_action_hold_notification(
-                       [
-                               { hold => $hold->id },
-                               { order_by => { anh => 'notify_time desc' } },
-                       ],
-                       {idlist=>1}
-               );
+    for my $hold (@$holds) {
+        my $notices = $e->search_action_hold_notification(
+            [
+                { hold => $hold->id },
+                { order_by => { anh => 'notify_time desc' } },
+            ],
+            {idlist=>1}
+        );
 
-               $hold->notify_count(scalar(@$notices));
-               if( @$notices ) {
-                       my $n = $e->retrieve_action_hold_notification($$notices[0])
-                               or return $e->event;
-                       $hold->notify_time($n->notify_time);
-               }
-       }
+        $hold->notify_count(scalar(@$notices));
+        if( @$notices ) {
+            my $n = $e->retrieve_action_hold_notification($$notices[0])
+                or return $e->event;
+            $hold->notify_time($n->notify_time);
+        }
+    }
 }
 
 
@@ -1855,72 +2069,117 @@ __PACKAGE__->register_method(
     method    => 'fetch_captured_holds',
     api_name  => 'open-ils.circ.captured_holds.on_shelf.retrieve',
     stream    => 1,
+    authoritative => 1,
     signature => q/
-               Returns a list of un-fulfilled holds (on the Holds Shelf) for a given title id
-               @param authtoken The login session key
-               @param org The org id of the location in question
-       /
+        Returns a list of un-fulfilled holds (on the Holds Shelf) for a given title id
+        @param authtoken The login session key
+        @param org The org id of the location in question
+        @param match_copy A specific copy to limit to
+    /
 );
 
 __PACKAGE__->register_method(
     method    => 'fetch_captured_holds',
     api_name  => 'open-ils.circ.captured_holds.id_list.on_shelf.retrieve',
     stream    => 1,
+    authoritative => 1,
     signature => q/
-               Returns list ids of un-fulfilled holds (on the Holds Shelf) for a given title id
-               @param authtoken The login session key
-               @param org The org id of the location in question
-       /
+        Returns list ids of un-fulfilled holds (on the Holds Shelf) for a given title id
+        @param authtoken The login session key
+        @param org The org id of the location in question
+        @param match_copy A specific copy to limit to
+    /
 );
 
 __PACKAGE__->register_method(
     method    => 'fetch_captured_holds',
     api_name  => 'open-ils.circ.captured_holds.id_list.expired_on_shelf.retrieve',
     stream    => 1,
+    authoritative => 1,
     signature => q/
-               Returns list ids of shelf-expired un-fulfilled holds for a given title id
-               @param authtoken The login session key
-               @param org The org id of the location in question
-       /
+        Returns list ids of shelf-expired un-fulfilled holds for a given title id
+        @param authtoken The login session key
+        @param org The org id of the location in question
+        @param match_copy A specific copy to limit to
+    /
+);
+
+__PACKAGE__->register_method(
+    method    => 'fetch_captured_holds',
+    api_name  => 
+      'open-ils.circ.captured_holds.expired_on_shelf_or_wrong_shelf.retrieve',
+    stream    => 1,
+    authoritative => 1,
+    signature => q/
+        Returns list of shelf-expired un-fulfilled holds OR wrong shelf holds
+        for a given shelf lib
+    /
+);
+
+__PACKAGE__->register_method(
+    method    => 'fetch_captured_holds',
+    api_name  => 
+      'open-ils.circ.captured_holds.id_list.expired_on_shelf_or_wrong_shelf.retrieve',
+    stream    => 1,
+    authoritative => 1,
+    signature => q/
+        Returns list of shelf-expired un-fulfilled holds OR wrong shelf holds
+        for a given shelf lib
+    /
 );
 
 
 sub fetch_captured_holds {
-       my( $self, $conn, $auth, $org ) = @_;
+    my( $self, $conn, $auth, $org, $match_copy ) = @_;
 
-       my $e = new_editor(authtoken => $auth);
-       return $e->die_event unless $e->checkauth;
-       return $e->die_event unless $e->allowed('VIEW_HOLD'); # XXX rely on editor perm
+    my $e = new_editor(authtoken => $auth);
+    return $e->die_event unless $e->checkauth;
+    return $e->die_event unless $e->allowed('VIEW_HOLD'); # XXX rely on editor perm
 
-       $org ||= $e->requestor->ws_ou;
+    $org ||= $e->requestor->ws_ou;
 
-    my $query = { 
-        select => { ahr => ['id'] },
+    my $current_copy = { '!=' => undef };
+    $current_copy = { '=' => $match_copy } if $match_copy;
+
+    my $query = {
+        select => { alhr => ['id'] },
         from   => {
-            ahr => {
+            alhr => {
                 acp => {
                     field => 'id',
                     fkey  => 'current_copy'
                 },
             }
-        }, 
+        },
         where => {
-            '+acp' => { status => OILS_COPY_STATUS_ON_HOLDS_SHELF },
-            '+ahr' => {
-                capture_time     => { "!=" => undef },
-                current_copy     => { "!=" => undef },
-                fulfillment_time => undef,
-                pickup_lib       => $org,
-                cancel_time      => undef,
-              }
+            '+acp' => { status => OILS_COPY_STATUS_ON_HOLDS_SHELF, deleted => 'f' },
+            '+alhr' => {
+                capture_time      => { "!=" => undef },
+                current_copy      => $current_copy,
+                fulfillment_time  => undef,
+                current_shelf_lib => $org
+            }
         }
     };
     if($self->api_name =~ /expired/) {
-        $query->{'where'}->{'+ahr'}->{'shelf_expire_time'} = {'<' => 'now'};
-        $query->{'where'}->{'+ahr'}->{'shelf_time'} = {'!=' => undef};
+        $query->{'where'}->{'+alhr'}->{'-or'} = {
+                shelf_expire_time => { '<' => 'today'},
+                cancel_time => { '!=' => undef },
+        };
     }
     my $hold_ids = $e->json_query( $query );
 
+    if ($self->api_name =~ /wrong_shelf/) {
+        # fetch holds whose current_shelf_lib is $org, but whose pickup 
+        # lib is some other org unit.  Ignore already-retrieved holds.
+        my $wrong_shelf =
+            pickup_lib_changed_on_shelf_holds(
+                $e, $org, [map {$_->{id}} @$hold_ids]);
+        # match the layout of other items in $hold_ids
+        push (@$hold_ids, {id => $_}) for @$wrong_shelf;
+    }
+
+
     for my $hold_id (@$hold_ids) {
         if($self->api_name =~ /id_list/) {
             $conn->respond($hold_id->{id});
@@ -2038,9 +2297,9 @@ __PACKAGE__->register_method(
 );
 
 sub check_title_hold_batch {
-    my($self, $client, $authtoken, $param_list) = @_;
+    my($self, $client, $authtoken, $param_list, $oargs) = @_;
     foreach (@$param_list) {
-        my ($res) = $self->method_lookup('open-ils.circ.title_hold.is_possible')->run($authtoken, $_);
+        my ($res) = $self->method_lookup('open-ils.circ.title_hold.is_possible')->run($authtoken, $_, $oargs);
         $client->respond($res);
     }
     return undef;
@@ -2055,7 +2314,7 @@ __PACKAGE__->register_method(
              'whether or not said hold would have any potential copies to fulfill it.' .
              'The named paramaters of the second argument include: ' .
              'patronid, titleid, volume_id, copy_id, mrid, depth, pickup_lib, hold_type, selection_ou. ' .
-             'See perldoc ' . __PACKAGE__ . ' for more info on these fields.' , 
+             'See perldoc ' . __PACKAGE__ . ' for more info on these fields.' ,
         params => [
             { desc => 'Authentication token',     type => 'string'},
             { desc => 'Hash of named parameters', type => 'object'},
@@ -2069,7 +2328,7 @@ __PACKAGE__->register_method(
 
 =head3 check_title_hold (token, hash)
 
-The named fields in the hash are: 
+The named fields in the hash are:
 
  patronid     - ID of the hold recipient  (required)
  depth        - hold range depth          (default 0)
@@ -2089,7 +2348,7 @@ All key/value pairs are passed on to do_possibility_checks.
 
 # FIXME: better params checking.  what other params are required, if any?
 # FIXME: 3 copies of values confusing: $x, $params->{x} and $params{x}
-# FIXME: for example, $depth gets a default value, but then $$params{depth} is still 
+# FIXME: for example, $depth gets a default value, but then $$params{depth} is still
 # used in conditionals, where it may be undefined, causing a warning.
 # FIXME: specify proper usage/interaction of selection_ou and pickup_lib
 
@@ -2101,19 +2360,25 @@ sub check_title_hold {
     my %params       = %$params;
     my $depth        = $params{depth}        || 0;
     my $selection_ou = $params{selection_ou} || $params{pickup_lib};
+    my $oargs        = $params{oargs}        || {};
+
+    if($oargs->{events}) {
+        @{$oargs->{events}} = grep { $e->allowed($_ . '.override', $e->requestor->ws_ou); } @{$oargs->{events}};
+    }
 
-       my $patron = $e->retrieve_actor_user($params{patronid})
-               or return $e->event;
 
-       if( $e->requestor->id ne $patron->id ) {
-               return $e->event unless 
-                       $e->allowed('VIEW_HOLD_PERMIT', $patron->home_ou);
-       }
+    my $patron = $e->retrieve_actor_user($params{patronid})
+        or return $e->event;
+
+    if( $e->requestor->id ne $patron->id ) {
+        return $e->event unless
+            $e->allowed('VIEW_HOLD_PERMIT', $patron->home_ou);
+    }
 
-       return OpenILS::Event->new('PATRON_BARRED') if $U->is_true($patron->barred);
+    return OpenILS::Event->new('PATRON_BARRED') if $U->is_true($patron->barred);
 
-       my $request_lib = $e->retrieve_actor_org_unit($e->requestor->ws_ou)
-               or return $e->event;
+    my $request_lib = $e->retrieve_actor_org_unit($e->requestor->ws_ou)
+        or return $e->event;
 
     my $soft_boundary = $U->ou_ancestor_setting_value($selection_ou, OILS_SETTING_HOLD_SOFT_BOUNDARY);
     my $hard_boundary = $U->ou_ancestor_setting_value($selection_ou, OILS_SETTING_HOLD_HARD_BOUNDARY);
@@ -2125,7 +2390,7 @@ sub check_title_hold {
         # also, make sure we don't go past the hard boundary if it exists
 
         # our min boundary is the greater of user-specified boundary or hard boundary
-        my $min_depth = (defined $hard_boundary and $hard_boundary > $depth) ?  
+        my $min_depth = (defined $hard_boundary and $hard_boundary > $depth) ?
             $hard_boundary : $depth;
 
         my $depth = $soft_boundary;
@@ -2148,6 +2413,9 @@ sub check_title_hold {
         @status = do_possibility_checks($e, $patron, $request_lib, $params{depth}, %params);
     }
 
+    my $place_unfillable = 0;
+    $place_unfillable = 1 if $e->allowed('PLACE_UNFILLABLE_HOLD', $e->requestor->ws_ou);
+
     if ($status[0]) {
         return {
             "success" => 1,
@@ -2156,9 +2424,9 @@ sub check_title_hold {
         };
     } elsif ($status[2]) {
         my $n = scalar @{$status[2]};
-        return {"success" => 0, "last_event" => $status[2]->[$n - 1]};
+        return {"success" => 0, "last_event" => $status[2]->[$n - 1], "age_protected_copy" => $status[3], "place_unfillable" => $place_unfillable};
     } else {
-        return {"success" => 0};
+        return {"success" => 0, "age_protected_copy" => $status[3], "place_unfillable" => $place_unfillable};
     }
 }
 
@@ -2177,74 +2445,100 @@ sub do_possibility_checks {
     my $hold_type    = $params{hold_type}    || 'T';
     my $selection_ou = $params{selection_ou} || $pickup_lib;
     my $holdable_formats = $params{holdable_formats};
+    my $oargs        = $params{oargs}        || {};
 
 
-       my $copy;
-       my $volume;
-       my $title;
+    my $copy;
+    my $volume;
+    my $title;
 
-       if( $hold_type eq OILS_HOLD_TYPE_FORCE || $hold_type eq OILS_HOLD_TYPE_RECALL || $hold_type eq OILS_HOLD_TYPE_COPY ) {
+    if( $hold_type eq OILS_HOLD_TYPE_FORCE || $hold_type eq OILS_HOLD_TYPE_RECALL || $hold_type eq OILS_HOLD_TYPE_COPY ) {
 
         return $e->event unless $copy   = $e->retrieve_asset_copy($copyid);
         return $e->event unless $volume = $e->retrieve_asset_call_number($copy->call_number);
         return $e->event unless $title  = $e->retrieve_biblio_record_entry($volume->record);
 
-        return verify_copy_for_hold( 
-            $patron, $e->requestor, $title, $copy, $pickup_lib, $request_lib
+        return (1, 1, []) if( $hold_type eq OILS_HOLD_TYPE_RECALL || $hold_type eq OILS_HOLD_TYPE_FORCE);
+        return verify_copy_for_hold(
+            $patron, $e->requestor, $title, $copy, $pickup_lib, $request_lib, $oargs
         );
 
-       } elsif( $hold_type eq OILS_HOLD_TYPE_VOLUME ) {
+    } elsif( $hold_type eq OILS_HOLD_TYPE_VOLUME ) {
 
-               return $e->event unless $volume = $e->retrieve_asset_call_number($volid);
-               return $e->event unless $title  = $e->retrieve_biblio_record_entry($volume->record);
+        return $e->event unless $volume = $e->retrieve_asset_call_number($volid);
+        return $e->event unless $title  = $e->retrieve_biblio_record_entry($volume->record);
 
-               return _check_volume_hold_is_possible(
-                       $volume, $title, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou
+        return _check_volume_hold_is_possible(
+            $volume, $title, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou, $oargs
         );
 
-       } elsif( $hold_type eq OILS_HOLD_TYPE_TITLE ) {
+    } elsif( $hold_type eq OILS_HOLD_TYPE_TITLE ) {
 
-               return _check_title_hold_is_possible(
-                       $titleid, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou
+        return _check_title_hold_is_possible(
+            $titleid, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou, undef, $oargs
         );
 
-       } elsif( $hold_type eq OILS_HOLD_TYPE_ISSUANCE ) {
+    } elsif( $hold_type eq OILS_HOLD_TYPE_ISSUANCE ) {
 
-               return _check_issuance_hold_is_possible(
-                       $issuanceid, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou
+        return _check_issuance_hold_is_possible(
+            $issuanceid, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou, $oargs
         );
 
-       } elsif( $hold_type eq OILS_HOLD_TYPE_MONOPART ) {
+    } elsif( $hold_type eq OILS_HOLD_TYPE_MONOPART ) {
 
-               return _check_monopart_hold_is_possible(
-                       $partid, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou
+        return _check_monopart_hold_is_possible(
+            $partid, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou, $oargs
         );
 
-       } elsif( $hold_type eq OILS_HOLD_TYPE_METARECORD ) {
-
-               my $maps = $e->search_metabib_metarecord_source_map({metarecord=>$mrid});
-               my @recs = map { $_->source } @$maps;
-               my @status = ();
-               for my $rec (@recs) {
-                       @status = _check_title_hold_is_possible(
-                               $rec, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou, $holdable_formats
-                       );
-                       last if $status[0];
-               }
-               return @status;
-       }
+    } elsif( $hold_type eq OILS_HOLD_TYPE_METARECORD ) {
+
+        # pasing undef as the depth to filtered_records causes the depth
+        # of the selection_ou to be used, which is not what we want here.
+        $depth ||= 0;
+
+        my ($recs) = __PACKAGE__->method_lookup('open-ils.circ.holds.metarecord.filtered_records')->run($mrid, $holdable_formats, $selection_ou, $depth);
+        my @status = ();
+        for my $rec (@$recs) {
+            @status = _check_title_hold_is_possible(
+                $rec, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou, $holdable_formats, $oargs
+            );
+            last if $status[0];
+        }
+        return @status;
+    }
 #   else { Unrecognized hold_type ! }   # FIXME: return error? or 0?
 }
 
+sub MR_filter_records {
+    my $self = shift;
+    my $client = shift;
+    my $m = shift;
+    my $f = shift;
+    my $o = shift;
+    my $d = shift;
+    my $opac_visible = shift;
+    
+    my $org_at_depth = defined($d) ? $U->org_unit_ancestor_at_depth($o, $d) : $o;
+    return $U->storagereq(
+        'open-ils.storage.metarecord.filtered_records.atomic', 
+        $m, $f, $org_at_depth, $opac_visible
+    );
+}
+__PACKAGE__->register_method(
+    method   => 'MR_filter_records',
+    api_name => 'open-ils.circ.holds.metarecord.filtered_records',
+);
+
+
 my %prox_cache;
 sub create_ranged_org_filter {
     my($e, $selection_ou, $depth) = @_;
 
-    # find the orgs from which this hold may be fulfilled, 
+    # find the orgs from which this hold may be fulfilled,
     # based on the selection_ou and depth
 
     my $top_org = $e->search_actor_org_unit([
-        {parent_ou => undef}, 
+        {parent_ou => undef},
         {flesh=>1, flesh_fields=>{aou=>['ou_type']}}])->[0];
     my %org_filter;
 
@@ -2262,48 +2556,28 @@ sub create_ranged_org_filter {
 
 
 sub _check_title_hold_is_possible {
-    my( $titleid, $depth, $request_lib, $patron, $requestor, $pickup_lib, $selection_ou, $holdable_formats ) = @_;
-   
-    my ($types, $formats, $lang);
-    if (defined($holdable_formats)) {
-        ($types, $formats, $lang) = split '-', $holdable_formats;
-    }
+    my( $titleid, $depth, $request_lib, $patron, $requestor, $pickup_lib, $selection_ou, $holdable_formats, $oargs ) = @_;
+    # $holdable_formats is now unused. We pre-filter the MR's records.
 
     my $e = new_editor();
     my %org_filter = create_ranged_org_filter($e, $selection_ou, $depth);
 
     # this monster will grab the id and circ_lib of all of the "holdable" copies for the given record
     my $copies = $e->json_query(
-        { 
+        {
             select => { acp => ['id', 'circ_lib'] },
               from => {
                 acp => {
                     acn => {
                         field  => 'id',
                         fkey   => 'call_number',
-                        'join' => {
-                            bre => {
-                                field  => 'id',
-                                filter => { id => $titleid },
-                                fkey   => 'record'
-                            },
-                            mrd => {
-                                field  => 'record',
-                                fkey   => 'record',
-                                filter => {
-                                    record => $titleid,
-                                    ( $types   ? (item_type => [split '', $types])   : () ),
-                                    ( $formats ? (item_form => [split '', $formats]) : () ),
-                                    ( $lang    ? (item_lang => $lang)                : () )
-                                }
-                            }
-                        }
+                        filter => { record => $titleid }
                     },
                     acpl => { field => 'id', filter => { holdable => 't'}, fkey => 'location' },
                     ccs  => { field => 'id', filter => { holdable => 't'}, fkey => 'status'   },
                     acpm => { field => 'target_copy', type => 'left' } # ignore part-linked copies
                 }
-            }, 
+            },
             where => {
                 '+acp' => { circulate => 't', deleted => 'f', holdable => 't', %org_filter },
                 '+acpm' => { target_copy => undef } # ignore part-linked copies
@@ -2322,8 +2596,8 @@ sub _check_title_hold_is_possible {
     ) unless @$copies;
 
     # -----------------------------------------------------------------------
-    # sort the copies into buckets based on their circ_lib proximity to 
-    # the patron's home_ou.  
+    # sort the copies into buckets based on their circ_lib proximity to
+    # the patron's home_ou.
     # -----------------------------------------------------------------------
 
     my $home_org = $patron->home_ou;
@@ -2331,7 +2605,7 @@ sub _check_title_hold_is_possible {
 
     $logger->info("prox cache $home_org " . $prox_cache{$home_org});
 
-    $prox_cache{$home_org} = 
+    $prox_cache{$home_org} =
         $e->search_actor_org_unit_proximity({from_org => $home_org})
         unless $prox_cache{$home_org};
     my $home_prox = $prox_cache{$home_org};
@@ -2345,11 +2619,11 @@ sub _check_title_hold_is_possible {
 
     if( $home_org ne $req_org ) {
       # -----------------------------------------------------------------------
-      # shove the copies close to the request_lib into the primary buckets 
-      # directly before the farthest away copies.  That way, they are not 
+      # shove the copies close to the request_lib into the primary buckets
+      # directly before the farthest away copies.  That way, they are not
       # given priority, but they are checked before the farthest copies.
       # -----------------------------------------------------------------------
-        $prox_cache{$req_org} = 
+        $prox_cache{$req_org} =
             $e->search_actor_org_unit_proximity({from_org => $req_org})
             unless $prox_cache{$req_org};
         my $req_prox = $prox_cache{$req_org};
@@ -2372,6 +2646,7 @@ sub _check_title_hold_is_possible {
     my $title;
     my %seen;
     my @status;
+    my $age_protect_only = 0;
     OUTER: for my $key (@keys) {
       my @cps = @{$buckets{$key}};
 
@@ -2389,26 +2664,28 @@ sub _check_title_hold_is_possible {
                [ $copy->call_number, { flesh => 1, flesh_fields => { bre => ['fixed_fields'], acn => ['record'] } } ] );
             $title = $vol->record;
          }
-   
+
          @status = verify_copy_for_hold(
-            $patron, $requestor, $title, $copy, $pickup_lib, $request_lib);
+            $patron, $requestor, $title, $copy, $pickup_lib, $request_lib, $oargs);
 
+         $age_protect_only ||= $status[3];
          last OUTER if $status[0];
       }
     }
 
+    $status[3] = $age_protect_only;
     return @status;
 }
 
 sub _check_issuance_hold_is_possible {
-    my( $issuanceid, $depth, $request_lib, $patron, $requestor, $pickup_lib, $selection_ou ) = @_;
-   
+    my( $issuanceid, $depth, $request_lib, $patron, $requestor, $pickup_lib, $selection_ou, $oargs ) = @_;
+
     my $e = new_editor();
     my %org_filter = create_ranged_org_filter($e, $selection_ou, $depth);
 
     # this monster will grab the id and circ_lib of all of the "holdable" copies for the given record
     my $copies = $e->json_query(
-        { 
+        {
             select => { acp => ['id', 'circ_lib'] },
               from => {
                 acp => {
@@ -2420,7 +2697,7 @@ sub _check_issuance_hold_is_possible {
                     acpl => { field => 'id', filter => { holdable => 't'}, fkey => 'location' },
                     ccs  => { field => 'id', filter => { holdable => 't'}, fkey => 'status'   }
                 }
-            }, 
+            },
             where => {
                 '+acp' => { circulate => 't', deleted => 'f', holdable => 't', %org_filter }
             },
@@ -2448,8 +2725,8 @@ sub _check_issuance_hold_is_possible {
     }
 
     # -----------------------------------------------------------------------
-    # sort the copies into buckets based on their circ_lib proximity to 
-    # the patron's home_ou.  
+    # sort the copies into buckets based on their circ_lib proximity to
+    # the patron's home_ou.
     # -----------------------------------------------------------------------
 
     my $home_org = $patron->home_ou;
@@ -2457,7 +2734,7 @@ sub _check_issuance_hold_is_possible {
 
     $logger->info("prox cache $home_org " . $prox_cache{$home_org});
 
-    $prox_cache{$home_org} = 
+    $prox_cache{$home_org} =
         $e->search_actor_org_unit_proximity({from_org => $home_org})
         unless $prox_cache{$home_org};
     my $home_prox = $prox_cache{$home_org};
@@ -2471,11 +2748,11 @@ sub _check_issuance_hold_is_possible {
 
     if( $home_org ne $req_org ) {
       # -----------------------------------------------------------------------
-      # shove the copies close to the request_lib into the primary buckets 
-      # directly before the farthest away copies.  That way, they are not 
+      # shove the copies close to the request_lib into the primary buckets
+      # directly before the farthest away copies.  That way, they are not
       # given priority, but they are checked before the farthest copies.
       # -----------------------------------------------------------------------
-        $prox_cache{$req_org} = 
+        $prox_cache{$req_org} =
             $e->search_actor_org_unit_proximity({from_org => $req_org})
             unless $prox_cache{$req_org};
         my $req_prox = $prox_cache{$req_org};
@@ -2498,6 +2775,7 @@ sub _check_issuance_hold_is_possible {
     my $title;
     my %seen;
     my @status;
+    my $age_protect_only = 0;
     OUTER: for my $key (@keys) {
       my @cps = @{$buckets{$key}};
 
@@ -2515,10 +2793,11 @@ sub _check_issuance_hold_is_possible {
                [ $copy->call_number, { flesh => 1, flesh_fields => { bre => ['fixed_fields'], acn => ['record'] } } ] );
             $title = $vol->record;
          }
-   
+
          @status = verify_copy_for_hold(
-            $patron, $requestor, $title, $copy, $pickup_lib, $request_lib);
+            $patron, $requestor, $title, $copy, $pickup_lib, $request_lib, $oargs);
 
+         $age_protect_only ||= $status[3];
          last OUTER if $status[0];
       }
     }
@@ -2531,18 +2810,19 @@ sub _check_issuance_hold_is_possible {
 
         return (1,0) if ($empty_ok);
     }
+    $status[3] = $age_protect_only;
     return @status;
 }
 
 sub _check_monopart_hold_is_possible {
-    my( $partid, $depth, $request_lib, $patron, $requestor, $pickup_lib, $selection_ou ) = @_;
-   
+    my( $partid, $depth, $request_lib, $patron, $requestor, $pickup_lib, $selection_ou, $oargs ) = @_;
+
     my $e = new_editor();
     my %org_filter = create_ranged_org_filter($e, $selection_ou, $depth);
 
     # this monster will grab the id and circ_lib of all of the "holdable" copies for the given record
     my $copies = $e->json_query(
-        { 
+        {
             select => { acp => ['id', 'circ_lib'] },
               from => {
                 acp => {
@@ -2554,7 +2834,7 @@ sub _check_monopart_hold_is_possible {
                     acpl => { field => 'id', filter => { holdable => 't'}, fkey => 'location' },
                     ccs  => { field => 'id', filter => { holdable => 't'}, fkey => 'status'   }
                 }
-            }, 
+            },
             where => {
                 '+acp' => { circulate => 't', deleted => 'f', holdable => 't', %org_filter }
             },
@@ -2582,8 +2862,8 @@ sub _check_monopart_hold_is_possible {
     }
 
     # -----------------------------------------------------------------------
-    # sort the copies into buckets based on their circ_lib proximity to 
-    # the patron's home_ou.  
+    # sort the copies into buckets based on their circ_lib proximity to
+    # the patron's home_ou.
     # -----------------------------------------------------------------------
 
     my $home_org = $patron->home_ou;
@@ -2591,7 +2871,7 @@ sub _check_monopart_hold_is_possible {
 
     $logger->info("prox cache $home_org " . $prox_cache{$home_org});
 
-    $prox_cache{$home_org} = 
+    $prox_cache{$home_org} =
         $e->search_actor_org_unit_proximity({from_org => $home_org})
         unless $prox_cache{$home_org};
     my $home_prox = $prox_cache{$home_org};
@@ -2605,11 +2885,11 @@ sub _check_monopart_hold_is_possible {
 
     if( $home_org ne $req_org ) {
       # -----------------------------------------------------------------------
-      # shove the copies close to the request_lib into the primary buckets 
-      # directly before the farthest away copies.  That way, they are not 
+      # shove the copies close to the request_lib into the primary buckets
+      # directly before the farthest away copies.  That way, they are not
       # given priority, but they are checked before the farthest copies.
       # -----------------------------------------------------------------------
-        $prox_cache{$req_org} = 
+        $prox_cache{$req_org} =
             $e->search_actor_org_unit_proximity({from_org => $req_org})
             unless $prox_cache{$req_org};
         my $req_prox = $prox_cache{$req_org};
@@ -2632,6 +2912,7 @@ sub _check_monopart_hold_is_possible {
     my $title;
     my %seen;
     my @status;
+    my $age_protect_only = 0;
     OUTER: for my $key (@keys) {
       my @cps = @{$buckets{$key}};
 
@@ -2649,10 +2930,11 @@ sub _check_monopart_hold_is_possible {
                [ $copy->call_number, { flesh => 1, flesh_fields => { bre => ['fixed_fields'], acn => ['record'] } } ] );
             $title = $vol->record;
          }
-   
+
          @status = verify_copy_for_hold(
-            $patron, $requestor, $title, $copy, $pickup_lib, $request_lib);
+            $patron, $requestor, $title, $copy, $pickup_lib, $request_lib, $oargs);
 
+         $age_protect_only ||= $status[3];
          last OUTER if $status[0];
       }
     }
@@ -2665,15 +2947,16 @@ sub _check_monopart_hold_is_possible {
 
         return (1,0) if ($empty_ok);
     }
+    $status[3] = $age_protect_only;
     return @status;
 }
 
 
 sub _check_volume_hold_is_possible {
-       my( $vol, $title, $depth, $request_lib, $patron, $requestor, $pickup_lib, $selection_ou ) = @_;
+    my( $vol, $title, $depth, $request_lib, $patron, $requestor, $pickup_lib, $selection_ou, $oargs ) = @_;
     my %org_filter = create_ranged_org_filter(new_editor(), $selection_ou, $depth);
-       my $copies = new_editor->search_asset_copy({call_number => $vol->id, %org_filter});
-       $logger->info("checking possibility of volume hold for volume ".$vol->id);
+    my $copies = new_editor->search_asset_copy({call_number => $vol->id, %org_filter});
+    $logger->info("checking possibility of volume hold for volume ".$vol->id);
 
     my $filter_copies = [];
     for my $copy (@$copies) {
@@ -2693,40 +2976,92 @@ sub _check_volume_hold_is_possible {
     ) unless @$copies;
 
     my @status;
-       for my $copy ( @$copies ) {
+    my $age_protect_only = 0;
+    for my $copy ( @$copies ) {
         @status = verify_copy_for_hold(
-                       $patron, $requestor, $title, $copy, $pickup_lib, $request_lib );
+            $patron, $requestor, $title, $copy, $pickup_lib, $request_lib, $oargs );
+        $age_protect_only ||= $status[3];
         last if $status[0];
-       }
-       return @status;
+    }
+    $status[3] = $age_protect_only;
+    return @status;
 }
 
 
 
 sub verify_copy_for_hold {
-       my( $patron, $requestor, $title, $copy, $pickup_lib, $request_lib ) = @_;
-       $logger->info("checking possibility of copy in hold request for copy ".$copy->id);
+    my( $patron, $requestor, $title, $copy, $pickup_lib, $request_lib, $oargs ) = @_;
+    # $oargs should be undef unless we're overriding.
+    $logger->info("checking possibility of copy in hold request for copy ".$copy->id);
     my $permitted = OpenILS::Utils::PermitHold::permit_copy_hold(
-               {       patron                          => $patron, 
-                       requestor                       => $requestor, 
-                       copy                            => $copy,
-                       title                           => $title, 
-                       title_descriptor        => $title->fixed_fields, # this is fleshed into the title object
-                       pickup_lib                      => $pickup_lib,
-                       request_lib                     => $request_lib,
-            new_hold            => 1,
-            show_event_list     => 1
-               } 
-       );
+        {
+            patron           => $patron,
+            requestor        => $requestor,
+            copy             => $copy,
+            title            => $title,
+            title_descriptor => $title->fixed_fields,
+            pickup_lib       => $pickup_lib,
+            request_lib      => $request_lib,
+            new_hold         => 1,
+            show_event_list  => 1
+        }
+    );
+
+    # Check for override permissions on events.
+    if ($oargs && $permitted && scalar @$permitted) {
+        # Remove the events from permitted that we can override.
+        if ($oargs->{events}) {
+            foreach my $evt (@{$oargs->{events}}) {
+                $permitted = [grep {$_->{textcode} ne $evt} @{$permitted}];
+            }
+        }
+        # Now, we handle the override all case by checking remaining
+        # events against override permissions.
+        if (scalar @$permitted && $oargs->{all}) {
+            # Pre-set events and failed members of oargs to empty
+            # arrays, if they are not set, yet.
+            $oargs->{events} = [] unless ($oargs->{events});
+            $oargs->{failed} = [] unless ($oargs->{failed});
+            # When we're done with these checks, we swap permitted
+            # with a reference to @disallowed.
+            my @disallowed = ();
+            foreach my $evt (@{$permitted}) {
+                # Check if we've already seen the event in this
+                # session and it failed.
+                if (grep {$_ eq $evt->{textcode}} @{$oargs->{failed}}) {
+                    push(@disallowed, $evt);
+                } else {
+                    # We have to check if the requestor has the
+                    # override permission.
+
+                    # AppUtils::check_user_perms returns the perm if
+                    # the user doesn't have it, undef if they do.
+                    if ($apputils->check_user_perms($requestor->id, $requestor->ws_ou, $evt->{textcode} . '.override')) {
+                        push(@disallowed, $evt);
+                        push(@{$oargs->{failed}}, $evt->{textcode});
+                    } else {
+                        push(@{$oargs->{events}}, $evt->{textcode});
+                    }
+                }
+            }
+            $permitted = \@disallowed;
+        }
+    }
+
+    my $age_protect_only = 0;
+    if (@$permitted == 1 && @$permitted[0]->{textcode} eq 'ITEM_AGE_PROTECTED') {
+        $age_protect_only = 1;
+    }
 
     return (
         (not scalar @$permitted), # true if permitted is an empty arrayref
         (   # XXX This test is of very dubious value; someone should figure
             # out what if anything is checking this value
-               ($copy->circ_lib == $pickup_lib) and 
+            ($copy->circ_lib == $pickup_lib) and
             ($copy->status == OILS_COPY_STATUS_AVAILABLE)
         ),
-        $permitted
+        $permitted,
+        $age_protect_only
     );
 }
 
@@ -2739,120 +3074,123 @@ sub find_nearest_permitted_hold {
     my $copy   = shift;     # copy to target
     my $user   = shift;     # staff
     my $check_only = shift; # do no updates, just see if the copy could fulfill a hold
-      
+
     my $evt = OpenILS::Event->new('ACTION_HOLD_REQUEST_NOT_FOUND');
 
     my $bc = $copy->barcode;
 
-       # find any existing holds that already target this copy
-       my $old_holds = $editor->search_action_hold_request(
-               {       current_copy => $copy->id, 
-                       cancel_time  => undef, 
-                       capture_time => undef 
-               } 
-       );
-
-       # hold->type "R" means we need this copy
-       for my $h (@$old_holds) { return ($h) if $h->hold_type eq 'R'; }
-
+    # find any existing holds that already target this copy
+    my $old_holds = $editor->search_action_hold_request(
+        {    current_copy => $copy->id,
+            cancel_time  => undef,
+            capture_time => undef
+        }
+    );
 
     my $hold_stall_interval = $U->ou_ancestor_setting_value($user->ws_ou, OILS_SETTING_HOLD_SOFT_STALL);
 
-       $logger->info("circulator: searching for best hold at org ".$user->ws_ou.
+    $logger->info("circulator: searching for best hold at org ".$user->ws_ou.
         " and copy $bc with a hold stalling interval of ". ($hold_stall_interval || "(none)"));
 
-       my $fifo = $U->ou_ancestor_setting_value($user->ws_ou, 'circ.holds_fifo');
+    my $fifo = $U->ou_ancestor_setting_value($user->ws_ou, 'circ.holds_fifo');
 
-       # search for what should be the best holds for this copy to fulfill
-       my $best_holds = $U->storagereq(
-        "open-ils.storage.action.hold_request.nearest_hold.atomic", 
-               $user->ws_ou, $copy->id, 10, $hold_stall_interval, $fifo );
+    # the nearest_hold API call now needs this
+    $copy->call_number($editor->retrieve_asset_call_number($copy->call_number))
+        unless ref $copy->call_number;
 
-       unless(@$best_holds) {
+    # search for what should be the best holds for this copy to fulfill
+    my $best_holds = $U->storagereq(
+        "open-ils.storage.action.hold_request.nearest_hold.atomic", 
+        $user->ws_ou, $copy, 100, $hold_stall_interval, $fifo );
 
-               if( my $hold = $$old_holds[0] ) {
-                       $logger->info("circulator: using existing pre-targeted hold ".$hold->id." in hold search");
-                       return ($hold);
-               }
+    # Add any pre-targeted holds to the list too? Unless they are already there, anyway.
+    if ($old_holds) {
+        for my $holdid (@$old_holds) {
+            next unless $holdid;
+            push(@$best_holds, $holdid) unless ( grep { ''.$holdid eq ''.$_ } @$best_holds );
+        }
+    }
 
-               $logger->info("circulator: no suitable holds found for copy $bc");
-               return (undef, $evt);
-       }
+    unless(@$best_holds) {
+        $logger->info("circulator: no suitable holds found for copy $bc");
+        return (undef, $evt);
+    }
 
 
-       my $best_hold;
+    my $best_hold;
 
-       # for each potential hold, we have to run the permit script
-       # to make sure the hold is actually permitted.
+    # for each potential hold, we have to run the permit script
+    # to make sure the hold is actually permitted.
     my %reqr_cache;
     my %org_cache;
-       for my $holdid (@$best_holds) {
-               next unless $holdid;
-               $logger->info("circulator: checking if hold $holdid is permitted for copy $bc");
-
-               my $hold = $editor->retrieve_action_hold_request($holdid) or next;
-               my $reqr = $reqr_cache{$hold->requestor} || $editor->retrieve_actor_user($hold->requestor);
-               my $rlib = $org_cache{$hold->request_lib} || $editor->retrieve_actor_org_unit($hold->request_lib);
-
-               $reqr_cache{$hold->requestor} = $reqr;
-               $org_cache{$hold->request_lib} = $rlib;
-
-               # see if this hold is permitted
-               my $permitted = OpenILS::Utils::PermitHold::permit_copy_hold(
-                       {       patron_id                       => $hold->usr,
-                               requestor                       => $reqr,
-                               copy                            => $copy,
-                               pickup_lib                      => $hold->pickup_lib,
-                               request_lib                     => $rlib,
-                               retarget                        => 1
-                       } 
-               );
-
-               if( $permitted ) {
-                       $best_hold = $hold;
-                       last;
-               }
-       }
-
-
-       unless( $best_hold ) { # no "good" permitted holds were found
-               if( my $hold = $$old_holds[0] ) { # can we return a pre-targeted hold?
-                       $logger->info("circulator: using existing pre-targeted hold ".$hold->id." in hold search");
-                       return ($hold);
-               }
-
-               # we got nuthin
-               $logger->info("circulator: no suitable holds found for copy $bc");
-               return (undef, $evt);
-       }
-
-       $logger->info("circulator: best hold ".$best_hold->id." found for copy $bc");
-
-       # indicate a permitted hold was found
-       return $best_hold if $check_only;
-
-       # we've found a permitted hold.  we need to "grab" the copy 
-       # to prevent re-targeted holds (next part) from re-grabbing the copy
-       $best_hold->current_copy($copy->id);
-       $editor->update_action_hold_request($best_hold) 
-               or return (undef, $editor->event);
+    for my $holdid (@$best_holds) {
+        next unless $holdid;
+        $logger->info("circulator: checking if hold $holdid is permitted for copy $bc");
+
+        my $hold = $editor->retrieve_action_hold_request($holdid) or next;
+        # Force and recall holds bypass all rules
+        if ($hold->hold_type eq 'R' || $hold->hold_type eq 'F') {
+            $best_hold = $hold;
+            last;
+        }
+        my $reqr = $reqr_cache{$hold->requestor} || $editor->retrieve_actor_user($hold->requestor);
+        my $rlib = $org_cache{$hold->request_lib} || $editor->retrieve_actor_org_unit($hold->request_lib);
+
+        $reqr_cache{$hold->requestor} = $reqr;
+        $org_cache{$hold->request_lib} = $rlib;
+
+        # see if this hold is permitted
+        my $permitted = OpenILS::Utils::PermitHold::permit_copy_hold(
+            {
+                patron_id   => $hold->usr,
+                requestor   => $reqr,
+                copy        => $copy,
+                pickup_lib  => $hold->pickup_lib,
+                request_lib => $rlib,
+                retarget    => 1
+            }
+        );
+
+        if( $permitted ) {
+            $best_hold = $hold;
+            last;
+        }
+    }
+
+
+    unless( $best_hold ) { # no "good" permitted holds were found
+        # we got nuthin
+        $logger->info("circulator: no suitable holds found for copy $bc");
+        return (undef, $evt);
+    }
+
+    $logger->info("circulator: best hold ".$best_hold->id." found for copy $bc");
+
+    # indicate a permitted hold was found
+    return $best_hold if $check_only;
+
+    # we've found a permitted hold.  we need to "grab" the copy
+    # to prevent re-targeted holds (next part) from re-grabbing the copy
+    $best_hold->current_copy($copy->id);
+    $editor->update_action_hold_request($best_hold)
+        or return (undef, $editor->event);
 
 
     my @retarget;
 
-       # re-target any other holds that already target this copy
-       for my $old_hold (@$old_holds) {
-               next if $old_hold->id eq $best_hold->id; # don't re-target the hold we want
-               $logger->info("circulator: clearing current_copy and prev_check_time on hold ".
+    # re-target any other holds that already target this copy
+    for my $old_hold (@$old_holds) {
+        next if $old_hold->id eq $best_hold->id; # don't re-target the hold we want
+        $logger->info("circulator: clearing current_copy and prev_check_time on hold ".
             $old_hold->id." after a better hold [".$best_hold->id."] was found");
         $old_hold->clear_current_copy;
         $old_hold->clear_prev_check_time;
-        $editor->update_action_hold_request($old_hold) 
+        $editor->update_action_hold_request($old_hold)
             or return (undef, $editor->event);
         push(@retarget, $old_hold->id);
-       }
+    }
 
-       return ($best_hold, undef, (@retarget) ? \@retarget : undef);
+    return ($best_hold, undef, (@retarget) ? \@retarget : undef);
 }
 
 
@@ -2866,34 +3204,43 @@ __PACKAGE__->register_method(
 );
 
 sub all_rec_holds {
-       my( $self, $conn, $auth, $title_id, $args ) = @_;
+    my( $self, $conn, $auth, $title_id, $args ) = @_;
 
-       my $e = new_editor(authtoken=>$auth);
-       $e->checkauth or return $e->event;
-       $e->allowed('VIEW_HOLD') or return $e->event;
+    my $e = new_editor(authtoken=>$auth);
+    $e->checkauth or return $e->event;
+    $e->allowed('VIEW_HOLD') or return $e->event;
 
-       $args ||= {};
+    $args ||= {};
     $args->{fulfillment_time} = undef; #  we don't want to see old fulfilled holds
-       $args->{cancel_time} = undef;
+    $args->{cancel_time} = undef;
 
-       my $resp = { volume_holds => [], copy_holds => [], recall_holds => [], force_holds => [], metarecord_holds => [], part_holds => [], issuance_holds => [] };
+    my $resp = {
+          metarecord_holds => []
+        , title_holds      => []
+        , volume_holds     => []
+        , copy_holds       => []
+        , recall_holds     => []
+        , force_holds      => []
+        , part_holds       => []
+        , issuance_holds   => []
+    };
 
     my $mr_map = $e->search_metabib_metarecord_source_map({source => $title_id})->[0];
     if($mr_map) {
         $resp->{metarecord_holds} = $e->search_action_hold_request(
             {   hold_type => OILS_HOLD_TYPE_METARECORD,
                 target => $mr_map->metarecord,
-                %$args 
+                %$args
             }, {idlist => 1}
         );
     }
 
-       $resp->{title_holds} = $e->search_action_hold_request(
-               { 
-                       hold_type => OILS_HOLD_TYPE_TITLE, 
-                       target => $title_id, 
-                       %$args 
-               }, {idlist=>1} );
+    $resp->{title_holds} = $e->search_action_hold_request(
+        {
+            hold_type => OILS_HOLD_TYPE_TITLE,
+            target => $title_id,
+            %$args
+        }, {idlist=>1} );
 
     my $parts = $e->search_biblio_monograph_part(
         {
@@ -2917,7 +3264,7 @@ sub all_rec_holds {
             {subscription => $subs}, {idlist=>1}
         );
 
-        if ($issuances) {
+        if (@$issuances) {
             $resp->{issuance_holds} = $e->search_action_hold_request(
                 {
                     hold_type => OILS_HOLD_TYPE_ISSUANCE,
@@ -2928,45 +3275,45 @@ sub all_rec_holds {
         }
     }
 
-       my $vols = $e->search_asset_call_number(
-               { record => $title_id, deleted => 'f' }, {idlist=>1});
+    my $vols = $e->search_asset_call_number(
+        { record => $title_id, deleted => 'f' }, {idlist=>1});
 
-       return $resp unless @$vols;
+    return $resp unless @$vols;
 
-       $resp->{volume_holds} = $e->search_action_hold_request(
-               { 
-                       hold_type => OILS_HOLD_TYPE_VOLUME, 
-                       target => $vols,
-                       %$args }, 
-               {idlist=>1} );
+    $resp->{volume_holds} = $e->search_action_hold_request(
+        {
+            hold_type => OILS_HOLD_TYPE_VOLUME,
+            target => $vols,
+            %$args },
+        {idlist=>1} );
 
-       my $copies = $e->search_asset_copy(
-               { call_number => $vols, deleted => 'f' }, {idlist=>1});
+    my $copies = $e->search_asset_copy(
+        { call_number => $vols, deleted => 'f' }, {idlist=>1});
 
-       return $resp unless @$copies;
+    return $resp unless @$copies;
 
-       $resp->{copy_holds} = $e->search_action_hold_request(
-               { 
-                       hold_type => OILS_HOLD_TYPE_COPY,
-                       target => $copies,
-                       %$args }, 
-               {idlist=>1} );
+    $resp->{copy_holds} = $e->search_action_hold_request(
+        {
+            hold_type => OILS_HOLD_TYPE_COPY,
+            target => $copies,
+            %$args },
+        {idlist=>1} );
 
-       $resp->{recall_holds} = $e->search_action_hold_request(
-               { 
-                       hold_type => OILS_HOLD_TYPE_RECALL,
-                       target => $copies,
-                       %$args }, 
-               {idlist=>1} );
+    $resp->{recall_holds} = $e->search_action_hold_request(
+        {
+            hold_type => OILS_HOLD_TYPE_RECALL,
+            target => $copies,
+            %$args },
+        {idlist=>1} );
 
-       $resp->{force_holds} = $e->search_action_hold_request(
-               { 
-                       hold_type => OILS_HOLD_TYPE_FORCE,
-                       target => $copies,
-                       %$args }, 
-               {idlist=>1} );
+    $resp->{force_holds} = $e->search_action_hold_request(
+        {
+            hold_type => OILS_HOLD_TYPE_FORCE,
+            target => $copies,
+            %$args },
+        {idlist=>1} );
 
-       return $resp;
+    return $resp;
 }
 
 
@@ -2980,9 +3327,9 @@ __PACKAGE__->register_method(
 );
 
 sub uber_hold {
-       my($self, $client, $auth, $hold_id, $args) = @_;
-       my $e = new_editor(authtoken=>$auth);
-       $e->checkauth or return $e->event;
+    my($self, $client, $auth, $hold_id, $args) = @_;
+    my $e = new_editor(authtoken=>$auth);
+    $e->checkauth or return $e->event;
     return uber_hold_impl($e, $hold_id, $args);
 }
 
@@ -2994,9 +3341,9 @@ __PACKAGE__->register_method(
 );
 
 sub batch_uber_hold {
-       my($self, $client, $auth, $hold_ids, $args) = @_;
-       my $e = new_editor(authtoken=>$auth);
-       $e->checkauth or return $e->event;
+    my($self, $client, $auth, $hold_ids, $args) = @_;
+    my $e = new_editor(authtoken=>$auth);
+    $e->checkauth or return $e->event;
     $client->respond(uber_hold_impl($e, $_, $args)) for @$hold_ids;
     return undef;
 }
@@ -3005,43 +3352,44 @@ sub uber_hold_impl {
     my($e, $hold_id, $args) = @_;
     $args ||= {};
 
-       my $hold = $e->retrieve_action_hold_request(
-               [
-                       $hold_id,
-                       {
-                               flesh => 1,
-                               flesh_fields => { ahr => [ 'current_copy', 'usr', 'notes' ] }
-                       }
-               ]
-       ) or return $e->event;
+    my $hold = $e->retrieve_action_hold_request(
+        [
+            $hold_id,
+            {
+                flesh => 1,
+                flesh_fields => { ahr => [ 'current_copy', 'usr', 'notes' ] }
+            }
+        ]
+    ) or return $e->event;
 
     if($hold->usr->id ne $e->requestor->id) {
-        # A user is allowed to see his/her own holds
-           $e->allowed('VIEW_HOLD') or return $e->event;
-        $hold->notes( # filter out any non-staff ("private") notes
-            [ grep { !$U->is_true($_->staff) } @{$hold->notes} ] );
+        # caller is asking for someone else's hold
+        $e->allowed('VIEW_HOLD') or return $e->event;
+        $hold->notes( # filter out any non-staff ("private") notes (unless marked as public)
+            [ grep { $U->is_true($_->staff) or $U->is_true($_->pub) } @{$hold->notes} ] );
 
     } else {
         # caller is asking for own hold, but may not have permission to view staff notes
-           unless($e->allowed('VIEW_HOLD')) {
-            $hold->notes( # filter out any staff notes
-                [ grep { $U->is_true($_->staff) } @{$hold->notes} ] );
+        unless($e->allowed('VIEW_HOLD')) {
+            $hold->notes( # filter out any staff notes (unless marked as public)
+                [ grep { !$U->is_true($_->staff) or $U->is_true($_->pub) } @{$hold->notes} ] );
         }
     }
 
-       my $user = $hold->usr;
-       $hold->usr($user->id);
+    my $user = $hold->usr;
+    $hold->usr($user->id);
 
 
-       my( $mvr, $volume, $copy, $issuance, $part, $bre ) = find_hold_mvr($e, $hold, $args->{suppress_mvr});
+    my( $mvr, $volume, $copy, $issuance, $part, $bre ) = find_hold_mvr($e, $hold, $args->{suppress_mvr});
 
-       flesh_hold_notices([$hold], $e) unless $args->{suppress_notices};
-       flesh_hold_transits([$hold]) unless $args->{suppress_transits};
+    flesh_hold_notices([$hold], $e) unless $args->{suppress_notices};
+    flesh_hold_transits([$hold]) unless $args->{suppress_transits};
 
     my $details = retrieve_hold_queue_status_impl($e, $hold);
 
     my $resp = {
-        hold           => $hold,
+        hold    => $hold,
+        bre_id  => $bre->id,
         ($copy     ? (copy           => $copy)     : ()),
         ($volume   ? (volume         => $volume)   : ()),
         ($issuance ? (issuance       => $issuance) : ()),
@@ -3051,8 +3399,12 @@ sub uber_hold_impl {
         %$details
     };
 
+    $resp->{copy}->location(
+        $e->retrieve_asset_copy_location($resp->{copy}->location))
+        if $resp->{copy} and $args->{flesh_acpl};
+
     unless($args->{suppress_patron_details}) {
-           my $card = $e->retrieve_actor_card($user->card) or return $e->event;
+        my $card = $e->retrieve_actor_card($user->card) or return $e->event;
         $resp->{patron_first}   = $user->first_given_name,
         $resp->{patron_last}    = $user->family_name,
         $resp->{patron_barcode} = $card->barcode,
@@ -3069,26 +3421,26 @@ sub uber_hold_impl {
 # hold is all about
 # -----------------------------------------------------
 sub find_hold_mvr {
-       my( $e, $hold, $no_mvr ) = @_;
+    my( $e, $hold, $no_mvr ) = @_;
 
-       my $tid;
-       my $copy;
-       my $volume;
+    my $tid;
+    my $copy;
+    my $volume;
     my $issuance;
     my $part;
 
-       if( $hold->hold_type eq OILS_HOLD_TYPE_METARECORD ) {
-               my $mr = $e->retrieve_metabib_metarecord($hold->target)
-                       or return $e->event;
-               $tid = $mr->master_record;
+    if( $hold->hold_type eq OILS_HOLD_TYPE_METARECORD ) {
+        my $mr = $e->retrieve_metabib_metarecord($hold->target)
+            or return $e->event;
+        $tid = $mr->master_record;
 
-       } elsif( $hold->hold_type eq OILS_HOLD_TYPE_TITLE ) {
-               $tid = $hold->target;
+    } elsif( $hold->hold_type eq OILS_HOLD_TYPE_TITLE ) {
+        $tid = $hold->target;
 
-       } elsif( $hold->hold_type eq OILS_HOLD_TYPE_VOLUME ) {
-               $volume = $e->retrieve_asset_call_number($hold->target)
-                       or return $e->event;
-               $tid = $volume->record;
+    } elsif( $hold->hold_type eq OILS_HOLD_TYPE_VOLUME ) {
+        $volume = $e->retrieve_asset_call_number($hold->target)
+            or return $e->event;
+        $tid = $volume->record;
 
     } elsif( $hold->hold_type eq OILS_HOLD_TYPE_ISSUANCE ) {
         $issuance = $e->retrieve_serial_issuance([
@@ -3105,28 +3457,28 @@ sub find_hold_mvr {
 
         $tid = $part->record;
 
-       } elsif( $hold->hold_type eq OILS_HOLD_TYPE_COPY || $hold->hold_type eq OILS_HOLD_TYPE_RECALL || $hold->hold_type eq OILS_HOLD_TYPE_FORCE ) {
-               $copy = $e->retrieve_asset_copy([
-            $hold->target, 
+    } elsif( $hold->hold_type eq OILS_HOLD_TYPE_COPY || $hold->hold_type eq OILS_HOLD_TYPE_RECALL || $hold->hold_type eq OILS_HOLD_TYPE_FORCE ) {
+        $copy = $e->retrieve_asset_copy([
+            $hold->target,
             {flesh => 1, flesh_fields => {acp => ['call_number']}}
         ]) or return $e->event;
-        
-               $volume = $copy->call_number;
-               $tid = $volume->record;
-       }
 
-       if(!$copy and ref $hold->current_copy ) {
-               $copy = $hold->current_copy;
-               $hold->current_copy($copy->id);
-       }
+        $volume = $copy->call_number;
+        $tid = $volume->record;
+    }
 
-       if(!$volume and $copy) {
-               $volume = $e->retrieve_asset_call_number($copy->call_number);
-       }
+    if(!$copy and ref $hold->current_copy ) {
+        $copy = $hold->current_copy;
+        $hold->current_copy($copy->id);
+    }
+
+    if(!$volume and $copy) {
+        $volume = $e->retrieve_asset_call_number($copy->call_number);
+    }
 
     # TODO return metarcord mvr for M holds
-       my $title = $e->retrieve_biblio_record_entry($tid);
-       return ( ($no_mvr) ? undef : $U->record_to_mvr($title), $volume, $copy, $issuance, $part, $title );
+    my $title = $e->retrieve_biblio_record_entry($tid);
+    return ( ($no_mvr) ? undef : $U->record_to_mvr($title), $volume, $copy, $issuance, $part, $title );
 }
 
 __PACKAGE__->register_method(
@@ -3171,6 +3523,8 @@ sub clear_shelf_cache {
                         first_given_name second_given_name family_name alias
                     /],
                     "acn" => ["label"],
+                    "acnp" => [{column => "label", alias => "prefix"}],
+                    "acns" => [{column => "label", alias => "suffix"}],
                     "bre" => ["marc"],
                     "acpl" => ["name"],
                     "ahr" => ["id"]
@@ -3185,6 +3539,12 @@ sub clear_shelf_cache {
                                     "join" => {
                                         "bre" => {
                                             "field" => "id", "fkey" => "record"
+                                        },
+                                        "acnp" => {
+                                            "field" => "id", "fkey" => "prefix"
+                                        },
+                                        "acns" => {
+                                            "field" => "id", "fkey" => "suffix"
                                         }
                                     }
                                 },
@@ -3227,51 +3587,44 @@ __PACKAGE__->register_method(
 );
 
 sub clear_shelf_process {
-       my($self, $client, $auth, $org_id, $match_copy) = @_;
+    my($self, $client, $auth, $org_id, $match_copy) = @_;
 
-    my $current_copy = { '!=' => undef };
-    $current_copy = { '=' => $match_copy } if $match_copy;
-
-       my $e = new_editor(authtoken=>$auth, xact => 1);
-       $e->checkauth or return $e->die_event;
-       my $cache = OpenSRF::Utils::Cache->new('global');
+    my $e = new_editor(authtoken=>$auth);
+    $e->checkauth or return $e->die_event;
+    my $cache = OpenSRF::Utils::Cache->new('global');
 
     $org_id ||= $e->requestor->ws_ou;
-       $e->allowed('UPDATE_HOLD', $org_id) or return $e->die_event;
+    $e->allowed('UPDATE_HOLD', $org_id) or return $e->die_event;
 
     my $copy_status = $U->ou_ancestor_setting_value($org_id, 'circ.holds.clear_shelf.copy_status');
 
-    # Find holds on the shelf that have been there too long
-    my $hold_ids = $e->search_action_hold_request(
-        {   shelf_expire_time => {'<' => 'now'},
-            pickup_lib        => $org_id,
-            cancel_time       => undef,
-            fulfillment_time  => undef,
-            shelf_time        => {'!=' => undef},
-            capture_time      => {'!=' => undef},
-            current_copy      => $current_copy,
-        },
-        { idlist => 1 }
-    );
+    my @hold_ids = $self->method_lookup(
+        "open-ils.circ.captured_holds.id_list.expired_on_shelf.retrieve"
+    )->run($auth, $org_id, $match_copy);
+
+    $e->xact_begin;
 
     my @holds;
+    my @canceled_holds; # newly canceled holds
     my $chunk_size = 25; # chunked status updates
     my $counter = 0;
-    for my $hold_id (@$hold_ids) {
+    for my $hold_id (@hold_ids) {
 
         $logger->info("Clear shelf processing hold $hold_id");
-        
+
         my $hold = $e->retrieve_action_hold_request([
-            $hold_id, {   
+            $hold_id, {
                 flesh => 1,
                 flesh_fields => {ahr => ['current_copy']}
             }
         ]);
 
-        $hold->cancel_time('now');
-        $hold->cancel_cause(2); # Hold Shelf expiration
-        $e->update_action_hold_request($hold) or return $e->die_event;
-        delete_hold_copy_maps($self, $e, $hold->id) and return $e->die_event;
+        if (!$hold->cancel_time) { # may be canceled but still on the holds shelf
+            $hold->cancel_time('now');
+            $hold->cancel_cause(2); # Hold Shelf expiration
+            $e->update_action_hold_request($hold) or return $e->die_event;
+            push(@canceled_holds, $hold_id);
+        }
 
         my $copy = $hold->current_copy;
 
@@ -3292,7 +3645,8 @@ sub clear_shelf_process {
         my %cache_data = (
             hold => [],
             transit => [],
-            shelf => []
+            shelf => [],
+            pl_changed => pickup_lib_changed_on_shelf_holds($e, $org_id, \@hold_ids)
         );
 
         for my $hold (@holds) {
@@ -3321,10 +3675,19 @@ sub clear_shelf_process {
         # tell the client we're done
         $client->respond_complete({cache_key => $cache_key});
 
+        # ------------
         # fire off the hold cancelation trigger and wait for response so don't flood the service
+
+        # refetch the holds to pick up the caclulated cancel_time,
+        # which may be needed by Action/Trigger
+        $e->xact_begin;
+        my $updated_holds = [];
+        $updated_holds = $e->search_action_hold_request({id => \@canceled_holds}, {substream => 1}) if (@canceled_holds > 0);
+        $e->rollback;
+
         $U->create_events_for_hook(
-            'hold_request.cancel.expire_holds_shelf', 
-            $_, $org_id, undef, undef, 1) for @holds;
+            'hold_request.cancel.expire_holds_shelf',
+            $_, $org_id, undef, undef, 1) for @$updated_holds;
 
     } else {
         # tell the client we're done
@@ -3332,6 +3695,42 @@ sub clear_shelf_process {
     }
 }
 
+# returns IDs for holds that are on the holds shelf but 
+# have had their pickup_libs change while on the shelf.
+sub pickup_lib_changed_on_shelf_holds {
+    my $e = shift;
+    my $org_id = shift;
+    my $ignore_holds = shift;
+    $ignore_holds = [$ignore_holds] if !ref($ignore_holds);
+
+    my $query = {
+        select => { alhr => ['id'] },
+        from   => {
+            alhr => {
+                acp => {
+                    field => 'id',
+                    fkey  => 'current_copy'
+                },
+            }
+        },
+        where => {
+            '+acp' => { status => OILS_COPY_STATUS_ON_HOLDS_SHELF },
+            '+alhr' => {
+                capture_time     => { "!=" => undef },
+                fulfillment_time => undef,
+                current_shelf_lib => $org_id,
+                pickup_lib => {'!='  => {'+alhr' => 'current_shelf_lib'}}
+            }
+        }
+    };
+
+    $query->{where}->{'+alhr'}->{id} =
+        {'not in' => $ignore_holds} if @$ignore_holds;
+
+    my $hold_ids = $e->json_query($query);
+    return [ map { $_->{id} } @$hold_ids ];
+}
+
 __PACKAGE__->register_method(
     method    => 'usr_hold_summary',
     api_name  => 'open-ils.circ.holds.user_summary',
@@ -3343,13 +3742,13 @@ __PACKAGE__->register_method(
 sub usr_hold_summary {
     my($self, $conn, $auth, $user_id) = @_;
 
-       my $e = new_editor(authtoken=>$auth);
-       $e->checkauth or return $e->event;
-       $e->allowed('VIEW_HOLD') or return $e->event;
+    my $e = new_editor(authtoken=>$auth);
+    $e->checkauth or return $e->event;
+    $e->allowed('VIEW_HOLD') or return $e->event;
 
     my $holds = $e->search_action_hold_request(
-        {  
-            usr =>  $user_id , 
+        {
+            usr =>  $user_id ,
             fulfillment_time => undef,
             cancel_time      => undef,
         }
@@ -3366,7 +3765,7 @@ __PACKAGE__->register_method(
     method    => 'hold_has_copy_at',
     api_name  => 'open-ils.circ.hold.has_copy_at',
     signature => {
-        desc   => 
+        desc   =>
                 'Returns the ID of the found copy and name of the shelving location if there is ' .
                 'an available copy at the specified org unit.  Returns empty hash otherwise.  '   .
                 'The anticipated use for this method is to determine whether an item is '         .
@@ -3375,16 +3774,16 @@ __PACKAGE__->register_method(
                 'checking out the item.' ,
         params => [
             { desc => 'Authentication Token', type => 'string' },
-            { desc => 'Method Arguments.  Options include: hold_type, hold_target, org_unit.  ' 
+            { desc => 'Method Arguments.  Options include: hold_type, hold_target, org_unit.  '
                     . 'hold_type is the hold type code (T, V, C, M, ...).  '
-                    . 'hold_target is the identifier of the hold target object.  ' 
-                    . 'org_unit is org unit ID.', 
-              type => 'object' 
+                    . 'hold_target is the identifier of the hold target object.  '
+                    . 'org_unit is org unit ID.',
+              type => 'object'
             }
         ],
-        return => { 
+        return => {
             desc => q/Result hash like { "copy" : copy_id, "location" : location_name }, empty hash on misses, event on error./,
-            type => 'object' 
+            type => 'object'
         }
     }
 );
@@ -3392,8 +3791,8 @@ __PACKAGE__->register_method(
 sub hold_has_copy_at {
     my($self, $conn, $auth, $args) = @_;
 
-       my $e = new_editor(authtoken=>$auth);
-       $e->checkauth or return $e->event;
+    my $e = new_editor(authtoken=>$auth);
+    $e->checkauth or return $e->event;
 
     my $hold_type   = $$args{hold_type};
     my $hold_target = $$args{hold_target};
@@ -3407,18 +3806,34 @@ sub hold_has_copy_at {
                 ccs  => {field => 'id', filter => { holdable => 't'}, fkey => 'status'  }
             }
         },
-        where => {'+acp' => { circulate => 't', deleted => 'f', holdable => 't', circ_lib => $org_unit}},
+        where => {'+acp' => { circulate => 't', deleted => 'f', holdable => 't', circ_lib => $org_unit, status => [0,7]}},
         limit => 1
     };
 
-    if($hold_type eq 'C') {
+    if($hold_type eq 'C' or $hold_type eq 'F' or $hold_type eq 'R') {
 
         $query->{where}->{'+acp'}->{id} = $hold_target;
 
     } elsif($hold_type eq 'V') {
 
         $query->{where}->{'+acp'}->{call_number} = $hold_target;
-    
+
+    } elsif($hold_type eq 'P') {
+
+        $query->{from}->{acp}->{acpm} = {
+            field  => 'target_copy',
+            fkey   => 'id',
+            filter => {part => $hold_target},
+        };
+
+    } elsif($hold_type eq 'I') {
+
+        $query->{from}->{acp}->{sitem} = {
+            field  => 'unit',
+            fkey   => 'id',
+            filter => {issuance => $hold_target},
+        };
+
     } elsif($hold_type eq 'T') {
 
         $query->{from}->{acp}->{acn} = {
@@ -3459,7 +3874,7 @@ sub hold_has_copy_at {
 }
 
 
-# returns true if the user already has an item checked out 
+# returns true if the user already has an item checked out
 # that could be used to fulfill the requested hold.
 sub hold_item_is_checked_out {
     my($e, $user_id, $hold_type, $hold_target) = @_;
@@ -3657,10 +4072,15 @@ __PACKAGE__->register_method(
     method    => 'rec_hold_count',
     api_name  => 'open-ils.circ.bre.holds.count',
     signature => {
-        desc => q/Returns the total number of holds that target the 
+        desc => q/Returns the total number of holds that target the
             selected bib record or its associated copies and call_numbers/,
         params => [
             { desc => 'Bib ID', type => 'number' },
+            { desc => q/Optional arguments.  Supported arguments include:
+                "pickup_lib_descendant" -> limit holds to those whose pickup
+                library is equal to or is a child of the provided org unit/,
+                type => 'object'
+            }
         ],
         return => {desc => 'Hold count', type => 'number'}
     }
@@ -3670,7 +4090,7 @@ __PACKAGE__->register_method(
     method    => 'rec_hold_count',
     api_name  => 'open-ils.circ.mmr.holds.count',
     signature => {
-        desc => q/Returns the total number of holds that target the 
+        desc => q/Returns the total number of holds that target the
             selected metarecord or its associated copies, call_numbers, and bib records/,
         params => [
             { desc => 'Metarecord ID', type => 'number' },
@@ -3679,15 +4099,15 @@ __PACKAGE__->register_method(
     }
 );
 
-# XXX Need to add type I (and, soon, type P) holds to these counts
+# XXX Need to add type I holds to these counts
 sub rec_hold_count {
-    my($self, $conn, $target_id) = @_;
-
+    my($self, $conn, $target_id, $args) = @_;
+    $args ||= {};
 
     my $mmr_join = {
         mmrsm => {
-            field => 'id',
-            fkey => 'source',
+            field => 'source',
+            fkey => 'id',
             filter => {metarecord => $target_id}
         }
     };
@@ -3718,7 +4138,7 @@ sub rec_hold_count {
         from => 'ahr',
         where => {
             '+ahr' => {
-                cancel_time => undef, 
+                cancel_time => undef,
                 fulfillment_time => undef,
                 '-or' => [
                     {
@@ -3745,6 +4165,17 @@ sub rec_hold_count {
                     },
                     {
                         '-and' => {
+                            hold_type => 'P',
+                            target => {
+                                in => {
+                                    select => {bmp => ['id']},
+                                    from => {bmp => $bre_join}
+                                }
+                            }
+                        }
+                    },
+                    {
+                        '-and' => {
                             hold_type => 'T',
                             target => $target_id
                         }
@@ -3755,7 +4186,7 @@ sub rec_hold_count {
     };
 
     if($self->api_name =~ /mmr/) {
-        $query->{where}->{'+ahr'}->{'-or'}->[2] = {
+        $query->{where}->{'+ahr'}->{'-or'}->[3] = {
             '-and' => {
                 hold_type => 'T',
                 target => {
@@ -3767,7 +4198,7 @@ sub rec_hold_count {
             }
         };
 
-        $query->{where}->{'+ahr'}->{'-or'}->[3] = {
+        $query->{where}->{'+ahr'}->{'-or'}->[4] = {
             '-and' => {
                 hold_type => 'M',
                 target => $target_id
@@ -3776,12 +4207,181 @@ sub rec_hold_count {
     }
 
 
+    if (my $pld = $args->{pickup_lib_descendant}) {
+
+        my $top_ou = new_editor()->search_actor_org_unit(
+            {parent_ou => undef}
+        )->[0]; # XXX Assumes single root node. Not alone in this...
+
+        $query->{where}->{'+ahr'}->{pickup_lib} = {
+            in => {
+                select  => {aou => [{ 
+                    column => 'id', 
+                    transform => 'actor.org_unit_descendants', 
+                    result_field => 'id' 
+                }]},
+                from    => 'aou',
+                where   => {id => $pld}
+            }
+        } if ($pld != $top_ou->id);
+    }
+
+
     return new_editor()->json_query($query)->[0]->{count};
 }
 
+# A helper function to calculate a hold's expiration time at a given
+# org_unit. Takes the org_unit as an argument and returns either the
+# hold expire time as an ISO8601 string or undef if there is no hold
+# expiration interval set for the subject ou.
+sub calculate_expire_time
+{
+    my $ou = shift;
+    my $interval = $U->ou_ancestor_setting_value($ou, OILS_SETTING_HOLD_EXPIRE);
+    if($interval) {
+        my $date = DateTime->now->add(seconds => OpenSRF::Utils::interval_to_seconds($interval));
+        return $U->epoch2ISO8601($date->epoch);
+    }
+    return undef;
+}
 
 
+__PACKAGE__->register_method(
+    method    => 'mr_hold_filter_attrs',
+    api_name  => 'open-ils.circ.mmr.holds.filters',
+    authoritative => 1,
+    stream => 1,
+    signature => {
+        desc => q/
+            Returns the set of available formats and languages for the
+            constituent records of the provided metarcord.
+            If an array of hold IDs is also provided, information about
+            each is returned as well.  This information includes:
+            1. a slightly easier to read version of holdable_formats
+            2. attributes describing the set of format icons included
+               in the set of desired, constituent records.
+        /,
+        params => [
+            {desc => 'Metarecord ID', type => 'number'},
+            {desc => 'Context Org ID', type => 'number'},
+            {desc => 'Hold ID List', type => 'array'},
+        ],
+        return => {
+            desc => q/
+                Stream of objects.  The first will have a 'metarecord' key
+                containing non-hold-specific metarecord information, subsequent
+                responses will contain a 'hold' key containing hold-specific
+                information
+            /, 
+            type => 'object'
+        }
+    }
+);
+
+sub mr_hold_filter_attrs { 
+    my ($self, $client, $mr_id, $org_id, $hold_ids) = @_;
+    my $e = new_editor();
+
+    # by default, return MR / hold attributes for all constituent
+    # records with holdable copies.  If there is a hard boundary,
+    # though, limit to records with copies within the boundary,
+    # since anything outside the boundary can never be held.
+    my $org_depth = 0;
+    if ($org_id) {
+        $org_depth = $U->ou_ancestor_setting_value(
+            $org_id, OILS_SETTING_HOLD_HARD_BOUNDARY) || 0;
+    }
+
+    # get all org-scoped records w/ holdable copies for this metarecord
+    my ($bre_ids) = $self->method_lookup(
+        'open-ils.circ.holds.metarecord.filtered_records')->run(
+            $mr_id, undef, $org_id, $org_depth);
+
+    my $item_lang_attr = 'item_lang'; # configurable?
+    my $format_attr = $e->retrieve_config_global_flag(
+        'opac.metarecord.holds.format_attr')->value;
+
+    # helper sub for fetching ccvms for a batch of record IDs
+    sub get_batch_ccvms {
+        my ($e, $attr, $bre_ids) = @_;
+        return [] unless $bre_ids and @$bre_ids;
+        my $vals = $e->search_metabib_record_attr_flat({
+            attr => $attr,
+            id => $bre_ids
+        });
+        return [] unless @$vals;
+        return $e->search_config_coded_value_map({
+            ctype => $attr,
+            code => [map {$_->value} @$vals]
+        });
+    }
+
+    my $langs = get_batch_ccvms($e, $item_lang_attr, $bre_ids);
+    my $formats = get_batch_ccvms($e, $format_attr, $bre_ids);
+
+    $client->respond({
+        metarecord => {
+            id => $mr_id,
+            formats => $formats,
+            langs => $langs
+        }
+    });
 
+    return unless $hold_ids;
+    my $icon_attr = $e->retrieve_config_global_flag('opac.icon_attr');
+    $icon_attr = $icon_attr ? $icon_attr->value : '';
 
+    for my $hold_id (@$hold_ids) {
+        my $hold = $e->retrieve_action_hold_request($hold_id) 
+            or return $e->event;
+
+        next unless $hold->hold_type eq 'M';
+
+        my $resp = {
+            hold => {
+                id => $hold_id,
+                formats => [],
+                langs => []
+            }
+        };
+
+        # collect the ccvm's for the selected formats / language
+        # (i.e. the holdable formats) on the MR.
+        # this assumes a two-key structure for format / language,
+        # though no assumption is made about the keys themselves.
+        my $hformats = OpenSRF::Utils::JSON->JSON2perl($hold->holdable_formats);
+        my $lang_vals = [];
+        my $format_vals = [];
+        for my $val (values %$hformats) {
+            # val is either a single ccvm or an array of them
+            $val = [$val] unless ref $val eq 'ARRAY';
+            for my $node (@$val) {
+                push (@$lang_vals, $node->{_val})   
+                    if $node->{_attr} eq $item_lang_attr; 
+                push (@$format_vals, $node->{_val})   
+                    if $node->{_attr} eq $format_attr;
+            }
+        }
+
+        # fetch the ccvm's for consistency with the {metarecord} blob
+        $resp->{hold}{formats} = $e->search_config_coded_value_map({
+            ctype => $format_attr, code => $format_vals});
+        $resp->{hold}{langs} = $e->search_config_coded_value_map({
+            ctype => $item_lang_attr, code => $lang_vals});
+
+        # find all of the bib records within this metarcord whose 
+        # format / language match the holdable formats on the hold
+        my ($bre_ids) = $self->method_lookup(
+            'open-ils.circ.holds.metarecord.filtered_records')->run(
+                $hold->target, $hold->holdable_formats, 
+                $hold->selection_ou, $hold->selection_depth);
+
+        # now find all of the 'icon' attributes for the records
+        $resp->{hold}{icons} = get_batch_ccvms($e, $icon_attr, $bre_ids);
+        $client->respond($resp);
+    }
+
+    return;
+}
 
 1;