biz logic for alerts
authorMike Rylander <mrylander@gmail.com>
Wed, 4 Nov 2015 21:27:04 +0000 (16:27 -0500)
committerGalen Charlton <gmc@equinoxinitiative.org>
Mon, 27 Mar 2017 20:04:05 +0000 (16:04 -0400)
Signed-off-by: Mike Rylander <mrylander@gmail.com>
Open-ILS/src/perlmods/lib/OpenILS/Application/AppUtils.pm
Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Circulate.pm
Open-ILS/web/js/ui/default/staff/circ/services/circ.js

index a8c6c9f..d2940c6 100644 (file)
@@ -2260,6 +2260,14 @@ sub unique_unnested_numbers {
     );
 }
 
+# Given a list of numbers, turn them into a PG array, skipping undef's
+sub intarray2pgarray {
+    my $class = shift;
+    no warnings 'numeric';
+
+    return '{' . join( ',', map(int, grep { defined && /^\d+$/ } @_) ) . '}';
+}
+
 # Check if a transaction should be left open or closed. Close the
 # transaction if it should be closed or open it otherwise. Returns
 # undef on success or a failure event.
index 74e425b..e8f007f 100644 (file)
@@ -155,6 +155,7 @@ sub run_method {
     my( $self, $conn, $auth, $args ) = @_;
     translate_legacy_args($args);
     $args->{override_args} = { all => 1 } unless defined $args->{override_args};
+    $args->{new_copy_alerts} ||= $self->api_level > 1 ? 1 : 0;
     my $api = $self->api_name;
 
     my $circulator = 
@@ -227,13 +228,13 @@ sub run_method {
 
     $circulator->is_renewal(1) if $api =~ /renew/;
     $circulator->is_checkin(1) if $api =~ /checkin/;
+    $circulator->is_checkout(1) if $api =~ /checkout/;
+    $circulator->override(1) if $api =~ /override/o;
 
     $circulator->mk_env();
     $circulator->noop(1) if $circulator->claims_never_checked_out;
 
     return circ_events($circulator) if $circulator->bail_out;
-    
-    $circulator->override(1) if $api =~ /override/o;
 
     if( $api =~ /checkout\.permit/ ) {
         $circulator->do_permit();
@@ -260,7 +261,6 @@ sub run_method {
         return $data;
 
     } elsif( $api =~ /checkout/ ) {
-        $circulator->is_checkout(1);
         $circulator->do_checkout();
 
     } elsif( $circulator->is_res_checkin ) {
@@ -270,7 +270,6 @@ sub run_method {
         $circulator->do_checkin();
 
     } elsif( $api =~ /renew/ ) {
-        $circulator->is_renewal(1);
         $circulator->do_renew();
     }
 
@@ -394,6 +393,10 @@ my @AUTOLOAD_FIELDS = qw/
     copy
     copy_id
     copy_barcode
+    user_copy_alerts
+    system_copy_alerts
+    next_copy_status
+    copy_state
     patron
     patron_id
     patron_barcode
@@ -630,10 +633,174 @@ sub save_trimmed_copy {
     }
 }
 
+sub collect_user_copy_alerts {
+    my $self = shift;
+    my $e = $self->editor;
+
+    if($self->copy) {
+        my $alerts = $e->search_asset_copy_alert([
+            {copy => $self->copy->id, ack_time => undef}
+            {flesh => 1, flesh_fields => { ccat => [ qw/ alert_type / ] }}
+        ]);
+        if (ref $alerts eq "ARRAY") {
+            $logger->info("circulator: found " . scalar(@$alerts) . " alerts for copy" .
+                $self->copy->id);
+            $self->user_copy_alerts($alerts);
+        }
+    }
+}
+
+sub filter_user_copy_alerts {
+    my $self = shift;
+
+    if(my $alerts = $self->user_copy_alerts) {
+
+        my $suppress_orgs = $U->get_org_descendants($self->circ_lib);
+        my $suppressions = $e->search_actor_copy_alert_suppress(
+            {org => $suppress_orgs}
+        );
+
+        my @final_alerts;
+        foreach my $a (@$alerts) {
+            # filter on event type
+            if (defined $a->alert_type) {
+                next if ($a->alert_type->event eq 'CHECKIN' && !$self->is_checkin);
+                next if ($a->alert_type->event eq 'CHECKOUT' && !$self->is_checkout);
+                next if (defined $a->alert_type->in_renew && $U->is_true($a->in_renew) && !$self->is_renewal);
+            }
+
+            # filter on suppression
+            next if (grep { $a->alert_type->id == $_->alert_type} @$suppressions);
+
+            # filter on "only at circ lib"
+            if (defined $a->alert_type->at_circ) {
+                my $copy_circ_lib = (ref $self->copy->circ_lib) ? 
+                    $self->copy->circ_lib->id : $self->copy->circ_lib;
+                my $orgs = $U->get_org_descendants($copy_circ_lib);
+
+                if ($U->is_true($a->alert_type->invert_location)) {
+                    next if (grep {$_ == $copy_circ_lib} @$orgs);
+                } else {
+                    next unless (grep {$_ == $copy_circ_lib} @$orgs);
+                }
+            }
+
+            # filter on "only at owning lib"
+            if (defined $a->alert_type->at_owning) {
+                my $copy_owning_lib = (ref $self->volume->owning_lib) ? 
+                    $self->volume->owning_lib->id : $self->volume->owning_lib;
+                my $orgs = $U->get_org_descendants($copy_owning_lib);
+
+                if ($U->is_true($a-->alert_type>invert_location)) {
+                    next if (grep {$_ == $copy_owning_lib} @$orgs);
+                } else {
+                    next unless (grep {$_ == $copy_owning_lib} @$orgs);
+                }
+            }
+
+            $a->alert_type->next_status($U->unique_unnested_numbers($a->alert_type->next_status));
+
+            push @final_alerts, $a;
+        }
+
+        $self->user_copy_alerts(\@final_alerts);
+    }
+}
+
+sub generate_system_copy_alerts {
+    my $self = shift;
+    return unless($self->copy);
+
+    my $e = $self->editor;
+
+    my $suppress_orgs = $U->get_org_descendants($self->circ_lib);
+    my $suppressions = $e->search_actor_copy_alert_suppress(
+        {org => $suppress_orgs}
+    );
+
+    # events we care about ...
+    my $event = [];
+    push(@$event, 'CHECKIN') if $self->is_checkin;
+    push(@$event, 'CHECKOUT') if $self->is_checkout;
+    return unless scalar(@$event);
+
+    my $alert_orgs = $U->get_org_ancestors($self->circ_lib);
+    my $alert_types = $e->search_config_copy_alert_type({
+        active    => 't'
+        scope_org => $alert_orgs,
+        event     => $event,
+        state => $self->copy_state,
+        '-or' => [ { in_renew => $self->is_renewal }, { in_renew => undef } ],
+    });
+
+    my @final_types;
+    foreach my $a (@$alert_types) {
+        # filter on "only at circ lib"
+        if (defined $a->at_circ) {
+            my $copy_circ_lib = (ref $self->copy->circ_lib) ? 
+                $self->copy->circ_lib->id : $self->copy->circ_lib;
+            my $orgs = $U->get_org_descendants($copy_circ_lib);
+
+            if ($U->is_true($a->invert_location)) {
+                next if (grep {$_ == $copy_circ_lib} @$orgs);
+            } else {
+                next unless (grep {$_ == $copy_circ_lib} @$orgs);
+            }
+        }
+
+        # filter on "only at owning lib"
+        if (defined $a->at_owning) {
+            my $copy_owning_lib = (ref $self->volume->owning_lib) ? 
+                $self->volume->owning_lib->id : $self->volume->owning_lib;
+            my $orgs = $U->get_org_descendants($copy_owning_lib);
+
+            if ($U->is_true($a->invert_location)) {
+                next if (grep {$_ == $copy_owning_lib} @$orgs);
+            } else {
+                next unless (grep {$_ == $copy_owning_lib} @$orgs);
+            }
+        }
+
+        push @final_types, $a;
+    }
+
+    if (@final_types) {
+        $logger->info("circulator: found " . scalar(@final_types) . " system alert types for copy" .
+            $self->copy->id);
+    }
+
+    my @alerts;
+    foreach my $t (@final_types) {
+        $t->next_status($U->unique_unnested_numbers($t->next_status));
+
+        my $alert = new Fieldmapper::asset::copy_alert;
+        $alert->alert_type($t->id);
+        $alert->copy($self->copy->id);
+        $alert->temp(1);
+        $alert->create_staff($e->requestor->id);
+        $alert->create_time('now');
+        $alert->ack_staff($e->requestor->id);
+        $alert->ack_time('now');
+
+        $alert = $e->create_asset_copy_alert($alert);
+
+        next unless $alert;
+
+        $alert->alert_type($t->clone);
+
+        push(@{$self->next_copy_status}, $t->next_status) if ($t->next_status);
+        push(@alerts, $alert) unless (grep {$_->alert_type == $t->id} @$suppressions);
+    }
+
+    $self->system_copy_alerts(\@alerts);
+}
+
 sub mk_env {
     my $self = shift;
     my $e = $self->editor;
 
+    $self->next_copy_status([]) unless (defined $self->next_copy_status);
+
     # --------------------------------------------------------------------------
     # Grab the fleshed copy
     # --------------------------------------------------------------------------
@@ -666,6 +833,7 @@ sub mk_env {
                         }
                     },
                     "where" => {
+                        deleted => 'f',
                         "+bresv" => {
                             "id" => (ref $self->reservation) ?
                                 $self->reservation->id : $self->reservation
@@ -682,6 +850,18 @@ sub mk_env {
     
         if($copy) {
             $self->save_trimmed_copy($copy);
+
+            # alerts!
+            $self->copy_state(
+                $e->json_query(
+                    {from => ['asset.copy_state', $copy->id]}
+                )->[0]{'asset.copy_state'}
+            );
+
+            $self->generate_system_copy_alerts;
+            $self->collect_user_copy_alerts;
+            $self->filter_user_copy_alerts;
+
         } else {
             # We can't renew if there is no copy
             return $self->bail_on_events(OpenILS::Event->new('ASSET_COPY_NOT_FOUND'))
@@ -1195,6 +1375,21 @@ sub run_copy_permit_scripts {
 
 sub check_copy_alert {
     my $self = shift;
+
+    if ($self->new_copy_alerts) {
+        my @alerts;
+        push @alerts, @{$self->user_copy_alerts}) # we have preexisting alerts 
+            if ($self->user_copy_alerts && @{$self->user_copy_alerts});
+
+        push @alerts, @{$self->system_copy_alerts}) # we have new dynamic alerts 
+            if ($self->system_copy_alerts && @{$self->system_copy_alerts});
+
+        if (@alerts) {
+            $self->bail_out(1) if (!$self->override);
+            return OpenILS::Event->new( 'COPY_ALERT_MESSAGE', payload => \@alerts);
+        }
+    }
+
     return undef if $self->is_renewal;
     return OpenILS::Event->new(
         'COPY_ALERT_MESSAGE', payload => $self->copy->alert_message)
@@ -2628,7 +2823,8 @@ sub do_checkin {
             $U->ou_ancestor_setting_value($self->circ->circ_lib, 'circ.claim_never_checked_out.mark_missing')) {
 
         # the item was not supposed to be checked out to the user and should now be marked as missing
-        $self->copy->status(OILS_COPY_STATUS_MISSING);
+        my $next_status = $self->next_copy_status->[0] || OILS_COPY_STATUS_MISSING;
+        $self->copy->status($next_status);
         $self->update_copy;
 
     } else {
@@ -2704,13 +2900,15 @@ sub reshelve_copy {
 
    my $stat = $U->copy_status($copy->status)->id;
 
+   my $next_status = $self->next_copy_status->[0] || OILS_COPY_STATUS_RESHELVING;
+
    if($force || (
       $stat != OILS_COPY_STATUS_ON_HOLDS_SHELF and
       $stat != OILS_COPY_STATUS_CATALOGING and
       $stat != OILS_COPY_STATUS_IN_TRANSIT and
-      $stat != OILS_COPY_STATUS_RESHELVING  )) {
+      $stat != $next_status  )) {
 
-        $copy->status( OILS_COPY_STATUS_RESHELVING );
+        $copy->status( $next_status );
             $self->update_copy;
             $self->checkin_changed(1);
     }
@@ -3321,7 +3519,8 @@ sub checkin_handle_circ_start {
     } elsif ($circ_lib != $self->circ_lib and $stat == OILS_COPY_STATUS_MISSING) {
         $logger->info("circulator: not updating copy status on checkin because copy is missing");
     } else {
-        $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
+        my $next_status = $self->next_copy_status->[0] || OILS_COPY_STATUS_RESHELVING;
+        $self->copy->status($U->copy_status($next_status));
         $self->update_copy;
     }
 
@@ -3491,7 +3690,8 @@ sub checkin_handle_lost_or_longoverdue {
         if ($immediately_available) {
             # item status does not need to be retained, so give it a
             # reshelving status as if it were a normal checkin
-            $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
+            my $next_status = $self->next_copy_status->[0] || OILS_COPY_STATUS_RESHELVING;
+            $self->copy->status($U->copy_status($next_status));
             $self->update_copy;
         } else {
             $logger->info("circulator: leaving lost/longoverdue copy".
@@ -3500,7 +3700,8 @@ sub checkin_handle_lost_or_longoverdue {
     } else {
         # lost/longoverdue item is home and processed, treat like a normal 
         # checkin from this point on
-        $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
+        my $next_status = $self->next_copy_status->[0] || OILS_COPY_STATUS_RESHELVING;
+        $self->copy->status($U->copy_status($next_status));
         $self->update_copy;
     }
 }
index df43abb..6703b50 100644 (file)
@@ -121,6 +121,7 @@ function($uibModal , $q , egCore , egAlertDialog , egConfirmDialog,
     // options : non-parameter controls.  e.g. "override", "check_barcode"
     service.checkout = function(params, options) {
         if (!options) options = {};
+        params.new_copy_alerts = 1;
 
         console.debug('egCirc.checkout() : ' 
             + js2JSON(params) + ' : ' + js2JSON(options));
@@ -167,6 +168,7 @@ function($uibModal , $q , egCore , egAlertDialog , egConfirmDialog,
     // Rejected if the renewal cannot be completed.
     service.renew = function(params, options) {
         if (!options) options = {};
+        params.new_copy_alerts = 1;
 
         console.debug('egCirc.renew() : ' 
             + js2JSON(params) + ' : ' + js2JSON(options));
@@ -208,6 +210,7 @@ function($uibModal , $q , egCore , egAlertDialog , egConfirmDialog,
     // Rejected if the checkin cannot be completed.
     service.checkin = function(params, options) {
         if (!options) options = {};
+        params.new_copy_alerts = 1;
 
         console.debug('egCirc.checkin() : ' 
             + js2JSON(params) + ' : ' + js2JSON(options));
@@ -1457,17 +1460,21 @@ function($uibModal , $q , egCore , egAlertDialog , egConfirmDialog,
     // action == what action to take if the user confirms the alert
     service.copy_alert_dialog = function(evt, params, options, action) {
         if (angular.isArray(evt)) evt = evt[0];
-        return egConfirmDialog.open(
-            egCore.strings.COPY_ALERT_MSG_DIALOG_TITLE, 
-            evt.payload,  // payload == alert message text
-            {   copy_barcode : params.copy_barcode,
-                ok : function() {},
-                cancel : function() {}
-            }
-        ).result.then(function() {
-            options.override = true;
-            return service[action](params, options);
-        });
+        if (!angular.isArray(evt.payload)) {
+            return egConfirmDialog.open(
+                egCore.strings.COPY_ALERT_MSG_DIALOG_TITLE, 
+                evt.payload,  // payload == alert message text
+                {   copy_barcode : params.copy_barcode,
+                    ok : function() {},
+                    cancel : function() {}
+                }
+            ).result.then(function() {
+                options.override = true;
+                return service[action](params, options);
+            });
+        } else { // we got a list of copy alert objects ...
+            // TODO: build a chain of alert ack (and maybe status selection) dialogs
+        }
     }
 
     // check the barcode.  If it's no good, show the warning dialog