refactored copy targeter to use new JS copy tester; adjusted object relationships...
[Evergreen.git] / Open-ILS / src / perlmods / OpenILS / Application / Storage / Publisher / action.pm
1 package OpenILS::Application::Storage::Publisher::action;
2 use base qw/OpenILS::Application::Storage/;
3 use OpenSRF::Utils::Logger qw/:level/;
4 use OpenSRF::Utils qw/:datetime/;
5 use OpenSRF::AppSession;
6 use OpenSRF::EX qw/:try/;
7 use OpenILS::Utils::Fieldmapper;
8 use OpenILS::Utils::PermitHold;
9 use DateTime;
10 use DateTime::Format::ISO8601;
11
12 my $parser = DateTime::Format::ISO8601->new;
13 my $log = 'OpenSRF::Utils::Logger';
14
15 sub overdue_circs {
16         my $grace = shift;
17
18         my $c_t = action::circulation->table;
19
20         $grace = " - ($grace * (fine_interval))" if ($grace);
21
22         my $sql = <<"   SQL";
23                 SELECT  *
24                   FROM  $c_t
25                   WHERE stop_fines IS NULL
26                         AND due_date < ( CURRENT_TIMESTAMP $grace)
27         SQL
28
29         my $sth = action::circulation->db_Main->prepare_cached($sql);
30         $sth->execute;
31
32         return ( map { action::circulation->construct($_) } $sth->fetchall_hash );
33
34 }
35
36 sub grab_overdue {
37         my $self = shift;
38         my $client = shift;
39         my $grace = shift || '';
40
41         $client->respond( $_->to_fieldmapper ) for ( overdue_circs($grace) );
42
43         return undef;
44
45 }
46 __PACKAGE__->register_method(
47         api_name        => 'open-ils.storage.action.circulation.overdue',
48         api_level       => 1,
49         stream          => 1,
50         method          => 'grab_overdue',
51 );
52
53 sub nearest_hold {
54         my $self = shift;
55         my $client = shift;
56         my $pl = shift;
57         my $cp = shift;
58
59         my ($id) = action::hold_request->db_Main->selectrow_array(<<"   SQL", {}, $pl,$cp);
60                 SELECT  h.id
61                   FROM  action.hold_request h
62                         JOIN action.hold_copy_map hm ON (hm.hold = h.id)
63                   WHERE h.pickup_lib = ?
64                         AND hm.target_copy = ?
65                         AND h.capture_time IS NULL
66                 ORDER BY h.pickup_lib - (SELECT home_ou FROM actor.usr a WHERE a.id = h.usr), h.request_time
67                 LIMIT 1
68         SQL
69         return $id;
70 }
71 __PACKAGE__->register_method(
72         api_name        => 'open-ils.storage.action.hold_request.nearest_hold',
73         api_level       => 1,
74         method          => 'nearest_hold',
75 );
76
77 sub next_resp_group_id {
78         my $self = shift;
79         my $client = shift;
80
81         # XXX This is not replication safe!!!
82
83         my ($id) = action::survey->db_Main->selectrow_array(<<" SQL");
84                 SELECT NEXTVAL('action.survey_response_group_id_seq'::TEXT)
85         SQL
86         return $id;
87 }
88 __PACKAGE__->register_method(
89         api_name        => 'open-ils.storage.action.survey_response.next_group_id',
90         api_level       => 1,
91         method          => 'next_resp_group_id',
92 );
93
94 sub patron_circ_summary {
95         my $self = shift;
96         my $client = shift;
97         my $id = ''.shift();
98
99         return undef unless ($id);
100         my $c_table = action::circulation->table;
101         my $b_table = money::billing->table;
102
103         $log->debug("Retrieving patron summary for id $id", DEBUG);
104
105         my $select = <<"        SQL";
106                 SELECT  COUNT(DISTINCT c.id), SUM( COALESCE(b.amount,0) )
107                   FROM  $c_table c
108                         LEFT OUTER JOIN $b_table b ON (c.id = b.xact AND b.voided = FALSE)
109                   WHERE c.usr = ?
110                         AND c.xact_finish IS NULL
111                         AND (
112                                 c.stop_fines NOT IN ('CLAIMSRETURNED','LOST')
113                                 OR c.stop_fines IS NULL
114                         )
115         SQL
116
117         return action::survey->db_Main->selectrow_arrayref($select, {}, $id);
118 }
119 __PACKAGE__->register_method(
120         api_name        => 'open-ils.storage.action.circulation.patron_summary',
121         api_level       => 1,
122         method          => 'patron_circ_summary',
123 );
124
125 #XXX Fix stored proc calls
126 sub find_local_surveys {
127         my $self = shift;
128         my $client = shift;
129         my $ou = ''.shift();
130
131         return undef unless ($ou);
132         my $s_table = action::survey->table;
133
134         my $select = <<"        SQL";
135                 SELECT  s.*
136                   FROM  $s_table s
137                         JOIN actor.org_unit_full_path(?) p ON (p.id = s.owner)
138                   WHERE CURRENT_DATE BETWEEN s.start_date AND s.end_date
139         SQL
140
141         my $sth = action::survey->db_Main->prepare_cached($select);
142         $sth->execute($ou);
143
144         $client->respond( $_->to_fieldmapper ) for ( map { action::survey->construct($_) } $sth->fetchall_hash );
145
146         return undef;
147 }
148 __PACKAGE__->register_method(
149         api_name        => 'open-ils.storage.action.survey.all',
150         api_level       => 1,
151         stream          => 1,
152         method          => 'find_local_surveys',
153 );
154
155 #XXX Fix stored proc calls
156 sub find_opac_surveys {
157         my $self = shift;
158         my $client = shift;
159         my $ou = ''.shift();
160
161         return undef unless ($ou);
162         my $s_table = action::survey->table;
163
164         my $select = <<"        SQL";
165                 SELECT  s.*
166                   FROM  $s_table s
167                         JOIN actor.org_unit_full_path(?) p ON (p.id = s.owner)
168                   WHERE CURRENT_DATE BETWEEN s.start_date AND s.end_date
169                         AND s.opac IS TRUE;
170         SQL
171
172         my $sth = action::survey->db_Main->prepare_cached($select);
173         $sth->execute($ou);
174
175         $client->respond( $_->to_fieldmapper ) for ( map { action::survey->construct($_) } $sth->fetchall_hash );
176
177         return undef;
178 }
179 __PACKAGE__->register_method(
180         api_name        => 'open-ils.storage.action.survey.opac',
181         api_level       => 1,
182         stream          => 1,
183         method          => 'find_opac_surveys',
184 );
185
186 sub find_optional_surveys {
187         my $self = shift;
188         my $client = shift;
189         my $ou = ''.shift();
190
191         return undef unless ($ou);
192         my $s_table = action::survey->table;
193
194         my $select = <<"        SQL";
195                 SELECT  s.*
196                   FROM  $s_table s
197                         JOIN actor.org_unit_full_path(?) p ON (p.id = s.owner)
198                   WHERE CURRENT_DATE BETWEEN s.start_date AND s.end_date
199                         AND s.required IS FALSE;
200         SQL
201
202         my $sth = action::survey->db_Main->prepare_cached($select);
203         $sth->execute($ou);
204
205         $client->respond( $_->to_fieldmapper ) for ( map { action::survey->construct($_) } $sth->fetchall_hash );
206
207         return undef;
208 }
209 __PACKAGE__->register_method(
210         api_name        => 'open-ils.storage.action.survey.optional',
211         api_level       => 1,
212         stream          => 1,
213         method          => 'find_optional_surveys',
214 );
215
216 sub find_required_surveys {
217         my $self = shift;
218         my $client = shift;
219         my $ou = ''.shift();
220
221         return undef unless ($ou);
222         my $s_table = action::survey->table;
223
224         my $select = <<"        SQL";
225                 SELECT  s.*
226                   FROM  $s_table s
227                         JOIN actor.org_unit_full_path(?) p ON (p.id = s.owner)
228                   WHERE CURRENT_DATE BETWEEN s.start_date AND s.end_date
229                         AND s.required IS TRUE;
230         SQL
231
232         my $sth = action::survey->db_Main->prepare_cached($select);
233         $sth->execute($ou);
234
235         $client->respond( $_->to_fieldmapper ) for ( map { action::survey->construct($_) } $sth->fetchall_hash );
236
237         return undef;
238 }
239 __PACKAGE__->register_method(
240         api_name        => 'open-ils.storage.action.survey.required',
241         api_level       => 1,
242         stream          => 1,
243         method          => 'find_required_surveys',
244 );
245
246 sub find_usr_summary_surveys {
247         my $self = shift;
248         my $client = shift;
249         my $ou = ''.shift();
250
251         return undef unless ($ou);
252         my $s_table = action::survey->table;
253
254         my $select = <<"        SQL";
255                 SELECT  s.*
256                   FROM  $s_table s
257                         JOIN actor.org_unit_full_path(?) p ON (p.id = s.owner)
258                   WHERE CURRENT_DATE BETWEEN s.start_date AND s.end_date
259                         AND s.usr_summary IS TRUE;
260         SQL
261
262         my $sth = action::survey->db_Main->prepare_cached($select);
263         $sth->execute($ou);
264
265         $client->respond( $_->to_fieldmapper ) for ( map { action::survey->construct($_) } $sth->fetchall_hash );
266
267         return undef;
268 }
269 __PACKAGE__->register_method(
270         api_name        => 'open-ils.storage.action.survey.usr_summary',
271         api_level       => 1,
272         stream          => 1,
273         method          => 'find_usr_summary_surveys',
274 );
275
276
277 sub generate_fines {
278         my $self = shift;
279         my $client = shift;
280         my $grace = shift;
281         my $circ = shift;
282         
283         
284         my @circs;
285         if ($circ) {
286                 push @circs, action::circulation->search_where( { id => $circ, stop_fines => undef } );
287         } else {
288                 push @circs, overdue_circs($grace);
289         }
290
291         for my $c (@circs) {
292         
293                 try {
294                         my $due_dt = $parser->parse_datetime( clense_ISO8601( $c->due_date ) );
295         
296                         my $due = $due_dt->epoch;
297                         my $now = time;
298                         my $fine_interval = interval_to_seconds( $c->fine_interval );
299         
300                         if ( interval_to_seconds( $c->fine_interval ) >= interval_to_seconds('1d') ) {  
301                                 my $tz_offset_s = 0;;
302                                 if ($due_dt->strftime('%z') =~ /(-|\+)(\d{2}):?(\d{2})/) {
303                                         $tz_offset_s = $1 . interval_to_seconds( "${2}h ${3}m"); 
304                                 }
305         
306                                 $due -= ($due % $fine_interval) + $tz_offset_s;
307                                 $now -= ($now % $fine_interval) + $tz_offset_s;
308                         }
309         
310                         $client->respond(
311                                 "ARG! Overdue circulation ".$c->id.
312                                 " for item ".$c->target_copy.
313                                 " (user ".$c->usr.").\n".
314                                 "\tItem was due on or before: ".localtime($due)."\n");
315         
316                         my ($fine) = money::billing->search(
317                                 xact => $c->id, voided => 'f',
318                                 { order_by => 'billing_ts DESC', limit => '1' }
319                         );
320         
321                         my $last_fine;
322                         if ($fine) {
323                                 $last_fine = $parser->parse_datetime( clense_ISO8601( $fine->billing_ts ) )->epoch;
324                         } else {
325                                 $last_fine = $due;
326                                 $last_fine += $fine_interval * $grace;
327                         }
328         
329                         my $pending_fine_count = int( ($now - $last_fine) / $fine_interval ); 
330                         unless($pending_fine_count) {
331                                 $client->respond( "\tNo fines to create.  " );
332                                 if ($grace && $now < $due + $fine_interval * $grace) {
333                                         $client->respond( "Still inside grace period of: ". seconds_to_interval( $fine_interval * $grace)."\n" );
334                                 } else {
335                                         $client->respond( "Last fine generated for: ".localtime($last_fine)."\n" );
336                                 }
337                                 next;
338                         }
339         
340                         $client->respond( "\t$pending_fine_count pending fine(s)\n" );
341         
342                         for my $bill (1 .. $pending_fine_count) {
343         
344                                 my ($total) = money::billable_transaction_summary->retrieve( $c->id );
345         
346                                 if ($total && $total->balance_owed > $c->max_fine) {
347                                         $c->update({stop_fines => 'MAXFINES'});
348                                         $client->respond(
349                                                 "\tMaximum fine level of ".$c->max_fine.
350                                                 " reached for this circulation.\n".
351                                                 "\tNo more fines will be generated.\n" );
352                                         last;
353                                 }
354         
355                                 my $billing = money::billing->create(
356                                         { xact          => ''.$c->id,
357                                           note          => "Overdue Fine",
358                                           billing_type  => "Overdue materials",
359                                           amount        => ''.$c->recuring_fine,
360                                           billing_ts    => DateTime->from_epoch( epoch => $last_fine + $fine_interval * $bill )->strftime('%FT%T%z')
361                                         }
362                                 );
363         
364                                 $client->respond(
365                                         "\t\tCreating fine of ".$billing->amount." for period starting ".
366                                         localtime(
367                                                 $parser->parse_datetime(
368                                                         clense_ISO8601( $billing->billing_ts )
369                                                 )->epoch
370                                         )."\n" );
371                         }
372                 } catch Error with {
373                         my $e = shift;
374                         $client->respond( "Error processing overdue circulation [".$c->id."]:\n\n$e\n" );
375                 };
376         }
377 }
378 __PACKAGE__->register_method(
379         api_name        => 'open-ils.storage.action.circulation.overdue.generate_fines',
380         api_level       => 1,
381         stream          => 1,
382         method          => 'generate_fines',
383 );
384
385
386
387 sub new_hold_copy_targeter {
388         my $self = shift;
389         my $client = shift;
390         my $check_expire = shift;
391         my $one_hold = shift;
392
393         my $time = time;
394         $check_expire ||= '12h';
395         $check_expire = interval_to_seconds( $check_expire );
396
397         my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = gmtime(time() - $check_expire);
398         $year += 1900;
399         $mon += 1;
400         my $expire_threshold = sprintf(
401                 '%s-%0.2d-%0.2dT%0.2d:%0.2d:%0.2d-00',
402                 $year, $mon, $mday, $hour, $min, $sec
403         );
404
405
406         my $holds;
407
408         try {
409                 if ($one_hold) {
410                         $holds = [ action::hold_request->search(id => $one_hold) ];
411                 } else {
412                         $holds = [ action::hold_request->search_where(
413                                                         { capture_time => undef,
414                                                           fulfillment_time => undef,
415                                                           prev_check_time => { '<=' => $expire_threshold },
416                                                         },
417                                                         { order_by => 'request_time,prev_check_time' } ) ];
418                         push @$holds, action::hold_request->search(
419                                                         capture_time => undef,
420                                                         fulfillment_time => undef,
421                                                         prev_check_time => undef,
422                                                         { order_by => 'request_time' } );
423                 }
424         } catch Error with {
425                 my $e = shift;
426                 die "Could not retrieve uncaptured hold requests:\n\n$e\n";
427         };
428
429         my @successes;
430         for my $hold (@$holds) {
431                 try {
432                         #action::hold_request->db_Main->begin_work;
433                         if ($self->method_lookup('open-ils.storage.transaction.current')->run) {
434                                 $log->debug("Cleaning up after previous transaction\n");
435                                 $self->method_lookup('open-ils.storage.transaction.rollback')->run;
436                         }
437                         $self->method_lookup('open-ils.storage.transaction.begin')->run( $client );
438                         $log->info("Processing hold ".$hold->id."...\n");
439
440                         action::hold_copy_map->search( { hold => $hold->id } )->delete_all;
441         
442                         my $all_copies = [];
443
444                         # find all the potential copies
445                         if ($hold->hold_type eq 'M') {
446                                 for my $r ( map
447                                                 {$_->record}
448                                                 metabib::record_descriptor
449                                                         ->search(
450                                                                 record => [ map { $_->id } metabib::metarecord
451                                                                                         ->retrieve($hold->target)
452                                                                                         ->source_records ],
453                                                                 item_type => [split '', $hold->holdable_formats]
454                                                         )
455                                 ) {
456                                         my ($rtree) = $self
457                                                 ->method_lookup( 'open-ils.storage.biblio.record_entry.ranged_tree')
458                                                 ->run( $r->id, $hold->request_lib->id, $hold->selection_depth );
459
460                                         for my $cn ( @{ $rtree->call_numbers } ) {
461                                                 push @$all_copies,
462                                                         asset::copy->search( id => [map {$_->id} @{ $cn->copies }] );
463                                         }
464                                 }
465                         } elsif ($hold->hold_type eq 'T') {
466                                 my ($rtree) = $self
467                                         ->method_lookup( 'open-ils.storage.biblio.record_entry.ranged_tree')
468                                         ->run( $hold->target, $hold->request_lib->id, $hold->selection_depth );
469
470                                 unless ($rtree) {
471                                         push @successes, { hold => $hold->id, eligible_copies => 0, error => 'NO_RECORD' };
472                                         die 'OK';
473                                 }
474
475                                 for my $cn ( @{ $rtree->call_numbers } ) {
476                                         push @$all_copies,
477                                                 asset::copy->search( id => [map {$_->id} @{ $cn->copies }] );
478                                 }
479                         } elsif ($hold->hold_type eq 'V') {
480                                 my ($vtree) = $self
481                                         ->method_lookup( 'open-ils.storage.asset.call_number.ranged_tree')
482                                         ->run( $hold->target, $hold->request_lib->id, $hold->selection_depth );
483
484                                 push @$all_copies,
485                                         asset::copy->search( id => [map {$_->id} @{ $vtree->copies }] );
486                                         
487                         } elsif  ($hold->hold_type eq 'C') {
488
489                                 $all_copies = [asset::copy->retrieve($hold->target)];
490                         }
491
492                         # let 'em know we're still working
493                         $client->status( new OpenSRF::DomainObject::oilsContinueStatus );
494                         
495                         if (!ref $all_copies || !@$all_copies) {
496                                 $log->info("\tNo copies available for targeting at all!\n");
497                                 $self->method_lookup('open-ils.storage.transaction.commit')->run;
498                                 push @successes, { hold => $hold->id, eligible_copies => 0, error => 'NO_COPIES' };
499                                 die 'OK';
500                         }
501
502                         my $copies = [];
503                         for my $c ( @$all_copies ) {
504                                 push @$copies, $c
505                                         if ( OpenILS::Utils::PermitHold::permit_copy_hold(
506                                                 { title => $c->call_number->record->to_fieldmapper,
507                                                   title_descriptor => $c->call_number->record->record_descriptor->next->to_fieldmapper,
508                                                   patron => $hold->usr->to_fieldmapper,
509                                                   copy => $c->to_fieldmapper,
510                                                   requestor => $hold->requestor->to_fieldmapper,
511                                                   request_lib => $hold->request_lib->to_fieldmapper,
512                                                 } ));
513                         }
514                         my $copy_count = @$copies;
515                         $client->status( new OpenSRF::DomainObject::oilsContinueStatus );
516
517                         # map the potentials, so that we can pick up checkins
518                         $log->debug( "\tMapping ".scalar(@copies)." potential copies for hold ".$hold->id);
519                         action::hold_copy_map->create( { hold => $hold->id, target_copy => $_->id } ) for (@copies);
520
521                         my @good_copies;
522                         for my $c (@$copies) {
523                                 next if ($c->id == $hold->current_copy);
524                                 push @good_copies, $c if ($c);
525                         }
526
527                         $log->debug("\t".scalar(@good_copies)." (non-current) copies available for targeting...");
528
529                         my $old_best = $hold->current_copy;
530                         $hold->update({ current_copy => undef });
531         
532                         if (!scalar(@good_copies)) {
533                                 $log->info("\tNo (non-current) copies eligible to fill the hold.");
534                                 if ( $old_best && grep { $old_best == $_ } @$copies ) {
535                                         $log->debug("\tPushing current_copy back onto the targeting list");
536                                         push @good_copies, $old_best;
537                                 } else {
538                                         $log->debug("\tcurrent_copy is no longer available for targeting... NEXT HOLD, PLEASE!");
539                                         $self->method_lookup('open-ils.storage.transaction.commit')->run;
540                                         push @successes, { hold => $hold->id, eligible_copies => 0, error => 'NO_TARGETS' };
541                                         die 'OK';
542                                 }
543                         }
544
545                         $client->status( new OpenSRF::DomainObject::oilsContinueStatus );
546                         my $prox_list = [];
547                         $$prox_list[0] =
548                         [
549                                 grep {
550                                         $_->circ_lib == $hold->pickup_lib && 
551                                         ( $_->status == 0 || $_->status == 7 )
552                                 } @good_copies
553                         ];
554
555                         $copies = [grep {$_->circ_lib != $hold->pickup_lib } @good_copies];
556
557                         my $best = $self->choose_nearest_copy($hold, $prox_list);
558
559                         if (!$best) {
560                                 $prox_list = $self->create_prox_list( $hold->pickup_lib, $copies );
561                                 $best = $self->choose_nearest_copy($hold, $prox_list);
562                         }
563
564                         $client->status( new OpenSRF::DomainObject::oilsContinueStatus );
565                         if ($old_best) {
566                                 # hold wasn't fulfilled, record the fact
567                         
568                                 $log->info("\tHold was not (but should have been) fulfilled by ".$old_best->id);
569                                 action::unfulfilled_hold_list->create(
570                                                 { hold => ''.$hold->id,
571                                                   current_copy => ''.$old_best->id,
572                                                   circ_lib => ''.$old_best->circ_lib,
573                                                 });
574                         }
575
576                         if ($best) {
577                                 $hold->update( { current_copy => ''.$best->id } );
578                                 $log->debug("\tTargeting copy ".$best->id." for hold fulfillment.");
579                         } else {
580                                 $log->debug( "\tThere were no targetable copies for the hold" );
581                         }
582
583                         $hold->update( { prev_check_time => 'now' } );
584                         $log->info("\tUpdating hold [".$hold->id."] with new 'current_copy' [".$best->id."] for hold fulfillment.");
585
586                         $self->method_lookup('open-ils.storage.transaction.commit')->run;
587                         $log->info("\tProcessing of hold ".$hold->id." complete.");
588
589                         push @successes,
590                                 { hold => $hold->id,
591                                   old_target => ($old_best ? $old_best->id : undef),
592                                   eligible_copies => $copy_count,
593                                   target => ($best ? $best->id : undef) };
594
595                 } otherwise {
596                         my $e = shift;
597                         if ($e !~ /^OK/o) {
598                                 $log->error("Processing of hold failed:  $e");
599                                 $self->method_lookup('open-ils.storage.transaction.rollback')->run;
600                         }
601                 };
602         }
603
604         return \@successes;
605 }
606 __PACKAGE__->register_method(
607         api_name        => 'open-ils.storage.action.hold_request.copy_targeter',
608         api_level       => 1,
609         method          => 'new_hold_copy_targeter',
610 );
611
612 my $locations;
613 my $statuses;
614 my %cache = (titles => {}, cns => {});
615 sub hold_copy_targeter {
616         my $self = shift;
617         my $client = shift;
618         my $check_expire = shift;
619         my $one_hold = shift;
620
621         $self->{user_filter} = OpenSRF::AppSession->create('open-ils.circ');
622         $self->{user_filter}->connect;
623         $self->{client} = $client;
624
625         my $time = time;
626         $check_expire ||= '12h';
627         $check_expire = interval_to_seconds( $check_expire );
628
629         my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = gmtime(time() - $check_expire);
630         $year += 1900;
631         $mon += 1;
632         my $expire_threshold = sprintf(
633                 '%s-%0.2d-%0.2dT%0.2d:%0.2d:%0.2d-00',
634                 $year, $mon, $mday, $hour, $min, $sec
635         );
636
637
638         $statuses ||= [ config::copy_status->search(holdable => 't') ];
639
640         $locations ||= [ asset::copy_location->search(holdable => 't') ];
641
642         my $holds;
643
644         %cache = (titles => {}, cns => {});
645
646         try {
647                 if ($one_hold) {
648                         $holds = [ action::hold_request->search(id => $one_hold) ];
649                 } else {
650                         $holds = [ action::hold_request->search_where(
651                                                         { capture_time => undef,
652                                                           prev_check_time => { '<=' => $expire_threshold },
653                                                         },
654                                                         { order_by => 'request_time,prev_check_time' } ) ];
655                         push @$holds, action::hold_request->search(
656                                                         capture_time => undef,
657                                                         prev_check_time => undef,
658                                                         { order_by => 'request_time' } );
659                 }
660         } catch Error with {
661                 my $e = shift;
662                 die "Could not retrieve uncaptured hold requests:\n\n$e\n";
663         };
664
665         for my $hold (@$holds) {
666                 try {
667                         #action::hold_request->db_Main->begin_work;
668                         if ($self->method_lookup('open-ils.storage.transaction.current')->run) {
669                                 $client->respond("Cleaning up after previous transaction\n");
670                                 $self->method_lookup('open-ils.storage.transaction.rollback')->run;
671                         }
672                         $self->method_lookup('open-ils.storage.transaction.begin')->run( $client );
673                         $client->respond("Processing hold ".$hold->id."...\n");
674
675                         my $copies;
676
677                         $copies = $self->metarecord_hold_capture($hold) if ($hold->hold_type eq 'M');
678                         $self->{client}->status( new OpenSRF::DomainObject::oilsContinueStatus );
679
680                         $copies = $self->title_hold_capture($hold) if ($hold->hold_type eq 'T');
681                         $self->{client}->status( new OpenSRF::DomainObject::oilsContinueStatus );
682                         
683                         $copies = $self->volume_hold_capture($hold) if ($hold->hold_type eq 'V');
684                         $self->{client}->status( new OpenSRF::DomainObject::oilsContinueStatus );
685                         
686                         $copies = $self->copy_hold_capture($hold) if ($hold->hold_type eq 'C');
687
688                         unless (ref $copies || !@$copies) {
689                                 $client->respond("\tNo copies available for targeting at all!\n");
690                         }
691
692                         my @good_copies;
693                         for my $c (@$copies) {
694                                 next if ( grep {$c->id == $hold->current_copy} @good_copies);
695                                 push @good_copies, $c if ($c);
696                         }
697
698                         $client->respond("\t".scalar(@good_copies)." (non-current) copies available for targeting...\n");
699
700                         my $old_best = $hold->current_copy;
701                         $hold->update({ current_copy => undef });
702         
703                         if (!scalar(@good_copies)) {
704                                 $client->respond("\tNo (non-current) copies available to fill the hold.\n");
705                                 if ( $old_best && grep {$c->id == $hold->current_copy} @$copies ) {
706                                         $client->respond("\tPushing current_copy back onto the targeting list\n");
707                                         push @good_copies, asset::copy->retrieve( $old_best );
708                                 } else {
709                                         $client->respond("\tcurrent_copy is no longer available for targeting... NEXT HOLD, PLEASE!\n");
710                                         next;
711                                 }
712                         }
713
714                         my $prox_list;
715                         $$prox_list[0] = [grep {$_->circ_lib == $hold->pickup_lib } @good_copies];
716                         $copies = [grep {$_->circ_lib != $hold->pickup_lib } @good_copies];
717
718                         my $best = $self->choose_nearest_copy($hold, $prox_list);
719
720                         if (!$best) {
721                                 $prox_list = $self->create_prox_list( $hold->pickup_lib, $copies );
722                                 $best = $self->choose_nearest_copy($hold, $prox_list);
723                         }
724
725                         if ($old_best) {
726                                 # hold wasn't fulfilled, record the fact
727                         
728                                 $client->respond("\tHold was not (but should have been) fulfilled by ".$old_best->id.".\n");
729                                 action::unfulfilled_hold_list->create(
730                                                 { hold => ''.$hold->id,
731                                                   current_copy => ''.$old_best->id,
732                                                   circ_lib => ''.$old_best->circ_lib,
733                                                 });
734                         }
735
736                         if ($best) {
737                                 $hold->update( { current_copy => ''.$best->id } );
738                                 $client->respond("\tTargeting copy ".$best->id." for hold fulfillment.\n");
739                         }
740
741                         $hold->update( { prev_check_time => 'now' } );
742                         $client->respond("\tUpdating hold ".$hold->id." with new 'current_copy' for hold fulfillment.\n");
743
744                         $client->respond("\tProcessing of hold ".$hold->id." complete.\n");
745                         $self->method_lookup('open-ils.storage.transaction.commit')->run;
746
747                         #action::hold_request->dbi_commit;
748
749                 } otherwise {
750                         my $e = shift;
751                         $log->error("Processing of hold failed:  $e");
752                         $client->respond("\tProcessing of hold failed!.\n\t\t$e\n");
753                         $self->method_lookup('open-ils.storage.transaction.rollback')->run;
754                         #action::hold_request->dbi_rollback;
755                 };
756         }
757
758         $self->{user_filter}->disconnect;
759         $self->{user_filter}->finish;
760         delete $$self{user_filter};
761         return undef;
762 }
763 __PACKAGE__->register_method(
764         api_name        => 'open-ils.storage.action.hold_request.copy_targeter',
765         api_level       => 0,
766         stream          => 1,
767         method          => 'hold_copy_targeter',
768 );
769
770
771 sub copy_hold_capture {
772         my $self = shift;
773         my $hold = shift;
774         my $cps = shift;
775
776         if (!defined($cps)) {
777                 try {
778                         $cps = [ asset::copy->search( id => $hold->target ) ];
779                 } catch Error with {
780                         my $e = shift;
781                         die "Could not retrieve initial volume list:\n\n$e\n";
782                 };
783         }
784
785         my @copies = grep { $_->holdable } @$cps;
786
787         for (my $i = 0; $i < @$cps; $i++) {
788                 next unless $$cps[$i];
789                 
790                 my $cn = $cache{cns}{$copies[$i]->call_number};
791                 my $rec = $cache{titles}{$cn->record};
792                 $copies[$i] = undef if ($copies[$i] && !grep{ $copies[$i]->status eq $_->id}@$statuses);
793                 $copies[$i] = undef if ($copies[$i] && !grep{ $copies[$i]->location eq $_->id}@$locations);
794                 $copies[$i] = undef if (
795                         !$copies[$i] ||
796                         !$self->{user_filter}->request(
797                                 'open-ils.circ.permit_hold',
798                                 $hold->to_fieldmapper, do {
799                                         my $cp_fm = $copies[$i]->to_fieldmapper;
800                                         $cp_fm->circ_lib( $copies[$i]->circ_lib->to_fieldmapper );
801                                         $cp_fm->location( $copies[$i]->location->to_fieldmapper );
802                                         $cp_fm->status( $copies[$i]->status->to_fieldmapper );
803                                         $cp_fm;
804                                 },
805                                 { title => $rec->to_fieldmapper,
806                                   usr => actor::user->retrieve($hold->usr)->to_fieldmapper,
807                                   requestor => actor::user->retrieve($hold->requestor)->to_fieldmapper,
808                                 })->gather(1)
809                 );
810                 $self->{client}->status( new OpenSRF::DomainObject::oilsContinueStatus );
811         }
812
813         @copies = grep { $_ } @copies;
814
815         my $count = @copies;
816
817         return unless ($count);
818         
819         action::hold_copy_map->search( { hold => $hold->id } )->delete_all;
820         
821         my @maps;
822         $self->{client}->respond( "\tMapping ".scalar(@copies)." eligable copies for hold ".$hold->id."\n");
823         for my $c (@copies) {
824                 push @maps, action::hold_copy_map->create( { hold => $hold->id, target_copy => $c->id } );
825         }
826         $self->{client}->respond( "\tA total of ".scalar(@maps)." mapping were created for hold ".$hold->id."\n");
827
828         return \@copies;
829 }
830
831
832 sub choose_nearest_copy {
833         my $self = shift;
834         my $hold = shift;
835         my $prox_list = shift;
836
837         for my $p ( 0 .. int( scalar(@$prox_list) - 1) ) {
838                 next unless (ref $$prox_list[$p]);
839                 my @capturable = grep { $_->status == 0 } @{ $$prox_list[$p] };
840                 next unless (@capturable);
841                 return $capturable[rand(scalar(@capturable))];
842         }
843 }
844
845 sub create_prox_list {
846         my $self = shift;
847         my $lib = shift;
848         my $copies = shift;
849
850         my @prox_list;
851         for my $cp (@$copies) {
852                 my ($prox) = $self->method_lookup('open-ils.storage.asset.copy.proximity')->run( $cp->id, $lib );
853                 $prox_list[$prox] = [] unless defined($prox_list[$prox]);
854                 push @{$prox_list[$prox]}, $cp;
855         }
856         return \@prox_list;
857 }
858
859 sub volume_hold_capture {
860         my $self = shift;
861         my $hold = shift;
862         my $vols = shift;
863
864         if (!defined($vols)) {
865                 try {
866                         $vols = [ asset::call_number->search( id => $hold->target ) ];
867                         $cache{cns}{$_->id} = $_ for (@$vols);
868                 } catch Error with {
869                         my $e = shift;
870                         die "Could not retrieve initial volume list:\n\n$e\n";
871                 };
872         }
873
874         my @v_ids = map { $_->id } @$vols;
875
876         my $cp_list;
877         try {
878                 $cp_list = [ asset::copy->search( call_number => \@v_ids ) ];
879         
880         } catch Error with {
881                 my $e = shift;
882                 warn "Could not retrieve copy list:\n\n$e\n";
883         };
884
885         $self->copy_hold_capture($hold,$cp_list) if (ref $cp_list and @$cp_list);
886 }
887
888 sub title_hold_capture {
889         my $self = shift;
890         my $hold = shift;
891         my $titles = shift;
892
893         if (!defined($titles)) {
894                 try {
895                         $titles = [ biblio::record_entry->search( id => $hold->target ) ];
896                         $cache{titles}{$_->id} = $_ for (@$titles);
897                 } catch Error with {
898                         my $e = shift;
899                         die "Could not retrieve initial title list:\n\n$e\n";
900                 };
901         }
902
903         my @t_ids = map { $_->id } @$titles;
904         my $cn_list;
905         try {
906                 ($cn_list) = $self->method_lookup('open-ils.storage.direct.asset.call_number.search.record.atomic')->run( \@t_ids );
907         
908         } catch Error with {
909                 my $e = shift;
910                 warn "Could not retrieve volume list:\n\n$e\n";
911         };
912
913         $cache{cns}{$_->id} = $_ for (@$cn_list);
914
915         $self->volume_hold_capture($hold,$cn_list) if (ref $cn_list and @$cn_list);
916 }
917
918 sub metarecord_hold_capture {
919         my $self = shift;
920         my $hold = shift;
921
922         my $titles;
923         try {
924                 $titles = [ metabib::metarecord_source_map->search( metarecord => $hold->target) ];
925         
926         } catch Error with {
927                 my $e = shift;
928                 die "Could not retrieve initial title list:\n\n$e\n";
929         };
930
931         try {
932                 my @recs = map {$_->record} metabib::record_descriptor->search( record => $titles, item_type => [split '', $hold->holdable_formats] ); 
933
934                 $titles = [ biblio::record_entry->search( id => \@recs ) ];
935         
936         } catch Error with {
937                 my $e = shift;
938                 die "Could not retrieve format-pruned title list:\n\n$e\n";
939         };
940
941
942         $cache{titles}{$_->id} = $_ for (@$titles);
943         $self->title_hold_capture($hold,$titles) if (ref $titles and @$titles);
944 }
945
946 1;