]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/support-scripts/patron-penalties-batch.pl
LP 2061136 follow-up: ng lint --fix
[Evergreen.git] / Open-ILS / src / support-scripts / patron-penalties-batch.pl
1 #!/usr/bin/perl
2 # ---------------------------------------------------------------
3 # Copyright (C) 2022 King County Library System
4 # Author: Bill Erickson <berickxx@gmail.com>
5 #
6 # This program is free software; you can redistribute it and/or
7 # modify it under the terms of the GNU General Public License
8 # as published by the Free Software Foundation; either version 2
9 # of the License, or (at your option) any later version.
10 #
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14 # GNU General Public License for more details.
15 # ---------------------------------------------------------------
16 use strict;
17 use warnings;
18 use Getopt::Long;
19 use OpenSRF::System;
20 use OpenSRF::AppSession;
21 use OpenSRF::Utils::SettingsClient;
22 use OpenILS::Utils::Fieldmapper;
23 use OpenSRF::Utils::Logger q/$logger/;
24 use OpenILS::Utils::CStoreEditor;
25 use OpenILS::Application::AppUtils;
26
27 my $U = 'OpenILS::Application::AppUtils';
28 $ENV{OSRF_LOG_CLIENT} = 1;
29
30 my $osrf_config = '/openils/conf/opensrf_core.xml';
31 my $ids_file;
32 my $process_as = 'admin';
33 my $min_id = 0;
34 my $max_id;
35 my $has_open_circ = 0;
36 my @included_penalties;
37 my $owes_more_than;
38 my $owes_less_than;
39 my $has_penalty;
40 my $home_ou_context;
41 my $no_has_penalty;
42 my $verbose;
43 my $help;
44 my $batch_size = 100;
45 my $authtoken;
46 my $auth_user_home;
47 my $e;
48
49 my $ops = GetOptions(
50     'osrf-config=s'     => \$osrf_config,
51     'ids-file=s'        => \$ids_file,
52     'process-as=s'      => \$process_as,
53     'min-id=s'          => \$min_id,
54     'max-id=s'          => \$max_id,
55     'has-open-circ'     => \$has_open_circ,
56     'owes-more-than=s'  => \$owes_more_than,
57     'owes-less-than=s'  => \$owes_less_than,
58     'has-penalty=s'     => \$has_penalty,
59     'no-has-penalty=s'  => \$no_has_penalty,
60     'include-penalty=s' => \@included_penalties,
61     'patron-home-context'   => \$home_ou_context,
62     'verbose'           => \$verbose,
63     'help'              => \$help
64 );
65
66 sub help {
67     print <<HELP;
68
69     Synopsis:
70         Update patron penalties in batch with options for filtering which
71         patrons to process.
72
73     Usage:
74
75         $0
76
77         --osrf-config [/openils/conf/opensrf_core.xml]
78
79         --process-as <eg-account>
80             Username of an Evergreen account to use for creating the
81             internal auth session.  Defaults to 'admin'.
82
83         --patron-home-context
84             Use each user's home library as the penalty calculation
85             context. Otherwise the home library of the --process-as user
86             is used to identify the thresholds and custom penalties to
87             process.
88
89         --has-penalty <penalty-name-or-id>
90             Limit to patrons that currently have a specific penalty. If
91             an id is specified, only that exact penalty is checked. If
92             a name is supplied, the system will check for a custom penalty
93             configured for use at the selected users' home libraries.
94
95         --no-has-penalty <penalty-name-or-id>
96             Limit to patrons that do not currently have a specific penalty.
97             If an id is specified, only that exact penalty is checked. If
98             a name is supplied, the system will check for a custom penalty
99             configured for use at the selected users' home libraries.
100
101         --include-penalty <penalty-name-or-id>
102             Limit to a specific penalty.  Specify multiple times for
103             multiple penalties. If an id is specified, only the exact
104             penalties will be calculated.  Custom penalties will be looked
105             up as needed if a name is supplied.
106
107         --min-id <id>
108             Lowest patron ID to process. 
109
110         --max-id <id>
111             Highest patron ID to process. 
112
113             Together with --min-id, these are useful for running parallel
114             batches of this script without overlapping and/or processing
115             chunks of a controlled size.
116
117         --has-open-circ
118             Limit to patrons that have at least on open circulation.
119             For simplicity, "open" in this context means null xact finish.
120
121         --owes-more-than <amount>
122             Limit to patrons who have an outstanding balance greater than
123             the specified amount.
124
125         --owes-less-than <amount>
126             Limit to patrons who have an outstanding balance less than
127             the specified amount.
128
129         --verbose
130             Log debug info to STDOUT.  This script logs various information
131             via \$logger regardless of this option.
132
133         --help
134             Show this message.
135 HELP
136     exit 0;
137 }
138
139 help() if $help or !$ops;
140
141 # $lvl should match a $logger logging function.  E.g. 'info', 'error', etc.
142 sub announce {
143     my $lvl = shift;
144         my $msg = shift;
145     $logger->$lvl($msg);
146
147     # always announce errors and warnings
148     return unless $verbose || $lvl =~ /error|warn/;
149
150     my $date_str = DateTime->now(time_zone => 'local')->strftime('%F %T');
151     print "$date_str $msg\n";
152 }
153
154 sub get_user_ids {
155     my ($limit, $offset) = @_;
156
157     if ($ids_file) {
158
159         open(IDS_FILE, '<', $ids_file)
160             or die "Cannot open user IDs file: $ids_file: $!\n";
161
162         my @ids = <IDS_FILE>;
163
164         chomp @ids;
165
166         @ids = grep { defined $_ } @ids[$offset..($offset + $limit)];
167
168         return \@ids;
169     }
170
171     my $query = {
172         select => {
173             au => ['id'], 
174             mus => ['balance_owed']
175         },
176         from => {
177             au => {
178                 mus => {
179                     type => 'left',
180                     field => 'usr',
181                     fkey => 'id'
182                 }
183             }
184         },
185         limit => $limit,
186         offset => $offset,
187         order_by => [{class => 'au',  field => 'id'}]
188     };
189
190     my @where = ({'+au' => {deleted => 'f'}});
191
192     if (defined $max_id) {
193
194         push(@where, {
195             '+au' => { # min_id defaults to 0.
196                 id => {between => [$min_id, $max_id]}
197             }
198         });
199
200     } elsif (defined $min_id) {
201
202         push(@where, {
203             '+au' => {
204                 # min_id defaults to 0.
205                 id => {'>' => $min_id}
206             }
207         });
208     }
209
210     if ($has_penalty) {
211
212         if ($has_penalty !~ /^\d+$/) { # got a penalty name, look up possible custom ones for the patron or processing user home org
213             $has_penalty = {in => { union => [
214                 {select => { csp => ['id'] }, from => csp => where => { name => $has_penalty }},
215                 {select =>
216                     { aous => [{column => value => transform => btrim => params => '"'}] },
217                  from => 'aous',
218                  where => {
219                     name => 'circ.custom_penalty_override.'.$has_penalty,
220                     org_unit => { in =>
221                         {select => { aou => [{column => id => transform => 'actor.org_unit_ancestors' => result_field => id => alias => 'id'}]},
222                          from => 'aou',
223                          where => { id => ($home_ou_context ? { '+au' => 'home_ou' } : $auth_user_home) }}
224                     }
225                  }
226                 }
227             ]}};
228         }
229
230         push(@where, {
231             '-exists' => {
232                 select => {ausp => ['id']},
233                 from => 'ausp',
234                 where => {
235                     usr => {'=' => {'+au' => 'id'}},
236                     standing_penalty => $has_penalty,
237                     '-or' => [
238                         {stop_date => undef},
239                         {stop_date => {'>' => 'now'}}
240                     ]
241                 },
242                 limit => 1
243             }
244         });
245     }
246
247     if ($no_has_penalty) {
248
249         if ($no_has_penalty !~ /^\d+$/) { # got a penalty name, look up possible custom ones for the patron or processing user home org
250             $no_has_penalty = {in => { union => [
251                 {select => { csp => ['id'] }, from => csp => where => { name => $no_has_penalty }},
252                 {select =>
253                     { aous => [{column => value => transform => btrim => params => '"'}] },
254                  from => 'aous',
255                  where => {
256                     name => 'circ.custom_penalty_override.'.$no_has_penalty,
257                     org_unit => { in =>
258                         {select => { aou => [{column => id => transform => 'actor.org_unit_ancestors' => result_field => id => alias => 'id'}]},
259                          from => 'aou',
260                          where => { id => ($home_ou_context ? { '+au' => 'home_ou' } : $auth_user_home) }}
261                     }
262                  }
263                 }
264             ]}};
265         }
266
267         push(@where, {
268             '-not' => {
269                 '-exists' => {
270                     select => {ausp => ['id']},
271                     from => 'ausp',
272                     where => {
273                         usr => {'=' => {'+au' => 'id'}},
274                         standing_penalty => $no_has_penalty,
275                         '-or' => [
276                             {stop_date => undef},
277                             {stop_date => {'>' => 'now'}}
278                         ]
279                     },
280                     limit => 1
281                 }
282             }
283         });
284     }
285
286     # For owes more / less, there is a special case because not all
287     # patrons have a money.usr_summary row.  If they don't, they
288     # effectively owe $0.00.
289
290     if (defined $owes_more_than) {
291
292         if ($owes_more_than > 0) {
293
294             push(@where, {
295                 '+mus' => {
296                     balance_owed => {'>' => $owes_more_than}
297                 }
298             });
299
300         } else {
301             push(@where, {
302                 '-or' => [{
303                     '+mus' => {
304                         balance_owed => {'>' => $owes_more_than}
305                     },
306                 }, {
307                     '+mus' => {
308                         usr => undef # owes $0.00
309                     }
310                 }]
311             });
312         }
313     }
314
315     if (defined $owes_less_than) {
316
317         if ($owes_less_than < 0) {
318             push(@where, {
319                 '+mus' => {
320                     balance_owed => {'<' => $owes_less_than}
321                 }
322             }) if $owes_less_than;
323
324         } else {
325
326             push(@where, {
327                 '-or' => [{
328                     '+mus' => {
329                         balance_owed => {'<' => $owes_less_than}
330                     },
331                 }, {
332                     '+mus' => {
333                         usr => undef # owes $0.00
334                     }
335                 }]
336             });
337         }
338     }
339
340     push(@where, {
341         '-exists' => {
342             select => {circ => ['id']},
343             from => 'circ',
344             where => {
345                 usr => {'=' => {'+au' => 'id'}},
346                 xact_finish => undef
347             },
348             limit => 1
349         }
350     }) if $has_open_circ;
351
352     $query->{where}->{'-and'} = \@where;
353
354     my $resp = $e->json_query($query);
355
356     return [map {$_->{id}} @$resp];
357 }
358
359 sub process_users {
360
361     my $limit = $batch_size;
362     my $offset = 0;
363     my $counter = 0;
364     my $batches = 0;
365     my $method = 'open-ils.actor.user.penalties.update';
366     $method .= '_at_home' if $home_ou_context;
367
368     while (1) {
369         my $user_ids = get_user_ids($limit, $offset);
370
371         my $num = scalar(@$user_ids);
372
373         last unless $num;
374
375         $batches++;
376
377         announce('debug', 
378             "Processing batch $batches; count=$num; offset=$offset; ids=" .
379             @$user_ids[0] . '..' . @$user_ids[$#$user_ids]);
380
381         for my $user_id (@$user_ids) {
382
383             $U->simplereq(
384                 'open-ils.actor', $method,
385                 $authtoken, $user_id, @included_penalties
386             );
387
388             $counter++;
389         }
390
391         $offset += $batch_size;
392     }
393
394     announce('debug', "$counter total patrons processed.");
395 }
396
397 sub login {
398
399     my $auth_user = $e->search_actor_user(
400         {usrname => $process_as, deleted => 'f'})->[0];
401
402     die "No such user '$process_as' to use for authentication\n" unless $auth_user;
403
404     my $auth_resp = $U->simplereq(
405         'open-ils.auth_internal',
406         'open-ils.auth_internal.session.create',
407         {user_id => $auth_user->id, login_type => 'staff'}
408     );
409
410     die "Could not create an internal auth session\n" unless (
411         $auth_resp && 
412         $auth_resp->{payload} && 
413         ($authtoken = $auth_resp->{payload}->{authtoken}) &&
414         ($auth_user_home = $auth_user->home_ou)
415     );
416 }
417
418 # connect to osrf...
419 OpenSRF::System->bootstrap_client(config_file => $osrf_config);
420 Fieldmapper->import(IDL => 
421     OpenSRF::Utils::SettingsClient->new->config_value("IDL"));
422 OpenILS::Utils::CStoreEditor::init();
423 $e = OpenILS::Utils::CStoreEditor->new;
424
425 login();
426 process_users();
427