]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/Publisher/actor.pm
LP#1350042 streaming patron search API
[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 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 AGE(dob) > ?::INTERVAL
36               AND juvenile IS TRUE;
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 sub _clean_regex_chars {
618     my ($search) = @_;
619
620     # Escape metacharacters for SIMILAR TO 
621     # (http://www.postgresql.org/docs/8.4/interactive/functions-matching.html)
622     $search =~ s/\_/\\_/g;
623     $search =~ s/\%/\\%/g;
624     $search =~ s/\|/\\|/g;
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
632     return $search;
633 }
634
635 sub patron_search {
636     my $self = shift;
637     my $client = shift;
638     my $search = shift;
639     my $limit = shift || 1000;
640     my $sort = shift;
641     my $inactive = shift;
642     my $ws_ou = shift;
643     my $search_org = shift || $ws_ou;
644     my $opt_boundary = shift || 0;
645     my $offset = shift || 0;
646
647     my $penalty_sort = 0;
648
649     my $strict_opt_in = OpenSRF::Utils::SettingsClient->new->config_value( share => user => 'opt_in' );
650
651     $sort = ['family_name','first_given_name'] unless ($$sort[0]);
652     push @$sort,'id';
653
654     if ($$sort[0] eq 'penalties') {
655         shift @$sort;
656         $penalty_sort = 1;
657     }
658
659     # group 0 = user
660     # group 1 = address
661     # group 2 = phone, ident
662     # group 3 = barcode
663
664     my $usr = join ' AND ', map { "evergreen.lowercase(CAST($_ AS text)) ~ ?" } grep { ''.$$search{$_}{group} eq '0' } keys %$search;
665     my @usrv = map { "^" . _clean_regex_chars($$search{$_}{value}) } grep { ''.$$search{$_}{group} eq '0' } keys %$search;
666
667     my $addr = join ' AND ', map { "evergreen.lowercase(CAST($_ AS text)) ~ ?" } grep { ''.$$search{$_}{group} eq '1' } keys %$search;
668     my @addrv = map { "^" . _clean_regex_chars($$search{$_}{value}) } grep { ''.$$search{$_}{group} eq '1' } keys %$search;
669
670     my $pv = _clean_regex_chars($$search{phone}{value});
671     my $iv = _clean_regex_chars($$search{ident}{value});
672     my $nv = _clean_regex_chars($$search{name}{value});
673     my $cv = _clean_regex_chars($$search{card}{value});
674
675     my $card = '';
676     if ($cv) {
677         $card = 'JOIN (SELECT DISTINCT usr FROM actor.card WHERE evergreen.lowercase(barcode) LIKE ?||\'%\') AS card ON (card.usr = users.id)';
678         unshift(@usrv, $cv);
679     }
680
681     my $phone = '';
682     my @ps;
683     my @phonev;
684     if ($pv) {
685         for my $p ( qw/day_phone evening_phone other_phone/ ) {
686             if ($pv =~ /^\d+$/) {
687                 push @ps, "evergreen.lowercase(REGEXP_REPLACE($p, '[^0-9]', '', 'g')) ~ ?";
688             } else {
689                 push @ps, "evergreen.lowercase($p) ~ ?";
690             }
691             push @phonev, "^$pv";
692         }
693         $phone = '(' . join(' OR ', @ps) . ')';
694     }
695
696     my $ident = '';
697     my @is;
698     my @identv;
699     if ($iv) {
700         for my $i ( qw/ident_value ident_value2/ ) {
701             push @is, "evergreen.lowercase($i) ~ ?";
702             push @identv, "^$iv";
703         }
704         $ident = '(' . join(' OR ', @is) . ')';
705     }
706
707     my $name = '';
708     my @ns;
709     my @namev;
710     if (0 && $nv) {
711         for my $n ( qw/first_given_name second_given_name family_name/ ) {
712             push @ns, "evergreen.lowercase($n) ~ ?";
713             push @namev, "^$nv";
714         }
715         $name = '(' . join(' OR ', @ns) . ')';
716     }
717
718     my $usr_where = join ' AND ', grep { $_ } ($usr,$phone,$ident,$name);
719     my $addr_where = $addr;
720
721
722     my $u_table = actor::user->table;
723     my $a_table = actor::user_address->table;
724     my $opt_in_table = actor::usr_org_unit_opt_in->table;
725     my $ou_table = actor::org_unit->table;
726
727     my $u_select = "SELECT id as id FROM $u_table u WHERE $usr_where";
728     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";
729
730     my $clone_select = '';
731
732     #$clone_select = "JOIN (SELECT cu.id as id FROM $a_table ca ".
733     #          "JOIN $u_table cu ON (cu.mailing_address = ca.id OR cu.billing_address = ca.id) ".
734     #          "WHERE $addr_where) AS clone ON (clone.id = users.id)" if ($addr_where);
735
736     my $select = '';
737     if ($usr_where) {
738         if ($addr_where) {
739             $select = "$u_select INTERSECT $a_select";
740         } else {
741             $select = $u_select;
742         }
743     } elsif ($addr_where) {
744         $select = "$a_select";
745     }
746
747     return undef if (!$select && !$card);
748
749     my $order_by = join ', ', map { 'evergreen.lowercase(CAST(users.'. (split / /,$_)[0] . ' AS text)) ' . (split / /,$_)[1] } @$sort;
750     my $distinct_list = join ', ', map { 'evergreen.lowercase(CAST(users.'. (split / /,$_)[0] . ' AS text))' } @$sort;
751     my $group_list = $distinct_list;
752
753     if ($inactive) {
754         $inactive = '';
755     } else {
756         $inactive = 'AND users.active = TRUE';
757     }
758
759     if (!$ws_ou) {  # XXX This should be required!!
760         $ws_ou = actor::org_unit->search( { parent_ou => undef } )->next->id;
761     }
762
763     my $descendants = "actor.org_unit_descendants($search_org)";
764
765     my $opt_in_where = '';
766     if (lc($strict_opt_in) eq 'true') {
767         $opt_in_where = "AND (";
768         $opt_in_where .= "EXISTS (select id FROM $opt_in_table ";
769         $opt_in_where .= " WHERE org_unit in (select (actor.org_unit_ancestors($ws_ou)).id)";
770         $opt_in_where .= " AND usr = users.id) ";
771         $opt_in_where .= "OR";
772         $opt_in_where .= " users.home_ou IN (select (actor.org_unit_descendants($ws_ou,$opt_boundary)).id))";
773     }
774
775     my $penalty_join = '';
776     if ($penalty_sort) {
777         $distinct_list = 'COUNT(penalties.id), ' . $distinct_list;
778         $order_by = 'COUNT(penalties.id) DESC, ' . $order_by;
779         unshift @$sort, 'COUNT(penalties.id)';
780         $penalty_join = <<"        SQL";
781             LEFT JOIN actor.usr_standing_penalty penalties
782                 ON (users.id = penalties.usr AND (penalties.stop_date IS NULL OR penalties.stop_date > NOW()))
783         SQL
784     }
785
786     $select = "JOIN ($select) AS search ON (search.id = users.id)" if ($select);
787     $select = <<"    SQL";
788         SELECT  $distinct_list
789           FROM  $u_table AS users $card
790             JOIN $descendants d ON (d.id = users.home_ou)
791             $select
792             $clone_select
793             $penalty_join
794           WHERE users.deleted = FALSE
795             $inactive
796             $opt_in_where
797           GROUP BY $group_list
798           ORDER BY $order_by
799           LIMIT $limit
800           OFFSET $offset
801     SQL
802
803     return actor::user->db_Main->selectcol_arrayref($select, {Columns=>[scalar(@$sort)]}, map {lc($_)} (@usrv,@phonev,@identv,@namev,@addrv));
804 }
805 __PACKAGE__->register_method(
806     api_name    => 'open-ils.storage.actor.user.crazy_search',
807     api_level   => 1,
808     method      => 'patron_search',
809 );
810
811 sub org_unit_list {
812     my $self = shift;
813     my $client = shift;
814
815     my $select =<<"    SQL";
816     SELECT  *
817       FROM  actor.org_unit
818       ORDER BY CASE WHEN parent_ou IS NULL THEN 0 ELSE 1 END, name;
819     SQL
820
821     my $sth = actor::org_unit->db_Main->prepare_cached($select);
822     $sth->execute;
823
824     $client->respond( $_->to_fieldmapper ) for ( map { actor::org_unit->construct($_) } $sth->fetchall_hash );
825
826     return undef;
827 }
828 __PACKAGE__->register_method(
829     api_name    => 'open-ils.storage.direct.actor.org_unit.retrieve.all',
830     api_level   => 1,
831     stream      => 1,
832     method      => 'org_unit_list',
833 );
834
835 sub org_unit_type_list {
836     my $self = shift;
837     my $client = shift;
838
839     my $select =<<"    SQL";
840     SELECT  *
841       FROM  actor.org_unit_type
842       ORDER BY depth, name;
843     SQL
844
845     my $sth = actor::org_unit_type->db_Main->prepare_cached($select);
846     $sth->execute;
847
848     $client->respond( $_->to_fieldmapper ) for ( map { actor::org_unit_type->construct($_) } $sth->fetchall_hash );
849
850     return undef;
851 }
852 __PACKAGE__->register_method(
853     api_name    => 'open-ils.storage.direct.actor.org_unit_type.retrieve.all',
854     api_level   => 1,
855     stream      => 1,
856     method      => 'org_unit_type_list',
857 );
858
859 sub org_unit_full_path {
860     my $self = shift;
861     my $client = shift;
862     my @binds = @_;
863
864     return undef unless (@binds);
865
866     my $func = 'actor.org_unit_full_path(?)';
867     $func = 'actor.org_unit_full_path(?,?)' if (@binds > 1);
868
869     my $sth = actor::org_unit->db_Main->prepare_cached("SELECT * FROM $func");
870     $sth->execute(@binds);
871
872     $client->respond( $_->to_fieldmapper ) for ( map { actor::org_unit->construct($_) } $sth->fetchall_hash );
873
874     return undef;
875 }
876 __PACKAGE__->register_method(
877     api_name    => 'open-ils.storage.actor.org_unit.full_path',
878     api_level   => 1,
879     stream      => 1,
880     method      => 'org_unit_full_path',
881 );
882
883 sub org_unit_ancestors {
884     my $self = shift;
885     my $client = shift;
886     my $id = shift;
887
888     return undef unless ($id);
889
890     my $func = 'actor.org_unit_ancestors(?)';
891
892     my $sth = actor::org_unit->db_Main->prepare_cached(<<"    SQL");
893         SELECT  f.*
894           FROM  $func f
895             JOIN actor.org_unit_type t ON (f.ou_type = t.id)
896           ORDER BY t.depth, f.name;
897     SQL
898     $sth->execute(''.$id);
899
900     $client->respond( $_->to_fieldmapper ) for ( map { actor::org_unit->construct($_) } $sth->fetchall_hash );
901
902     return undef;
903 }
904 __PACKAGE__->register_method(
905     api_name    => 'open-ils.storage.actor.org_unit.ancestors',
906     api_level   => 1,
907     stream      => 1,
908     method      => 'org_unit_ancestors',
909 );
910
911 sub org_unit_descendants {
912     my $self = shift;
913     my $client = shift;
914     my $id = shift;
915     my $depth = shift;
916
917     return undef unless ($id);
918
919     my $func = 'actor.org_unit_descendants(?)';
920     if (defined $depth) {
921         $func = 'actor.org_unit_descendants(?,?)';
922     }
923
924     my $sth = actor::org_unit->db_Main->prepare_cached("SELECT * FROM $func");
925     $sth->execute(''.$id, ''.$depth) if (defined $depth);
926     $sth->execute(''.$id) unless (defined $depth);
927
928     $client->respond( $_->to_fieldmapper ) for ( map { actor::org_unit->construct($_) } $sth->fetchall_hash );
929
930     return undef;
931 }
932 __PACKAGE__->register_method(
933     api_name    => 'open-ils.storage.actor.org_unit.descendants',
934     api_level   => 1,
935     stream      => 1,
936     method      => 'org_unit_descendants',
937 );
938
939 sub fleshed_actor_stat_cat {
940         my $self = shift;
941         my $client = shift;
942         my @list = @_;
943         
944     @list = ($list[0]) unless ($self->api_name =~ /batch/o);
945
946     for my $sc (@list) {
947         my $cat = actor::stat_cat->retrieve($sc);
948         next unless ($cat);
949
950         my $sc_fm = $cat->to_fieldmapper;
951         $sc_fm->entries( [ map { $_->to_fieldmapper } $cat->entries ] );
952         $sc_fm->default_entries( [ map { $_->to_fieldmapper } $cat->default_entries ] );
953
954         $client->respond( $sc_fm );
955
956     }
957
958     return undef;
959 }
960 __PACKAGE__->register_method(
961         api_name        => 'open-ils.storage.fleshed.actor.stat_cat.retrieve',
962         api_level       => 1,
963     argc        => 1,
964         method          => 'fleshed_actor_stat_cat',
965 );
966
967 __PACKAGE__->register_method(
968         api_name        => 'open-ils.storage.fleshed.actor.stat_cat.retrieve.batch',
969         api_level       => 1,
970     argc        => 1,
971         stream          => 1,
972         method          => 'fleshed_actor_stat_cat',
973 );
974
975 #XXX Fix stored proc calls
976 sub ranged_actor_stat_cat_all {
977         my $self = shift;
978         my $client = shift;
979         my $ou = ''.shift();
980         
981         return undef unless ($ou);
982         my $s_table = actor::stat_cat->table;
983
984         my $select = <<"        SQL";
985                 SELECT  s.*
986                   FROM  $s_table s
987                         JOIN actor.org_unit_full_path(?) p ON (p.id = s.owner)
988           ORDER BY name
989         SQL
990
991     $fleshed = 0;
992     $fleshed = 1 if ($self->api_name =~ /fleshed/o);
993
994         my $sth = actor::stat_cat->db_Main->prepare_cached($select);
995         $sth->execute($ou);
996
997         for my $sc ( map { actor::stat_cat->construct($_) } $sth->fetchall_hash ) {
998         my $sc_fm = $sc->to_fieldmapper;
999         $sc_fm->entries(
1000             [ $self->method_lookup( 'open-ils.storage.ranged.actor.stat_cat_entry.search.stat_cat' )->run($ou,$sc->id) ]
1001         ) if ($fleshed);
1002         $sc_fm->default_entries(
1003             [ $self->method_lookup( 'open-ils.storage.actor.stat_cat_entry_default.ancestor.retrieve' )->run($ou,$sc->id) ]
1004         ) if ($fleshed);
1005         $client->respond( $sc_fm );
1006     }
1007
1008         return undef;
1009 }
1010 __PACKAGE__->register_method(
1011         api_name        => 'open-ils.storage.ranged.fleshed.actor.stat_cat.all',
1012         api_level       => 1,
1013     argc        => 1,
1014         stream          => 1,
1015         method          => 'ranged_actor_stat_cat_all',
1016 );
1017
1018 __PACKAGE__->register_method(
1019         api_name        => 'open-ils.storage.ranged.actor.stat_cat.all',
1020         api_level       => 1,
1021     argc        => 1,
1022         stream          => 1,
1023         method          => 'ranged_actor_stat_cat_all',
1024 );
1025
1026 #XXX Fix stored proc calls
1027 sub ranged_actor_stat_cat_entry {
1028         my $self = shift;
1029         my $client = shift;
1030         my $ou = ''.shift();
1031         my $sc = ''.shift();
1032         
1033         return undef unless ($ou);
1034         my $s_table = actor::stat_cat_entry->table;
1035
1036         my $select = <<"        SQL";
1037                 SELECT  s.*
1038                   FROM  $s_table s
1039                         JOIN actor.org_unit_full_path(?) p ON (p.id = s.owner)
1040           WHERE stat_cat = ?
1041           ORDER BY name
1042         SQL
1043
1044         my $sth = actor::stat_cat->db_Main->prepare_cached($select);
1045         $sth->execute($ou,$sc);
1046
1047         for my $sce ( map { actor::stat_cat_entry->construct($_) } $sth->fetchall_hash ) {
1048         my $sce_fm = $sce->to_fieldmapper;
1049         $client->respond( $sce_fm );
1050     }
1051
1052         return undef;
1053 }
1054 __PACKAGE__->register_method(
1055         api_name        => 'open-ils.storage.ranged.actor.stat_cat_entry.search.stat_cat',
1056         api_level       => 1,
1057         stream          => 1,
1058         method          => 'ranged_actor_stat_cat_entry',
1059 );
1060
1061 sub actor_stat_cat_entry_default {
1062     my $self = shift;
1063     my $client = shift;
1064     my $ou = ''.shift();
1065     my $sc = ''.shift();
1066         
1067     return undef unless ($ou);
1068     my $s_table = actor::stat_cat_entry_default->table;
1069
1070     my $select = <<"    SQL";
1071          SELECT  s.*
1072          FROM  $s_table s
1073          WHERE owner = ? AND stat_cat = ?
1074     SQL
1075
1076     my $sth = actor::stat_cat->db_Main->prepare_cached($select);
1077     $sth->execute($ou,$sc);
1078
1079     for my $sced ( map { actor::stat_cat_entry_default->construct($_) } $sth->fetchall_hash ) {
1080         $client->respond( $sced->to_fieldmapper );
1081     }
1082
1083     return undef;
1084 }
1085 __PACKAGE__->register_method(
1086     api_name        => 'open-ils.storage.actor.stat_cat_entry_default.retrieve',
1087     api_level       => 1,
1088     stream          => 1,
1089     method          => 'actor_stat_cat_entry_default',
1090 );
1091
1092 sub actor_stat_cat_entry_default_ancestor {
1093     my $self = shift;
1094     my $client = shift;
1095     my $ou = ''.shift();
1096     my $sc = ''.shift();
1097         
1098     return undef unless ($ou);
1099     my $s_table = actor::stat_cat_entry_default->table;
1100
1101     my $select = <<"    SQL";
1102         SELECT  s.*
1103         FROM  $s_table s
1104         JOIN actor.org_unit_ancestors(?) p ON (p.id = s.owner)
1105         WHERE stat_cat = ?
1106     SQL
1107
1108     my $sth = actor::stat_cat->db_Main->prepare_cached($select);
1109     $sth->execute($ou,$sc);
1110
1111     my @sced =  map { actor::stat_cat_entry_default->construct($_) } $sth->fetchall_hash;
1112
1113     my $ancestor_sced = pop @sced;
1114
1115     $client->respond( $ancestor_sced->to_fieldmapper ) if $ancestor_sced;
1116
1117     return undef;
1118 }
1119 __PACKAGE__->register_method(
1120     api_name        => 'open-ils.storage.actor.stat_cat_entry_default.ancestor.retrieve',
1121     api_level       => 1,
1122     stream          => 1,
1123     method          => 'actor_stat_cat_entry_default_ancestor',
1124 );
1125
1126 1;