]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/Publisher/actor.pm
LP#1251415: Skip deleted users in juvenile update
[Evergreen.git] / Open-ILS / src / perlmods / lib / OpenILS / Application / Storage / Publisher / actor.pm
1 package OpenILS::Application::Storage::Publisher::actor;
2 use base qw/OpenILS::Application::Storage/;
3 use OpenILS::Application::Storage::CDBI::actor;
4 use OpenSRF::Utils::Logger qw/:level/;
5 use OpenSRF::Utils qw/:datetime/;
6 use OpenILS::Utils::Fieldmapper;
7 use OpenSRF::Utils::SettingsClient;
8
9 use DateTime;           
10 use DateTime::Format::ISO8601;  
11 use DateTime::Set;
12 use DateTime::SpanSet;
13
14 my $_dt_parser = DateTime::Format::ISO8601->new;    
15
16 my $log = 'OpenSRF::Utils::Logger';
17
18 sub new_usergroup_id {
19     return actor::user->db_Main->selectrow_array("select nextval('actor.usr_usrgroup_seq'::regclass)");
20 }
21 __PACKAGE__->register_method(
22     api_name    => 'open-ils.storage.actor.user.group_id.new',
23     api_level   => 1,
24     method      => 'new_usergroup_id',
25 );
26
27 sub juv_to_adult {
28     my $self = shift;
29     my $client = shift;
30     my $adult_age = shift;
31
32     my $sql = <<"    SQL";
33         UPDATE actor.usr
34             SET juvenile = FALSE
35             WHERE juvenile IS TRUE
36             AND deleted IS FALSE
37             AND AGE(dob) > COALESCE( BTRIM( (
38                     SELECT value FROM actor.org_unit_ancestor_setting(
39                     'global.juvenile_age_threshold', home_ou)),'"' ), ?)::INTERVAL
40     SQL
41
42     my $sth = actor::user->db_Main->prepare_cached($sql);
43     $sth->execute($adult_age);
44
45     return $sth->rows;
46 }
47 __PACKAGE__->register_method(
48     api_name    => 'open-ils.storage.actor.user.juvenile_to_adult',
49     api_level   => 1,
50     method      => 'juv_to_adult',
51 );
52
53 sub usr_total_owed {
54     my $self = shift;
55     my $client = shift;
56     my $usr = shift;
57
58     my $sql = <<"    SQL";
59             SELECT  x.usr,
60                     SUM(COALESCE((SELECT SUM(b.amount) FROM money.billing b WHERE b.voided IS FALSE AND b.xact = x.id),0.0)) -
61                         SUM(COALESCE((SELECT SUM(p.amount) FROM money.payment p WHERE p.voided IS FALSE AND p.xact = x.id),0.0))
62               FROM  money.billable_xact x
63               WHERE x.usr = ? AND x.xact_finish IS NULL
64               GROUP BY 1
65     SQL
66
67     my (undef,$val) = actor::user->db_Main->selectrow_array($sql, {}, $usr);
68
69     return $val;
70 }
71 __PACKAGE__->register_method(
72     api_name    => 'open-ils.storage.actor.user.total_owed',
73     api_level   => 1,
74     method      => 'usr_total_owed',
75 );
76
77 sub usr_breakdown_out {
78     my $self = shift;
79     my $client = shift;
80     my $usr = shift;
81
82     $self->method_lookup('open-ils.storage.transaction.begin')->run($client);
83
84     my $out_sql = <<"    SQL";
85             SELECT  id
86               FROM  action.circulation
87               WHERE usr = ?
88                     AND checkin_time IS NULL
89                     AND (  (fine_interval >= '1 day' AND due_date >= 'today')
90                         OR (fine_interval < '1 day'  AND due_date > 'now'   ))
91                     AND (stop_fines IS NULL
92                         OR stop_fines NOT IN ('LOST','CLAIMSRETURNED','LONGOVERDUE'))
93     SQL
94
95     my $out = actor::user->db_Main->selectcol_arrayref($out_sql, {}, $usr);
96
97     my $od_sql = <<"    SQL";
98             SELECT  id
99               FROM  action.circulation
100               WHERE usr = ?
101                     AND checkin_time IS NULL
102                     AND (  (fine_interval >= '1 day' AND due_date < 'today')
103                         OR (fine_interval < '1 day'  AND due_date < 'now'  ))
104                     AND (stop_fines IS NULL
105                         OR stop_fines NOT IN ('LOST','CLAIMSRETURNED','LONGOVERDUE'))
106     SQL
107
108     my $od = actor::user->db_Main->selectcol_arrayref($od_sql, {}, $usr);
109
110     my $lost_sql = <<"    SQL";
111             SELECT  id
112               FROM  action.circulation
113               WHERE usr = ? AND checkin_time IS NULL AND xact_finish IS NULL AND stop_fines = 'LOST'
114     SQL
115
116     my $lost = actor::user->db_Main->selectcol_arrayref($lost_sql, {}, $usr);
117
118     my $cl_sql = <<"    SQL";
119             SELECT  id
120               FROM  action.circulation
121               WHERE usr = ? AND checkin_time IS NULL AND stop_fines = 'CLAIMSRETURNED'
122     SQL
123
124     my $cl = actor::user->db_Main->selectcol_arrayref($cl_sql, {}, $usr);
125
126     my $lo_sql = <<"    SQL";
127             SELECT  id
128               FROM  action.circulation
129               WHERE usr = ? AND checkin_time IS NULL AND stop_fines = 'LONGOVERDUE'
130     SQL
131
132     my $lo = actor::user->db_Main->selectcol_arrayref($lo_sql, {}, $usr);
133
134     $self->method_lookup('open-ils.storage.transaction.rollback')->run($client);
135
136     if ($self->api_name =~/count$/o) {
137         return {    total   => scalar(@$out) + scalar(@$od) + scalar(@$lost) + scalar(@$cl) + scalar(@$lo),
138                     out     => scalar(@$out),
139                     overdue => scalar(@$od),
140                     lost    => scalar(@$lost),
141                     claims_returned => scalar(@$cl),
142                     long_overdue        => scalar(@$lo),
143         };
144     }
145
146     return {    out     => $out,
147                 overdue => $od,
148                 lost    => $lost,
149                 claims_returned => $cl,
150                 long_overdue        => $lo,
151     };
152 }
153 __PACKAGE__->register_method(
154     api_name    => 'open-ils.storage.actor.user.checked_out',
155     api_level   => 1,
156     method      => 'usr_breakdown_out',
157 );
158 __PACKAGE__->register_method(
159     api_name    => 'open-ils.storage.actor.user.checked_out.count',
160     api_level   => 1,
161     method      => 'usr_breakdown_out',
162 );
163
164 sub usr_total_out {
165     my $self = shift;
166     my $client = shift;
167     my $usr = shift;
168
169     my $sql = <<"    SQL";
170             SELECT  count(*)
171               FROM  action.circulation
172               WHERE usr = ? AND checkin_time IS NULL
173     SQL
174
175     my ($val) = actor::user->db_Main->selectrow_array($sql, {}, $usr);
176
177     return $val;
178 }
179 __PACKAGE__->register_method(
180     api_name    => 'open-ils.storage.actor.user.total_out',
181     api_level   => 1,
182     method      => 'usr_total_out',
183 );
184
185 sub calc_proximity {
186     my $self = shift;
187     my $client = shift;
188
189     local $OpenILS::Application::Storage::WRITE = 1;
190
191     my $delete_sql = <<"    SQL";
192         DELETE FROM actor.org_unit_proximity;
193     SQL
194
195     my $insert_sql = <<"    SQL";
196         INSERT INTO actor.org_unit_proximity (from_org, to_org, prox)
197             SELECT  l.id,
198                 r.id,
199                 actor.org_unit_proximity(l.id,r.id)
200               FROM  actor.org_unit l,
201                 actor.org_unit r;
202     SQL
203
204     actor::org_unit_proximity->db_Main->do($delete_sql);
205     actor::org_unit_proximity->db_Main->do($insert_sql);
206
207     return 1;
208 }
209 __PACKAGE__->register_method(
210     api_name    => 'open-ils.storage.actor.org_unit.refresh_proximity',
211     api_level   => 1,
212     method      => 'calc_proximity',
213 );
214
215 sub make_hoo_spanset {
216     my $hoo = shift;
217     return undef unless $hoo;
218
219     my $today = shift || DateTime->now;
220
221     my $tz = OpenSRF::AppSession->create('open-ils.actor')->request(
222         'open-ils.actor.ou_setting.ancestor_default' => $hoo->id.'' => 'org_unit.timezone'
223     )->gather(1) || DateTime::TimeZone->new( name => 'local' )->name;
224
225     my $current_dow = $today->day_of_week_0;
226
227     my $spanset = DateTime::SpanSet->empty_set;
228     for my $d ( 0 .. 6 ) {
229
230         my $omethod = 'dow_'.$d.'_open';
231         my $cmethod = 'dow_'.$d.'_close';
232
233         my $open = interval_to_seconds($hoo->$omethod());
234         my $close = interval_to_seconds($hoo->$cmethod());
235
236         next if ($open == $close && $open == 0);
237
238         my $dow_offset = ($d - $current_dow) * $one_day;
239         $close += $one_day if ($close <= $open);
240
241         $spanset = $spanset->union(
242             DateTime::Span->new(
243                 start => $today->clone->add( seconds => $dow_offset + $open  ),
244                 end   => $today->clone->add( seconds => $dow_offset + $close )
245             )
246         );
247     }
248
249     return $spanset->complement;
250 }
251
252 sub make_closure_spanset {
253     my $closures = shift;
254     return undef unless $closures;
255
256     my $spanset = DateTime::SpanSet->empty_set;
257     for my $k ( keys %$closures ) {
258         my $c = $$closures{$k};
259
260         $spanset = $spanset->union(
261             DateTime::Span->new(
262                 start => $_dt_parser->parse_datetime(cleanse_ISO8601($c->{close_start})),
263                 end   => $_dt_parser->parse_datetime(cleanse_ISO8601($c->{close_end}))
264             )
265         );
266     }
267
268     return $spanset;
269 }
270
271 sub new_org_closed_overlap {
272     my $self = shift;
273     my $client = shift;
274     my $ou = shift;
275     my $date = shift;
276     my $direction = shift || 0;
277     my $no_hoo = shift || 0;
278
279     return undef unless ($date && $ou);
280
281     # we're given a date and a direction, find any closures that contain the date
282     my $t = actor::org_unit::closed_date->table;
283     my $sql = <<"    SQL";
284         SELECT  *
285           FROM  $t
286           WHERE close_end > ?
287             AND org_unit = ?
288           ORDER BY close_start ASC, close_end DESC
289           LIMIT 1
290     SQL
291
292     $date = cleanse_ISO8601($date);
293
294     my $target_date = $_dt_parser->parse_datetime( $date );
295     my ($begin, $end) = ($target_date, $target_date);
296
297     # create a spanset from the closures that contain the $date
298     my $closure_spanset = make_closure_spanset(
299         actor::org_unit::closed_date->db_Main->selectall_hashref( $sql, 'id', {}, $date, $ou )
300     );
301
302     if ($closure_spanset && $closure_spanset->intersects( $target_date )) {
303         my $closure_intersection = $closure_spanset->intersection( $target_date );
304         $begin = $closure_intersection->min;
305         $end = $closure_intersection->max;
306
307         if ( $direction <= 0 ) {
308             $begin->subtract( minutes => 1 );
309
310             while ( my $_b = new_org_closed_overlap($self, $client, $ou, $begin->strftime('%FT%T%z'), -1, 1 ) ) {
311                 $begin = $_dt_parser->parse_datetime( cleanse_ISO8601($_b->{start}) );
312             }
313         }
314
315         if ( $direction >= 0 ) {
316             $end->add( minutes => 1 );
317
318             while ( my $_a = new_org_closed_overlap($self, $client, $ou, $end->strftime('%FT%T%z'), 1, 1 ) ) {
319                 $end = $_dt_parser->parse_datetime( cleanse_ISO8601($_a->{end}) );
320             }
321         }
322     }
323
324     if ( !$no_hoo ) {
325
326         my $begin_hoo = make_hoo_spanset(actor::org_unit::hours_of_operation->retrieve($ou), $begin);
327         my $end_hoo   = make_hoo_spanset(actor::org_unit::hours_of_operation->retrieve($ou), $end  );
328
329
330         if ( $begin_hoo && $direction <= 0 && $begin_hoo->intersects($begin) ) {
331             my $hoo_intersection = $begin_hoo->intersection( $begin );
332             $begin = $hoo_intersection->min;
333             $begin->subtract( minutes => 1 );
334
335             while ( my $_b = new_org_closed_overlap($self, $client, $ou, $begin->strftime('%FT%T%z'), -1 ) ) {
336                 $begin = $_dt_parser->parse_datetime( cleanse_ISO8601($_b->{start}) );
337             }
338         }
339     
340         if ( $end_hoo && $direction >= 0 && $end_hoo->intersects($end) ) {
341             my $hoo_intersection = $end_hoo->intersection( $end );
342             $end = $hoo_intersection->max;
343             $end->add( minutes => 1 );
344
345
346             while ( my $_b = new_org_closed_overlap($self, $client, $ou, $end->strftime('%FT%T%z'), -1 ) ) {
347                 $end = $_dt_parser->parse_datetime( cleanse_ISO8601($_b->{end}) );
348             }
349         }
350     }
351
352     my $start = $begin->strftime('%FT%T%z');
353     my $stop = $end->strftime('%FT%T%z');
354
355     return undef if ($start eq $stop);
356     return { start => $start, end => $stop };
357 }
358 __PACKAGE__->register_method(
359     api_name    => 'open-ils.storage.actor.org_unit.closed_date.overlap',
360     api_level   => 0,
361     method      => 'new_org_closed_overlap',
362 );
363
364 sub org_closed_overlap {
365     my $self = shift;
366     my $client = shift;
367     my $ou = shift;
368     my $date = shift;
369     my $direction = shift || 0;
370     my $no_hoo = shift || 0;
371
372     return undef unless ($date && $ou);
373
374     my $t = actor::org_unit::closed_date->table;
375     my $sql = <<"    SQL";
376         SELECT  *
377           FROM  $t
378           WHERE ? between close_start and close_end
379             AND org_unit = ?
380           ORDER BY close_start ASC, close_end DESC
381           LIMIT 1
382     SQL
383
384     $date = cleanse_ISO8601($date);
385     my ($begin, $end) = ($date,$date);
386
387     my $hoo = actor::org_unit::hours_of_operation->retrieve($ou);
388
389     if (my $closure = actor::org_unit::closed_date->db_Main->selectrow_hashref( $sql, {}, $date, $ou )) {
390         $begin = cleanse_ISO8601($closure->{close_start});
391         $end = cleanse_ISO8601($closure->{close_end});
392
393         if ( $direction <= 0 ) {
394             $before = $_dt_parser->parse_datetime( $begin );
395             $before->subtract( minutes => 1 );
396
397             while ( my $_b = org_closed_overlap($self, $client, $ou, $before->strftime('%FT%T%z'), -1, 1 ) ) {
398                 $before = $_dt_parser->parse_datetime( cleanse_ISO8601($_b->{start}) );
399             }
400             $begin = cleanse_ISO8601($before->strftime('%FT%T%z'));
401         }
402
403         if ( $direction >= 0 ) {
404             $after = $_dt_parser->parse_datetime( $end );
405             $after->add( minutes => 1 );
406
407             while ( my $_a = org_closed_overlap($self, $client, $ou, $after->strftime('%FT%T%z'), 1, 1 ) ) {
408                 $after = $_dt_parser->parse_datetime( cleanse_ISO8601($_a->{end}) );
409             }
410             $end = cleanse_ISO8601($after->strftime('%FT%T%z'));
411         }
412     }
413
414     if ( !$no_hoo ) {
415         if ( $hoo ) {
416
417             if ( $direction <= 0 ) {
418                 my $begin_dow = $_dt_parser->parse_datetime( $begin )->day_of_week_0;
419                 my $begin_open_meth = "dow_".$begin_dow."_open";
420                 my $begin_close_meth = "dow_".$begin_dow."_close";
421
422                 my $count = 1;
423                 while ($hoo->$begin_open_meth eq '00:00:00' and $hoo->$begin_close_meth eq '00:00:00') {
424                     $begin = cleanse_ISO8601($_dt_parser->parse_datetime( $begin )->subtract( days => 1)->strftime('%FT%T%z'));
425                     $begin_dow++;
426                     $begin_dow %= 7;
427                     $count++;
428                     last if ($count > 6);
429                     $begin_open_meth = "dow_".$begin_dow."_open";
430                     $begin_close_meth = "dow_".$begin_dow."_close";
431                 }
432
433                 if (my $closure = actor::org_unit::closed_date->db_Main->selectrow_hashref( $sql, {}, $begin, $ou )) {
434                     $before = $_dt_parser->parse_datetime( $begin );
435                     $before->subtract( minutes => 1 );
436                     while ( my $_b = org_closed_overlap($self, $client, $ou, $before->strftime('%FT%T%z'), -1 ) ) {
437                         $before = $_dt_parser->parse_datetime( cleanse_ISO8601($_b->{start}) );
438                     }
439                 }
440             }
441     
442             if ( $direction >= 0 ) {
443                 my $end_dow = $_dt_parser->parse_datetime( $end )->day_of_week_0;
444                 my $end_open_meth = "dow_".$end_dow."_open";
445                 my $end_close_meth = "dow_".$end_dow."_close";
446     
447                 $count = 1;
448                 while ($hoo->$end_open_meth eq '00:00:00' and $hoo->$end_close_meth eq '00:00:00') {
449                     $end = cleanse_ISO8601($_dt_parser->parse_datetime( $end )->add( days => 1)->strftime('%FT%T%z'));
450                     $end_dow++;
451                     $end_dow %= 7;
452                     $count++;
453                     last if ($count > 6);
454                     $end_open_meth = "dow_".$end_dow."_open";
455                     $end_close_meth = "dow_".$end_dow."_close";
456                 }
457
458                 if (my $closure = actor::org_unit::closed_date->db_Main->selectrow_hashref( $sql, {}, $end, $ou )) {
459                     $after = $_dt_parser->parse_datetime( $end );
460                     $after->add( minutes => 1 );
461
462                     while ( my $_a = org_closed_overlap($self, $client, $ou, $after->strftime('%FT%T%z'), 1 ) ) {
463                         $after = $_dt_parser->parse_datetime( cleanse_ISO8601($_a->{end}) );
464                     }
465                     $end = cleanse_ISO8601($after->strftime('%FT%T%z'));
466                 }
467             }
468
469         }
470     }
471
472     if ($begin eq $date && $end eq $date) {
473         return undef;
474     }
475
476     return { start => $begin, end => $end };
477 }
478 __PACKAGE__->register_method(
479     api_name    => 'open-ils.storage.actor.org_unit.closed_date.overlap',
480     api_level   => 1,
481     method      => 'org_closed_overlap',
482 );
483
484 sub user_by_barcode {
485     my $self = shift;
486     my $client = shift;
487     my @barcodes = shift;
488
489     return undef unless @barcodes;
490
491     for my $card ( actor::card->search( { barcode => @barcodes } ) ) {
492         next unless $card;
493         if (@barcodes == 1) {
494             return $card->usr->to_fieldmapper;
495         }
496         $client->respond( $card->usr->to_fieldmapper);
497     }
498     return undef;
499 }
500 __PACKAGE__->register_method(
501     api_name    => 'open-ils.storage.direct.actor.user.search.barcode',
502     api_level   => 1,
503     method      => 'user_by_barcode',
504     stream      => 1,
505     cachable    => 1,
506 );
507
508 sub lost_barcodes {
509     my $self = shift;
510     my $client = shift;
511
512     my $c = actor::card->table;
513     my $p = actor::user->table;
514
515     my $sql = "SELECT c.barcode FROM $c c JOIN $p p ON (c.usr = p.id) WHERE p.card <> c.id";
516
517     my $list = actor::user->db_Main->selectcol_arrayref($sql);
518     for my $bc ( @$list ) {
519         $client->respond($bc);
520     }
521     return undef;
522 }
523 __PACKAGE__->register_method(
524     api_name    => 'open-ils.storage.actor.user.lost_barcodes',
525     api_level   => 1,
526     stream      => 1,
527     method      => 'lost_barcodes',
528     signature    => <<'    NOTE',
529         Returns an array of barcodes that belong to lost cards.
530         @return array of barcodes
531     NOTE
532 );
533
534 sub expired_barcodes {
535     my $self = shift;
536     my $client = shift;
537
538     my $c = actor::card->table;
539     my $p = actor::user->table;
540
541     my $sql = "SELECT c.barcode FROM $c c JOIN $p p ON (c.usr = p.id) WHERE p.expire_date < CURRENT_DATE";
542
543     my $list = actor::user->db_Main->selectcol_arrayref($sql);
544     for my $bc ( @$list ) {
545         $client->respond($bc);
546     }
547     return undef;
548 }
549 __PACKAGE__->register_method(
550     api_name    => 'open-ils.storage.actor.user.expired_barcodes',
551     api_level   => 1,
552     stream      => 1,
553     method      => 'expired_barcodes',
554     signature    => <<'    NOTE',
555         Returns an array of barcodes that are currently expired.
556         @return array of barcodes
557     NOTE
558 );
559
560 sub barred_barcodes {
561     my $self = shift;
562     my $client = shift;
563
564     my $c = actor::card->table;
565     my $p = actor::user->table;
566
567     my $sql = "SELECT c.barcode FROM $c c JOIN $p p ON (c.usr = p.id) WHERE p.barred IS TRUE";
568
569     my $list = actor::user->db_Main->selectcol_arrayref($sql);
570     for my $bc ( @$list ) {
571         $client->respond($bc);
572     }
573     return undef;
574 }
575 __PACKAGE__->register_method(
576     api_name    => 'open-ils.storage.actor.user.barred_barcodes',
577     api_level   => 1,
578     stream      => 1,
579     method      => 'barred_barcodes',
580     signature    => <<'    NOTE',
581         Returns an array of barcodes that are currently barred.
582         @return array of barcodes
583     NOTE
584 );
585
586 sub penalized_barcodes {
587     my $self = shift;
588     my $client = shift;
589
590     my $c = actor::card->table;
591     my $p = actor::user_standing_penalty->table;
592
593     my $sql = <<"    SQL";
594         SELECT  DISTINCT c.barcode
595           FROM  $c c
596             JOIN $p p USING (usr)
597             JOIN config.standing_penalty csp ON (csp.id = p.standing_penalty)
598           WHERE csp.block_list IS NOT NULL
599             AND p.set_date < CURRENT_DATE
600             AND (p.stop_date IS NULL OR p.stop_date > CURRENT_DATE);
601     SQL
602
603     my $list = actor::user->db_Main->selectcol_arrayref($sql);
604     for my $bc ( @$list ) {
605         $client->respond($bc);
606     }
607     return undef;
608 }
609 __PACKAGE__->register_method(
610     api_name    => 'open-ils.storage.actor.user.penalized_barcodes',
611     api_level   => 1,
612     stream      => 1,
613     method      => 'penalized_barcodes',
614     signature    => <<'    NOTE',
615         Returns an array of barcodes that have blocking penalties.
616         @return array of barcodes
617     NOTE
618 );
619
620 sub _clean_regex_chars {
621     my ($search) = @_;
622
623     # Escape metacharacters for SIMILAR TO 
624     # (http://www.postgresql.org/docs/8.4/interactive/functions-matching.html)
625     $search =~ s/\_/\\_/g;
626     $search =~ s/\%/\\%/g;
627     $search =~ s/\|/\\|/g;
628     $search =~ s/\*/\\*/g;
629     $search =~ s/\+/\\+/g;
630     $search =~ s/\[/\\[/g;
631     $search =~ s/\]/\\]/g;
632     $search =~ s/\(/\\(/g;
633     $search =~ s/\)/\\)/g;
634
635     return $search;
636 }
637
638 sub patron_search {
639     my $self = shift;
640     my $client = shift;
641     my $search = shift;
642     my $limit = shift || 1000;
643     my $sort = shift;
644     my $inactive = shift;
645     my $ws_ou = shift;
646     my $search_org = shift || $ws_ou;
647     my $opt_boundary = shift || 0;
648     my $offset = shift || 0;
649
650     my $penalty_sort = 0;
651
652     my $strict_opt_in = OpenSRF::Utils::SettingsClient->new->config_value( share => user => 'opt_in' );
653
654     $sort = ['family_name','first_given_name'] unless ($$sort[0]);
655     push @$sort,'id';
656
657     if ($$sort[0] eq 'penalties') {
658         shift @$sort;
659         $penalty_sort = 1;
660     }
661
662     # group 0 = user
663     # group 1 = address
664     # group 2 = phone, ident
665     # group 3 = barcode
666
667     my $usr = join ' AND ', map { "evergreen.lowercase(CAST($_ AS text)) ~ ?" } grep { ''.$$search{$_}{group} eq '0' } keys %$search;
668     my @usrv = map { "^" . _clean_regex_chars($$search{$_}{value}) } grep { ''.$$search{$_}{group} eq '0' } keys %$search;
669
670     my $addr = join ' AND ', map { "evergreen.lowercase(CAST($_ AS text)) ~ ?" } grep { ''.$$search{$_}{group} eq '1' } keys %$search;
671     my @addrv = map { "^" . _clean_regex_chars($$search{$_}{value}) } grep { ''.$$search{$_}{group} eq '1' } keys %$search;
672
673     my $pv = _clean_regex_chars($$search{phone}{value});
674     my $iv = _clean_regex_chars($$search{ident}{value});
675     my $nv = _clean_regex_chars($$search{name}{value});
676     my $cv = _clean_regex_chars($$search{card}{value});
677
678     my $card = '';
679     if ($cv) {
680         $card = 'JOIN (SELECT DISTINCT usr FROM actor.card WHERE evergreen.lowercase(barcode) LIKE ?||\'%\') AS card ON (card.usr = users.id)';
681         unshift(@usrv, $cv);
682     }
683
684     my $phone = '';
685     my @ps;
686     my @phonev;
687     if ($pv) {
688         for my $p ( qw/day_phone evening_phone other_phone/ ) {
689             if ($pv =~ /^\d+$/) {
690                 push @ps, "evergreen.lowercase(REGEXP_REPLACE($p, '[^0-9]', '', 'g')) ~ ?";
691             } else {
692                 push @ps, "evergreen.lowercase($p) ~ ?";
693             }
694             push @phonev, "^$pv";
695         }
696         $phone = '(' . join(' OR ', @ps) . ')';
697     }
698
699     my $ident = '';
700     my @is;
701     my @identv;
702     if ($iv) {
703         for my $i ( qw/ident_value ident_value2/ ) {
704             push @is, "evergreen.lowercase($i) ~ ?";
705             push @identv, "^$iv";
706         }
707         $ident = '(' . join(' OR ', @is) . ')';
708     }
709
710     my $name = '';
711     my @ns;
712     my @namev;
713     if (0 && $nv) {
714         for my $n ( qw/first_given_name second_given_name family_name/ ) {
715             push @ns, "evergreen.lowercase($n) ~ ?";
716             push @namev, "^$nv";
717         }
718         $name = '(' . join(' OR ', @ns) . ')';
719     }
720
721     my $usr_where = join ' AND ', grep { $_ } ($usr,$phone,$ident,$name);
722     my $addr_where = $addr;
723
724
725     my $u_table = actor::user->table;
726     my $a_table = actor::user_address->table;
727     my $opt_in_table = actor::usr_org_unit_opt_in->table;
728     my $ou_table = actor::org_unit->table;
729
730     my $u_select = "SELECT id as id FROM $u_table u WHERE $usr_where";
731     my $a_select = "SELECT u.id as id FROM $a_table a JOIN $u_table u ON (u.mailing_address = a.id OR u.billing_address = a.id) WHERE $addr_where";
732
733     my $clone_select = '';
734
735     #$clone_select = "JOIN (SELECT cu.id as id FROM $a_table ca ".
736     #          "JOIN $u_table cu ON (cu.mailing_address = ca.id OR cu.billing_address = ca.id) ".
737     #          "WHERE $addr_where) AS clone ON (clone.id = users.id)" if ($addr_where);
738
739     my $select = '';
740     if ($usr_where) {
741         if ($addr_where) {
742             $select = "$u_select INTERSECT $a_select";
743         } else {
744             $select = $u_select;
745         }
746     } elsif ($addr_where) {
747         $select = "$a_select";
748     }
749
750     return undef if (!$select && !$card);
751
752     my $order_by = join ', ', map { 'evergreen.lowercase(CAST(users.'. (split / /,$_)[0] . ' AS text)) ' . (split / /,$_)[1] } @$sort;
753     my $distinct_list = join ', ', map { 'evergreen.lowercase(CAST(users.'. (split / /,$_)[0] . ' AS text))' } @$sort;
754     my $group_list = $distinct_list;
755
756     if ($inactive) {
757         $inactive = '';
758     } else {
759         $inactive = 'AND users.active = TRUE';
760     }
761
762     if (!$ws_ou) {  # XXX This should be required!!
763         $ws_ou = actor::org_unit->search( { parent_ou => undef } )->next->id;
764     }
765
766     my $descendants = "actor.org_unit_descendants($search_org)";
767
768     my $opt_in_where = '';
769     if (lc($strict_opt_in) eq 'true') {
770         $opt_in_where = "AND (";
771         $opt_in_where .= "EXISTS (select id FROM $opt_in_table ";
772         $opt_in_where .= " WHERE org_unit in (select (actor.org_unit_ancestors($ws_ou)).id)";
773         $opt_in_where .= " AND usr = users.id) ";
774         $opt_in_where .= "OR";
775         $opt_in_where .= " users.home_ou IN (select (actor.org_unit_descendants($ws_ou,$opt_boundary)).id))";
776     }
777
778     my $penalty_join = '';
779     if ($penalty_sort) {
780         $distinct_list = 'COUNT(penalties.id), ' . $distinct_list;
781         $order_by = 'COUNT(penalties.id) DESC, ' . $order_by;
782         unshift @$sort, 'COUNT(penalties.id)';
783         $penalty_join = <<"        SQL";
784             LEFT JOIN actor.usr_standing_penalty penalties
785                 ON (users.id = penalties.usr AND (penalties.stop_date IS NULL OR penalties.stop_date > NOW()))
786         SQL
787     }
788
789     $select = "JOIN ($select) AS search ON (search.id = users.id)" if ($select);
790     $select = <<"    SQL";
791         SELECT  $distinct_list
792           FROM  $u_table AS users $card
793             JOIN $descendants d ON (d.id = users.home_ou)
794             $select
795             $clone_select
796             $penalty_join
797           WHERE users.deleted = FALSE
798             $inactive
799             $opt_in_where
800           GROUP BY $group_list
801           ORDER BY $order_by
802           LIMIT $limit
803           OFFSET $offset
804     SQL
805
806     return actor::user->db_Main->selectcol_arrayref($select, {Columns=>[scalar(@$sort)]}, map {lc($_)} (@usrv,@phonev,@identv,@namev,@addrv));
807 }
808 __PACKAGE__->register_method(
809     api_name    => 'open-ils.storage.actor.user.crazy_search',
810     api_level   => 1,
811     method      => 'patron_search',
812 );
813
814 sub org_unit_list {
815     my $self = shift;
816     my $client = shift;
817
818     my $select =<<"    SQL";
819     SELECT  *
820       FROM  actor.org_unit
821       ORDER BY CASE WHEN parent_ou IS NULL THEN 0 ELSE 1 END, name;
822     SQL
823
824     my $sth = actor::org_unit->db_Main->prepare_cached($select);
825     $sth->execute;
826
827     $client->respond( $_->to_fieldmapper ) for ( map { actor::org_unit->construct($_) } $sth->fetchall_hash );
828
829     return undef;
830 }
831 __PACKAGE__->register_method(
832     api_name    => 'open-ils.storage.direct.actor.org_unit.retrieve.all',
833     api_level   => 1,
834     stream      => 1,
835     method      => 'org_unit_list',
836 );
837
838 sub org_unit_type_list {
839     my $self = shift;
840     my $client = shift;
841
842     my $select =<<"    SQL";
843     SELECT  *
844       FROM  actor.org_unit_type
845       ORDER BY depth, name;
846     SQL
847
848     my $sth = actor::org_unit_type->db_Main->prepare_cached($select);
849     $sth->execute;
850
851     $client->respond( $_->to_fieldmapper ) for ( map { actor::org_unit_type->construct($_) } $sth->fetchall_hash );
852
853     return undef;
854 }
855 __PACKAGE__->register_method(
856     api_name    => 'open-ils.storage.direct.actor.org_unit_type.retrieve.all',
857     api_level   => 1,
858     stream      => 1,
859     method      => 'org_unit_type_list',
860 );
861
862 sub org_unit_full_path {
863     my $self = shift;
864     my $client = shift;
865     my @binds = @_;
866
867     return undef unless (@binds);
868
869     my $func = 'actor.org_unit_full_path(?)';
870     $func = 'actor.org_unit_full_path(?,?)' if (@binds > 1);
871
872     my $sth = actor::org_unit->db_Main->prepare_cached("SELECT * FROM $func");
873     $sth->execute(@binds);
874
875     $client->respond( $_->to_fieldmapper ) for ( map { actor::org_unit->construct($_) } $sth->fetchall_hash );
876
877     return undef;
878 }
879 __PACKAGE__->register_method(
880     api_name    => 'open-ils.storage.actor.org_unit.full_path',
881     api_level   => 1,
882     stream      => 1,
883     method      => 'org_unit_full_path',
884 );
885
886 sub org_unit_ancestors {
887     my $self = shift;
888     my $client = shift;
889     my $id = shift;
890
891     return undef unless ($id);
892
893     my $func = 'actor.org_unit_ancestors(?)';
894
895     my $sth = actor::org_unit->db_Main->prepare_cached(<<"    SQL");
896         SELECT  f.*
897           FROM  $func f
898             JOIN actor.org_unit_type t ON (f.ou_type = t.id)
899           ORDER BY t.depth, f.name;
900     SQL
901     $sth->execute(''.$id);
902
903     $client->respond( $_->to_fieldmapper ) for ( map { actor::org_unit->construct($_) } $sth->fetchall_hash );
904
905     return undef;
906 }
907 __PACKAGE__->register_method(
908     api_name    => 'open-ils.storage.actor.org_unit.ancestors',
909     api_level   => 1,
910     stream      => 1,
911     method      => 'org_unit_ancestors',
912 );
913
914 sub org_unit_descendants {
915     my $self = shift;
916     my $client = shift;
917     my $id = shift;
918     my $depth = shift;
919
920     return undef unless ($id);
921
922     my $func = 'actor.org_unit_descendants(?)';
923     if (defined $depth) {
924         $func = 'actor.org_unit_descendants(?,?)';
925     }
926
927     my $sth = actor::org_unit->db_Main->prepare_cached("SELECT * FROM $func");
928     $sth->execute(''.$id, ''.$depth) if (defined $depth);
929     $sth->execute(''.$id) unless (defined $depth);
930
931     $client->respond( $_->to_fieldmapper ) for ( map { actor::org_unit->construct($_) } $sth->fetchall_hash );
932
933     return undef;
934 }
935 __PACKAGE__->register_method(
936     api_name    => 'open-ils.storage.actor.org_unit.descendants',
937     api_level   => 1,
938     stream      => 1,
939     method      => 'org_unit_descendants',
940 );
941
942 sub fleshed_actor_stat_cat {
943         my $self = shift;
944         my $client = shift;
945         my @list = @_;
946         
947     @list = ($list[0]) unless ($self->api_name =~ /batch/o);
948
949     for my $sc (@list) {
950         my $cat = actor::stat_cat->retrieve($sc);
951         next unless ($cat);
952
953         my $sc_fm = $cat->to_fieldmapper;
954         $sc_fm->entries( [ map { $_->to_fieldmapper } $cat->entries ] );
955         $sc_fm->default_entries( [ map { $_->to_fieldmapper } $cat->default_entries ] );
956
957         $client->respond( $sc_fm );
958
959     }
960
961     return undef;
962 }
963 __PACKAGE__->register_method(
964         api_name        => 'open-ils.storage.fleshed.actor.stat_cat.retrieve',
965         api_level       => 1,
966     argc        => 1,
967         method          => 'fleshed_actor_stat_cat',
968 );
969
970 __PACKAGE__->register_method(
971         api_name        => 'open-ils.storage.fleshed.actor.stat_cat.retrieve.batch',
972         api_level       => 1,
973     argc        => 1,
974         stream          => 1,
975         method          => 'fleshed_actor_stat_cat',
976 );
977
978 #XXX Fix stored proc calls
979 sub ranged_actor_stat_cat_all {
980         my $self = shift;
981         my $client = shift;
982         my $ou = ''.shift();
983         
984         return undef unless ($ou);
985         my $s_table = actor::stat_cat->table;
986
987         my $select = <<"        SQL";
988                 SELECT  s.*
989                   FROM  $s_table s
990                         JOIN actor.org_unit_full_path(?) p ON (p.id = s.owner)
991           ORDER BY name
992         SQL
993
994     $fleshed = 0;
995     $fleshed = 1 if ($self->api_name =~ /fleshed/o);
996
997         my $sth = actor::stat_cat->db_Main->prepare_cached($select);
998         $sth->execute($ou);
999
1000         for my $sc ( map { actor::stat_cat->construct($_) } $sth->fetchall_hash ) {
1001         my $sc_fm = $sc->to_fieldmapper;
1002         $sc_fm->entries(
1003             [ $self->method_lookup( 'open-ils.storage.ranged.actor.stat_cat_entry.search.stat_cat' )->run($ou,$sc->id) ]
1004         ) if ($fleshed);
1005         $sc_fm->default_entries(
1006             [ $self->method_lookup( 'open-ils.storage.actor.stat_cat_entry_default.ancestor.retrieve' )->run($ou,$sc->id) ]
1007         ) if ($fleshed);
1008         $client->respond( $sc_fm );
1009     }
1010
1011         return undef;
1012 }
1013 __PACKAGE__->register_method(
1014         api_name        => 'open-ils.storage.ranged.fleshed.actor.stat_cat.all',
1015         api_level       => 1,
1016     argc        => 1,
1017         stream          => 1,
1018         method          => 'ranged_actor_stat_cat_all',
1019 );
1020
1021 __PACKAGE__->register_method(
1022         api_name        => 'open-ils.storage.ranged.actor.stat_cat.all',
1023         api_level       => 1,
1024     argc        => 1,
1025         stream          => 1,
1026         method          => 'ranged_actor_stat_cat_all',
1027 );
1028
1029 #XXX Fix stored proc calls
1030 sub ranged_actor_stat_cat_entry {
1031         my $self = shift;
1032         my $client = shift;
1033         my $ou = ''.shift();
1034         my $sc = ''.shift();
1035         
1036         return undef unless ($ou);
1037         my $s_table = actor::stat_cat_entry->table;
1038
1039         my $select = <<"        SQL";
1040                 SELECT  s.*
1041                   FROM  $s_table s
1042                         JOIN actor.org_unit_full_path(?) p ON (p.id = s.owner)
1043           WHERE stat_cat = ?
1044           ORDER BY name
1045         SQL
1046
1047         my $sth = actor::stat_cat->db_Main->prepare_cached($select);
1048         $sth->execute($ou,$sc);
1049
1050         for my $sce ( map { actor::stat_cat_entry->construct($_) } $sth->fetchall_hash ) {
1051         my $sce_fm = $sce->to_fieldmapper;
1052         $client->respond( $sce_fm );
1053     }
1054
1055         return undef;
1056 }
1057 __PACKAGE__->register_method(
1058         api_name        => 'open-ils.storage.ranged.actor.stat_cat_entry.search.stat_cat',
1059         api_level       => 1,
1060         stream          => 1,
1061         method          => 'ranged_actor_stat_cat_entry',
1062 );
1063
1064 sub actor_stat_cat_entry_default {
1065     my $self = shift;
1066     my $client = shift;
1067     my $ou = ''.shift();
1068     my $sc = ''.shift();
1069         
1070     return undef unless ($ou);
1071     my $s_table = actor::stat_cat_entry_default->table;
1072
1073     my $select = <<"    SQL";
1074          SELECT  s.*
1075          FROM  $s_table s
1076          WHERE owner = ? AND stat_cat = ?
1077     SQL
1078
1079     my $sth = actor::stat_cat->db_Main->prepare_cached($select);
1080     $sth->execute($ou,$sc);
1081
1082     for my $sced ( map { actor::stat_cat_entry_default->construct($_) } $sth->fetchall_hash ) {
1083         $client->respond( $sced->to_fieldmapper );
1084     }
1085
1086     return undef;
1087 }
1088 __PACKAGE__->register_method(
1089     api_name        => 'open-ils.storage.actor.stat_cat_entry_default.retrieve',
1090     api_level       => 1,
1091     stream          => 1,
1092     method          => 'actor_stat_cat_entry_default',
1093 );
1094
1095 sub actor_stat_cat_entry_default_ancestor {
1096     my $self = shift;
1097     my $client = shift;
1098     my $ou = ''.shift();
1099     my $sc = ''.shift();
1100         
1101     return undef unless ($ou);
1102     my $s_table = actor::stat_cat_entry_default->table;
1103
1104     my $select = <<"    SQL";
1105         SELECT  s.*
1106         FROM  $s_table s
1107         JOIN actor.org_unit_ancestors(?) p ON (p.id = s.owner)
1108         WHERE stat_cat = ?
1109     SQL
1110
1111     my $sth = actor::stat_cat->db_Main->prepare_cached($select);
1112     $sth->execute($ou,$sc);
1113
1114     my @sced =  map { actor::stat_cat_entry_default->construct($_) } $sth->fetchall_hash;
1115
1116     my $ancestor_sced = pop @sced;
1117
1118     $client->respond( $ancestor_sced->to_fieldmapper ) if $ancestor_sced;
1119
1120     return undef;
1121 }
1122 __PACKAGE__->register_method(
1123     api_name        => 'open-ils.storage.actor.stat_cat_entry_default.ancestor.retrieve',
1124     api_level       => 1,
1125     stream          => 1,
1126     method          => 'actor_stat_cat_entry_default_ancestor',
1127 );
1128
1129 1;