]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/support-scripts/patron-penalties-batch.pl
LP1944986 Has / No-Has Penalty filters
[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 $owes_more_than;
37 my $owes_less_than;
38 my $has_penalty;
39 my $no_has_penalty;
40 my $verbose;
41 my $help;
42 my $batch_size = 100;
43 my $authtoken;
44 my $e;
45
46 my $ops = GetOptions(
47     'osrf-config=s'     => \$osrf_config,
48     'ids-file=s'        => \$ids_file,
49     'process-as=s'      => \$process_as,
50     'min-id=s'          => \$min_id,
51     'max-id=s'          => \$max_id,
52     'has-open-circ'     => \$has_open_circ,
53     'owes-more-than=s'  => \$owes_more_than,
54     'owes-less-than=s'  => \$owes_less_than,
55     'has-penalty=s'     => \$has_penalty,
56     'no-has-penalty=s'  => \$no_has_penalty,
57     'verbose'           => \$verbose,
58     'help'              => \$help
59 );
60
61 sub help {
62     print <<HELP;
63
64     Synopsis:
65         Update patron penalties in batch with options for filtering which
66         patrons to process.
67
68     Usage:
69
70         $0
71
72         --osrf-config [/openils/conf/opensrf_core.xml]
73
74         --process-as <eg-account>
75             Username of an Evergreen account to use for creating the
76             internal auth session.  Defaults to 'admin'.
77
78         --has-penalty <penalty-type-id>
79             Limit to patrons that currently have a specific penalty.
80
81         --no-has-penalty <penalty-type-id>
82             Limit to patrons that do not currently have a specific penalty.
83
84         --min-id <id>
85             Lowest patron ID to process. 
86
87         --max-id <id>
88             Highest patron ID to process. 
89
90             Together with --min-id, these are useful for running parallel
91             batches of this script without overlapping and/or processing
92             chunks of a controlled size.
93
94         --has-open-circ
95             Limit to patrons that have at least on open circulation.
96             For simplicity, "open" in this context means null xact finish.
97
98         --owes-more-than <amount>
99             Limit to patrons who have an outstanding balance greater than
100             the specified amount.
101
102         --owes-less-than <amount>
103             Limit to patrons who have an outstanding balance less than
104             the specified amount.
105
106         --verbose
107             Log debug info to STDOUT.  This script logs various information
108             via \$logger regardless of this option.
109
110         --help
111             Show this message.
112 HELP
113     exit 0;
114 }
115
116 help() if $help or !$ops;
117
118 # $lvl should match a $logger logging function.  E.g. 'info', 'error', etc.
119 sub announce {
120     my $lvl = shift;
121         my $msg = shift;
122     $logger->$lvl($msg);
123
124     # always announce errors and warnings
125     return unless $verbose || $lvl =~ /error|warn/;
126
127     my $date_str = DateTime->now(time_zone => 'local')->strftime('%F %T');
128     print "$date_str $msg\n";
129 }
130
131 sub get_user_ids {
132     my ($limit, $offset) = @_;
133
134     if ($ids_file) {
135
136         open(IDS_FILE, '<', $ids_file)
137             or die "Cannot open user IDs file: $ids_file: $!\n";
138
139         my @ids = <IDS_FILE>;
140
141         chomp @ids;
142
143         @ids = grep { defined $_ } @ids[$offset..($offset + $limit)];
144
145         return \@ids;
146     }
147
148     my $query = {
149         select => {
150             au => ['id'], 
151             mus => ['balance_owed']
152         },
153         from => {
154             au => {
155                 mus => {
156                     type => 'left',
157                     field => 'usr',
158                     fkey => 'id'
159                 }
160             }
161         },
162         limit => $limit,
163         offset => $offset,
164         order_by => [{class => 'au',  field => 'id'}]
165     };
166
167     my @where = ({'+au' => {deleted => 'f'}});
168
169     if (defined $max_id) {
170
171         push(@where, {
172             '+au' => { # min_id defaults to 0.
173                 id => {between => [$min_id, $max_id]}
174             }
175         });
176
177     } elsif (defined $min_id) {
178
179         push(@where, {
180             '+au' => {
181                 # min_id defaults to 0.
182                 id => {'>' => $min_id}
183             }
184         });
185     }
186
187     if ($has_penalty) {
188         
189         push(@where, {
190             '-exists' => {
191                 select => {ausp => ['id']},
192                 from => 'ausp',
193                 where => {
194                     usr => {'=' => {'+au' => 'id'}},
195                     standing_penalty => $has_penalty,
196                     '-or' => [
197                         {stop_date => undef},
198                         {stop_date => {'>' => 'now'}}
199                     ]
200                 },
201                 limit => 1
202             }
203         });
204     }
205
206     if ($no_has_penalty) {
207         push(@where, {
208             '-not' => {
209                 '-exists' => {
210                     select => {ausp => ['id']},
211                     from => 'ausp',
212                     where => {
213                         usr => {'=' => {'+au' => 'id'}},
214                         standing_penalty => $no_has_penalty,
215                         '-or' => [
216                             {stop_date => undef},
217                             {stop_date => {'>' => 'now'}}
218                         ]
219                     },
220                     limit => 1
221                 }
222             }
223         });
224     }
225
226     # For owes more / less, there is a special case because not all
227     # patrons have a money.usr_summary row.  If they don't, they
228     # effectively owe $0.00.
229
230     if (defined $owes_more_than) {
231
232         if ($owes_more_than > 0) {
233
234             push(@where, {
235                 '+mus' => {
236                     balance_owed => {'>' => $owes_more_than}
237                 }
238             });
239
240         } else {
241             push(@where, {
242                 '-or' => [{
243                     '+mus' => {
244                         balance_owed => {'>' => $owes_more_than}
245                     },
246                 }, {
247                     '+mus' => {
248                         usr => undef # owes $0.00
249                     }
250                 }]
251             });
252         }
253     }
254
255     if (defined $owes_less_than) {
256
257         if ($owes_less_than < 0) {
258             push(@where, {
259                 '+mus' => {
260                     balance_owed => {'<' => $owes_less_than}
261                 }
262             }) if $owes_less_than;
263
264         } else {
265
266             push(@where, {
267                 '-or' => [{
268                     '+mus' => {
269                         balance_owed => {'<' => $owes_less_than}
270                     },
271                 }, {
272                     '+mus' => {
273                         usr => undef # owes $0.00
274                     }
275                 }]
276             });
277         }
278     }
279
280     push(@where, {
281         '-exists' => {
282             select => {circ => ['id']},
283             from => 'circ',
284             where => {
285                 usr => {'=' => {'+au' => 'id'}},
286                 xact_finish => undef
287             },
288             limit => 1
289         }
290     }) if $has_open_circ;
291
292     $query->{where}->{'-and'} = \@where;
293
294     my $resp = $e->json_query($query);
295
296     return [map {$_->{id}} @$resp];
297 }
298
299 sub process_users {
300
301     my $limit = $batch_size;
302     my $offset = 0;
303     my $counter = 0;
304     my $batches = 0;
305
306     while (1) {
307         my $user_ids = get_user_ids($limit, $offset);
308
309         my $num = scalar(@$user_ids);
310
311         last unless $num;
312
313         $batches++;
314
315         announce('debug', 
316             "Processing batch $batches; count=$num; offset=$offset; ids=" .
317             @$user_ids[0] . '..' . @$user_ids[$#$user_ids]);
318
319         for my $user_id (@$user_ids) {
320
321             $U->simplereq(
322                 'open-ils.actor',
323                 'open-ils.actor.user.penalties.update',
324                 $authtoken, $user_id
325             );
326
327             $counter++;
328         }
329
330         $offset += $batch_size;
331     }
332
333     announce('debug', "$counter total patrons processed.");
334 }
335
336 sub login {
337
338     my $auth_user = $e->search_actor_user(
339         {usrname => $process_as, deleted => 'f'})->[0];
340
341     die "No such user '$process_as' to use for authentication\n" unless $auth_user;
342
343     my $auth_resp = $U->simplereq(
344         'open-ils.auth_internal',
345         'open-ils.auth_internal.session.create',
346         {user_id => $auth_user->id, login_type => 'staff'}
347     );
348
349     die "Could not create an internal auth session\n" unless (
350         $auth_resp && 
351         $auth_resp->{payload} && 
352         ($authtoken = $auth_resp->{payload}->{authtoken})
353     );
354 }
355
356 # connect to osrf...
357 OpenSRF::System->bootstrap_client(config_file => $osrf_config);
358 Fieldmapper->import(IDL => 
359     OpenSRF::Utils::SettingsClient->new->config_value("IDL"));
360 OpenILS::Utils::CStoreEditor::init();
361 $e = OpenILS::Utils::CStoreEditor->new;
362
363 login();
364 process_users();
365