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