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