]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/perlmods/lib/OpenILS/Application/Collections.pm
270314a44cde7299916ce69704023000bf54b86c
[Evergreen.git] / Open-ILS / src / perlmods / lib / OpenILS / Application / Collections.pm
1 package OpenILS::Application::Collections;
2 use strict; use warnings;
3 use OpenSRF::EX qw(:try);
4 use OpenILS::Application::AppUtils;
5 use OpenSRF::Utils::Logger qw(:logger);
6 use OpenILS::Utils::DateTime qw/:datetime/;
7 use OpenILS::Application;
8 use OpenILS::Utils::Fieldmapper;
9 use base 'OpenILS::Application';
10 use OpenILS::Utils::CStoreEditor qw/:funcs/;
11 use OpenILS::Event;
12 use OpenILS::Const qw/:const/;
13 my $U = "OpenILS::Application::AppUtils";
14 use XML::LibXML;
15 use Scalar::Util 'blessed';
16 use File::Spec;
17 use File::Copy;
18 use File::Path;
19
20
21 # --------------------------------------------------------------
22 # Loads the config info
23 # --------------------------------------------------------------
24 sub initialize { return 1; }
25
26 __PACKAGE__->register_method(
27     method => 'user_from_bc',
28     api_name => 'open-ils.collections.user_id_from_barcode',
29 );
30
31 sub user_from_bc {
32     my( $self, $conn, $auth, $bc ) = @_;
33     my $e = new_editor(authtoken=>$auth);
34     return $e->event unless $e->checkauth;
35     return $e->event unless $e->allowed('VIEW_USER');
36     my $card = $e->search_actor_card({barcode=>$bc})->[0]
37         or return $e->event;
38     my $user = $e->retrieve_actor_user($card->usr)
39         or return $e->event;
40     return $user->id;
41 }
42
43
44 __PACKAGE__->register_method(
45     method    => 'users_of_interest',
46     api_name  => 'open-ils.collections.users_of_interest.retrieve',
47     api_level => 1,
48     argc      => 4,
49     stream    => 1,
50     signature => {
51         desc     => q/
52             Returns an array of user information objects that the system
53             based on the search criteria provided.  If the total fines
54             a user owes reaches or exceeds "fine_level" on or befre "age"
55             and the fines were created at "location", the user will be
56             included in the return set/,
57
58         params   => [
59             {    name => 'auth',
60                 desc => 'The authentication token',
61                 type => 'string' },
62
63             {    name => 'age',
64                 desc => q/Number of days back to check/,
65                 type => q/number/,
66             },
67
68             {    name => 'fine_level',
69                 desc => q/The fine threshold at which users will be included in the search results /,
70                 type => q/number/,
71             },
72             {    name => 'location',
73                 desc => q/The short-name of the orginization unit (library) at which the fines were created.
74                             If a selected location has 'child' locations (e.g. a library region), the
75                             child locations will be included in the search/,
76                 type => q/string/,
77             },
78         ],
79
80           'return' => {
81             desc        => q/An array of user information objects.
82                         usr : Array of user information objects containing id, dob, profile, and groups
83                         threshold_amount : The total amount the patron owes that is at least as old
84                             as the fine "age" and whose transaction was created at the searched location
85                         last_pertinent_billing : The time of the last billing that relates to this query
86                         /,
87             type        => 'array',
88             example    => {
89                 usr    => {
90                     id            => 'id',
91                     dob        => '1970-01-01',
92                     profile    => 'Patron',
93                     groups    => [ 'Patron', 'Staff' ],
94                 },
95                 threshold_amount => 99,
96             }
97         }
98     }
99 );
100
101
102 sub users_of_interest {
103     my( $self, $conn, $auth, $age, $fine_level, $location ) = @_;
104
105     return OpenILS::Event->new('BAD_PARAMS')
106         unless ($auth and $age and $location);
107
108     my $e = new_editor(authtoken => $auth);
109     return $e->event unless $e->checkauth;
110
111     my $org = $e->search_actor_org_unit({shortname => $location})
112         or return $e->event; $org = $org->[0];
113
114     # they need global perms to view users so no org is provided
115     return $e->event unless $e->allowed('VIEW_USER');
116
117     my $data = [];
118
119     my $ses = OpenSRF::AppSession->create('open-ils.storage');
120
121     my $start = time;
122     my $req = $ses->request(
123         'open-ils.storage.money.collections.users_of_interest',
124         $age, $fine_level, $location);
125
126     # let the client know we're still here
127     $conn->status( new OpenSRF::DomainObject::oilsContinueStatus );
128
129     return process_users_of_interest_results(
130         $self, $conn, $e, $req, $start, $age, $fine_level, $location);
131 }
132
133
134 __PACKAGE__->register_method(
135     method    => 'users_of_interest_warning_penalty',
136     api_name  => 'open-ils.collections.users_of_interest.warning_penalty.retrieve',
137     api_level => 1,
138     argc      => 4,
139     stream    => 1,
140     signature => {
141         desc     => q/
142             Returns an array of user information objects for users that have the
143             PATRON_EXCEEDS_COLLECTIONS_WARNING penalty applied,
144             based on the search criteria provided./,
145
146         params   => [
147             {    name => 'auth',
148                 desc => 'The authentication token',
149                 type => 'string'
150             }, {
151                 name => 'location',
152                 desc => q/The short-name of the orginization unit (library) at which the penalty is applied.
153                             If a selected location has 'child' locations (e.g. a library region), the
154                             child locations will be included in the search/,
155                 type => q/string/,
156             }, {
157                 name => 'min_age',
158                 desc => q/Optional.  Minimum age of the penalty application/,
159                 type => q/interval, e.g "30 days"/,
160             }, {
161                 name => 'max_age',
162                 desc => q/Optional.  Maximum age of the penalty application/,
163                 type => q/interval, e.g "90 days"/,
164             }
165         ],
166
167           'return' => {
168             desc        => q/An array of user information objects.
169                         usr : Array of user information objects containing id, dob, profile, and groups
170                         threshold_amount : The total amount the patron owes that is at least as old
171                             as the fine "age" and whose transaction was created at the searched location
172                         last_pertinent_billing : The time of the last billing that relates to this query
173                         /,
174             type        => 'array',
175             example    => {
176                 usr    => {
177                     id            => 'id',
178                     dob        => '1970-01-01',
179                     profile    => 'Patron',
180                     groups    => [ 'Patron', 'Staff' ],
181                 },
182                 threshold_amount => 99, # TODO: still needed?
183             }
184         }
185     }
186 );
187
188
189
190 sub users_of_interest_warning_penalty {
191     my( $self, $conn, $auth, $location, $min_age, $max_age ) = @_;
192
193     return OpenILS::Event->new('BAD_PARAMS') unless ($auth and $location);
194
195     my $e = new_editor(authtoken => $auth);
196     return $e->event unless $e->checkauth;
197
198     my $org = $e->search_actor_org_unit({shortname => $location})
199         or return $e->event; $org = $org->[0];
200
201     # they need global perms to view users so no org is provided
202     return $e->event unless $e->allowed('VIEW_USER');
203
204     my $org_ids = $e->json_query({from => ['actor.org_unit_full_path', $org->id]});
205
206     my $ses = OpenSRF::AppSession->create('open-ils.cstore');
207
208     # max age == oldest
209     my $max_set_date = DateTime->now->subtract(seconds =>
210         interval_to_seconds($max_age))->strftime( '%F %T%z' ) if $max_age;
211     my $min_set_date = DateTime->now->subtract(seconds =>
212         interval_to_seconds($min_age))->strftime( '%F %T%z' ) if $min_age;
213
214     my $sp_id = $U->ou_ancestor_setting_value($org->id, 'circ.custom_penalty_override.PATRON_EXCEEDS_COLLECTIONS_WARNING') || 4;
215
216     my $start = time;
217     my $query = {
218         select => {ausp => ['usr']},
219         from => {
220             ausp => {
221                 au => {
222                     join => {
223                         aus => {
224                             type => 'left',
225                             filter => {name => 'circ.collections.exempt'}
226                         },
227                         mct => {
228                             type => 'left',
229                             filter => {
230                                 location => [ map {$_->{id}} @$org_ids ]
231                             }
232                         }
233                     }
234                 }
235             }
236         },
237         where => {
238             '+ausp' => {
239                 standing_penalty => [4,$sp_id], # PATRON_EXCEEDS_COLLECTIONS_WARNING
240                 org_unit => [ map {$_->{id}} @$org_ids ],
241                 '-or' => [
242                     {stop_date => undef},
243                     {stop_date => {'>' => 'now'}}
244                 ]
245             },
246             # We are only interested in users that do not have the
247             # circ.collections.exempt setting applied
248             '+aus' => {value => undef},
249             # and we're only interested in users that are not in the
250             # collections tracker table
251             '+mct' => {id => undef}
252         }
253     };
254
255     $query->{where}->{'-and'} = [] if $max_set_date or $min_set_date;
256     push(@{$query->{where}->{'-and'}}, {set_date => {'>' => $max_set_date}}) if $max_set_date;
257     push(@{$query->{where}->{'-and'}}, {set_date => {'<' => $min_set_date}}) if $min_set_date;
258
259     my $req = $ses->request('open-ils.cstore.json_query', $query);
260
261     # let the client know we're still here
262     $conn->status( new OpenSRF::DomainObject::oilsContinueStatus );
263
264     return process_users_of_interest_results(
265         $self, $conn, $e, $req, $start, $min_age, '', $location, $max_age);
266 }
267
268
269
270
271 sub process_users_of_interest_results {
272     my($self, $conn, $e, $req, $starttime, @params) = @_;
273
274    my $total;
275    while( my $resp = $req->recv(timeout => 7200) ) {
276
277         return $req->failed if $req->failed;
278         my $hash = $resp->content;
279         next unless $hash;
280
281         unless($total) {
282             $total = time - $starttime;
283             $logger->info("collections: request (@params) took $total seconds");
284         }
285
286         my $u = $e->retrieve_actor_user(
287             [
288                 $hash->{usr},
289                 {
290                     flesh                => 1,
291                     flesh_fields    => {au => ["groups","profile", "card"]},
292                 }
293             ]
294         ) or return $e->event;
295
296         $hash->{usr} = {
297             id            => $u->id,
298             dob        => $u->dob,
299             profile    => $u->profile->name,
300             barcode    => $u->card ? $u->card->barcode : undef ,
301             groups    => [ map { $_->name } @{$u->groups} ],
302         };
303
304         $conn->respond($hash);
305     }
306
307     return undef;
308 }
309
310
311 __PACKAGE__->register_method(
312     method    => 'users_with_activity',
313     api_name  => 'open-ils.collections.users_with_activity.retrieve',
314     api_level => 1,
315     argc      => 4,
316     stream    => 1,
317     signature => {
318         desc     => q/
319             Returns an array of users that are already in collections
320             and had any type of billing or payment activity within
321             the given time frame at the location (or child locations)
322             provided/,
323
324         params   => [
325             {    name => 'auth',
326                 desc => 'The authentication token',
327                 type => 'string' },
328
329             {    name => 'start_date',
330                 desc => 'The start of the time interval to check',
331                 type => q/string (ISO 8601 timestamp.  E.g. 2006-06-24, 1994-11-05T08:15:30-05:00 /,
332             },
333
334             {    name => 'end_date',
335                 desc => q/Then end date of the time interval to check/,
336                 type => q/string (ISO 8601 timestamp.  E.g. 2006-06-24, 1994-11-05T08:15:30-05:00 /,
337             },
338             {    name => 'location',
339                 desc => q/The short-name of the orginization unit (library) at which the activity occurred.
340                             If a selected location has 'child' locations (e.g. a library region), the
341                             child locations will be included in the search/,
342                 type => q'string',
343             },
344         ],
345
346           'return' => {
347             desc        => q/An array of user information objects/,
348             type        => 'array',
349         }
350     }
351 );
352
353 sub users_with_activity {
354     my( $self, $conn, $auth, $start_date, $end_date, $location ) = @_;
355     return OpenILS::Event->new('BAD_PARAMS')
356         unless ($auth and $start_date and $end_date and $location);
357
358     my $e = new_editor(authtoken => $auth);
359     return $e->event unless $e->checkauth;
360
361     my $org = $e->search_actor_org_unit({shortname => $location})
362         or return $e->event; $org = $org->[0];
363     return $e->event unless $e->allowed('VIEW_USER', $org->id);
364
365     my $ses = OpenSRF::AppSession->create('open-ils.storage');
366
367     my $start = time;
368     my $req = $ses->request(
369         'open-ils.storage.money.collections.users_with_activity.atomic',
370         $start_date, $end_date, $location);
371
372     $conn->status( new OpenSRF::DomainObject::oilsContinueStatus );
373
374     my $total;
375     while( my $resp = $req->recv(timeout => 7200) ) {
376
377         unless($total) {
378             $total = time - $start;
379             $logger->info("collections: users_with_activity search ".
380                 "($start_date, $end_date, $location) took $total seconds");
381         }
382
383         return $req->failed if $req->failed;
384         $conn->respond($resp->content);
385    }
386
387     return undef;
388 }
389
390
391
392 __PACKAGE__->register_method(
393     method    => 'put_into_collections',
394     api_name  => 'open-ils.collections.put_into_collections',
395     api_level => 1,
396     argc      => 3,
397     signature => {
398         desc     => q/
399             Marks a user as being "in collections" at a given location
400             /,
401
402         params   => [
403             {    name => 'auth',
404                 desc => 'The authentication token',
405                 type => 'string' },
406
407             {    name => 'user_id',
408                 desc => 'The id of the user to plact into collections',
409                 type => 'number',
410             },
411
412             {    name => 'location',
413                 desc => q/The short-name of the orginization unit (library)
414                     for which the user is being placed in collections/,
415                 type => q'string',
416             },
417             {    name => 'fee_amount',
418                 desc => q/
419                     The amount of money that a patron should be fined.
420                     If this field is empty, no fine is created.
421                 /,
422                 type => 'string',
423             },
424             {    name => 'fee_note',
425                 desc => q/
426                     Custom note that is added to the the billing.
427                     This field is not required.
428                     Note: fee_note is not the billing_type.  Billing_type type is
429                     decided by the system. (e.g. "fee for collections").
430                     fee_note is purely used for any additional needed information
431                     and is only visible to staff.
432                 /,
433                 type => 'string',
434             },
435         ],
436
437           'return' => {
438             desc        => q/A SUCCESS event on success, error event on failure/,
439             type        => 'object',
440         }
441     }
442 );
443 sub put_into_collections {
444     my( $self, $conn, $auth, $user_id, $location, $fee_amount, $fee_note ) = @_;
445
446     return OpenILS::Event->new('BAD_PARAMS')
447         unless ($auth and $user_id and $location);
448
449     my $e = new_editor(authtoken => $auth, xact =>1);
450     return $e->event unless $e->checkauth;
451
452     my $org = $e->search_actor_org_unit({shortname => $location});
453     return $e->event unless $org = $org->[0];
454     return $e->event unless $e->allowed('money.collections_tracker.create', $org->id);
455
456     my $existing = $e->search_money_collections_tracker(
457         {
458             location        => $org->id,
459             usr            => $user_id,
460             collector    => $e->requestor->id
461         },
462         {idlist => 1}
463     );
464
465     return OpenILS::Event->new('MONEY_COLLECTIONS_TRACKER_EXISTS') if @$existing;
466
467     $logger->info("collect: user ".$e->requestor->id.
468         " putting user $user_id into collections for $location");
469
470     my $tracker = Fieldmapper::money::collections_tracker->new;
471
472     $tracker->usr($user_id);
473     $tracker->collector($e->requestor->id);
474     $tracker->location($org->id);
475     $tracker->enter_time('now');
476
477     $e->create_money_collections_tracker($tracker)
478         or return $e->event;
479
480     if( $fee_amount ) {
481         my $evt = add_collections_fee($e, $user_id, $org, $fee_amount, $fee_note );
482         return $evt if $evt;
483     }
484
485     $e->commit;
486
487     my $sp_id = $U->ou_ancestor_setting_value($org->id, 'circ.custom_penalty_override.PATRON_IN_COLLECTIONS') || 30;
488
489     my $pen = Fieldmapper::actor::user_standing_penalty->new;
490     $pen->org_unit($org->id);
491     $pen->usr($user_id);
492     $pen->standing_penalty($sp_id); # PATRON_IN_COLLECTIONS
493     $pen->staff($e->requestor->id);
494     my $msg = { 'pub' => 0, 'title' => 'PATRON_IN_COLLECTIONS', 'message' => $fee_note };
495     $U->simplereq('open-ils.actor', 'open-ils.actor.user.penalty.apply', $auth, $pen, $msg);
496
497     return OpenILS::Event->new('SUCCESS');
498 }
499
500 sub add_collections_fee {
501     my( $e, $patron_id, $org, $fee_amount, $fee_note ) = @_;
502
503     $fee_note ||= "";
504
505     $logger->info("collect: adding fee to user $patron_id : $fee_amount : $fee_note");
506
507     my $xact = Fieldmapper::money::grocery->new;
508     $xact->usr($patron_id);
509     $xact->xact_start('now');
510     $xact->billing_location($org->id);
511
512     $xact = $e->create_money_grocery($xact) or return $e->event;
513
514     my $bill = Fieldmapper::money::billing->new;
515     $bill->note($fee_note);
516     $bill->xact($xact->id);
517     $bill->btype(2);
518     $bill->billing_type(OILS_BILLING_TYPE_COLLECTION_FEE);
519     $bill->amount($fee_amount);
520
521     $e->create_money_billing($bill) or return $e->event;
522     return undef;
523 }
524
525
526
527
528 __PACKAGE__->register_method(
529     method        => 'remove_from_collections',
530     api_name        => 'open-ils.collections.remove_from_collections',
531     signature    => q/
532         Returns the users that are currently in collections and
533         had activity during the provided interval.  Dates are inclusive.
534         @param start_date The beginning of the activity interval
535         @param end_date The end of the activity interval
536         @param location The location at which the fines were created
537     /
538 );
539
540
541 __PACKAGE__->register_method(
542     method    => 'remove_from_collections',
543     api_name  => 'open-ils.collections.remove_from_collections',
544     api_level => 1,
545     argc      => 3,
546     signature => {
547         desc     => q/
548             Removes a user from the collections table for the given location
549             /,
550
551         params   => [
552             {    name => 'auth',
553                 desc => 'The authentication token',
554                 type => 'string' },
555
556             {    name => 'user_id',
557                 desc => 'The id of the user to plact into collections',
558                 type => 'number',
559             },
560
561             {    name => 'location',
562                 desc => q/The short-name of the orginization unit (library)
563                     for which the user is being removed from collections/,
564                 type => q'string',
565             },
566         ],
567
568           'return' => {
569             desc        => q/A SUCCESS event on success, error event on failure/,
570             type        => 'object',
571         }
572     }
573 );
574
575 sub remove_from_collections {
576     my( $self, $conn, $auth, $user_id, $location ) = @_;
577
578     return OpenILS::Event->new('BAD_PARAMS')
579         unless ($auth and $user_id and $location);
580
581     my $e = new_editor(authtoken => $auth, xact=>1);
582     return $e->event unless $e->checkauth;
583
584     my $org = $e->search_actor_org_unit({shortname => $location})
585         or return $e->event; $org = $org->[0];
586     return $e->event unless $e->allowed('money.collections_tracker.delete', $org->id);
587
588     my $tracker = $e->search_money_collections_tracker(
589         { usr => $user_id, location => $org->id })
590         or return $e->event;
591
592     $e->delete_money_collections_tracker($tracker->[0])
593         or return $e->event;
594
595     $e->commit;
596     return OpenILS::Event->new('SUCCESS');
597 }
598
599
600 #__PACKAGE__->register_method(
601 #    method        => 'transaction_details',
602 #    api_name        => 'open-ils.collections.user_transaction_details.retrieve',
603 #    signature    => q/
604 #    /
605 #);
606
607
608 __PACKAGE__->register_method(
609     method    => 'transaction_details',
610     api_name  => 'open-ils.collections.user_transaction_details.retrieve',
611     api_level => 1,
612     argc      => 5,
613     signature => {
614         desc     => q/
615             Returns a list of fleshed user objects with transaction details
616             /,
617
618         params   => [
619             {    name => 'auth',
620                 desc => 'The authentication token',
621                 type => 'string' },
622
623             {    name => 'start_date',
624                 desc => 'The start of the time interval to check',
625                 type => q/string (ISO 8601 timestamp.  E.g. 2006-06-24, 1994-11-05T08:15:30-05:00 /,
626             },
627
628             {    name => 'end_date',
629                 desc => q/Then end date of the time interval to check/,
630                 type => q/string (ISO 8601 timestamp.  E.g. 2006-06-24, 1994-11-05T08:15:30-05:00 /,
631             },
632             {    name => 'location',
633                 desc => q/The short-name of the orginization unit (library) at which the activity occurred.
634                             If a selected location has 'child' locations (e.g. a library region), the
635                             child locations will be included in the search/,
636                 type => q'string',
637             },
638             {
639                 name => 'user_list',
640                 desc => 'An array of user ids',
641                 type => 'array',
642             },
643         ],
644
645           'return' => {
646             desc        => q/A list of objects.  Object keys include:
647                 usr :
648                 transactions : An object with keys :
649                     circulations : Fleshed circulation objects
650                     grocery : Fleshed 'grocery' transaction objects
651                 /,
652             type        => 'object'
653         }
654     }
655 );
656
657 sub transaction_details {
658     my( $self, $conn, $auth, $start_date, $end_date, $location, $user_list ) = @_;
659
660     return OpenILS::Event->new('BAD_PARAMS')
661         unless ($auth and $start_date and $end_date and $location and $user_list);
662
663     my $e = new_editor(authtoken => $auth);
664     return $e->event unless $e->checkauth;
665
666     # they need global perms to view users so no org is provided
667     return $e->event unless $e->allowed('VIEW_USER');
668
669     my $org = $e->search_actor_org_unit({shortname => $location})
670         or return $e->event; $org = $org->[0];
671
672     # get a reference to the org inside of the tree
673     $org = $U->find_org($U->get_org_tree(), $org->id);
674
675     my @data;
676     for my $uid (@$user_list) {
677         my $blob = {};
678
679         $blob->{usr} = $e->retrieve_actor_user(
680             [
681                 $uid,
682              {
683                 "flesh"        => 1,
684                 "flesh_fields" =>  {
685                    "au" => [
686                       "cards",
687                       "card",
688                       "standing_penalties",
689                       "addresses",
690                       "billing_address",
691                       "mailing_address",
692                       "stat_cat_entries"
693                    ]
694                 }
695              }
696             ]
697         );
698
699         $blob->{transactions} = {
700             circulations    =>
701                 fetch_circ_xacts($e, $uid, $org, $start_date, $end_date),
702             grocery            =>
703                 fetch_grocery_xacts($e, $uid, $org, $start_date, $end_date),
704             reservations    =>
705                 fetch_reservation_xacts($e, $uid, $org, $start_date, $end_date)
706         };
707
708         # for each transaction, flesh the workstatoin on any attached payment
709         # and make the payment object a real object (e.g. cash payment),
710         # not just a generic payment object
711         for my $xact (
712             @{$blob->{transactions}->{circulations}},
713             @{$blob->{transactions}->{reservations}},
714             @{$blob->{transactions}->{grocery}} ) {
715
716             my $ps;
717             if( $ps = $xact->payments and @$ps ) {
718                 my @fleshed; my $evt;
719                 for my $p (@$ps) {
720                     ($p, $evt) = flesh_payment($e,$p);
721                     return $evt if $evt;
722                     push(@fleshed, $p);
723                 }
724                 $xact->payments(\@fleshed);
725             }
726         }
727
728         push( @data, $blob );
729     }
730
731     return \@data;
732 }
733
734 __PACKAGE__->register_method(
735     method    => 'user_balance_summary',
736     api_name  => 'open-ils.collections.user_balance_summary.generate',
737     api_level => 1,
738     stream    => 1,
739     argc      => 2,
740     signature => {
741         desc     => q/Collect balance information for users in collections.  By default,
742                         only the total balance owed is calculated.  Use the "include_xacts"
743                         param to include per-transaction summaries as well./,
744         params   => [
745             {   name => 'auth',
746                 desc => 'The authentication token',
747                 type => 'string' },
748             {   name => 'args',
749                 desc => q/
750                     Hash of API arguments.  Options include:
751                     location   -- org unit shortname
752                     start_date -- ISO 8601 date. limit to patrons added to collections on or after this date (optional).
753                     end_date   -- ISO 8601 date. limit to patrons added to collections on or before this date (optional).
754                     user_id    -- retrieve information only for this user (takes preference over
755                         start and end_date).  May be a single ID or list of IDs. (optional).
756                     include_xacts -- If true, include a summary object per transaction in addition to the full balance owed
757                 /,
758                 type => q/hash/
759             },
760         ],
761         'return' => {
762             desc => q/
763                 The file name prefix of the file to be created.
764                 The file name format will be:
765                 user_balance_YYYY-MM-DD_${location}_${start_date}_${end_date}_${user_id}.[tmp|xml]
766                 Optional params not provided by the caller will not be part of the file name.
767                 Examples:
768                     user_balance_BR1_2012-05-25_2012-01-01_2012-12-31 # start and end dates
769                     user_balance_BR2_2012-05-25_153244 # user id only.
770                 In-process files will have a .tmp suffix
771                 Completed files will have a .xml suffix
772             /,
773             type => 'string'
774         }
775     }
776 );
777
778 sub user_balance_summary {
779     my ($self, $client, $auth, $args) = @_;
780
781     my $location = $$args{location};
782     my $start_date = $$args{start_date};
783     my $end_date = $$args{end_date};
784     my $user_id = $$args{user_id};
785
786     return OpenILS::Event->new('BAD_PARAMS')
787         unless $auth and $location and
788         ($start_date or $end_date or $user_id);
789
790     my $e = new_editor(authtoken => $auth);
791     return $e->event unless $e->checkauth;
792
793     my $org = $e->search_actor_org_unit({shortname => $location})->[0]
794         or return $e->event;
795
796     # they need global perms to view users so no org is provided
797     return $e->event unless $e->allowed('VIEW_USER', $org->id);
798
799     my $org_list = $U->get_org_descendants($org->id);
800
801     my ($evt, $file_prefix, $file_name, $FILE) = setup_batch_file('user_balance', $args);
802
803     $client->respond_complete($evt || $file_prefix);
804
805     my @user_list;
806
807     if ($user_id) {
808         @user_list = (ref $user_id eq 'ARRAY') ? @$user_id : ($user_id);
809
810     } else {
811         # collect the users from the tracker table based on the provided filters
812
813         my $query = {
814             select => {mct => ['usr']},
815             from => 'mct',
816             where => {location => $org_list}
817         };
818
819         $query->{where}->{enter_time} = {'>=' => $start_date};
820         $query->{where}->{enter_time} = {'<=' => $end_date};
821         my $users = $e->json_query($query);
822         @user_list = map {$_->{usr}} @$users;
823     }
824
825     print $FILE "<Collections>\n"; # append to the document as we have data
826
827     for my $user_id (@user_list) {
828         my $user_doc = XML::LibXML::Document->new;
829         my $root = $user_doc->createElement('User');
830         $user_doc->setDocumentElement($root);
831
832         my $user = $e->retrieve_actor_user([
833             $user_id, {
834             flesh        => 1,
835             flesh_fields => {
836                 au => [
837                     'card',
838                     'cards',
839                     'standing_penalties',
840                     'addresses',
841                     'billing_address',
842                     'mailing_address',
843                     'stat_cat_entries'
844                 ]
845             }}
846         ]);
847
848         my $au_doc = $user->toXML({no_virt => 1, skip_fields => {au => ['passwd']}});
849         my $au_node = $au_doc->documentElement;
850         $user_doc->adoptNode($au_node);
851         $root->appendChild($au_node);
852
853         my $circ_ids = $e->search_action_circulation(
854             {usr => $user_id, circ_lib => $org_list, xact_finish => undef},
855             {idlist => 1}
856         );
857
858         my $groc_ids = $e->search_money_grocery(
859             {usr => $user_id, billing_location => $org_list, xact_finish => undef},
860             {idlist => 1}
861         );
862
863         my $res_ids = $e->search_booking_reservation(
864             {usr => $user_id, pickup_lib => $org_list, xact_finish => undef},
865             {idlist => 1}
866         );
867
868         # get the sum owed an all transactions
869         my $balance = undef;
870         if (@{[@$circ_ids, @$groc_ids, @$res_ids]}) {
871             $balance = $e->json_query({
872                 select => {mbts => [
873                     {   column => 'balance_owed',
874                         transform => 'sum',
875                         aggregate => 1
876                     }
877                 ]},
878                 from => 'mbts',
879                 where => {id => [@$circ_ids, @$groc_ids, @$res_ids]}
880             })->[0];
881         }
882
883         $balance = $balance ? $balance->{balance_owed} : '0';
884
885         my $xacts_node = $user_doc->createElement('Transactions');
886         my $balance_node = $user_doc->createElement('BalanceOwed');
887         $balance_node->appendChild($user_doc->createTextNode($balance));
888         $xacts_node->appendChild($balance_node);
889         $root->appendChild($xacts_node);
890
891         if ($$args{include_xacts}) {
892             my $xacts = $e->search_money_billable_transaction_summary(
893                 {id => [@$circ_ids, @$groc_ids, @$res_ids]},
894                 {substream => 1}
895             );
896
897             for my $xact (@$xacts) {
898                 my $xact_node = $xact->toXML({no_virt => 1})->documentElement;
899                 $user_doc->adoptNode($xact_node);
900                 $xacts_node->appendChild($xact_node);
901             }
902         }
903
904         print $FILE $user_doc->documentElement->toString(1) . "\n";
905     }
906
907     print $FILE "\n</Collections>";
908     close($FILE);
909
910     (my $complete_file = $file_name) =~ s|.tmp$|.xml|og;
911
912     unless (move($file_name, $complete_file)) {
913         $logger->error("collections: unable to move ".
914             "user_balance file $file_name => $complete_file : $@");
915     }
916
917     return undef;
918 }
919
920 sub setup_batch_file {
921     my $prefix = shift;
922     my $args = shift;
923     my $location = $$args{location};
924     my $start_date = $$args{start_date};
925     my $end_date = $$args{end_date};
926     my $user_id = $$args{user_id};
927
928     my $conf = OpenSRF::Utils::SettingsClient->new;
929     my $dir_name = $conf->config_value(apps =>
930         'open-ils.collections' => app_settings => 'batch_file_dir');
931
932     if (!$dir_name) {
933         $logger->error("collections: no batch_file_dir directory configured");
934         return OpenILS::Event->new('COLLECTIONS_FILE_ERROR');
935     }
936
937     unless (-e $dir_name) {
938         eval { mkpath($dir_name); };
939         if ($@) {
940             $logger->error("collections: unable to create batch_file_dir directory $dir_name : $@");
941             return OpenILS::Event->new('COLLECTIONS_FILE_ERROR');
942         }
943     }
944
945     my $file_prefix = "${prefix}_" . DateTime->now->strftime('%F') . "_$location";
946     $file_prefix .= "_$start_date" if $start_date;
947     $file_prefix .= "_$end_date" if $end_date;
948     $file_prefix .= "_$user_id" if $user_id;
949
950     my $FILE;
951     my $file_name = File::Spec->catfile($dir_name, "$file_prefix.tmp");
952
953     unless (open($FILE, '>', $file_name)) {
954         $logger->error("collections: unable to open user_balance_summary file $file_name : $@");
955         return OpenILS::Event->new('COLLECTIONS_FILE_ERROR');
956     }
957
958     return (undef, $file_prefix, $file_name, $FILE);
959 }
960
961 sub flesh_payment {
962     my $e = shift;
963     my $p = shift;
964     my $type = $p->payment_type;
965     $logger->debug("collect: fleshing workstation on payment $type : ".$p->id);
966     my $meth = "retrieve_money_$type";
967     $p = $e->$meth($p->id) or return (undef, $e->event);
968     try {
969         $p->payment_type($type);
970         $p->cash_drawer(
971             $e->retrieve_actor_workstation(
972                 [
973                     $p->cash_drawer,
974                     {
975                         flesh => 1,
976                         flesh_fields => { aws => [ 'owning_lib' ] }
977                     }
978                 ]
979             )
980         );
981     } catch Error with {};
982     return ($p);
983 }
984
985
986 # --------------------------------------------------------------
987 # Collect all open circs for the user
988 # For each circ, see if any billings or payments were created
989 # during the given time period.
990 # --------------------------------------------------------------
991 sub fetch_circ_xacts {
992     my $e                = shift;
993     my $uid            = shift;
994     my $org            = shift;
995     my $start_date = shift;
996     my $end_date    = shift;
997
998     my @circs;
999
1000     # at the specified org and each descendent org,
1001     # fetch the open circs for this user
1002     $U->walk_org_tree( $org,
1003         sub {
1004             my $n = shift;
1005             $logger->debug("collect: searching for open circs at " . $n->shortname);
1006             push( @circs,
1007                 @{
1008                     $e->search_action_circulation(
1009                         {
1010                             usr            => $uid,
1011                             circ_lib        => $n->id,
1012                         },
1013                         {idlist => 1}
1014                     )
1015                 }
1016             );
1017         }
1018     );
1019
1020
1021     my @data;
1022     my $active_ids = fetch_active($e, \@circs, $start_date, $end_date);
1023
1024     for my $cid (@$active_ids) {
1025         push( @data,
1026             $e->retrieve_action_circulation(
1027                 [
1028                     $cid,
1029                     {
1030                         flesh => 1,
1031                         flesh_fields => {
1032                             circ => [ "billings", "payments", "circ_lib", 'target_copy' ]
1033                         }
1034                     }
1035                 ]
1036             )
1037         );
1038     }
1039
1040     return \@data;
1041 }
1042
1043 sub fetch_grocery_xacts {
1044     my $e                = shift;
1045     my $uid            = shift;
1046     my $org            = shift;
1047     my $start_date = shift;
1048     my $end_date    = shift;
1049
1050     my @xacts;
1051     $U->walk_org_tree( $org,
1052         sub {
1053             my $n = shift;
1054             $logger->debug("collect: searching for open grocery xacts at " . $n->shortname);
1055             push( @xacts,
1056                 @{
1057                     $e->search_money_grocery(
1058                         {
1059                             usr                    => $uid,
1060                             billing_location    => $n->id,
1061                         },
1062                         {idlist => 1}
1063                     )
1064                 }
1065             );
1066         }
1067     );
1068
1069     my @data;
1070     my $active_ids = fetch_active($e, \@xacts, $start_date, $end_date);
1071
1072     for my $id (@$active_ids) {
1073         push( @data,
1074             $e->retrieve_money_grocery(
1075                 [
1076                     $id,
1077                     {
1078                         flesh => 1,
1079                         flesh_fields => {
1080                             mg => [ "billings", "payments", "billing_location" ] }
1081                     }
1082                 ]
1083             )
1084         );
1085     }
1086
1087     return \@data;
1088 }
1089
1090 sub fetch_reservation_xacts {
1091     my $e                = shift;
1092     my $uid            = shift;
1093     my $org            = shift;
1094     my $start_date = shift;
1095     my $end_date    = shift;
1096
1097     my @xacts;
1098     $U->walk_org_tree( $org,
1099         sub {
1100             my $n = shift;
1101             $logger->debug("collect: searching for open grocery xacts at " . $n->shortname);
1102             push( @xacts,
1103                 @{
1104                     $e->search_booking_reservation(
1105                         {
1106                             usr                    => $uid,
1107                             pickup_lib          => $n->id,
1108                         },
1109                         {idlist => 1}
1110                     )
1111                 }
1112             );
1113         }
1114     );
1115
1116     my @data;
1117     my $active_ids = fetch_active($e, \@xacts, $start_date, $end_date);
1118
1119     for my $id (@$active_ids) {
1120         push( @data,
1121             $e->retrieve_booking_reservation(
1122                 [
1123                     $id,
1124                     {
1125                         flesh => 1,
1126                         flesh_fields => {
1127                             bresv => [ "billings", "payments", "pickup_lib" ] }
1128                     }
1129                 ]
1130             )
1131         );
1132     }
1133
1134     return \@data;
1135 }
1136
1137
1138
1139 # --------------------------------------------------------------
1140 # Given a list of xact id's, this returns a list of id's that
1141 # had any activity within the given time span
1142 # --------------------------------------------------------------
1143 sub fetch_active {
1144     my( $e, $ids, $start_date, $end_date ) = @_;
1145
1146     # use this..
1147     # { payment_ts => { between => [ $start, $end ] } } ' ;)
1148
1149     my @active;
1150     for my $id (@$ids) {
1151
1152         # see if any billings were created in the given time range
1153         my $bills = $e->search_money_billing (
1154             {
1155                 xact            => $id,
1156                 billing_ts    => { between => [ $start_date, $end_date ] },
1157             },
1158             {idlist =>1}
1159         );
1160
1161         my $payments = [];
1162
1163         if( !@$bills ) {
1164
1165             # see if any payments were created in the given range
1166             $payments = $e->search_money_payment (
1167                 {
1168                     xact            => $id,
1169                     payment_ts    => { between => [ $start_date, $end_date ] },
1170                 },
1171                 {idlist =>1}
1172             );
1173         }
1174
1175
1176         push( @active, $id ) if @$bills or @$payments;
1177     }
1178
1179     return \@active;
1180 }
1181
1182
1183 __PACKAGE__->register_method(
1184     method    => 'create_user_note',
1185     api_name  => 'open-ils.collections.patron_note.create',
1186     api_level => 1,
1187     argc      => 4,
1188     signature => {
1189         desc     => q/ Adds a note to a patron's account /,
1190         params   => [
1191             {    name => 'auth',
1192                 desc => 'The authentication token',
1193                 type => 'string' },
1194
1195             {    name => 'user_barcode',
1196                 desc => q/The patron's barcode/,
1197                 type => q/string/,
1198             },
1199             {    name => 'title',
1200                 desc => q/The title of the note/,
1201                 type => q/string/,
1202             },
1203
1204             {    name => 'note',
1205                 desc => q/The text of the note/,
1206                 type => q/string/,
1207             },
1208         ],
1209
1210           'return' => {
1211             desc        => q/
1212                 Returns SUCCESS event on success, error event otherwise.
1213                 /,
1214             type        => 'object'
1215         }
1216     }
1217 );
1218
1219
1220 sub create_user_note {
1221     my( $self, $conn, $auth, $user_barcode, $title, $note_txt ) = @_;
1222
1223     my $e = new_editor(authtoken=>$auth, xact=>1);
1224     return $e->event unless $e->checkauth;
1225     return $e->event unless $e->allowed('UPDATE_USER'); # XXX Makre more specific perm for this
1226
1227     return $e->event unless
1228         my $card = $e->search_actor_card({barcode=>$user_barcode})->[0];
1229
1230     my $note = Fieldmapper::actor::usr_message->new;
1231     $note->usr($card->usr);
1232     $note->title($title);
1233     $note->sending_lib($e->requestor->home_ou);
1234     $note->create_date('now');
1235     $note->pub('f');
1236     $note->message($note_txt);
1237
1238     $e->create_actor_usr_message($note) or return $e->event;
1239     $e->commit;
1240     return OpenILS::Event->new('SUCCESS');
1241 }
1242
1243
1244
1245 1;