]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/perlmods/lib/OpenILS/Application/Circ.pm
Docs: incorporating offline circ docs
[Evergreen.git] / Open-ILS / src / perlmods / lib / OpenILS / Application / Circ.pm
1 package OpenILS::Application::Circ;
2 use OpenILS::Application;
3 use base qw/OpenILS::Application/;
4 use strict; use warnings;
5
6 use OpenILS::Application::Circ::Circulate;
7 use OpenILS::Application::Circ::Survey;
8 use OpenILS::Application::Circ::StatCat;
9 use OpenILS::Application::Circ::Holds;
10 use OpenILS::Application::Circ::HoldNotify;
11 use OpenILS::Application::Circ::CircNotify;
12 use OpenILS::Application::Circ::CreditCard;
13 use OpenILS::Application::Circ::Money;
14 use OpenILS::Application::Circ::NonCat;
15 use OpenILS::Application::Circ::CopyLocations;
16 use OpenILS::Application::Circ::CircCommon;
17
18 use DateTime;
19 use DateTime::Format::ISO8601;
20
21 use OpenILS::Application::AppUtils;
22
23 use OpenSRF::Utils qw/:datetime/;
24 use OpenSRF::AppSession;
25 use OpenILS::Utils::ModsParser;
26 use OpenILS::Event;
27 use OpenSRF::EX qw(:try);
28 use OpenSRF::Utils::Logger qw(:logger);
29 use OpenILS::Utils::Fieldmapper;
30 use OpenILS::Utils::CStoreEditor q/:funcs/;
31 use OpenILS::Const qw/:const/;
32 use OpenSRF::Utils::SettingsClient;
33 use OpenILS::Application::Cat::AssetCommon;
34
35 my $apputils = "OpenILS::Application::AppUtils";
36 my $U = $apputils;
37
38 my $holdcode    = "OpenILS::Application::Circ::Holds";
39
40 # ------------------------------------------------------------------------
41 # Top level Circ package;
42 # ------------------------------------------------------------------------
43
44 sub initialize {
45     my $self = shift;
46     OpenILS::Application::Circ::Circulate->initialize();
47 }
48
49
50 __PACKAGE__->register_method(
51     method => 'retrieve_circ',
52     authoritative   => 1,
53     api_name    => 'open-ils.circ.retrieve',
54     signature => q/
55         Retrieve a circ object by id
56         @param authtoken Login session key
57         @pararm circid The id of the circ object
58         @param all_circ Returns an action.all_circulation_slim object instead
59             of an action.circulation object to pick up aged circs.
60     /
61 );
62
63 sub retrieve_circ {
64     my( $s, $c, $a, $i, $all_circ ) = @_;
65     my $e = new_editor(authtoken => $a);
66     return $e->event unless $e->checkauth;
67     my $method = $all_circ ?
68         'retrieve_action_all_circulation_slim' :
69         'retrieve_action_circulation';
70     my $circ = $e->$method($i) or return $e->event;
71     if( $e->requestor->id ne ($circ->usr || '') ) {
72         return $e->event unless $e->allowed('VIEW_CIRCULATIONS');
73     }
74     return $circ;
75 }
76
77
78 __PACKAGE__->register_method(
79     method => 'fetch_circ_mods',
80     api_name => 'open-ils.circ.circ_modifier.retrieve.all');
81 sub fetch_circ_mods {
82     my($self, $conn, $args) = @_;
83     my $mods = new_editor()->retrieve_all_config_circ_modifier;
84     return [ map {$_->code} @$mods ] unless $$args{full};
85     return $mods;
86 }
87
88 __PACKAGE__->register_method(
89     method => 'ranged_billing_types',
90     api_name => 'open-ils.circ.billing_type.ranged.retrieve.all');
91
92 sub ranged_billing_types {
93     my($self, $conn, $auth, $org_id, $depth) = @_;
94     my $e = new_editor(authtoken => $auth);
95     return $e->event unless $e->checkauth;
96     return $e->event unless $e->allowed('VIEW_BILLING_TYPE', $org_id);
97     return $e->search_config_billing_type(
98         {owner => $U->get_org_full_path($org_id, $depth)});
99 }
100
101
102
103 # ------------------------------------------------------------------------
104 # Returns an array of {circ, record} hashes checked out by the user.
105 # ------------------------------------------------------------------------
106 __PACKAGE__->register_method(
107     method  => "checkouts_by_user",
108     api_name    => "open-ils.circ.actor.user.checked_out",
109     stream => 1,
110     NOTES        => <<"    NOTES");
111     Returns a list of open circulations as a pile of objects.  Each object
112     contains the relevant copy, circ, and record
113     NOTES
114
115 sub checkouts_by_user {
116     my($self, $client, $auth, $user_id) = @_;
117
118     my $e = new_editor(authtoken=>$auth);
119     return $e->event unless $e->checkauth;
120
121     my $circ_ids = $e->search_action_circulation(
122         {   usr => $user_id,
123             checkin_time => undef,
124             '-or' => [
125                 {stop_fines => undef},
126                 {stop_fines => ['MAXFINES','LONGOVERDUE']}
127             ]
128         },
129         {idlist => 1}
130     );
131
132     for my $id (@$circ_ids) {
133         my $circ = $e->retrieve_action_circulation([
134             $id,
135             {   flesh => 3,
136                 flesh_fields => {
137                     circ => ['target_copy'],
138                     acp => ['call_number'],
139                     acn => ['record']
140                 }
141             }
142         ]);
143
144         # un-flesh for consistency
145         my $c = $circ->target_copy;
146         $circ->target_copy($c->id);
147
148         my $cn = $c->call_number;
149         $c->call_number($cn->id);
150
151         my $t = $cn->record;
152         $cn->record($t->id);
153
154         $client->respond(
155             {   circ => $circ,
156                 copy => $c,
157                 record => $U->record_to_mvr($t)
158             }
159         );
160     }
161
162     return undef;
163 }
164
165
166
167 __PACKAGE__->register_method(
168     method  => "checkouts_by_user_slim",
169     api_name    => "open-ils.circ.actor.user.checked_out.slim",
170     NOTES        => <<"    NOTES");
171     Returns a list of open circulation objects
172     NOTES
173
174 # DEPRECAT ME?? XXX
175 sub checkouts_by_user_slim {
176     my( $self, $client, $user_session, $user_id ) = @_;
177
178     my( $requestor, $target, $copy, $record, $evt );
179
180     ( $requestor, $target, $evt ) = 
181         $apputils->checkses_requestor( $user_session, $user_id, 'VIEW_CIRCULATIONS');
182     return $evt if $evt;
183
184     $logger->debug( 'User ' . $requestor->id . 
185         " retrieving checked out items for user " . $target->id );
186
187     # XXX Make the call correct..
188     return $apputils->simplereq(
189         'open-ils.cstore',
190         "open-ils.cstore.direct.action.open_circulation.search.atomic", 
191         { usr => $target->id, checkin_time => undef } );
192 #       { usr => $target->id } );
193 }
194
195
196 __PACKAGE__->register_method(
197     method  => "checkouts_by_user_opac",
198     api_name    => "open-ils.circ.actor.user.checked_out.opac",);
199
200 # XXX Deprecate Me
201 sub checkouts_by_user_opac {
202     my( $self, $client, $auth, $user_id ) = @_;
203
204     my $e = new_editor( authtoken => $auth );
205     return $e->event unless $e->checkauth;
206     $user_id ||= $e->requestor->id;
207     return $e->event unless 
208         my $patron = $e->retrieve_actor_user($user_id);
209
210     my $data;
211     my $search = {usr => $user_id, stop_fines => undef};
212
213     if( $user_id ne $e->requestor->id ) {
214         $data = $e->search_action_circulation(
215             $search, {checkperm=>1, permorg=>$patron->home_ou})
216             or return $e->event;
217
218     } else {
219         $data = $e->search_action_circulation($search);
220     }
221
222     return $data;
223 }
224
225
226 __PACKAGE__->register_method(
227     method  => "title_from_transaction",
228     api_name    => "open-ils.circ.circ_transaction.find_title",
229     NOTES        => <<"    NOTES");
230     Returns a mods object for the title that is linked to from the 
231     copy from the hold that created the given transaction
232     NOTES
233
234 sub title_from_transaction {
235     my( $self, $client, $login_session, $transactionid ) = @_;
236
237     my( $user, $circ, $title, $evt );
238
239     ( $user, $evt ) = $apputils->checkses( $login_session );
240     return $evt if $evt;
241
242     ( $circ, $evt ) = $apputils->fetch_circulation($transactionid);
243     return $evt if $evt;
244     
245     ($title, $evt) = $apputils->fetch_record_by_copy($circ->target_copy);
246     return $evt if $evt;
247
248     return $apputils->record_to_mvr($title);
249 }
250
251 __PACKAGE__->register_method(
252     method  => "staff_age_to_lost",
253     api_name    => "open-ils.circ.circulation.age_to_lost",
254     stream => 1,
255     signature   => q/
256         This fires a circ.staff_age_to_lost Action-Trigger event against all
257         overdue circulations in scope of the specified context library and
258         user profile, which effectively marks the associated items as Lost.
259         This is likely to be done at the end of a semester in an academic
260         library, etc.
261         @param auth
262         @param args : circ_lib, user_profile
263     /
264 );
265
266 sub staff_age_to_lost {
267     my( $self, $conn, $auth, $args ) = @_;
268     my $e = new_editor(authtoken=>$auth);
269     return $e->event unless $e->checkauth;
270     return $e->event unless $e->allowed('SET_CIRC_LOST', $args->{'circ_lib'});
271
272     my $orgs = $U->get_org_descendants($args->{'circ_lib'});
273     my $profiles = $U->fetch_permission_group_descendants($args->{'user_profile'});
274
275     my $ses = OpenSRF::AppSession->create('open-ils.trigger');
276
277     my $method = 'open-ils.trigger.passive.event.autocreate.batch';
278     my $hook = 'circ.staff_age_to_lost';
279     my $context_org = 'circ_lib';
280     my $opt_granularity = undef;
281     my $filter = { 
282         "checkin_time" => undef,
283         "due_date" => { "<" => "now" }, 
284         "-or" => [ 
285             { "stop_fines"  => ["MAXFINES", "LONGOVERDUE"] }, # FIXME: CLAIMSRETURNED also?
286             { "stop_fines"  => undef }
287         ],
288         "-and" => [
289             {"-exists" => {
290                 "select" => {"au" => ["id"]},
291                 "from"   => "au",
292                 "where"  => {
293                     "profile" => $profiles,
294                     "id" => { "=" => {"+circ" => "usr"} }
295                 }
296             }},
297             {"-exists" => {
298                 "select" => {"aou" => ["id"]},
299                 "from"   => "aou",
300                 "where"  => {
301                     "-and" => [
302                         {"id" => { "=" => {"+circ" => "circ_lib"} }},
303                         {"id" => $orgs}
304                     ]
305                 }
306             }}
307         ]
308     };
309     my $req_timeout = 10800;
310     my $chunk_size = 100;
311     my $progress = 1;
312
313     my $req = $ses->request($method, $hook, $context_org, $filter, $opt_granularity);
314     my @event_ids; my @chunked_ids;
315     while (my $resp = $req->recv(timeout => $req_timeout)) {
316         push(@event_ids, $resp->content);
317         push(@chunked_ids, $resp->content);
318         if (scalar(@chunked_ids) > $chunk_size) {
319             $conn->respond({'progress'=>$progress++}); # 'event_ids'=>@chunked_ids
320             @chunked_ids = ();
321         }
322     }
323     if (scalar(@chunked_ids) > 0) {
324         $conn->respond({'progress'=>$progress++}); # 'event_ids'=>@chunked_ids
325     }
326
327     if(@event_ids) {
328         $logger->info("staff_age_to_lost: created ".scalar(@event_ids)." events for circ.staff_age_to_lost");
329         $conn->respond_complete({'total_progress'=>$progress-1,'created'=>scalar(@event_ids)});
330     } elsif($req->complete) {
331         $logger->info("staff_age_to_lost: no events to create for circ.staff_age_to_lost");
332         $conn->respond_complete({'total_progress'=>$progress-1,'created'=>0});
333     } else {
334         $logger->warn("staff_age_to_lost: timeout occurred during event creation for circ.staff_age_to_lost");
335         $conn->respond_complete({'total_progress'=>$progress-1,'error'=>'timeout'});
336     }
337
338     return undef;
339 }
340
341
342 __PACKAGE__->register_method(
343     method  => "new_set_circ_lost",
344     api_name    => "open-ils.circ.circulation.set_lost",
345     signature   => q/
346         Sets the copy and related open circulation to lost
347         @param auth
348         @param args : barcode
349     /
350 );
351
352
353 # ---------------------------------------------------------------------
354 # Sets a circulation to lost.  updates copy status to lost
355 # applies copy and/or prcoessing fees depending on org settings
356 # ---------------------------------------------------------------------
357 sub new_set_circ_lost {
358     my( $self, $conn, $auth, $args ) = @_;
359
360     my $e = new_editor(authtoken=>$auth, xact=>1);
361     return $e->die_event unless $e->checkauth;
362
363     my $copy = $e->search_asset_copy({barcode=>$$args{barcode}, deleted=>'f'})->[0]
364         or return $e->die_event;
365
366     my $evt = OpenILS::Application::Cat::AssetCommon->set_item_lost($e, $copy->id);
367     return $evt if $evt;
368
369     $e->commit;
370     return 1;
371 }
372
373
374 __PACKAGE__->register_method(
375     method  => "set_circ_claims_returned",
376     api_name    => "open-ils.circ.circulation.set_claims_returned",
377     signature => {
378         desc => q/Sets the circ for a given item as claims returned
379                 If a backdate is provided, overdue fines will be voided
380                 back to the backdate/,
381         params => [
382             {desc => 'Authentication token', type => 'string'},
383             {desc => 'Arguments, including "barcode" and optional "backdate"', type => 'object'}
384         ],
385         return => {desc => q/1 on success, failure event on error, and 
386             PATRON_EXCEEDS_CLAIMS_RETURN_COUNT if the patron exceeds the 
387             configured claims return maximum/}
388     }
389 );
390
391 __PACKAGE__->register_method(
392     method  => "set_circ_claims_returned",
393     api_name    => "open-ils.circ.circulation.set_claims_returned.override",
394     signature => {
395         desc => q/This adds support for overrideing the configured max 
396                 claims returned amount. 
397                 @see open-ils.circ.circulation.set_claims_returned./,
398     }
399 );
400
401 sub set_circ_claims_returned {
402     my( $self, $conn, $auth, $args, $oargs ) = @_;
403
404     my $e = new_editor(authtoken=>$auth, xact=>1);
405     return $e->die_event unless $e->checkauth;
406
407     $oargs = { all => 1 } unless defined $oargs;
408
409     my $barcode = $$args{barcode};
410     my $backdate = $$args{backdate};
411
412     my $copy = $e->search_asset_copy({barcode=>$barcode, deleted=>'f'})->[0] 
413         or return $e->die_event;
414
415     my $circ = $e->search_action_circulation(
416         {checkin_time => undef, target_copy => $copy->id})->[0]
417             or return $e->die_event;
418
419     $backdate = $circ->due_date if $$args{use_due_date};
420
421     $logger->info("marking circ for item $barcode as claims returned".
422         (($backdate) ? " with backdate $backdate" : ''));
423
424     my $patron = $e->retrieve_actor_user($circ->usr);
425     my $max_count = $U->ou_ancestor_setting_value(
426         $circ->circ_lib, 'circ.max_patron_claim_return_count', $e);
427
428     # If the patron has too instances of many claims returned, 
429     # require an override to continue.  A configured max of 
430     # 0 means all attempts require an override
431     if(defined $max_count and $patron->claims_returned_count >= $max_count) {
432
433         if($self->api_name =~ /override/ && ($oargs->{all} || grep { $_ eq 'PATRON_EXCEEDS_CLAIMS_RETURN_COUNT' } @{$oargs->{events}})) {
434
435             # see if we're allowed to override
436             return $e->die_event unless 
437                 $e->allowed('SET_CIRC_CLAIMS_RETURNED.override', $circ->circ_lib);
438
439         } else {
440
441             # exit early and return the max claims return event
442             $e->rollback;
443             return OpenILS::Event->new(
444                 'PATRON_EXCEEDS_CLAIMS_RETURN_COUNT', 
445                 payload => {
446                     patron_count => $patron->claims_returned_count,
447                     max_count => $max_count
448                 }
449             );
450         }
451     }
452
453     $e->allowed('SET_CIRC_CLAIMS_RETURNED', $circ->circ_lib) 
454         or return $e->die_event;
455
456     $circ->stop_fines(OILS_STOP_FINES_CLAIMSRETURNED);
457     $circ->stop_fines_time('now') unless $circ->stop_fines_time;
458
459     if( $backdate ) {
460         $backdate = cleanse_ISO8601($backdate);
461
462         my $original_date = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($circ->due_date));
463         my $new_date = DateTime::Format::ISO8601->new->parse_datetime($backdate);
464         $backdate = $new_date->ymd . 'T' . $original_date->strftime('%T%z');
465
466         # clean it up once again; need a : in the timezone offset. E.g. -06:00 not -0600
467         $backdate = cleanse_ISO8601($backdate);
468
469         # make it look like the circ stopped at the cliams returned time
470         $circ->stop_fines_time($backdate);
471         my $evt = OpenILS::Application::Circ::CircCommon->void_or_zero_overdues($e, $circ, {backdate => $backdate, note => 'System: OVERDUE REVERSED FOR CLAIMS-RETURNED', force_zero => 1});
472         return $evt if $evt;
473     }
474
475     $e->update_action_circulation($circ) or return $e->die_event;
476
477     # see if there is a configured post-claims-return copy status
478     if(my $stat = $U->ou_ancestor_setting_value($circ->circ_lib, 'circ.claim_return.copy_status')) {
479         $copy->status($stat);
480         $copy->edit_date('now');
481         $copy->editor($e->requestor->id);
482         $e->update_asset_copy($copy) or return $e->die_event;
483     }
484
485     # Check if the copy circ lib wants lost fees voided on claims
486     # returned.
487     if ($U->is_true($U->ou_ancestor_setting_value($copy->circ_lib, 'circ.void_lost_on_claimsreturned', $e))) {
488         my $result = OpenILS::Application::Circ::CircCommon->void_lost(
489             $e,
490             $circ,
491             3
492         );
493         if ($result) {
494             $e->rollback;
495             return $result;
496         }
497     }
498
499     # Check if the copy circ lib wants lost processing fees voided on
500     # claims returned.
501     if ($U->is_true($U->ou_ancestor_setting_value($copy->circ_lib, 'circ.void_lost_proc_fee_on_claimsreturned', $e))) {
502         my $result = OpenILS::Application::Circ::CircCommon->void_lost(
503             $e,
504             $circ,
505             4
506         );
507         if ($result) {
508             $e->rollback;
509             return $result;
510         }
511     }
512
513     # Check if the copy circ lib wants longoverdue fees voided on claims
514     # returned.
515     if ($U->is_true($U->ou_ancestor_setting_value($copy->circ_lib, 'circ.void_longoverdue_on_claimsreturned', $e))) {
516         my $result = OpenILS::Application::Circ::CircCommon->void_lost(
517             $e,
518             $circ,
519             10
520         );
521         if ($result) {
522             $e->rollback;
523             return $result;
524         }
525     }
526
527     # Check if the copy circ lib wants longoverdue processing fees voided on
528     # claims returned.
529     if ($U->is_true($U->ou_ancestor_setting_value($copy->circ_lib, 'circ.void_longoverdue_proc_fee_on_claimsreturned', $e))) {
530         my $result = OpenILS::Application::Circ::CircCommon->void_lost(
531             $e,
532             $circ,
533             11
534         );
535         if ($result) {
536             $e->rollback;
537             return $result;
538         }
539     }
540
541     # Now that all data has been munged, do a no-op update of 
542     # the patron to force a change of the last_xact_id value.
543     $e->update_actor_user($e->retrieve_actor_user($circ->usr))
544         or return $e->die_event;
545
546     $e->commit;
547     return 1;
548 }
549
550
551 __PACKAGE__->register_method(
552     method  => "post_checkin_backdate_circ",
553     api_name    => "open-ils.circ.post_checkin_backdate",
554     signature => {
555         desc => q/Back-date an already checked in circulation/,
556         params => [
557             {desc => 'Authentication token', type => 'string'},
558             {desc => 'Circ ID', type => 'number'},
559             {desc => 'ISO8601 backdate', type => 'string'},
560         ],
561         return => {desc => q/1 on success, failure event on error/}
562     }
563 );
564
565 __PACKAGE__->register_method(
566     method  => "post_checkin_backdate_circ",
567     api_name    => "open-ils.circ.post_checkin_backdate.batch",
568     stream => 1,
569     signature => {
570         desc => q/@see open-ils.circ.post_checkin_backdate.  Batch mode/,
571         params => [
572             {desc => 'Authentication token', type => 'string'},
573             {desc => 'List of Circ ID', type => 'array'},
574             {desc => 'ISO8601 backdate', type => 'string'},
575         ],
576         return => {desc => q/Set of: 1 on success, failure event on error/}
577     }
578 );
579
580
581 sub post_checkin_backdate_circ {
582     my( $self, $conn, $auth, $circ_id, $backdate ) = @_;
583     my $e = new_editor(authtoken=>$auth);
584     return $e->die_event unless $e->checkauth;
585     if($self->api_name =~ /batch/) {
586         foreach my $c (@$circ_id) {
587             $conn->respond(post_checkin_backdate_circ_impl($e, $c, $backdate));
588         }
589     } else {
590         $conn->respond_complete(post_checkin_backdate_circ_impl($e, $circ_id, $backdate));
591     }
592
593     $e->disconnect;
594     return undef;
595 }
596
597
598 sub post_checkin_backdate_circ_impl {
599     my($e, $circ_id, $backdate) = @_;
600
601     $e->xact_begin;
602
603     my $circ = $e->retrieve_action_circulation($circ_id)
604         or return $e->die_event;
605
606     # anyone with checkin perms can backdate (more restrictive?)
607     return $e->die_event unless $e->allowed('COPY_CHECKIN', $circ->circ_lib);
608
609     # don't allow back-dating an open circulation
610     return OpenILS::Event->new('BAD_PARAMS') unless 
611         $backdate and $circ->checkin_time;
612
613     # update the checkin and stop_fines times to reflect the new backdate
614     $circ->stop_fines_time(cleanse_ISO8601($backdate));
615     $circ->checkin_time(cleanse_ISO8601($backdate));
616     $e->update_action_circulation($circ) or return $e->die_event;
617
618     # now void the overdues "erased" by the back-dating
619     my $evt = OpenILS::Application::Circ::CircCommon->void_or_zero_overdues($e, $circ, {backdate => $backdate});
620     return $evt if $evt;
621
622     # If the circ was closed before and the balance owned !=0, re-open the transaction
623     $evt = OpenILS::Application::Circ::CircCommon->reopen_xact($e, $circ->id);
624     return $evt if $evt;
625
626     $e->xact_commit;
627     return 1;
628 }
629
630
631
632 __PACKAGE__->register_method (
633     method      => 'set_circ_due_date',
634     api_name        => 'open-ils.circ.circulation.due_date.update',
635     signature   => q/
636         Updates the due_date on the given circ
637         @param authtoken
638         @param circid The id of the circ to update
639         @param date The timestamp of the new due date
640     /
641 );
642
643 sub set_circ_due_date {
644     my( $self, $conn, $auth, $circ_id, $date ) = @_;
645
646     my $e = new_editor(xact=>1, authtoken=>$auth);
647     return $e->die_event unless $e->checkauth;
648     my $circ = $e->retrieve_action_circulation($circ_id)
649         or return $e->die_event;
650
651     return $e->die_event unless $e->allowed('CIRC_OVERRIDE_DUE_DATE', $circ->circ_lib);
652     $date = cleanse_ISO8601($date);
653
654     if (!(interval_to_seconds($circ->duration) % 86400)) { # duration is divisible by days
655         my $original_date = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($circ->due_date));
656         my $new_date = DateTime::Format::ISO8601->new->parse_datetime($date);
657         $date = cleanse_ISO8601( $new_date->ymd . 'T' . $original_date->strftime('%T%z') );
658     }
659
660     $circ->due_date($date);
661     $e->update_action_circulation($circ) or return $e->die_event;
662     $e->commit;
663
664     return $circ;
665 }
666
667
668 __PACKAGE__->register_method(
669     method      => "create_in_house_use",
670     api_name        => 'open-ils.circ.in_house_use.create',
671     signature   =>  q/
672         Creates an in-house use action.
673         @param $authtoken The login session key
674         @param params A hash of params including
675             'location' The org unit id where the in-house use occurs
676             'copyid' The copy in question
677             'count' The number of in-house uses to apply to this copy
678         @return An array of id's representing the id's of the newly created
679         in-house use objects or an event on an error
680     /);
681
682 __PACKAGE__->register_method(
683     method      => "create_in_house_use",
684     api_name        => 'open-ils.circ.non_cat_in_house_use.create',
685 );
686
687
688 sub create_in_house_use {
689     my( $self, $client, $auth, $params ) = @_;
690
691     my( $evt, $copy );
692     my $org         = $params->{location};
693     my $copyid      = $params->{copyid};
694     my $count       = $params->{count} || 1;
695     my $nc_type     = $params->{non_cat_type};
696     my $use_time    = $params->{use_time} || 'now';
697
698     my $e = new_editor(xact=>1,authtoken=>$auth);
699     return $e->event unless $e->checkauth;
700     return $e->event unless $e->allowed('CREATE_IN_HOUSE_USE');
701
702     my $non_cat = 1 if $self->api_name =~ /non_cat/;
703
704     unless( $non_cat ) {
705         if( $copyid ) {
706             $copy = $e->retrieve_asset_copy($copyid) or return $e->event;
707         } else {
708             $copy = $e->search_asset_copy({barcode=>$params->{barcode}, deleted => 'f'})->[0]
709                 or return $e->event;
710             $copyid = $copy->id;
711         }
712     }
713
714     if( $use_time ne 'now' ) {
715         $use_time = cleanse_ISO8601($use_time);
716         $logger->debug("in_house_use setting use time to $use_time");
717     }
718
719     my @ids;
720     for(1..$count) {
721
722         my $ihu;
723         my $method;
724         my $cmeth;
725
726         if($non_cat) {
727             $ihu = Fieldmapper::action::non_cat_in_house_use->new;
728             $ihu->item_type($nc_type);
729             $method = 'open-ils.storage.direct.action.non_cat_in_house_use.create';
730             $cmeth = "create_action_non_cat_in_house_use";
731
732         } else {
733             $ihu = Fieldmapper::action::in_house_use->new;
734             $ihu->item($copyid);
735             $method = 'open-ils.storage.direct.action.in_house_use.create';
736             $cmeth = "create_action_in_house_use";
737         }
738
739         $ihu->staff($e->requestor->id);
740         $ihu->org_unit($org);
741         $ihu->use_time($use_time);
742
743         $ihu = $e->$cmeth($ihu) or return $e->event;
744         push( @ids, $ihu->id );
745     }
746
747     $e->commit;
748     return \@ids;
749 }
750
751
752
753
754
755 __PACKAGE__->register_method(
756     method  => "view_circs",
757     api_name    => "open-ils.circ.copy_checkout_history.retrieve",
758     notes       => q/
759         Retrieves the last X circs for a given copy
760         @param authtoken The login session key
761         @param copyid The copy to check
762         @param count How far to go back in the item history
763         @return An array of circ ids
764     /);
765
766 # ----------------------------------------------------------------------
767 # Returns $count most recent circs.  If count exceeds the configured 
768 # max, use the configured max instead
769 # ----------------------------------------------------------------------
770 sub view_circs {
771     my( $self, $client, $authtoken, $copyid, $count ) = @_; 
772
773     my $e = new_editor(authtoken => $authtoken);
774     return $e->event unless $e->checkauth;
775     
776     my $copy = $e->retrieve_asset_copy([
777         $copyid,
778         {   flesh => 1,
779             flesh_fields => {acp => ['call_number']}
780         }
781     ]) or return $e->event;
782
783     return $e->event unless $e->allowed(
784         'VIEW_COPY_CHECKOUT_HISTORY', 
785         ($copy->call_number == OILS_PRECAT_CALL_NUMBER) ? 
786             $copy->circ_lib : $copy->call_number->owning_lib);
787         
788     my $max_history = $U->ou_ancestor_setting_value(
789         $e->requestor->ws_ou, 'circ.item_checkout_history.max', $e);
790
791     if(defined $max_history) {
792         $count = $max_history unless defined $count and $count < $max_history;
793     } else {
794         $count = 4 unless defined $count;
795     }
796
797     return $e->search_action_all_circulation_slim([
798         {target_copy => $copyid}, 
799         {limit => $count, order_by => { aacs => "xact_start DESC" }} 
800     ]);
801 }
802
803
804 __PACKAGE__->register_method(
805     method  => "circ_count",
806     api_name    => "open-ils.circ.circulation.count",
807     notes       => q/
808         Returns the number of times the item has circulated
809         @param copyid The copy to check
810     /);
811
812 sub circ_count {
813     my( $self, $client, $copyid ) = @_; 
814
815     my $count = new_editor()->json_query({
816         select => {
817             circbyyr => [{
818                 column => 'count',
819                 transform => 'sum',
820                 aggregate => 1
821             }]
822         },
823         from => 'circbyyr',
824         where => {'+circbyyr' => {copy => $copyid}}
825     })->[0]->{count};
826
827     return {
828         total => {
829             when => 'total',
830             count => $count
831         }
832     };
833 }
834
835
836 __PACKAGE__->register_method(
837     method      => 'fetch_notes',
838     authoritative   => 1,
839     api_name        => 'open-ils.circ.copy_note.retrieve.all',
840     signature   => q/
841         Returns an array of copy note objects.  
842         @param args A named hash of parameters including:
843             authtoken   : Required if viewing non-public notes
844             itemid      : The id of the item whose notes we want to retrieve
845             pub         : True if all the caller wants are public notes
846         @return An array of note objects
847     /);
848
849 __PACKAGE__->register_method(
850     method      => 'fetch_notes',
851     api_name        => 'open-ils.circ.call_number_note.retrieve.all',
852     signature   => q/@see open-ils.circ.copy_note.retrieve.all/);
853
854 __PACKAGE__->register_method(
855     method      => 'fetch_notes',
856     api_name        => 'open-ils.circ.title_note.retrieve.all',
857     signature   => q/@see open-ils.circ.copy_note.retrieve.all/);
858
859
860 # NOTE: VIEW_COPY/VOLUME/TITLE_NOTES perms should always be global
861 sub fetch_notes {
862     my( $self, $connection, $args ) = @_;
863
864     my $id = $$args{itemid};
865     my $authtoken = $$args{authtoken};
866     my( $r, $evt);
867
868     if( $self->api_name =~ /copy/ ) {
869         if( $$args{pub} ) {
870             return $U->cstorereq(
871                 'open-ils.cstore.direct.asset.copy_note.search.atomic',
872                 { owning_copy => $id, pub => 't' } );
873         } else {
874             ( $r, $evt ) = $U->checksesperm($authtoken, 'VIEW_COPY_NOTES');
875             return $evt if $evt;
876             return $U->cstorereq(
877                 'open-ils.cstore.direct.asset.copy_note.search.atomic', {owning_copy => $id} );
878         }
879
880     } elsif( $self->api_name =~ /call_number/ ) {
881         if( $$args{pub} ) {
882             return $U->cstorereq(
883                 'open-ils.cstore.direct.asset.call_number_note.search.atomic',
884                 { call_number => $id, pub => 't' } );
885         } else {
886             ( $r, $evt ) = $U->checksesperm($authtoken, 'VIEW_VOLUME_NOTES');
887             return $evt if $evt;
888             return $U->cstorereq(
889                 'open-ils.cstore.direct.asset.call_number_note.search.atomic', { call_number => $id } );
890         }
891
892     } elsif( $self->api_name =~ /title/ ) {
893         if( $$args{pub} ) {
894             return $U->cstorereq(
895                 'open-ils.cstore.direct.bilbio.record_note.search.atomic',
896                 { record => $id, pub => 't' } );
897         } else {
898             ( $r, $evt ) = $U->checksesperm($authtoken, 'VIEW_TITLE_NOTES');
899             return $evt if $evt;
900             return $U->cstorereq(
901                 'open-ils.cstore.direct.biblio.record_note.search.atomic', { record => $id } );
902         }
903     }
904
905     return undef;
906 }
907
908 __PACKAGE__->register_method(
909     method  => 'has_notes',
910     api_name    => 'open-ils.circ.copy.has_notes');
911 __PACKAGE__->register_method(
912     method  => 'has_notes',
913     api_name    => 'open-ils.circ.call_number.has_notes');
914 __PACKAGE__->register_method(
915     method  => 'has_notes',
916     api_name    => 'open-ils.circ.title.has_notes');
917
918
919 sub has_notes {
920     my( $self, $conn, $authtoken, $id ) = @_;
921     my $editor = new_editor(authtoken => $authtoken);
922     return $editor->event unless $editor->checkauth;
923
924     my $n = $editor->search_asset_copy_note(
925         {owning_copy=>$id}, {idlist=>1}) if $self->api_name =~ /copy/;
926
927     $n = $editor->search_asset_call_number_note(
928         {call_number=>$id}, {idlist=>1}) if $self->api_name =~ /call_number/;
929
930     $n = $editor->search_biblio_record_note(
931         {record=>$id}, {idlist=>1}) if $self->api_name =~ /title/;
932
933     return scalar @$n;
934 }
935
936
937
938 __PACKAGE__->register_method(
939     method      => 'create_copy_note',
940     api_name        => 'open-ils.circ.copy_note.create',
941     signature   => q/
942         Creates a new copy note
943         @param authtoken The login session key
944         @param note The note object to create
945         @return The id of the new note object
946     /);
947
948 sub create_copy_note {
949     my( $self, $connection, $authtoken, $note ) = @_;
950
951     my $e = new_editor(xact=>1, authtoken=>$authtoken);
952     return $e->event unless $e->checkauth;
953     my $copy = $e->retrieve_asset_copy(
954         [
955             $note->owning_copy,
956             {   flesh => 1,
957                 flesh_fields => { 'acp' => ['call_number'] }
958             }
959         ]
960     );
961
962     return $e->event unless 
963         $e->allowed('CREATE_COPY_NOTE', $copy->call_number->owning_lib);
964
965     $note->create_date('now');
966     $note->creator($e->requestor->id);
967     $note->pub( ($U->is_true($note->pub)) ? 't' : 'f' );
968     $note->clear_id;
969
970     $e->create_asset_copy_note($note) or return $e->event;
971     $e->commit;
972     return $note->id;
973 }
974
975
976 __PACKAGE__->register_method(
977     method      => 'delete_copy_note',
978     api_name        =>  'open-ils.circ.copy_note.delete',
979     signature   => q/
980         Deletes an existing copy note
981         @param authtoken The login session key
982         @param noteid The id of the note to delete
983         @return 1 on success - Event otherwise.
984         /);
985 sub delete_copy_note {
986     my( $self, $conn, $authtoken, $noteid ) = @_;
987
988     my $e = new_editor(xact=>1, authtoken=>$authtoken);
989     return $e->die_event unless $e->checkauth;
990
991     my $note = $e->retrieve_asset_copy_note([
992         $noteid,
993         { flesh => 2,
994             flesh_fields => {
995                 'acpn' => [ 'owning_copy' ],
996                 'acp' => [ 'call_number' ],
997             }
998         }
999     ]) or return $e->die_event;
1000
1001     if( $note->creator ne $e->requestor->id ) {
1002         return $e->die_event unless 
1003             $e->allowed('DELETE_COPY_NOTE', $note->owning_copy->call_number->owning_lib);
1004     }
1005
1006     $e->delete_asset_copy_note($note) or return $e->die_event;
1007     $e->commit;
1008     return 1;
1009 }
1010
1011 __PACKAGE__->register_method(
1012     method      => 'fetch_copy_tags',
1013     authoritative   => 1,
1014     api_name        => 'open-ils.circ.copy_tags.retrieve',
1015     signature   => q/
1016         Returns an array of publicly-visible copy tag objects.  
1017         @param args A named hash of parameters including:
1018             copy_id     : The id of the item whose notes we want to retrieve
1019             tag_type    : Type of copy tags to retrieve, e.g., 'bookplate' (optional)
1020             scope       : top of org subtree whose copy tags we want to see
1021             depth       : how far down to look for copy tags (optional)
1022         @return An array of copy tag objects
1023     /);
1024 __PACKAGE__->register_method(
1025     method      => 'fetch_copy_tags',
1026     authoritative   => 1,
1027     api_name        => 'open-ils.circ.copy_tags.retrieve.staff',
1028     signature   => q/
1029         Returns an array of all copy tag objects.  
1030         @param args A named hash of parameters including:
1031             authtoken   : Required to view non-public notes
1032             copy_id     : The id of the item whose notes we want to retrieve (optional)
1033             tag_type    : Type of copy tags to retrieve, e.g., 'bookplate'
1034             scope       : top of org subtree whose copy tags we want to see
1035             depth       : how far down to look for copy tags (optional)
1036         @return An array of copy tag objects
1037     /);
1038
1039 sub fetch_copy_tags {
1040     my ($self, $conn, $args) = @_;
1041
1042     my $org = $args->{scope};
1043     my $depth = $args->{depth};
1044
1045     my $filter = {};
1046     my $e;
1047     if ($self->api_name =~ /\.staff/) {
1048         my $authtoken = $args->{authtoken};
1049         return new OpenILS::Event("BAD_PARAMS", "desc" => "authtoken required") unless defined $authtoken;    
1050         $e = new_editor(authtoken => $args->{authtoken});
1051         return $e->event unless $e->checkauth;
1052         return $e->event unless $e->allowed('STAFF_LOGIN', $org);
1053     } else {
1054         $e = new_editor();
1055         $filter->{pub} = 't';
1056     }
1057     $filter->{tag_type} = $args->{tag_type} if exists($args->{tag_type});
1058     $filter->{'+acptcm'} = {
1059         copy => $args->{copy_id}
1060     };
1061
1062     # filter by owner of copy tag and depth
1063     $filter->{owner} = {
1064         in => {
1065             select => {aou => [{
1066                 column => 'id',
1067                 transform => 'actor.org_unit_descendants',
1068                 result_field => 'id',
1069                 (defined($depth) ? ( params => [$depth] ) : ()),
1070             }]},
1071             from => 'aou',
1072             where => {id => $org}
1073         }
1074     };
1075
1076     return $e->search_asset_copy_tag([$filter, { join => { acptcm => {} } }]);
1077 }
1078
1079
1080 __PACKAGE__->register_method(
1081     method => 'age_hold_rules',
1082     api_name    =>  'open-ils.circ.config.rules.age_hold_protect.retrieve.all',
1083 );
1084
1085 sub age_hold_rules {
1086     my( $self, $conn ) = @_;
1087     return new_editor()->retrieve_all_config_rules_age_hold_protect();
1088 }
1089
1090
1091
1092 __PACKAGE__->register_method(
1093     method => 'copy_details_barcode',
1094     authoritative => 1,
1095     api_name => 'open-ils.circ.copy_details.retrieve.barcode');
1096 sub copy_details_barcode {
1097     my( $self, $conn, $auth, $barcode ) = @_;
1098     my $e = new_editor();
1099     my $cid = $e->search_asset_copy({barcode=>$barcode, deleted=>'f'}, {idlist=>1})->[0];
1100     return $e->event unless $cid;
1101     return copy_details( $self, $conn, $auth, $cid );
1102 }
1103
1104
1105 __PACKAGE__->register_method(
1106     method => 'copy_details',
1107     api_name => 'open-ils.circ.copy_details.retrieve');
1108
1109 sub copy_details {
1110     my( $self, $conn, $auth, $copy_id ) = @_;
1111     my $e = new_editor(authtoken=>$auth);
1112     return $e->event unless $e->checkauth;
1113
1114     my $flesh = { flesh => 1 };
1115
1116     my $copy = $e->retrieve_asset_copy(
1117         [
1118             $copy_id,
1119             {
1120                 flesh => 2,
1121                 flesh_fields => {
1122                     acp => ['call_number','parts','peer_record_maps','floating'],
1123                     acn => ['record','prefix','suffix','label_class']
1124                 }
1125             }
1126         ]) or return $e->event;
1127
1128
1129     # De-flesh the copy for backwards compatibility
1130     my $mvr;
1131     my $vol = $copy->call_number;
1132     if( ref $vol ) {
1133         $copy->call_number($vol->id);
1134         my $record = $vol->record;
1135         if( ref $record ) {
1136             $vol->record($record->id);
1137             $mvr = $U->record_to_mvr($record);
1138         }
1139     }
1140
1141
1142     my $hold = $e->search_action_hold_request(
1143         { 
1144             current_copy        => $copy_id, 
1145             capture_time        => { "!=" => undef },
1146             fulfillment_time    => undef,
1147             cancel_time         => undef,
1148         }
1149     )->[0];
1150
1151     OpenILS::Application::Circ::Holds::flesh_hold_transits([$hold]) if $hold;
1152
1153     my $transit = $e->search_action_transit_copy(
1154         { target_copy => $copy_id, dest_recv_time => undef, cancel_time => undef } )->[0];
1155
1156     # find the most recent circulation for the requested copy,
1157     # be it active, completed, or aged.
1158     my $circ = $e->search_action_all_circulation_slim([
1159         { target_copy => $copy_id },
1160         {
1161             flesh => 1,
1162             flesh_fields => {
1163                 aacs => [
1164                     'workstation',
1165                     'checkin_workstation',
1166                     'duration_rule',
1167                     'max_fine_rule',
1168                     'recurring_fine_rule'
1169                 ],
1170             },
1171             order_by => { aacs => 'xact_start desc' },
1172             limit => 1
1173         }
1174     ])->[0];
1175
1176     return {
1177         copy    => $copy,
1178         hold    => $hold,
1179         transit => $transit,
1180         circ    => $circ,
1181         volume  => $vol,
1182         mvr     => $mvr
1183     };
1184 }
1185
1186
1187
1188
1189 __PACKAGE__->register_method(
1190     method => 'mark_item',
1191     api_name => 'open-ils.circ.mark_item_damaged',
1192     signature   => q/
1193         Changes the status of a copy to "damaged". Requires MARK_ITEM_DAMAGED permission.
1194         @param authtoken The login session key
1195         @param copy_id The ID of the copy to mark as damaged
1196         @return 1 on success - Event otherwise.
1197         /
1198 );
1199 __PACKAGE__->register_method(
1200     method => 'mark_item',
1201     api_name => 'open-ils.circ.mark_item_missing',
1202     signature   => q/
1203         Changes the status of a copy to "missing". Requires MARK_ITEM_MISSING permission.
1204         @param authtoken The login session key
1205         @param copy_id The ID of the copy to mark as missing 
1206         @return 1 on success - Event otherwise.
1207         /
1208 );
1209 __PACKAGE__->register_method(
1210     method => 'mark_item',
1211     api_name => 'open-ils.circ.mark_item_bindery',
1212     signature   => q/
1213         Changes the status of a copy to "bindery". Requires MARK_ITEM_BINDERY permission.
1214         @param authtoken The login session key
1215         @param copy_id The ID of the copy to mark as bindery
1216         @return 1 on success - Event otherwise.
1217         /
1218 );
1219 __PACKAGE__->register_method(
1220     method => 'mark_item',
1221     api_name => 'open-ils.circ.mark_item_on_order',
1222     signature   => q/
1223         Changes the status of a copy to "on order". Requires MARK_ITEM_ON_ORDER permission.
1224         @param authtoken The login session key
1225         @param copy_id The ID of the copy to mark as on order 
1226         @return 1 on success - Event otherwise.
1227         /
1228 );
1229 __PACKAGE__->register_method(
1230     method => 'mark_item',
1231     api_name => 'open-ils.circ.mark_item_ill',
1232     signature   => q/
1233         Changes the status of a copy to "inter-library loan". Requires MARK_ITEM_ILL permission.
1234         @param authtoken The login session key
1235         @param copy_id The ID of the copy to mark as inter-library loan
1236         @return 1 on success - Event otherwise.
1237         /
1238 );
1239 __PACKAGE__->register_method(
1240     method => 'mark_item',
1241     api_name => 'open-ils.circ.mark_item_cataloging',
1242     signature   => q/
1243         Changes the status of a copy to "cataloging". Requires MARK_ITEM_CATALOGING permission.
1244         @param authtoken The login session key
1245         @param copy_id The ID of the copy to mark as cataloging 
1246         @return 1 on success - Event otherwise.
1247         /
1248 );
1249 __PACKAGE__->register_method(
1250     method => 'mark_item',
1251     api_name => 'open-ils.circ.mark_item_reserves',
1252     signature   => q/
1253         Changes the status of a copy to "reserves". Requires MARK_ITEM_RESERVES permission.
1254         @param authtoken The login session key
1255         @param copy_id The ID of the copy to mark as reserves
1256         @return 1 on success - Event otherwise.
1257         /
1258 );
1259 __PACKAGE__->register_method(
1260     method => 'mark_item',
1261     api_name => 'open-ils.circ.mark_item_discard',
1262     signature   => q/
1263         Changes the status of a copy to "discard". Requires MARK_ITEM_DISCARD permission.
1264         @param authtoken The login session key
1265         @param copy_id The ID of the copy to mark as discard
1266         @return 1 on success - Event otherwise.
1267         /
1268 );
1269
1270 sub mark_item {
1271     my( $self, $conn, $auth, $copy_id, $args ) = @_;
1272     $args ||= {};
1273
1274     # Items must be checked in before any attempt is made to mark damaged
1275     my $evt = try_checkin($auth, $copy_id) if
1276         ($self->api_name=~ /damaged/ && $args->{handle_checkin});
1277     return $evt if $evt;
1278
1279     my $e = new_editor(authtoken=>$auth, xact =>1);
1280     return $e->die_event unless $e->checkauth;
1281     my $copy = $e->retrieve_asset_copy([
1282         $copy_id,
1283         {flesh => 1, flesh_fields => {'acp' => ['call_number']}}])
1284             or return $e->die_event;
1285
1286     my $owning_lib = 
1287         ($copy->call_number->id == OILS_PRECAT_CALL_NUMBER) ? 
1288             $copy->circ_lib : $copy->call_number->owning_lib;
1289
1290     my $perm = 'MARK_ITEM_MISSING';
1291     my $stat = OILS_COPY_STATUS_MISSING;
1292
1293     if( $self->api_name =~ /damaged/ ) {
1294         $perm = 'MARK_ITEM_DAMAGED';
1295         $stat = OILS_COPY_STATUS_DAMAGED;
1296         my $evt = handle_mark_damaged($e, $copy, $owning_lib, $args);
1297         return $evt if $evt;
1298
1299     } elsif ( $self->api_name =~ /bindery/ ) {
1300         $perm = 'MARK_ITEM_BINDERY';
1301         $stat = OILS_COPY_STATUS_BINDERY;
1302     } elsif ( $self->api_name =~ /on_order/ ) {
1303         $perm = 'MARK_ITEM_ON_ORDER';
1304         $stat = OILS_COPY_STATUS_ON_ORDER;
1305     } elsif ( $self->api_name =~ /ill/ ) {
1306         $perm = 'MARK_ITEM_ILL';
1307         $stat = OILS_COPY_STATUS_ILL;
1308     } elsif ( $self->api_name =~ /cataloging/ ) {
1309         $perm = 'MARK_ITEM_CATALOGING';
1310         $stat = OILS_COPY_STATUS_CATALOGING;
1311     } elsif ( $self->api_name =~ /reserves/ ) {
1312         $perm = 'MARK_ITEM_RESERVES';
1313         $stat = OILS_COPY_STATUS_RESERVES;
1314     } elsif ( $self->api_name =~ /discard/ ) {
1315         $perm = 'MARK_ITEM_DISCARD';
1316         $stat = OILS_COPY_STATUS_DISCARD;
1317     }
1318
1319     # caller may proceed if either perm is allowed
1320     return $e->die_event unless $e->allowed([$perm, 'UPDATE_COPY'], $owning_lib);
1321
1322     $copy->status($stat);
1323     $copy->edit_date('now');
1324     $copy->editor($e->requestor->id);
1325
1326     $e->update_asset_copy($copy) or return $e->die_event;
1327
1328     my $holds = $e->search_action_hold_request(
1329         { 
1330             current_copy => $copy->id,
1331             fulfillment_time => undef,
1332             cancel_time => undef,
1333         }
1334     );
1335
1336     $e->commit;
1337
1338     if( $self->api_name =~ /damaged/ ) {
1339         # now that we've committed the changes, create related A/T events
1340         my $ses = OpenSRF::AppSession->create('open-ils.trigger');
1341         $ses->request('open-ils.trigger.event.autocreate', 'damaged', $copy, $owning_lib);
1342     }
1343
1344     $logger->debug("resetting holds that target the marked copy");
1345     OpenILS::Application::Circ::Holds->_reset_hold($e->requestor, $_) for @$holds;
1346
1347     return 1;
1348 }
1349
1350 sub try_checkin {
1351     my($auth, $copy_id) = @_;
1352
1353     my $checkin = $U->simplereq(
1354         'open-ils.circ',
1355         'open-ils.circ.checkin.override',
1356         $auth, {
1357             copy_id => $copy_id,
1358             noop => 1
1359         }
1360     );
1361     if(ref $checkin ne 'ARRAY') { $checkin = [$checkin]; }
1362
1363     my $evt_code = $checkin->[0]->{textcode};
1364     $logger->info("try_checkin() received event: $evt_code");
1365
1366     if($evt_code eq 'SUCCESS' || $evt_code eq 'NO_CHANGE') {
1367         $logger->info('try_checkin() successful checkin');
1368         return undef;
1369     } else {
1370         $logger->warn('try_checkin() un-successful checkin');
1371         return $checkin;
1372     }
1373 }
1374
1375 sub handle_mark_damaged {
1376     my($e, $copy, $owning_lib, $args) = @_;
1377
1378     my $apply = $args->{apply_fines} || '';
1379     return undef if $apply eq 'noapply';
1380
1381     my $new_amount = $args->{override_amount};
1382     my $new_btype = $args->{override_btype};
1383     my $new_note = $args->{override_note};
1384
1385     # grab the last circulation
1386     my $circ = $e->search_action_circulation([
1387         {   target_copy => $copy->id}, 
1388         {   limit => 1, 
1389             order_by => {circ => "xact_start DESC"},
1390             flesh => 2,
1391             flesh_fields => {circ => ['target_copy', 'usr'], au => ['card']}
1392         }
1393     ])->[0];
1394
1395     return undef unless $circ;
1396
1397     my $charge_price = $U->ou_ancestor_setting_value(
1398         $owning_lib, 'circ.charge_on_damaged', $e);
1399
1400     my $proc_fee = $U->ou_ancestor_setting_value(
1401         $owning_lib, 'circ.damaged_item_processing_fee', $e) || 0;
1402
1403     my $void_overdue = $U->ou_ancestor_setting_value(
1404         $owning_lib, 'circ.damaged.void_ovedue', $e) || 0;
1405
1406     return undef unless $charge_price or $proc_fee;
1407
1408     my $copy_price = ($charge_price) ? $U->get_copy_price($e, $copy) : 0;
1409     my $total = $copy_price + $proc_fee;
1410
1411     if($apply) {
1412         
1413         if($new_amount and $new_btype) {
1414
1415             # Allow staff to override the amount to charge for a damaged item
1416             # Consider the case where the item is only partially damaged
1417             # This value is meant to take the place of the item price and
1418             # optional processing fee.
1419
1420             my $evt = OpenILS::Application::Circ::CircCommon->create_bill(
1421                 $e, $new_amount, $new_btype, 'Damaged Item Override', $circ->id, $new_note);
1422             return $evt if $evt;
1423
1424         } else {
1425
1426             if($charge_price and $copy_price) {
1427                 my $evt = OpenILS::Application::Circ::CircCommon->create_bill(
1428                     $e, $copy_price, 7, 'Damaged Item', $circ->id);
1429                 return $evt if $evt;
1430             }
1431
1432             if($proc_fee) {
1433                 my $evt = OpenILS::Application::Circ::CircCommon->create_bill(
1434                     $e, $proc_fee, 8, 'Damaged Item Processing Fee', $circ->id);
1435                 return $evt if $evt;
1436             }
1437         }
1438
1439         # the assumption is that you would not void the overdues unless you 
1440         # were also charging for the item and/or applying a processing fee
1441         if($void_overdue) {
1442             my $evt = OpenILS::Application::Circ::CircCommon->void_or_zero_overdues($e, $circ, {note => 'System: OVERDUE REVERSED FOR DAMAGE CHARGE'});
1443             return $evt if $evt;
1444         }
1445
1446         my $evt = OpenILS::Application::Circ::CircCommon->reopen_xact($e, $circ->id);
1447         return $evt if $evt;
1448
1449         my $ses = OpenSRF::AppSession->create('open-ils.trigger');
1450         $ses->request('open-ils.trigger.event.autocreate', 'checkout.damaged', $circ, $circ->circ_lib);
1451
1452         my $evt2 = OpenILS::Utils::Penalty->calculate_penalties($e, $circ->usr->id, $e->requestor->ws_ou);
1453         return $evt2 if $evt2;
1454
1455     } else {
1456         return OpenILS::Event->new('DAMAGE_CHARGE', 
1457             payload => {
1458                 circ => $circ,
1459                 charge => $total
1460             }
1461         );
1462     }
1463 }
1464
1465
1466
1467 # ----------------------------------------------------------------------
1468 __PACKAGE__->register_method(
1469     method => 'mark_item_missing_pieces',
1470     api_name => 'open-ils.circ.mark_item_missing_pieces',
1471     signature   => q/
1472         Changes the status of a copy to "damaged" or to a custom status based on the 
1473         circ.missing_pieces.copy_status org unit setting. Requires MARK_ITEM_MISSING_PIECES
1474         permission.
1475         @param authtoken The login session key
1476         @param copy_id The ID of the copy to mark as damaged
1477         @return Success event with circ and copy objects in the payload, or error Event otherwise.
1478         /
1479 );
1480
1481 sub mark_item_missing_pieces {
1482     my( $self, $conn, $auth, $copy_id, $args ) = @_;
1483     ### FIXME: We're starting a transaction here, but we're doing a lot of things outside of the transaction
1484     ### FIXME: Even better, we're going to use two transactions, the first to affect pertinent holds before checkout can
1485
1486     my $e2 = new_editor(authtoken=>$auth, xact =>1);
1487     return $e2->die_event unless $e2->checkauth;
1488     $args ||= {};
1489
1490     my $copy = $e2->retrieve_asset_copy([
1491         $copy_id,
1492         {flesh => 1, flesh_fields => {'acp' => ['call_number']}}])
1493             or return $e2->die_event;
1494
1495     my $owning_lib = 
1496         ($copy->call_number->id == OILS_PRECAT_CALL_NUMBER) ? 
1497             $copy->circ_lib : $copy->call_number->owning_lib;
1498
1499     return $e2->die_event unless $e2->allowed('MARK_ITEM_MISSING_PIECES', $owning_lib);
1500
1501     #### grab the last circulation
1502     my $circ = $e2->search_action_circulation([
1503         {   target_copy => $copy->id}, 
1504         {   limit => 1, 
1505             order_by => {circ => "xact_start DESC"}
1506         }
1507     ])->[0];
1508
1509     if (!$circ) {
1510         $logger->info('open-ils.circ.mark_item_missing_pieces: no previous checkout');
1511         $e2->rollback;
1512         return OpenILS::Event->new('ACTION_CIRCULATION_NOT_FOUND',{'copy'=>$copy});
1513     }
1514
1515     my $holds = $e2->search_action_hold_request(
1516         { 
1517             current_copy => $copy->id,
1518             fulfillment_time => undef,
1519             cancel_time => undef,
1520         }
1521     );
1522
1523     $logger->debug("resetting holds that target the marked copy");
1524     OpenILS::Application::Circ::Holds->_reset_hold($e2->requestor, $_) for @$holds;
1525
1526     
1527     if (! $e2->commit) {
1528         return $e2->die_event;
1529     }
1530
1531     my $e = new_editor(authtoken=>$auth, xact =>1);
1532     return $e->die_event unless $e->checkauth;
1533
1534     if (! $circ->checkin_time) { # if circ active, attempt renew
1535         my ($res) = $self->method_lookup('open-ils.circ.renew')->run($e->authtoken,{'copy_id'=>$circ->target_copy});
1536         if (ref $res ne 'ARRAY') { $res = [ $res ]; }
1537         if ( $res->[0]->{textcode} eq 'SUCCESS' ) {
1538             $circ = $res->[0]->{payload}{'circ'};
1539             $circ->target_copy( $copy->id );
1540             $logger->info('open-ils.circ.mark_item_missing_pieces: successful renewal');
1541         } else {
1542             $logger->info('open-ils.circ.mark_item_missing_pieces: non-successful renewal');
1543         }
1544     } else {
1545
1546         my $co_params = {
1547             'copy_id'=>$circ->target_copy,
1548             'patron_id'=>$circ->usr,
1549             'skip_deposit_fee'=>1,
1550             'skip_rental_fee'=>1
1551         };
1552
1553         if ($U->ou_ancestor_setting_value($e->requestor->ws_ou, 'circ.block_renews_for_holds')) {
1554
1555             my ($hold, undef, $retarget) = $holdcode->find_nearest_permitted_hold(
1556                 $e, $copy, $e->requestor, 1 );
1557
1558             if ($hold) { # needed for hold? then due now
1559
1560                 $logger->info('open-ils.circ.mark_item_missing_pieces: item needed for hold, shortening due date');
1561                 my $due_date = DateTime->now(time_zone => 'local');
1562                 $co_params->{'due_date'} = cleanse_ISO8601( $due_date->strftime('%FT%T%z') );
1563             } else {
1564                 $logger->info('open-ils.circ.mark_item_missing_pieces: item not needed for hold');
1565             }
1566         }
1567
1568         my ($res) = $self->method_lookup('open-ils.circ.checkout.full.override')->run($e->authtoken,$co_params,{ all => 1 });
1569         if (ref $res ne 'ARRAY') { $res = [ $res ]; }
1570         if ( $res->[0]->{textcode} eq 'SUCCESS' ) {
1571             $logger->info('open-ils.circ.mark_item_missing_pieces: successful checkout');
1572             $circ = $res->[0]->{payload}{'circ'};
1573         } else {
1574             $logger->info('open-ils.circ.mark_item_missing_pieces: non-successful checkout');
1575             $e->rollback;
1576             return $res;
1577         }
1578     }
1579
1580     ### Update the item status
1581
1582     my $custom_stat = $U->ou_ancestor_setting_value(
1583         $owning_lib, 'circ.missing_pieces.copy_status', $e);
1584     my $stat = $custom_stat || OILS_COPY_STATUS_DAMAGED;
1585
1586     my $ses = OpenSRF::AppSession->create('open-ils.trigger');
1587     $ses->request('open-ils.trigger.event.autocreate', 'missing_pieces', $copy, $owning_lib);
1588
1589     $copy->status($stat);
1590     $copy->edit_date('now');
1591     $copy->editor($e->requestor->id);
1592
1593     $e->update_asset_copy($copy) or return $e->die_event;
1594
1595     if ($e->commit) {
1596
1597         my $ses = OpenSRF::AppSession->create('open-ils.trigger');
1598         $ses->request('open-ils.trigger.event.autocreate', 'circ.missing_pieces', $circ, $circ->circ_lib);
1599
1600         return OpenILS::Event->new('SUCCESS',
1601             payload => {
1602                 circ => $circ,
1603                 copy => $copy,
1604                 slip => $U->fire_object_event(undef, 'circ.format.missing_pieces.slip.print', $circ, $circ->circ_lib),
1605                 letter => $U->fire_object_event(undef, 'circ.format.missing_pieces.letter.print', $circ, $circ->circ_lib)
1606             }
1607         ); 
1608
1609     } else {
1610         return $e->die_event;
1611     }
1612 }
1613
1614
1615
1616
1617
1618 # ----------------------------------------------------------------------
1619 __PACKAGE__->register_method(
1620     method => 'magic_fetch',
1621     api_name => 'open-ils.agent.fetch'
1622 );
1623
1624 my @FETCH_ALLOWED = qw/ aou aout acp acn bre /;
1625
1626 sub magic_fetch {
1627     my( $self, $conn, $auth, $args ) = @_;
1628     my $e = new_editor( authtoken => $auth );
1629     return $e->event unless $e->checkauth;
1630
1631     my $hint = $$args{hint};
1632     my $id  = $$args{id};
1633
1634     # Is the call allowed to fetch this type of object?
1635     return undef unless grep { $_ eq $hint } @FETCH_ALLOWED;
1636
1637     # Find the class the implements the given hint
1638     my ($class) = grep { 
1639         $Fieldmapper::fieldmap->{$_}{hint} eq $hint } Fieldmapper->classes;
1640
1641     $class =~ s/Fieldmapper:://og;
1642     $class =~ s/::/_/og;
1643     my $method = "retrieve_$class";
1644
1645     my $obj = $e->$method($id) or return $e->event;
1646     return $obj;
1647 }
1648 # ----------------------------------------------------------------------
1649
1650
1651 __PACKAGE__->register_method(
1652     method  => "fleshed_circ_retrieve",
1653     authoritative => 1,
1654     api_name    => "open-ils.circ.fleshed.retrieve",);
1655
1656 sub fleshed_circ_retrieve {
1657     my( $self, $client, $id ) = @_;
1658     my $e = new_editor();
1659     my $circ = $e->retrieve_action_circulation(
1660         [
1661             $id,
1662             { 
1663                 flesh               => 4,
1664                 flesh_fields    => { 
1665                     circ => [ qw/ target_copy / ],
1666                     acp => [ qw/ location status stat_cat_entry_copy_maps notes age_protect call_number parts / ],
1667                     ascecm => [ qw/ stat_cat stat_cat_entry / ],
1668                     acn => [ qw/ record / ],
1669                 }
1670             }
1671         ]
1672     ) or return $e->event;
1673     
1674     my $copy = $circ->target_copy;
1675     my $vol = $copy->call_number;
1676     my $rec = $circ->target_copy->call_number->record;
1677
1678     $vol->record($rec->id);
1679     $copy->call_number($vol->id);
1680     $circ->target_copy($copy->id);
1681
1682     my $mvr;
1683
1684     if( $rec->id == OILS_PRECAT_RECORD ) {
1685         $rec = undef;
1686         $vol = undef;
1687     } else { 
1688         $mvr = $U->record_to_mvr($rec);
1689         $rec->marc(''); # drop the bulky marc data
1690     }
1691
1692     return {
1693         circ => $circ,
1694         copy => $copy,
1695         volume => $vol,
1696         record => $rec,
1697         mvr => $mvr,
1698     };
1699 }
1700
1701
1702
1703 __PACKAGE__->register_method(
1704     method  => "test_batch_circ_events",
1705     api_name    => "open-ils.circ.trigger_event_by_def_and_barcode.fire"
1706 );
1707
1708 #  method for testing the behavior of a given event definition
1709 sub test_batch_circ_events {
1710     my($self, $conn, $auth, $event_def, $barcode) = @_;
1711
1712     my $e = new_editor(authtoken => $auth);
1713     return $e->event unless $e->checkauth;
1714     return $e->event unless $e->allowed('VIEW_CIRCULATIONS');
1715
1716     my $copy = $e->search_asset_copy({barcode => $barcode, deleted => 'f'})->[0]
1717         or return $e->event;
1718
1719     my $circ = $e->search_action_circulation(
1720         {target_copy => $copy->id, checkin_time => undef})->[0]
1721         or return $e->event;
1722         
1723     return undef unless $circ;
1724
1725     return $U->fire_object_event($event_def, undef, $circ, $e->requestor->ws_ou)
1726 }
1727
1728
1729 __PACKAGE__->register_method(
1730     method  => "fire_circ_events", 
1731     api_name    => "open-ils.circ.fire_circ_trigger_events",
1732     signature => q/
1733         General event def runner for circ objects.  If no event def ID
1734         is provided, the hook will be used to find the best event_def
1735         match based on the context org unit
1736     /
1737 );
1738
1739 __PACKAGE__->register_method(
1740     method  => "fire_circ_events", 
1741     api_name    => "open-ils.circ.fire_hold_trigger_events",
1742     signature => q/
1743         General event def runner for hold objects.  If no event def ID
1744         is provided, the hook will be used to find the best event_def
1745         match based on the context org unit
1746     /
1747 );
1748
1749 __PACKAGE__->register_method(
1750     method  => "fire_circ_events", 
1751     api_name    => "open-ils.circ.fire_user_trigger_events",
1752     signature => q/
1753         General event def runner for user objects.  If no event def ID
1754         is provided, the hook will be used to find the best event_def
1755         match based on the context org unit
1756     /
1757 );
1758
1759
1760 sub fire_circ_events {
1761     my($self, $conn, $auth, $org_id, $event_def, $hook, $granularity, $target_ids, $user_data) = @_;
1762
1763     my $e = new_editor(authtoken => $auth, xact => 1);
1764     return $e->event unless $e->checkauth;
1765
1766     my $targets;
1767
1768     if($self->api_name =~ /hold/) {
1769         return $e->event unless $e->allowed('VIEW_HOLD', $org_id);
1770         $targets = $e->batch_retrieve_action_hold_request($target_ids);
1771     } elsif($self->api_name =~ /user/) {
1772         return $e->event unless $e->allowed('VIEW_USER', $org_id);
1773         $targets = $e->batch_retrieve_actor_user($target_ids);
1774     } else {
1775         return $e->event unless $e->allowed('VIEW_CIRCULATIONS', $org_id);
1776         $targets = $e->batch_retrieve_action_circulation($target_ids);
1777     }
1778     $e->rollback; # FIXME using transaction because of pgpool/slony setups, but not
1779                   # simply making this method authoritative because of weirdness
1780                   # with transaction handling in A/T code that causes rollback
1781                   # failure down the line if handling many targets
1782
1783     return undef unless @$targets;
1784     return $U->fire_object_event($event_def, $hook, $targets, $org_id, $granularity, $user_data);
1785 }
1786
1787 __PACKAGE__->register_method(
1788     method  => "user_payments_list",
1789     api_name    => "open-ils.circ.user_payments.filtered.batch",
1790     stream => 1,
1791     signature => {
1792         desc => q/Returns a fleshed, date-limited set of all payments a user
1793                 has made.  By default, ordered by payment date.  Optionally
1794                 ordered by other columns in the top-level "mp" object/,
1795         params => [
1796             {desc => 'Authentication token', type => 'string'},
1797             {desc => 'User ID', type => 'number'},
1798             {desc => 'Order by column(s), optional.  Array of "mp" class columns', type => 'array'}
1799         ],
1800         return => {desc => q/List of "mp" objects, fleshed with the billable transaction 
1801             and the related fully-realized payment object (e.g money.cash_payment)/}
1802     }
1803 );
1804
1805 sub user_payments_list {
1806     my($self, $conn, $auth, $user_id, $start_date, $end_date, $order_by) = @_;
1807
1808     my $e = new_editor(authtoken => $auth);
1809     return $e->event unless $e->checkauth;
1810
1811     my $user = $e->retrieve_actor_user($user_id) or return $e->event;
1812     return $e->event unless $e->allowed('VIEW_CIRCULATIONS', $user->home_ou);
1813
1814     $order_by ||= ['payment_ts'];
1815
1816     # all payments by user, between start_date and end_date
1817     my $payments = $e->json_query({
1818         select => {mp => ['id']}, 
1819         from => {
1820             mp => {
1821                 mbt => {
1822                     fkey => 'xact', field => 'id'}
1823             }
1824         }, 
1825         where => {
1826             '+mbt' => {usr => $user_id}, 
1827             '+mp' => {payment_ts => {between => [$start_date, $end_date]}}
1828         },
1829         order_by => {mp => $order_by}
1830     });
1831
1832     for my $payment_id (@$payments) {
1833         my $payment = $e->retrieve_money_payment([
1834             $payment_id->{id}, 
1835             {   
1836                 flesh => 2,
1837                 flesh_fields => {
1838                     mp => [
1839                         'xact',
1840                         'cash_payment',
1841                         'credit_card_payment',
1842                         'credit_payment',
1843                         'check_payment',
1844                         'work_payment',
1845                         'forgive_payment',
1846                         'goods_payment'
1847                     ],
1848                     mbt => [
1849                         'circulation', 
1850                         'grocery',
1851                         'reservation'
1852                     ]
1853                 }
1854             }
1855         ]);
1856         $conn->respond($payment);
1857     }
1858
1859     return undef;
1860 }
1861
1862
1863 __PACKAGE__->register_method(
1864     method  => "retrieve_circ_chain",
1865     api_name    => "open-ils.circ.renewal_chain.retrieve_by_circ",
1866     stream => 1,
1867     signature => {
1868         desc => q/Given a circulation, this returns all circulation objects
1869                 that are part of the same chain of renewals./,
1870         params => [
1871             {desc => 'Authentication token', type => 'string'},
1872             {desc => 'Circ ID', type => 'number'},
1873         ],
1874         return => {desc => q/List of circ objects, orderd by oldest circ first/}
1875     }
1876 );
1877
1878 __PACKAGE__->register_method(
1879     method  => "retrieve_circ_chain",
1880     api_name    => "open-ils.circ.renewal_chain.retrieve_by_circ.summary",
1881     signature => {
1882         desc => q/Given a circulation, this returns a summary of the circulation objects
1883                 that are part of the same chain of renewals./,
1884         params => [
1885             {desc => 'Authentication token', type => 'string'},
1886             {desc => 'Circ ID', type => 'number'},
1887         ],
1888         return => {desc => q/Circulation Chain Summary/}
1889     }
1890 );
1891
1892 sub retrieve_circ_chain {
1893     my($self, $conn, $auth, $circ_id) = @_;
1894
1895     my $e = new_editor(authtoken => $auth);
1896     return $e->event unless $e->checkauth;
1897     return $e->event unless $e->allowed('VIEW_CIRCULATIONS');
1898
1899     if($self->api_name =~ /summary/) {
1900         return $U->create_circ_chain_summary($e, $circ_id);
1901
1902     } else {
1903
1904         my $chain = $e->json_query({from => ['action.all_circ_chain', $circ_id]});
1905
1906         for my $circ_info (@$chain) {
1907             my $circ = Fieldmapper::action::all_circulation_slim->new;
1908             $circ->$_($circ_info->{$_}) for keys %$circ_info;
1909             $conn->respond($circ);
1910         }
1911     }
1912
1913     return undef;
1914 }
1915
1916 __PACKAGE__->register_method(
1917     method  => "retrieve_prev_circ_chain",
1918     api_name    => "open-ils.circ.prev_renewal_chain.retrieve_by_circ",
1919     stream => 1,
1920     signature => {
1921         desc => q/Given a circulation, this returns all circulation objects
1922                 that are part of the previous chain of renewals./,
1923         params => [
1924             {desc => 'Authentication token', type => 'string'},
1925             {desc => 'Circ ID', type => 'number'},
1926         ],
1927         return => {desc => q/List of circ objects, orderd by oldest circ first/}
1928     }
1929 );
1930
1931 __PACKAGE__->register_method(
1932     method  => "retrieve_prev_circ_chain",
1933     api_name    => "open-ils.circ.prev_renewal_chain.retrieve_by_circ.summary",
1934     signature => {
1935         desc => q/Given a circulation, this returns a summary of the circulation objects
1936                 that are part of the previous chain of renewals./,
1937         params => [
1938             {desc => 'Authentication token', type => 'string'},
1939             {desc => 'Circ ID', type => 'number'},
1940         ],
1941         return => {desc => q/Object containing Circulation Chain Summary and User Id/}
1942     }
1943 );
1944
1945 sub retrieve_prev_circ_chain {
1946     my($self, $conn, $auth, $circ_id) = @_;
1947
1948     my $e = new_editor(authtoken => $auth);
1949     return $e->event unless $e->checkauth;
1950     return $e->event unless $e->allowed('VIEW_CIRCULATIONS');
1951
1952     my $first_circ = 
1953         $e->json_query({from => ['action.all_circ_chain', $circ_id]})->[0];
1954
1955     my $prev_circ = $e->search_action_all_circulation_slim([
1956         {   target_copy => $first_circ->{target_copy},
1957             xact_start => {'<' => $first_circ->{xact_start}}
1958         }, {   
1959             flesh => 1,
1960             flesh_fields => {
1961                 aacs => [
1962                     'active_circ',
1963                     'aged_circ'
1964                 ]
1965             },
1966             order_by => { aacs => 'xact_start desc' },
1967             limit => 1 
1968         }
1969     ])->[0];
1970
1971     return undef unless $prev_circ;
1972
1973     my $chain_usr = $prev_circ->usr; # note: may be undef
1974
1975     if ($self->api_name =~ /summary/) {
1976         my $sum = $e->json_query({
1977             from => [
1978                 'action.summarize_all_circ_chain', 
1979                 $prev_circ->id
1980             ]
1981         })->[0];
1982
1983         my $summary = Fieldmapper::action::circ_chain_summary->new;
1984         $summary->$_($sum->{$_}) for keys %$sum;
1985
1986         return {summary => $summary, usr => $chain_usr};
1987     }
1988
1989
1990     my $chain = $e->json_query(
1991         {from => ['action.all_circ_chain', $prev_circ->id]});
1992
1993     for my $circ_info (@$chain) {
1994         my $circ = Fieldmapper::action::all_circulation_slim->new;
1995         $circ->$_($circ_info->{$_}) for keys %$circ_info;
1996         $conn->respond($circ);
1997     }
1998
1999     return undef;
2000 }
2001
2002
2003 __PACKAGE__->register_method(
2004     method  => "get_copy_due_date",
2005     api_name    => "open-ils.circ.copy.due_date.retrieve",
2006     signature => {
2007         desc => q/
2008             Given a copy ID, returns the due date for the copy if it's 
2009             currently circulating.  Otherwise, returns null.  Note, this is a public 
2010             method requiring no authentication.  Only the due date is exposed.
2011             /,
2012         params => [
2013             {desc => 'Copy ID', type => 'number'}
2014         ],
2015         return => {desc => q/
2016             Due date (ISO date stamp) if the copy is circulating, null otherwise.
2017         /}
2018     }
2019 );
2020
2021 sub get_copy_due_date {
2022     my($self, $conn, $copy_id) = @_;
2023     my $e = new_editor();
2024
2025     my $circ = $e->json_query({
2026         select => {circ => ['due_date']},
2027         from => 'circ',
2028         where => {
2029             target_copy => $copy_id,
2030             checkin_time => undef,
2031             '-or' => [
2032                 {stop_fines => ["MAXFINES","LONGOVERDUE"]},
2033                 {stop_fines => undef}
2034             ],
2035         },
2036         limit => 1
2037     })->[0] or return undef;
2038
2039     return $circ->{due_date};
2040 }
2041
2042
2043
2044
2045
2046 # {"select":{"acp":["id"],"circ":[{"aggregate":true,"transform":"count","alias":"count","column":"id"}]},"from":{"acp":{"circ":{"field":"target_copy","fkey":"id","type":"left"},"acn"{"field":"id","fkey":"call_number"}}},"where":{"+acn":{"record":200057}}
2047
2048
2049 1;