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/;
12 use OpenILS::Const qw/:const/;
13 my $U = "OpenILS::Application::AppUtils";
16 # --------------------------------------------------------------
17 # Loads the config info
18 # --------------------------------------------------------------
19 sub initialize { return 1; }
21 __PACKAGE__->register_method(
22 method => 'user_from_bc',
23 api_name => 'open-ils.collections.user_id_from_barcode',
27 my( $self, $conn, $auth, $bc ) = @_;
28 my $e = new_editor(authtoken=>$auth);
29 return $e->event unless $e->checkauth;
30 return $e->event unless $e->allowed('VIEW_USER');
31 my $card = $e->search_actor_card({barcode=>$bc})->[0]
33 my $user = $e->retrieve_actor_user($card->usr)
39 __PACKAGE__->register_method(
40 method => 'users_of_interest',
41 api_name => 'open-ils.collections.users_of_interest.retrieve',
47 Returns an array of user information objects that the system
48 based on the search criteria provided. If the total fines
49 a user owes reaches or exceeds "fine_level" on or befre "age"
50 and the fines were created at "location", the user will be
51 included in the return set/,
55 desc => 'The authentication token',
59 desc => q/Number of days back to check/,
63 { name => 'fine_level',
64 desc => q/The fine threshold at which users will be included in the search results /,
68 desc => q/The short-name of the orginization unit (library) at which the fines were created.
69 If a selected location has 'child' locations (e.g. a library region), the
70 child locations will be included in the search/,
76 desc => q/An array of user information objects.
77 usr : Array of user information objects containing id, dob, profile, and groups
78 threshold_amount : The total amount the patron owes that is at least as old
79 as the fine "age" and whose transaction was created at the searched location
80 last_pertinent_billing : The time of the last billing that relates to this query
88 groups => [ 'Patron', 'Staff' ],
90 threshold_amount => 99,
97 sub users_of_interest {
98 my( $self, $conn, $auth, $age, $fine_level, $location ) = @_;
100 return OpenILS::Event->new('BAD_PARAMS')
101 unless ($auth and $age and $location);
103 my $e = new_editor(authtoken => $auth);
104 return $e->event unless $e->checkauth;
106 my $org = $e->search_actor_org_unit({shortname => $location})
107 or return $e->event; $org = $org->[0];
109 # they need global perms to view users so no org is provided
110 return $e->event unless $e->allowed('VIEW_USER');
114 my $ses = OpenSRF::AppSession->create('open-ils.storage');
117 my $req = $ses->request(
118 'open-ils.storage.money.collections.users_of_interest',
119 $age, $fine_level, $location);
121 # let the client know we're still here
122 $conn->status( new OpenSRF::DomainObject::oilsContinueStatus );
124 return process_users_of_interest_results(
125 $self, $conn, $e, $req, $start, $age, $fine_level, $location);
129 __PACKAGE__->register_method(
130 method => 'users_of_interest_warning_penalty',
131 api_name => 'open-ils.collections.users_of_interest.warning_penalty.retrieve',
137 Returns an array of user information objects for users that have the
138 PATRON_EXCEEDS_COLLECTIONS_WARNING penalty applied,
139 based on the search criteria provided./,
143 desc => 'The authentication token',
147 desc => q/The short-name of the orginization unit (library) at which the penalty is applied.
148 If a selected location has 'child' locations (e.g. a library region), the
149 child locations will be included in the search/,
153 desc => q/Optional. Minimum age of the penalty application/,
154 type => q/interval, e.g "30 days"/,
157 desc => q/Optional. Maximum age of the penalty application/,
158 type => q/interval, e.g "90 days"/,
163 desc => q/An array of user information objects.
164 usr : Array of user information objects containing id, dob, profile, and groups
165 threshold_amount : The total amount the patron owes that is at least as old
166 as the fine "age" and whose transaction was created at the searched location
167 last_pertinent_billing : The time of the last billing that relates to this query
175 groups => [ 'Patron', 'Staff' ],
177 threshold_amount => 99, # TODO: still needed?
185 sub users_of_interest_warning_penalty {
186 my( $self, $conn, $auth, $location, $min_age, $max_age ) = @_;
188 return OpenILS::Event->new('BAD_PARAMS') unless ($auth and $location);
190 my $e = new_editor(authtoken => $auth);
191 return $e->event unless $e->checkauth;
193 my $org = $e->search_actor_org_unit({shortname => $location})
194 or return $e->event; $org = $org->[0];
196 # they need global perms to view users so no org is provided
197 return $e->event unless $e->allowed('VIEW_USER');
199 my $org_ids = $e->json_query({from => ['actor.org_unit_full_path', $org->id]});
201 my $ses = OpenSRF::AppSession->create('open-ils.cstore');
204 my $max_set_date = DateTime->now->subtract(seconds =>
205 interval_to_seconds($max_age))->strftime( '%F %T%z' ) if $max_age;
206 my $min_set_date = DateTime->now->subtract(seconds =>
207 interval_to_seconds($min_age))->strftime( '%F %T%z' ) if $min_age;
211 select => {ausp => ['usr']},
218 filter => {name => 'circ.collections.exempt'}
226 standing_penalty => 4, # PATRON_EXCEEDS_COLLECTIONS_WARNING
227 org_unit => [ map {$_->{id}} @$org_ids ],
229 {stop_date => undef},
230 {stop_date => {'>' => 'now'}}
233 # We are only interested in users that do not have the
234 # circ.collections.exempt setting applied
235 '+aus' => {value => undef}
239 $query->{where}->{'-and'} = [] if $max_set_date or $min_set_date;
240 push(@{$query->{where}->{'-and'}}, {set_date => {'>' => $max_set_date}}) if $max_set_date;
241 push(@{$query->{where}->{'-and'}}, {set_date => {'<' => $min_set_date}}) if $min_set_date;
243 my $req = $ses->request('open-ils.cstore.json_query', $query);
245 # let the client know we're still here
246 $conn->status( new OpenSRF::DomainObject::oilsContinueStatus );
248 return process_users_of_interest_results(
249 $self, $conn, $e, $req, $start, $min_age, '', $location, $max_age);
255 sub process_users_of_interest_results {
256 my($self, $conn, $e, $req, $starttime, @params) = @_;
259 while( my $resp = $req->recv(timeout => 7200) ) {
261 return $req->failed if $req->failed;
262 my $hash = $resp->content;
266 $total = time - $starttime;
267 $logger->info("collections: request (@params) took $total seconds");
270 my $u = $e->retrieve_actor_user(
275 flesh_fields => {au => ["groups","profile", "card"]},
278 ) or return $e->event;
283 profile => $u->profile->name,
284 barcode => $u->card->barcode,
285 groups => [ map { $_->name } @{$u->groups} ],
288 $conn->respond($hash);
295 __PACKAGE__->register_method(
296 method => 'users_owing_money',
297 api_name => 'open-ils.collections.users_owing_money.retrieve',
303 Returns an array of users that owe money during
304 the given time frame at the location (or child locations)
309 desc => 'The authentication token',
312 { name => 'start_date',
313 desc => 'The start of the time interval to check',
314 type => q/string (ISO 8601 timestamp. E.g. 2006-06-24, 1994-11-05T08:15:30-05:00 /,
317 { name => 'end_date',
318 desc => q/Then end date of the time interval to check/,
319 type => q/string (ISO 8601 timestamp. E.g. 2006-06-24, 1994-11-05T08:15:30-05:00 /,
321 { name => 'fine_level',
322 desc => q/The fine threshold at which users will be included in the search results /,
325 { name => 'locations',
326 desc => q/ A list of one or more org-unit short names.
327 If a selected location has 'child' locations (e.g. a library region), the
328 child locations will be included in the search/,
333 desc => q/An array of user information objects/,
340 sub users_owing_money {
341 my( $self, $conn, $auth, $start_date, $end_date, $fine_level, @locations ) = @_;
343 return OpenILS::Event->new('BAD_PARAMS')
344 unless ($auth and $start_date and $end_date and @locations);
346 my $e = new_editor(authtoken => $auth);
347 return $e->event unless $e->checkauth;
349 # they need global perms to view users so no org is provided
350 return $e->event unless $e->allowed('VIEW_USER');
354 my $ses = OpenSRF::AppSession->create('open-ils.storage');
357 my $req = $ses->request(
358 'open-ils.storage.money.collections.users_owing_money',
359 $start_date, $end_date, $fine_level, @locations);
361 # let the client know we're still here
362 $conn->status( new OpenSRF::DomainObject::oilsContinueStatus );
364 return process_users_of_interest_results(
365 $self, $conn, $e, $req, $start, $start_date, $end_date, $fine_level, @locations);
370 __PACKAGE__->register_method(
371 method => 'users_with_activity',
372 api_name => 'open-ils.collections.users_with_activity.retrieve',
378 Returns an array of users that are already in collections
379 and had any type of billing or payment activity within
380 the given time frame at the location (or child locations)
385 desc => 'The authentication token',
388 { name => 'start_date',
389 desc => 'The start of the time interval to check',
390 type => q/string (ISO 8601 timestamp. E.g. 2006-06-24, 1994-11-05T08:15:30-05:00 /,
393 { name => 'end_date',
394 desc => q/Then end date of the time interval to check/,
395 type => q/string (ISO 8601 timestamp. E.g. 2006-06-24, 1994-11-05T08:15:30-05:00 /,
397 { name => 'location',
398 desc => q/The short-name of the orginization unit (library) at which the activity occurred.
399 If a selected location has 'child' locations (e.g. a library region), the
400 child locations will be included in the search/,
406 desc => q/An array of user information objects/,
412 sub users_with_activity {
413 my( $self, $conn, $auth, $start_date, $end_date, $location ) = @_;
414 return OpenILS::Event->new('BAD_PARAMS')
415 unless ($auth and $start_date and $end_date and $location);
417 my $e = new_editor(authtoken => $auth);
418 return $e->event unless $e->checkauth;
420 my $org = $e->search_actor_org_unit({shortname => $location})
421 or return $e->event; $org = $org->[0];
422 return $e->event unless $e->allowed('VIEW_USER', $org->id);
424 my $ses = OpenSRF::AppSession->create('open-ils.storage');
427 my $req = $ses->request(
428 'open-ils.storage.money.collections.users_with_activity.atomic',
429 $start_date, $end_date, $location);
431 $conn->status( new OpenSRF::DomainObject::oilsContinueStatus );
434 while( my $resp = $req->recv(timeout => 7200) ) {
437 $total = time - $start;
438 $logger->info("collections: users_with_activity search ".
439 "($start_date, $end_date, $location) took $total seconds");
442 return $req->failed if $req->failed;
443 $conn->respond($resp->content);
451 __PACKAGE__->register_method(
452 method => 'put_into_collections',
453 api_name => 'open-ils.collections.put_into_collections',
458 Marks a user as being "in collections" at a given location
463 desc => 'The authentication token',
467 desc => 'The id of the user to plact into collections',
471 { name => 'location',
472 desc => q/The short-name of the orginization unit (library)
473 for which the user is being placed in collections/,
476 { name => 'fee_amount',
478 The amount of money that a patron should be fined.
479 If this field is empty, no fine is created.
483 { name => 'fee_note',
485 Custom note that is added to the the billing.
486 This field is not required.
487 Note: fee_note is not the billing_type. Billing_type type is
488 decided by the system. (e.g. "fee for collections").
489 fee_note is purely used for any additional needed information
490 and is only visible to staff.
497 desc => q/A SUCCESS event on success, error event on failure/,
502 sub put_into_collections {
503 my( $self, $conn, $auth, $user_id, $location, $fee_amount, $fee_note ) = @_;
505 return OpenILS::Event->new('BAD_PARAMS')
506 unless ($auth and $user_id and $location);
508 my $e = new_editor(authtoken => $auth, xact =>1);
509 return $e->event unless $e->checkauth;
511 my $org = $e->search_actor_org_unit({shortname => $location});
512 return $e->event unless $org = $org->[0];
513 return $e->event unless $e->allowed('money.collections_tracker.create', $org->id);
515 my $existing = $e->search_money_collections_tracker(
517 location => $org->id,
519 collector => $e->requestor->id
524 return OpenILS::Event->new('MONEY_COLLECTIONS_TRACKER_EXISTS') if @$existing;
526 $logger->info("collect: user ".$e->requestor->id.
527 " putting user $user_id into collections for $location");
529 my $tracker = Fieldmapper::money::collections_tracker->new;
531 $tracker->usr($user_id);
532 $tracker->collector($e->requestor->id);
533 $tracker->location($org->id);
534 $tracker->enter_time('now');
536 $e->create_money_collections_tracker($tracker)
540 my $evt = add_collections_fee($e, $user_id, $org, $fee_amount, $fee_note );
546 my $pen = Fieldmapper::actor::user_standing_penalty->new;
547 $pen->org_unit($org->id);
549 $pen->standing_penalty(30); # PATRON_IN_COLLECTIONS
550 $pen->staff($e->requestor->id);
551 $pen->note($fee_note) if $fee_note;
552 $U->simplereq('open-ils.actor', 'open-ils.actor.user.penalty.apply', $auth, $pen);
554 return OpenILS::Event->new('SUCCESS');
557 sub add_collections_fee {
558 my( $e, $patron_id, $org, $fee_amount, $fee_note ) = @_;
562 $logger->info("collect: adding fee to user $patron_id : $fee_amount : $fee_note");
564 my $xact = Fieldmapper::money::grocery->new;
565 $xact->usr($patron_id);
566 $xact->xact_start('now');
567 $xact->billing_location($org->id);
569 $xact = $e->create_money_grocery($xact) or return $e->event;
571 my $bill = Fieldmapper::money::billing->new;
572 $bill->note($fee_note);
573 $bill->xact($xact->id);
575 $bill->billing_type(OILS_BILLING_TYPE_COLLECTION_FEE);
576 $bill->amount($fee_amount);
578 $e->create_money_billing($bill) or return $e->event;
585 __PACKAGE__->register_method(
586 method => 'remove_from_collections',
587 api_name => 'open-ils.collections.remove_from_collections',
589 Returns the users that are currently in collections and
590 had activity during the provided interval. Dates are inclusive.
591 @param start_date The beginning of the activity interval
592 @param end_date The end of the activity interval
593 @param location The location at which the fines were created
598 __PACKAGE__->register_method(
599 method => 'remove_from_collections',
600 api_name => 'open-ils.collections.remove_from_collections',
605 Removes a user from the collections table for the given location
610 desc => 'The authentication token',
614 desc => 'The id of the user to plact into collections',
618 { name => 'location',
619 desc => q/The short-name of the orginization unit (library)
620 for which the user is being removed from collections/,
626 desc => q/A SUCCESS event on success, error event on failure/,
632 sub remove_from_collections {
633 my( $self, $conn, $auth, $user_id, $location ) = @_;
635 return OpenILS::Event->new('BAD_PARAMS')
636 unless ($auth and $user_id and $location);
638 my $e = new_editor(authtoken => $auth, xact=>1);
639 return $e->event unless $e->checkauth;
641 my $org = $e->search_actor_org_unit({shortname => $location})
642 or return $e->event; $org = $org->[0];
643 return $e->event unless $e->allowed('money.collections_tracker.delete', $org->id);
645 my $tracker = $e->search_money_collections_tracker(
646 { usr => $user_id, location => $org->id })
649 $e->delete_money_collections_tracker($tracker->[0])
653 return OpenILS::Event->new('SUCCESS');
657 #__PACKAGE__->register_method(
658 # method => 'transaction_details',
659 # api_name => 'open-ils.collections.user_transaction_details.retrieve',
665 __PACKAGE__->register_method(
666 method => 'transaction_details',
667 api_name => 'open-ils.collections.user_transaction_details.retrieve',
672 Returns a list of fleshed user objects with transaction details
677 desc => 'The authentication token',
680 { name => 'start_date',
681 desc => 'The start of the time interval to check',
682 type => q/string (ISO 8601 timestamp. E.g. 2006-06-24, 1994-11-05T08:15:30-05:00 /,
685 { name => 'end_date',
686 desc => q/Then end date of the time interval to check/,
687 type => q/string (ISO 8601 timestamp. E.g. 2006-06-24, 1994-11-05T08:15:30-05:00 /,
689 { name => 'location',
690 desc => q/The short-name of the orginization unit (library) at which the activity occurred.
691 If a selected location has 'child' locations (e.g. a library region), the
692 child locations will be included in the search/,
697 desc => 'An array of user ids',
703 desc => q/A list of objects. Object keys include:
705 transactions : An object with keys :
706 circulations : Fleshed circulation objects
707 grocery : Fleshed 'grocery' transaction objects
714 sub transaction_details {
715 my( $self, $conn, $auth, $start_date, $end_date, $location, $user_list ) = @_;
717 return OpenILS::Event->new('BAD_PARAMS')
718 unless ($auth and $start_date and $end_date and $location and $user_list);
720 my $e = new_editor(authtoken => $auth);
721 return $e->event unless $e->checkauth;
723 # they need global perms to view users so no org is provided
724 return $e->event unless $e->allowed('VIEW_USER');
726 my $org = $e->search_actor_org_unit({shortname => $location})
727 or return $e->event; $org = $org->[0];
729 # get a reference to the org inside of the tree
730 $org = $U->find_org($U->fetch_org_tree(), $org->id);
733 for my $uid (@$user_list) {
736 $blob->{usr} = $e->retrieve_actor_user(
745 "standing_penalties",
756 $blob->{transactions} = {
758 fetch_circ_xacts($e, $uid, $org, $start_date, $end_date),
760 fetch_grocery_xacts($e, $uid, $org, $start_date, $end_date),
762 fetch_reservation_xacts($e, $uid, $org, $start_date, $end_date)
765 # for each transaction, flesh the workstatoin on any attached payment
766 # and make the payment object a real object (e.g. cash payment),
767 # not just a generic payment object
769 @{$blob->{transactions}->{circulations}},
770 @{$blob->{transactions}->{reservations}},
771 @{$blob->{transactions}->{grocery}} ) {
774 if( $ps = $xact->payments and @$ps ) {
775 my @fleshed; my $evt;
777 ($p, $evt) = flesh_payment($e,$p);
781 $xact->payments(\@fleshed);
785 push( @data, $blob );
794 my $type = $p->payment_type;
795 $logger->debug("collect: fleshing workstation on payment $type : ".$p->id);
796 my $meth = "retrieve_money_$type";
797 $p = $e->$meth($p->id) or return (undef, $e->event);
799 $p->payment_type($type);
801 $e->retrieve_actor_workstation(
806 flesh_fields => { aws => [ 'owning_lib' ] }
811 } catch Error with {};
816 # --------------------------------------------------------------
817 # Collect all open circs for the user
818 # For each circ, see if any billings or payments were created
819 # during the given time period.
820 # --------------------------------------------------------------
821 sub fetch_circ_xacts {
825 my $start_date = shift;
826 my $end_date = shift;
830 # at the specified org and each descendent org,
831 # fetch the open circs for this user
832 $U->walk_org_tree( $org,
835 $logger->debug("collect: searching for open circs at " . $n->shortname);
838 $e->search_action_circulation(
852 my $active_ids = fetch_active($e, \@circs, $start_date, $end_date);
854 for my $cid (@$active_ids) {
856 $e->retrieve_action_circulation(
862 circ => [ "billings", "payments", "circ_lib", 'target_copy' ]
873 sub fetch_grocery_xacts {
877 my $start_date = shift;
878 my $end_date = shift;
881 $U->walk_org_tree( $org,
884 $logger->debug("collect: searching for open grocery xacts at " . $n->shortname);
887 $e->search_money_grocery(
890 billing_location => $n->id,
900 my $active_ids = fetch_active($e, \@xacts, $start_date, $end_date);
902 for my $id (@$active_ids) {
904 $e->retrieve_money_grocery(
910 mg => [ "billings", "payments", "billing_location" ] }
920 sub fetch_reservation_xacts {
924 my $start_date = shift;
925 my $end_date = shift;
928 $U->walk_org_tree( $org,
931 $logger->debug("collect: searching for open grocery xacts at " . $n->shortname);
934 $e->search_booking_reservation(
937 pickup_lib => $n->id,
947 my $active_ids = fetch_active($e, \@xacts, $start_date, $end_date);
949 for my $id (@$active_ids) {
951 $e->retrieve_booking_reservation(
957 bresv => [ "billings", "payments", "pickup_lib" ] }
969 # --------------------------------------------------------------
970 # Given a list of xact id's, this returns a list of id's that
971 # had any activity within the given time span
972 # --------------------------------------------------------------
974 my( $e, $ids, $start_date, $end_date ) = @_;
977 # { payment_ts => { between => [ $start, $end ] } } ' ;)
982 # see if any billings were created in the given time range
983 my $bills = $e->search_money_billing (
986 billing_ts => { between => [ $start_date, $end_date ] },
995 # see if any payments were created in the given range
996 $payments = $e->search_money_payment (
999 payment_ts => { between => [ $start_date, $end_date ] },
1006 push( @active, $id ) if @$bills or @$payments;
1013 __PACKAGE__->register_method(
1014 method => 'create_user_note',
1015 api_name => 'open-ils.collections.patron_note.create',
1019 desc => q/ Adds a note to a patron's account /,
1022 desc => 'The authentication token',
1025 { name => 'user_barcode',
1026 desc => q/The patron's barcode/,
1030 desc => q/The title of the note/,
1035 desc => q/The text of the note/,
1042 Returns SUCCESS event on success, error event otherwise.
1050 sub create_user_note {
1051 my( $self, $conn, $auth, $user_barcode, $title, $note_txt ) = @_;
1053 my $e = new_editor(authtoken=>$auth, xact=>1);
1054 return $e->event unless $e->checkauth;
1055 return $e->event unless $e->allowed('UPDATE_USER'); # XXX Makre more specific perm for this
1057 return $e->event unless
1058 my $card = $e->search_actor_card({barcode=>$user_barcode})->[0];
1060 my $note = Fieldmapper::actor::usr_note->new;
1061 $note->usr($card->usr);
1062 $note->title($title);
1063 $note->creator($e->requestor->id);
1064 $note->create_date('now');
1066 $note->value($note_txt);
1068 $e->create_actor_usr_note($note) or return $e->event;
1070 return OpenILS::Event->new('SUCCESS');