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']},
214 standing_penalty => 4, # PATRON_EXCEEDS_COLLECTIONS_WARNING
215 org_unit => [ map {$_->{id}} @$org_ids ],
217 {stop_date => undef},
218 {stop_date => {'>' => 'now'}}
223 $query->{where}->{'-and'} = [] if $max_set_date or $min_set_date;
224 push(@{$query->{where}->{'-and'}}, {set_date => {'>' => $max_set_date}}) if $max_set_date;
225 push(@{$query->{where}->{'-and'}}, {set_date => {'<' => $min_set_date}}) if $min_set_date;
227 my $req = $ses->request('open-ils.cstore.json_query', $query);
229 # let the client know we're still here
230 $conn->status( new OpenSRF::DomainObject::oilsContinueStatus );
232 return process_users_of_interest_results(
233 $self, $conn, $e, $req, $start, $min_age, '', $location, $max_age);
239 sub process_users_of_interest_results {
240 my($self, $conn, $e, $req, $starttime, @params) = @_;
243 while( my $resp = $req->recv(timeout => 7200) ) {
245 return $req->failed if $req->failed;
246 my $hash = $resp->content;
250 $total = time - $starttime;
251 $logger->info("collections: request (@params) took $total seconds");
254 my $u = $e->retrieve_actor_user(
259 flesh_fields => {au => ["groups","profile", "card"]},
262 ) or return $e->event;
267 profile => $u->profile->name,
268 barcode => $u->card->barcode,
269 groups => [ map { $_->name } @{$u->groups} ],
272 $conn->respond($hash);
279 __PACKAGE__->register_method(
280 method => 'users_owing_money',
281 api_name => 'open-ils.collections.users_owing_money.retrieve',
287 Returns an array of users that owe money during
288 the given time frame at the location (or child locations)
293 desc => 'The authentication token',
296 { name => 'start_date',
297 desc => 'The start of the time interval to check',
298 type => q/string (ISO 8601 timestamp. E.g. 2006-06-24, 1994-11-05T08:15:30-05:00 /,
301 { name => 'end_date',
302 desc => q/Then end date of the time interval to check/,
303 type => q/string (ISO 8601 timestamp. E.g. 2006-06-24, 1994-11-05T08:15:30-05:00 /,
305 { name => 'fine_level',
306 desc => q/The fine threshold at which users will be included in the search results /,
309 { name => 'locations',
310 desc => q/ A list of one or more org-unit short names.
311 If a selected location has 'child' locations (e.g. a library region), the
312 child locations will be included in the search/,
317 desc => q/An array of user information objects/,
324 sub users_owing_money {
325 my( $self, $conn, $auth, $start_date, $end_date, $fine_level, @locations ) = @_;
327 return OpenILS::Event->new('BAD_PARAMS')
328 unless ($auth and $start_date and $end_date and @locations);
330 my $e = new_editor(authtoken => $auth);
331 return $e->event unless $e->checkauth;
333 # they need global perms to view users so no org is provided
334 return $e->event unless $e->allowed('VIEW_USER');
338 my $ses = OpenSRF::AppSession->create('open-ils.storage');
341 my $req = $ses->request(
342 'open-ils.storage.money.collections.users_owing_money',
343 $start_date, $end_date, $fine_level, @locations);
345 # let the client know we're still here
346 $conn->status( new OpenSRF::DomainObject::oilsContinueStatus );
348 return process_users_of_interest_results(
349 $self, $conn, $e, $req, $start, $start_date, $end_date, $fine_level, @locations);
354 __PACKAGE__->register_method(
355 method => 'users_with_activity',
356 api_name => 'open-ils.collections.users_with_activity.retrieve',
362 Returns an array of users that are already in collections
363 and had any type of billing or payment activity within
364 the given time frame at the location (or child locations)
369 desc => 'The authentication token',
372 { name => 'start_date',
373 desc => 'The start of the time interval to check',
374 type => q/string (ISO 8601 timestamp. E.g. 2006-06-24, 1994-11-05T08:15:30-05:00 /,
377 { name => 'end_date',
378 desc => q/Then end date of the time interval to check/,
379 type => q/string (ISO 8601 timestamp. E.g. 2006-06-24, 1994-11-05T08:15:30-05:00 /,
381 { name => 'location',
382 desc => q/The short-name of the orginization unit (library) at which the activity occurred.
383 If a selected location has 'child' locations (e.g. a library region), the
384 child locations will be included in the search/,
390 desc => q/An array of user information objects/,
396 sub users_with_activity {
397 my( $self, $conn, $auth, $start_date, $end_date, $location ) = @_;
398 return OpenILS::Event->new('BAD_PARAMS')
399 unless ($auth and $start_date and $end_date and $location);
401 my $e = new_editor(authtoken => $auth);
402 return $e->event unless $e->checkauth;
404 my $org = $e->search_actor_org_unit({shortname => $location})
405 or return $e->event; $org = $org->[0];
406 return $e->event unless $e->allowed('VIEW_USER', $org->id);
408 my $ses = OpenSRF::AppSession->create('open-ils.storage');
411 my $req = $ses->request(
412 'open-ils.storage.money.collections.users_with_activity.atomic',
413 $start_date, $end_date, $location);
415 $conn->status( new OpenSRF::DomainObject::oilsContinueStatus );
418 while( my $resp = $req->recv(timeout => 7200) ) {
421 $total = time - $start;
422 $logger->info("collections: users_with_activity search ".
423 "($start_date, $end_date, $location) took $total seconds");
426 return $req->failed if $req->failed;
427 $conn->respond($resp->content);
435 __PACKAGE__->register_method(
436 method => 'put_into_collections',
437 api_name => 'open-ils.collections.put_into_collections',
442 Marks a user as being "in collections" at a given location
447 desc => 'The authentication token',
451 desc => 'The id of the user to plact into collections',
455 { name => 'location',
456 desc => q/The short-name of the orginization unit (library)
457 for which the user is being placed in collections/,
460 { name => 'fee_amount',
462 The amount of money that a patron should be fined.
463 If this field is empty, no fine is created.
467 { name => 'fee_note',
469 Custom note that is added to the the billing.
470 This field is not required.
471 Note: fee_note is not the billing_type. Billing_type type is
472 decided by the system. (e.g. "fee for collections").
473 fee_note is purely used for any additional needed information
474 and is only visible to staff.
481 desc => q/A SUCCESS event on success, error event on failure/,
486 sub put_into_collections {
487 my( $self, $conn, $auth, $user_id, $location, $fee_amount, $fee_note ) = @_;
489 return OpenILS::Event->new('BAD_PARAMS')
490 unless ($auth and $user_id and $location);
492 my $e = new_editor(authtoken => $auth, xact =>1);
493 return $e->event unless $e->checkauth;
495 my $org = $e->search_actor_org_unit({shortname => $location});
496 return $e->event unless $org = $org->[0];
497 return $e->event unless $e->allowed('money.collections_tracker.create', $org->id);
499 my $existing = $e->search_money_collections_tracker(
501 location => $org->id,
503 collector => $e->requestor->id
508 return OpenILS::Event->new('MONEY_COLLECTIONS_TRACKER_EXISTS') if @$existing;
510 $logger->info("collect: user ".$e->requestor->id.
511 " putting user $user_id into collections for $location");
513 my $tracker = Fieldmapper::money::collections_tracker->new;
515 $tracker->usr($user_id);
516 $tracker->collector($e->requestor->id);
517 $tracker->location($org->id);
518 $tracker->enter_time('now');
520 $e->create_money_collections_tracker($tracker)
524 my $evt = add_collections_fee($e, $user_id, $org, $fee_amount, $fee_note );
530 my $pen = Fieldmapper::actor::user_standing_penalty->new;
531 $pen->org_unit($org->id);
533 $pen->standing_penalty(30); # PATRON_IN_COLLECTIONS
534 $pen->staff($e->requestor->id);
535 $pen->note($fee_note) if $fee_note;
536 $U->simplereq('open-ils.actor', 'open-ils.actor.user.penalty.apply', $auth, $pen);
538 return OpenILS::Event->new('SUCCESS');
541 sub add_collections_fee {
542 my( $e, $patron_id, $org, $fee_amount, $fee_note ) = @_;
546 $logger->info("collect: adding fee to user $patron_id : $fee_amount : $fee_note");
548 my $xact = Fieldmapper::money::grocery->new;
549 $xact->usr($patron_id);
550 $xact->xact_start('now');
551 $xact->billing_location($org->id);
553 $xact = $e->create_money_grocery($xact) or return $e->event;
555 my $bill = Fieldmapper::money::billing->new;
556 $bill->note($fee_note);
557 $bill->xact($xact->id);
559 $bill->billing_type(OILS_BILLING_TYPE_COLLECTION_FEE);
560 $bill->amount($fee_amount);
562 $e->create_money_billing($bill) or return $e->event;
569 __PACKAGE__->register_method(
570 method => 'remove_from_collections',
571 api_name => 'open-ils.collections.remove_from_collections',
573 Returns the users that are currently in collections and
574 had activity during the provided interval. Dates are inclusive.
575 @param start_date The beginning of the activity interval
576 @param end_date The end of the activity interval
577 @param location The location at which the fines were created
582 __PACKAGE__->register_method(
583 method => 'remove_from_collections',
584 api_name => 'open-ils.collections.remove_from_collections',
589 Removes a user from the collections table for the given location
594 desc => 'The authentication token',
598 desc => 'The id of the user to plact into collections',
602 { name => 'location',
603 desc => q/The short-name of the orginization unit (library)
604 for which the user is being removed from collections/,
610 desc => q/A SUCCESS event on success, error event on failure/,
616 sub remove_from_collections {
617 my( $self, $conn, $auth, $user_id, $location ) = @_;
619 return OpenILS::Event->new('BAD_PARAMS')
620 unless ($auth and $user_id and $location);
622 my $e = new_editor(authtoken => $auth, xact=>1);
623 return $e->event unless $e->checkauth;
625 my $org = $e->search_actor_org_unit({shortname => $location})
626 or return $e->event; $org = $org->[0];
627 return $e->event unless $e->allowed('money.collections_tracker.delete', $org->id);
629 my $tracker = $e->search_money_collections_tracker(
630 { usr => $user_id, location => $org->id })
633 $e->delete_money_collections_tracker($tracker->[0])
637 return OpenILS::Event->new('SUCCESS');
641 #__PACKAGE__->register_method(
642 # method => 'transaction_details',
643 # api_name => 'open-ils.collections.user_transaction_details.retrieve',
649 __PACKAGE__->register_method(
650 method => 'transaction_details',
651 api_name => 'open-ils.collections.user_transaction_details.retrieve',
656 Returns a list of fleshed user objects with transaction details
661 desc => 'The authentication token',
664 { name => 'start_date',
665 desc => 'The start of the time interval to check',
666 type => q/string (ISO 8601 timestamp. E.g. 2006-06-24, 1994-11-05T08:15:30-05:00 /,
669 { name => 'end_date',
670 desc => q/Then end date of the time interval to check/,
671 type => q/string (ISO 8601 timestamp. E.g. 2006-06-24, 1994-11-05T08:15:30-05:00 /,
673 { name => 'location',
674 desc => q/The short-name of the orginization unit (library) at which the activity occurred.
675 If a selected location has 'child' locations (e.g. a library region), the
676 child locations will be included in the search/,
681 desc => 'An array of user ids',
687 desc => q/A list of objects. Object keys include:
689 transactions : An object with keys :
690 circulations : Fleshed circulation objects
691 grocery : Fleshed 'grocery' transaction objects
698 sub transaction_details {
699 my( $self, $conn, $auth, $start_date, $end_date, $location, $user_list ) = @_;
701 return OpenILS::Event->new('BAD_PARAMS')
702 unless ($auth and $start_date and $end_date and $location and $user_list);
704 my $e = new_editor(authtoken => $auth);
705 return $e->event unless $e->checkauth;
707 # they need global perms to view users so no org is provided
708 return $e->event unless $e->allowed('VIEW_USER');
710 my $org = $e->search_actor_org_unit({shortname => $location})
711 or return $e->event; $org = $org->[0];
713 # get a reference to the org inside of the tree
714 $org = $U->find_org($U->fetch_org_tree(), $org->id);
717 for my $uid (@$user_list) {
720 $blob->{usr} = $e->retrieve_actor_user(
729 "standing_penalties",
740 $blob->{transactions} = {
742 fetch_circ_xacts($e, $uid, $org, $start_date, $end_date),
744 fetch_grocery_xacts($e, $uid, $org, $start_date, $end_date),
746 fetch_reservation_xacts($e, $uid, $org, $start_date, $end_date)
749 # for each transaction, flesh the workstatoin on any attached payment
750 # and make the payment object a real object (e.g. cash payment),
751 # not just a generic payment object
753 @{$blob->{transactions}->{circulations}},
754 @{$blob->{transactions}->{reservations}},
755 @{$blob->{transactions}->{grocery}} ) {
758 if( $ps = $xact->payments and @$ps ) {
759 my @fleshed; my $evt;
761 ($p, $evt) = flesh_payment($e,$p);
765 $xact->payments(\@fleshed);
769 push( @data, $blob );
778 my $type = $p->payment_type;
779 $logger->debug("collect: fleshing workstation on payment $type : ".$p->id);
780 my $meth = "retrieve_money_$type";
781 $p = $e->$meth($p->id) or return (undef, $e->event);
783 $p->payment_type($type);
785 $e->retrieve_actor_workstation(
790 flesh_fields => { aws => [ 'owning_lib' ] }
795 } catch Error with {};
800 # --------------------------------------------------------------
801 # Collect all open circs for the user
802 # For each circ, see if any billings or payments were created
803 # during the given time period.
804 # --------------------------------------------------------------
805 sub fetch_circ_xacts {
809 my $start_date = shift;
810 my $end_date = shift;
814 # at the specified org and each descendent org,
815 # fetch the open circs for this user
816 $U->walk_org_tree( $org,
819 $logger->debug("collect: searching for open circs at " . $n->shortname);
822 $e->search_action_circulation(
836 my $active_ids = fetch_active($e, \@circs, $start_date, $end_date);
838 for my $cid (@$active_ids) {
840 $e->retrieve_action_circulation(
846 circ => [ "billings", "payments", "circ_lib", 'target_copy' ]
857 sub fetch_grocery_xacts {
861 my $start_date = shift;
862 my $end_date = shift;
865 $U->walk_org_tree( $org,
868 $logger->debug("collect: searching for open grocery xacts at " . $n->shortname);
871 $e->search_money_grocery(
874 billing_location => $n->id,
884 my $active_ids = fetch_active($e, \@xacts, $start_date, $end_date);
886 for my $id (@$active_ids) {
888 $e->retrieve_money_grocery(
894 mg => [ "billings", "payments", "billing_location" ] }
904 sub fetch_reservation_xacts {
908 my $start_date = shift;
909 my $end_date = shift;
912 $U->walk_org_tree( $org,
915 $logger->debug("collect: searching for open grocery xacts at " . $n->shortname);
918 $e->search_booking_reservation(
921 pickup_lib => $n->id,
931 my $active_ids = fetch_active($e, \@xacts, $start_date, $end_date);
933 for my $id (@$active_ids) {
935 $e->retrieve_booking_reservation(
941 bresv => [ "billings", "payments", "pickup_lib" ] }
953 # --------------------------------------------------------------
954 # Given a list of xact id's, this returns a list of id's that
955 # had any activity within the given time span
956 # --------------------------------------------------------------
958 my( $e, $ids, $start_date, $end_date ) = @_;
961 # { payment_ts => { between => [ $start, $end ] } } ' ;)
966 # see if any billings were created in the given time range
967 my $bills = $e->search_money_billing (
970 billing_ts => { between => [ $start_date, $end_date ] },
979 # see if any payments were created in the given range
980 $payments = $e->search_money_payment (
983 payment_ts => { between => [ $start_date, $end_date ] },
990 push( @active, $id ) if @$bills or @$payments;
997 __PACKAGE__->register_method(
998 method => 'create_user_note',
999 api_name => 'open-ils.collections.patron_note.create',
1003 desc => q/ Adds a note to a patron's account /,
1006 desc => 'The authentication token',
1009 { name => 'user_barcode',
1010 desc => q/The patron's barcode/,
1014 desc => q/The title of the note/,
1019 desc => q/The text of the note/,
1026 Returns SUCCESS event on success, error event otherwise.
1034 sub create_user_note {
1035 my( $self, $conn, $auth, $user_barcode, $title, $note_txt ) = @_;
1037 my $e = new_editor(authtoken=>$auth, xact=>1);
1038 return $e->event unless $e->checkauth;
1039 return $e->event unless $e->allowed('UPDATE_USER'); # XXX Makre more specific perm for this
1041 return $e->event unless
1042 my $card = $e->search_actor_card({barcode=>$user_barcode})->[0];
1044 my $note = Fieldmapper::actor::usr_note->new;
1045 $note->usr($card->usr);
1046 $note->title($title);
1047 $note->creator($e->requestor->id);
1048 $note->create_date('now');
1050 $note->value($note_txt);
1052 $e->create_actor_usr_note($note) or return $e->event;
1054 return OpenILS::Event->new('SUCCESS');