1 # ---------------------------------------------------------------
2 # Copyright (C) 2005 Georgia Public Library Service
3 # Bill Erickson <billserickson@gmail.com>
5 # This program is free software; you can redistribute it and/or
6 # modify it under the terms of the GNU General Public License
7 # as published by the Free Software Foundation; either version 2
8 # of the License, or (at your option) any later version.
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU General Public License for more details.
14 # ---------------------------------------------------------------
16 package OpenILS::Application::Circ::Money;
17 use base qw/OpenILS::Application/;
18 use strict; use warnings;
19 use OpenILS::Application::AppUtils;
20 use OpenILS::Application::Circ::CircCommon;
21 my $apputils = "OpenILS::Application::AppUtils";
22 my $U = "OpenILS::Application::AppUtils";
23 my $CC = "OpenILS::Application::Circ::CircCommon";
25 use OpenSRF::EX qw(:try);
26 use OpenSRF::Utils::JSON;
30 use OpenSRF::Utils::Logger qw/:logger/;
31 use OpenILS::Utils::CStoreEditor qw/:funcs/;
32 use OpenILS::Utils::Penalty;
34 $Data::Dumper::Indent = 0;
35 use OpenILS::Const qw/:const/;
36 use OpenILS::Utils::DateTime qw/:datetime/;
37 use DateTime::Format::ISO8601;
38 my $parser = DateTime::Format::ISO8601->new;
40 sub get_processor_settings {
43 my $processor = lc shift;
45 # Get the names of every credit processor setting for our given processor.
46 # They're a little different per processor.
47 my $setting_names = $e->json_query({
48 select => {coust => ["name"]},
49 from => {coust => {}},
50 where => {name => {like => "credit.processor.${processor}.%"}}
51 }) or return $e->die_event;
53 # Make keys for a hash we're going to build out of the last dot-delimited
54 # component of each setting name.
55 ($_->{key} = $_->{name}) =~ s/.+\.(\w+)$/$1/ for @$setting_names;
57 # Return a hash with those short keys, and for values the value of
58 # the corresponding OU setting within our scope.
61 $_->{key} => $U->ou_ancestor_setting_value($org_unit, $_->{name})
66 # process_stripe_or_bop_payment()
67 # This is a helper method to make_payments() below (specifically,
68 # the credit-card part). It's the first point in the Perl code where
69 # we need to care about the distinction between Stripe and the
70 # Paypal/PayflowPro/AuthorizeNet kinds of processors (the latter group
71 # uses B::OP and handles payment card info, whereas Stripe doesn't use
72 # B::OP and doesn't require us to know anything about the payment card
75 # Return an event in all cases. That means a success returns a SUCCESS
77 sub process_stripe_or_bop_payment {
78 my ($e, $user_id, $this_ou, $total_paid, $cc_args) = @_;
80 # A few stanzas to determine which processor we're using and whether we're
81 # really adequately set up for it.
82 if (!$cc_args->{processor}) {
83 if (!($cc_args->{processor} =
84 $U->ou_ancestor_setting_value(
85 $this_ou, 'credit.processor.default'
89 return OpenILS::Event->new('CREDIT_PROCESSOR_NOT_SPECIFIED');
93 # Make sure the configured credit processor has a safe/correct name.
94 return OpenILS::Event->new('CREDIT_PROCESSOR_NOT_ALLOWED')
95 unless $cc_args->{processor} =~ /^[a-z0-9_\-]+$/i;
97 # Get the settings for the processor and make sure they're serviceable.
98 my $psettings = get_processor_settings($e, $this_ou, $cc_args->{processor});
99 return $psettings if defined $U->event_code($psettings);
100 return OpenILS::Event->new('CREDIT_PROCESSOR_NOT_ENABLED')
101 unless $psettings->{enabled};
103 # Now we branch. Stripe is one thing, and everything else is another.
105 if ($cc_args->{processor} eq 'Stripe') { # Stripe
106 my $stripe = Business::Stripe->new(-api_key => $psettings->{secretkey});
107 $stripe->api('post','payment_intents/' . $cc_args->{stripe_payment_intent});
108 if ($stripe->success) {
109 $logger->debug('Stripe payment intent retrieved');
110 my $intent = $stripe->success;
111 if ($intent->{status} eq 'succeeded') {
112 $logger->info('Stripe payment succeeded');
113 return OpenILS::Event->new(
114 'SUCCESS', payload => {
115 invoice => $intent->{invoice},
116 customer => $intent->{customer},
117 balance_transaction => 'N/A',
119 created => $intent->{created},
124 $logger->info('Stripe payment failed');
125 return OpenILS::Event->new(
126 'CREDIT_PROCESSOR_DECLINED_TRANSACTION',
127 payload => $intent->{last_payment_error}
131 $logger->debug('Stripe payment intent not retrieved');
132 $logger->info('Stripe payment failed');
133 return OpenILS::Event->new(
134 "CREDIT_PROCESSOR_DECLINED_TRANSACTION",
135 payload => $stripe->error # XXX what happens if this contains
136 # JSON::backportPP::* objects?
140 } else { # B::OP style (Paypal/PayflowPro/AuthorizeNet)
141 return OpenILS::Event->new('BAD_PARAMS', note => 'Need CC number')
142 unless $cc_args->{number};
144 return OpenILS::Application::Circ::CreditCard::process_payment({
145 "processor" => $cc_args->{processor},
146 "desc" => $cc_args->{note},
147 "amount" => $total_paid,
148 "patron_id" => $user_id,
149 "cc" => $cc_args->{number},
150 "expiration" => sprintf(
152 $cc_args->{expire_month},
153 $cc_args->{expire_year}
156 "first_name" => $cc_args->{billing_first},
157 "last_name" => $cc_args->{billing_last},
158 "address" => $cc_args->{billing_address},
159 "city" => $cc_args->{billing_city},
160 "state" => $cc_args->{billing_state},
161 "zip" => $cc_args->{billing_zip},
162 "cvv2" => $cc_args->{cvv2},
169 __PACKAGE__->register_method(
170 method => "make_payments",
171 api_name => "open-ils.circ.money.payment",
173 desc => q/Create payments for a given user and set of transactions,
174 login must have CREATE_PAYMENT privileges.
175 If any payments fail, all are reverted back./,
177 {desc => 'Authtoken', type => 'string'},
178 {desc => q/Arguments Hash, supporting the following params:
185 where_process 1 to use processor, !1 for out-of-band
186 approval_code (for out-of-band payment)
187 type (for out-of-band payment)
188 number (for call to payment processor)
189 stripe_token (for call to Stripe payment processor)
190 expire_month (for call to payment processor)
191 expire_year (for call to payment processor)
192 billing_first (for out-of-band payments and for call to payment processor)
193 billing_last (for out-of-band payments and for call to payment processor)
194 billing_address (for call to payment processor)
195 billing_city (for call to payment processor)
196 billing_state (for call to payment processor)
197 billing_zip (for call to payment processor)
198 note (if payments->{note} is blank, use this)
208 desc => q/Last user transaction ID. This is the actor.usr.last_xact_id value/,
214 q{Array of payment IDs on success, event on failure. Event possibilities include:
216 Bad parameters were given to this API method itself.
219 The last user transaction ID does not match the ID in the database. This means
220 the user object has been updated since the last retrieval. The client should
221 be instructed to reload the user object and related transactions before attempting
223 REFUND_EXCEEDS_BALANCE
224 REFUND_EXCEEDS_DESK_PAYMENTS
225 CREDIT_PROCESSOR_NOT_SPECIFIED
226 Evergreen has not been set up to process CC payments.
227 CREDIT_PROCESSOR_NOT_ALLOWED
228 Evergreen has been incorrectly setup for CC payments.
229 CREDIT_PROCESSOR_NOT_ENABLED
230 Evergreen has been set up for CC payments, but an admin
231 has not explicitly enabled them.
232 CREDIT_PROCESSOR_BAD_PARAMS
233 Evergreen has been incorrectly setup for CC payments;
234 specifically, the login and/or password for the CC
235 processor weren't provided.
236 CREDIT_PROCESSOR_INVALID_CC_NUMBER
237 You have supplied a credit card number that Evergreen
238 has judged to be invalid even before attempting to contact
239 the payment processor.
240 CREDIT_PROCESSOR_DECLINED_TRANSACTION
241 We contacted the CC processor to attempt the charge, but
243 The error_message field of the event payload will
244 contain the payment processor's response. This
245 typically includes a message in plain English intended
246 for human consumption. In PayPal's case, the message
247 is preceded by an integer, a colon, and a space, so
248 a caller might take the 2nd match from /^(\d+: )?(.+)$/
249 to present to the user.
250 The payload also contains other fields from the payment
251 processor, but these are generally not user-friendly
253 CREDIT_PROCESSOR_SUCCESS_WO_RECORD
254 A payment was processed successfully, but couldn't be
255 recorded in Evergreen. This is _bad bad bad_, as it means
256 somebody made a payment but isn't getting credit for it.
257 See errors in the system log if this happens. Info from
258 the credit card transaction will also be available in the
259 event payload, although this probably won't be suitable for
260 staff client/OPAC display.
267 my($self, $client, $auth, $payments, $last_xact_id) = @_;
269 my $e = new_editor(authtoken => $auth, xact => 1);
270 return $e->die_event unless $e->checkauth;
272 my $type = $payments->{payment_type};
273 my $user_id = $payments->{userid};
274 my $credit = $payments->{patron_credit} || 0;
275 my $drawer = $e->requestor->wsid;
276 my $note = $payments->{note};
277 my $cc_args = $payments->{cc_args};
278 my $check_number = $payments->{check_number};
280 my $this_ou = $e->requestor->ws_ou || $e->requestor->home_ou;
284 # unless/until determined by payment processor API
285 my ($approval_code, $cc_processor, $cc_order_number) = (undef,undef,undef, undef);
287 my $patron = $e->retrieve_actor_user($user_id) or return $e->die_event;
289 if($patron->last_xact_id ne $last_xact_id) {
291 return OpenILS::Event->new('INVALID_USER_XACT_ID');
294 # A user is allowed to make credit card payments on his/her own behalf
295 # All other scenarious require permission
296 unless($type eq 'credit_card_payment' and $user_id == $e->requestor->id) {
297 return $e->die_event unless $e->allowed('CREATE_PAYMENT', $patron->home_ou);
300 # first collect the transactions and make sure the transaction
301 # user matches the requested user
304 # We rewrite the payments array for sanity's sake, to avoid more
305 # than one payment per transaction per call, which is not legitimate
306 # but has been seen in the wild coming from the staff client. This
307 # is presumably a staff client (xulrunner) bug.
308 my @unique_xact_payments;
309 for my $pay (@{$payments->{payments}}) {
310 my $xact_id = $pay->[0];
311 if (exists($xacts{$xact_id})) {
313 return OpenILS::Event->new('MULTIPLE_PAYMENTS_FOR_XACT');
316 my $xact = $e->retrieve_money_billable_transaction_summary($xact_id)
317 or return $e->die_event;
319 if($xact->usr != $user_id) {
321 return OpenILS::Event->new('BAD_PARAMS', note => q/user does not match transaction/);
324 $xacts{$xact_id} = $xact;
325 push @unique_xact_payments, $pay;
327 $payments->{payments} = \@unique_xact_payments;
331 for my $pay (@{$payments->{payments}}) {
332 my $transid = $pay->[0];
333 my $amount = $pay->[1];
334 $amount =~ s/\$//og; # just to be safe
335 my $trans = $xacts{$transid};
337 # add amounts as integers
338 $total_paid += (100 * $amount);
340 my $org_id = $U->xact_org($transid, $e);
342 if (!$orgs{$org_id}) {
345 # patron credit has to be allowed at all orgs receiving payment
346 if ($type eq 'credit_payment' and $U->ou_ancestor_setting_value(
347 $org_id, 'circ.disable_patron_credit', $e)) {
349 return OpenILS::Event->new('PATRON_CREDIT_DISABLED');
353 # A negative payment is a refund.
356 # Negative credit card payments are not allowed
357 if($type eq 'credit_card_payment') {
359 return OpenILS::Event->new(
361 note => q/Negative credit card payments not allowed/
365 # If the refund causes the transaction balance to exceed 0 dollars,
366 # we are in effect loaning the patron money. This is not allowed.
367 if( ($trans->balance_owed - $amount) > 0 ) {
369 return OpenILS::Event->new('REFUND_EXCEEDS_BALANCE');
372 # Otherwise, make sure the refund does not exceed desk payments
373 # This is also not allowed
375 my $desk_payments = $e->search_money_desk_payment({xact => $transid, voided => 'f'});
376 $desk_total += $_->amount for @$desk_payments;
378 if( (-$amount) > $desk_total ) {
380 return OpenILS::Event->new(
381 'REFUND_EXCEEDS_DESK_PAYMENTS',
382 payload => { allowed_refund => $desk_total, submitted_refund => -$amount } );
386 my $payobj = "Fieldmapper::money::$type";
387 $payobj = $payobj->new;
389 $payobj->amount($amount);
390 $payobj->amount_collected($amount);
391 $payobj->xact($transid);
392 $payobj->note($note);
393 if ((not $payobj->note) and ($type eq 'credit_card_payment')) {
394 $payobj->note($cc_args->{note});
397 if ($payobj->has_field('accepting_usr')) { $payobj->accepting_usr($e->requestor->id); }
398 if ($payobj->has_field('cash_drawer')) { $payobj->cash_drawer($drawer); }
399 if ($payobj->has_field('check_number')) { $payobj->check_number($check_number); }
401 # Store the last 4 digits of the CC number
402 if ($payobj->has_field('cc_number')) {
403 $payobj->cc_number(substr($cc_args->{number}, -4));
406 # Note: It is important not to set approval_code
407 # on the fieldmapper object yet.
409 push(@payment_objs, $payobj);
411 } # all payment objects have been created and inserted.
413 # return to decimal format, forcing X.YY format for consistency.
414 $total_paid = sprintf("%.2f", $total_paid / 100);
416 #### NO WRITES TO THE DB ABOVE THIS LINE -- THEY'LL ONLY BE DISCARDED ###
419 # After we try to externally process a credit card (if desired), we'll
420 # open a new transaction. We cannot leave one open while credit card
421 # processing might be happening, as it can easily time out the database
426 if($type eq 'credit_card_payment') {
427 $approval_code = $cc_args->{approval_code};
428 # If an approval code was not given, we'll need
429 # to call to the payment processor ourselves.
430 if ($cc_args->{where_process} == 1) {
431 my $response = process_stripe_or_bop_payment(
432 $e, $user_id, $this_ou, $total_paid, $cc_args
435 if ($U->event_code($response)) { # non-success (success is 0)
437 "Credit card payment for user $user_id failed: " .
438 $response->{textcode} . " " .
439 ($response->{payload}->{error_message} ||
440 $response->{payload}{message})
444 # We need to save this for later in case there's a failure on
445 # the EG side to store the processor's result.
447 $cc_payload = $response->{"payload"}; # also used way later
450 no warnings 'uninitialized';
451 $approval_code = $cc_payload->{authorization} ||
453 $cc_processor = $cc_payload->{processor} ||
454 $cc_args->{processor};
455 $cc_order_number = $cc_payload->{order_number} ||
456 $cc_payload->{invoice};
458 $logger->info("Credit card payment for user $user_id succeeded");
461 return OpenILS::Event->new(
462 'BAD_PARAMS', note => 'Need approval code'
463 ) if not $cc_args->{approval_code};
467 ### RE-OPEN TRANSACTION HERE ###
471 # create payment records
472 my $create_money_method = "create_money_" . $type;
473 for my $payment (@payment_objs) {
474 # update the transaction if it's done
475 my $amount = $payment->amount;
476 my $transid = $payment->xact;
477 my $trans = $xacts{$transid};
478 # making payment with existing patron credit.
479 $credit -= $amount if $type eq 'credit_payment';
480 if( (my $cred = ($trans->balance_owed - $amount)) <= 0 ) {
481 # Any overpay on this transaction goes directly into patron
486 # Attempt to close the transaction.
487 my $close_xact_fail = $CC->maybe_close_xact($e, $transid);
488 if ($close_xact_fail) {
489 return _recording_failure(
490 $e, $close_xact_fail->{message},
491 $payment, $cc_payload
496 # Urgh, clean up this mega-function one day.
497 if ($cc_processor eq 'Stripe' and $approval_code and $cc_payload) {
498 $payment->cc_number($cc_payload->{card}); # not actually available :)
501 $payment->approval_code($approval_code) if $approval_code;
502 $payment->cc_order_number($cc_order_number) if $cc_order_number;
503 $payment->cc_processor($cc_processor) if $cc_processor;
504 if (!$e->$create_money_method($payment)) {
505 return _recording_failure(
506 $e, "$create_money_method failed", $payment, $cc_payload
510 push(@payment_ids, $payment->id);
513 my $evt = _update_patron_credit($e, $patron, $credit);
515 return _recording_failure(
516 $e, "_update_patron_credit() failed", undef, $cc_payload
520 for my $org_id (keys %orgs) {
521 # calculate penalties for each of the affected orgs
522 $evt = OpenILS::Utils::Penalty->calculate_penalties(
523 $e, $user_id, $org_id
526 return _recording_failure(
527 $e, "calculate_penalties() failed", undef, $cc_payload
532 # update the user to create a new last_xact_id
533 $e->update_actor_user($patron) or return $e->die_event;
534 $patron = $e->retrieve_actor_user($patron) or return $e->die_event;
537 # update the cached user object if a user is making a payment toward
538 # his/her own account
539 $U->simplereq('open-ils.auth', 'open-ils.auth.session.reset_timeout', $auth, 1)
540 if $user_id == $e->requestor->id;
542 return {last_xact_id => $patron->last_xact_id, payments => \@payment_ids};
545 sub _recording_failure {
546 my ($e, $msg, $payment, $payload) = @_;
548 if ($payload) { # If the payment processor already accepted a payment:
549 $logger->error($msg);
550 $logger->error("Payment processor payload: " . Dumper($payload));
551 # payment shouldn't contain CC number
552 $logger->error("Payment: " . Dumper($payment)) if $payment;
556 return new OpenILS::Event(
557 "CREDIT_PROCESSOR_SUCCESS_WO_RECORD",
558 "payload" => $payload
560 } else { # Otherwise, the problem is somewhat less severe:
562 $logger->warn("Payment: " . Dumper($payment)) if $payment;
563 return $e->die_event;
567 sub _update_patron_credit {
568 my($e, $patron, $credit) = @_;
569 return undef if $credit == 0;
570 $patron->credit_forward_balance($patron->credit_forward_balance + $credit);
571 return OpenILS::Event->new('NEGATIVE_PATRON_BALANCE') if $patron->credit_forward_balance < 0;
572 $e->update_actor_user($patron) or return $e->die_event;
577 __PACKAGE__->register_method(
578 method => "retrieve_payments",
579 api_name => "open-ils.circ.money.payment.retrieve.all_",
580 notes => "Returns a list of payments attached to a given transaction"
582 sub retrieve_payments {
583 my( $self, $client, $login, $transid ) = @_;
586 $apputils->checksesperm($login, 'VIEW_TRANSACTION');
589 # XXX the logic here is wrong.. we need to check the owner of the transaction
590 # to make sure the requestor has access
592 # XXX grab the view, for each object in the view, grab the real object
594 return $apputils->simplereq(
596 'open-ils.cstore.direct.money.payment.search.atomic', { xact => $transid } );
600 __PACKAGE__->register_method(
601 method => "retrieve_payments2",
603 api_name => "open-ils.circ.money.payment.retrieve.all",
604 notes => "Returns a list of payments attached to a given transaction"
607 sub retrieve_payments2 {
608 my( $self, $client, $login, $transid ) = @_;
610 my $e = new_editor(authtoken=>$login);
611 return $e->event unless $e->checkauth;
612 return $e->event unless $e->allowed('VIEW_TRANSACTION');
615 my $pmnts = $e->search_money_payment({ xact => $transid });
617 my $type = $_->payment_type;
618 my $meth = "retrieve_money_$type";
619 my $p = $e->$meth($_->id) or return $e->event;
620 $p->payment_type($type);
621 $p->cash_drawer($e->retrieve_actor_workstation($p->cash_drawer))
622 if $p->has_field('cash_drawer');
623 push( @payments, $p );
629 __PACKAGE__->register_method(
630 method => "format_payment_receipt",
631 api_name => "open-ils.circ.money.payment_receipt.print",
633 desc => 'Returns a printable receipt for the specified payments',
635 { desc => 'Authentication token', type => 'string'},
636 { desc => 'Payment ID or array of payment IDs', type => 'number' },
639 desc => q/An action_trigger.event object or error event./,
644 __PACKAGE__->register_method(
645 method => "format_payment_receipt",
646 api_name => "open-ils.circ.money.payment_receipt.email",
648 desc => 'Emails a receipt for the specified payments to the user associated with the first payment',
650 { desc => 'Authentication token', type => 'string'},
651 { desc => 'Payment ID or array of payment IDs', type => 'number' },
654 desc => q/Undefined on success, otherwise an error event./,
660 sub format_payment_receipt {
661 my($self, $conn, $auth, $mp_id) = @_;
664 if (ref $mp_id ne 'ARRAY') {
665 $mp_ids = [ $mp_id ];
670 my $for_print = ($self->api_name =~ /print/);
671 my $for_email = ($self->api_name =~ /email/);
673 # manually use xact (i.e. authoritative) so we can kill the cstore
674 # connection before sending the action/trigger request. This prevents our cstore
675 # backend from sitting idle while A/T (which uses its own transactions) runs.
676 my $e = new_editor(xact => 1, authtoken => $auth);
677 return $e->die_event unless $e->checkauth;
680 for my $id (@$mp_ids) {
682 my $payment = $e->retrieve_money_payment([
690 ]) or return $e->die_event;
692 return $e->die_event unless
693 $e->requestor->id == $payment->xact->usr->id or
694 $e->allowed('VIEW_TRANSACTION', $payment->xact->usr->home_ou);
696 push @$payments, $payment;
703 return $U->fire_object_event(undef, 'money.format.payment_receipt.print', $payments, $$payments[0]->xact->usr->home_ou);
705 } elsif ($for_email) {
707 for my $p (@$payments) {
708 $U->create_events_for_hook('money.format.payment_receipt.email', $p, $p->xact->usr->home_ou, undef, undef, 1);
715 __PACKAGE__->register_method(
716 method => "create_grocery_bill",
717 api_name => "open-ils.circ.money.grocery.create",
719 Creates a new grocery transaction using the transaction object provided
720 PARAMS: (login_session, money.grocery (mg) object)
723 sub create_grocery_bill {
724 my( $self, $client, $login, $transaction ) = @_;
726 my( $staff, $evt ) = $apputils->checkses($login);
728 $evt = $apputils->check_perms($staff->id,
729 $transaction->billing_location, 'CREATE_TRANSACTION' );
733 $logger->activity("Creating grocery bill " . Dumper($transaction) );
735 $transaction->clear_id;
736 my $session = $apputils->start_db_session;
737 $apputils->set_audit_info($session, $login, $staff->id, $staff->wsid);
738 my $transid = $session->request(
739 'open-ils.storage.direct.money.grocery.create', $transaction)->gather(1);
741 throw OpenSRF::EX ("Error creating new money.grocery") unless defined $transid;
743 $logger->debug("Created new grocery transaction $transid");
745 $apputils->commit_db_session($session);
747 my $e = new_editor(xact=>1);
748 $evt = $U->check_open_xact($e, $transid);
756 __PACKAGE__->register_method(
757 method => 'fetch_reservation',
758 api_name => 'open-ils.circ.booking.reservation.retrieve'
760 sub fetch_reservation {
761 my( $self, $conn, $auth, $id ) = @_;
762 my $e = new_editor(authtoken=>$auth);
763 return $e->event unless $e->checkauth;
764 return $e->event unless $e->allowed('VIEW_TRANSACTION'); # eh.. basically the same permission
765 my $g = $e->retrieve_booking_reservation($id)
770 __PACKAGE__->register_method(
771 method => 'fetch_grocery',
772 api_name => 'open-ils.circ.money.grocery.retrieve'
775 my( $self, $conn, $auth, $id ) = @_;
776 my $e = new_editor(authtoken=>$auth);
777 return $e->event unless $e->checkauth;
778 return $e->event unless $e->allowed('VIEW_TRANSACTION'); # eh.. basically the same permission
779 my $g = $e->retrieve_money_grocery($id)
785 __PACKAGE__->register_method(
786 method => "billing_items",
787 api_name => "open-ils.circ.money.billing.retrieve.all",
790 desc => 'Returns a list of billing items for the given transaction ID. ' .
791 'If the operator is not the owner of the transaction, the VIEW_TRANSACTION permission is required.',
793 { desc => 'Authentication token', type => 'string'},
794 { desc => 'Transaction ID', type => 'number'}
797 desc => 'Transaction object, event on error'
803 my( $self, $client, $login, $transid ) = @_;
805 my( $trans, $evt ) = $U->fetch_billable_xact($transid);
809 ($staff, $evt ) = $apputils->checkses($login);
812 if($staff->id ne $trans->usr) {
813 $evt = $U->check_perms($staff->id, $staff->home_ou, 'VIEW_TRANSACTION');
817 return $apputils->simplereq( 'open-ils.cstore',
818 'open-ils.cstore.direct.money.billing.search.atomic', { xact => $transid } )
822 __PACKAGE__->register_method(
823 method => "billing_items_create",
824 api_name => "open-ils.circ.money.billing.create",
826 Creates a new billing line item
827 PARAMS( login, bill_object (mb) )
830 sub billing_items_create {
831 my( $self, $client, $login, $billing ) = @_;
833 my $e = new_editor(authtoken => $login, xact => 1);
834 return $e->die_event unless $e->checkauth;
835 return $e->die_event unless $e->allowed('CREATE_BILL');
837 my $xact = $e->retrieve_money_billable_transaction($billing->xact)
838 or return $e->die_event;
840 # if the transaction was closed, re-open it
841 if($xact->xact_finish) {
842 $xact->clear_xact_finish;
843 $e->update_money_billable_transaction($xact)
844 or return $e->die_event;
847 my $amt = $billing->amount;
849 $billing->amount($amt);
851 $e->create_money_billing($billing) or return $e->die_event;
852 my $evt = OpenILS::Utils::Penalty->calculate_penalties($e, $xact->usr, $U->xact_org($xact->id,$e));
855 $evt = $U->check_open_xact($e, $xact->id, $xact);
864 __PACKAGE__->register_method(
865 method => 'void_bill',
866 api_name => 'open-ils.circ.money.billing.void',
869 @param authtoken Login session key
870 @param billid Id for the bill to void. This parameter may be repeated to reference other bills.
871 @return 1 on success, Event on error
875 my( $s, $c, $authtoken, @billids ) = @_;
876 my $editor = new_editor(authtoken=>$authtoken, xact=>1);
877 return $editor->die_event unless $editor->checkauth;
878 return $editor->die_event unless $editor->allowed('VOID_BILLING');
879 my $rv = $CC->void_bills($editor, \@billids);
880 if (ref($rv) eq 'HASH') {
884 # We should have gotten 1.
891 __PACKAGE__->register_method(
892 method => 'adjust_bills_to_zero_manual',
893 api_name => 'open-ils.circ.money.billable_xact.adjust_to_zero',
896 Given a list of billable transactions, manipulate the
897 transaction using account adjustments to result in a
901 {desc => 'Authtoken', type => 'string'},
902 {desc => 'Array of transaction IDs', type => 'array'}
905 desc => q/Array of IDs for each transaction updated,
914 my $xact_id = $xact->id;
915 # the plan: rebill voided billings until we get a positive balance
917 # step 1: get the voided/adjusted billings
918 my $billings = $e->search_money_billing([
923 order_by => {mb => 'amount desc'},
925 flesh_fields => {mb => ['adjustments']},
928 my @billings = grep { $U->is_true($_->voided) or @{$_->adjustments} } @$billings;
930 my $xact_balance = $xact->balance_owed;
931 $logger->debug("rebilling for xact $xact_id with balance $xact_balance");
933 my $rebill_amount = 0;
935 # step 2: generate new bills just like the old ones
936 for my $billing (@billings) {
938 if ($U->is_true($billing->voided)) {
939 $amount = $billing->amount;
940 } else { # adjusted billing
941 map { $amount = $U->fpsum($amount, $_->amount) } @{$billing->adjustments};
943 my $evt = $CC->create_bill(
947 $billing->billing_type,
949 "System: MANUAL ADJUSTMENT, BILLING #".$billing->id." REINSTATED\n(PREV: ".$billing->note.")",
950 $billing->period_start(),
951 $billing->period_end()
954 $rebill_amount += $billing->amount;
956 # if we have a postive (or zero) balance now, stop
957 last if ($xact_balance + $rebill_amount >= 0);
961 sub _is_fully_adjusted {
965 map { $amount_adj = $U->fpsum($amount_adj, $_->amount) } @{$billing->adjustments};
967 return $billing->amount == $amount_adj;
970 sub adjust_bills_to_zero_manual {
971 my ($self, $client, $auth, $xact_ids) = @_;
973 my $e = new_editor(xact => 1, authtoken => $auth);
974 return $e->die_event unless $e->checkauth;
976 # in case a bare ID is passed
977 $xact_ids = [$xact_ids] unless ref $xact_ids;
980 for my $xact_id (@$xact_ids) {
983 $e->retrieve_money_billable_transaction_summary([
985 {flesh => 1, flesh_fields => {mbts => ['usr']}}
986 ]) or return $e->die_event;
988 if ($xact->balance_owed == 0) {
989 # zero already, all done
993 return $e->die_event unless
994 $e->allowed('ADJUST_BILLS', $xact->usr->home_ou);
996 if ($xact->balance_owed < 0) {
997 my $evt = _rebill_xact($e, $xact);
999 # refetch xact to get new balance
1001 $e->retrieve_money_billable_transaction_summary([
1003 {flesh => 1, flesh_fields => {mbts => ['usr']}}
1004 ]) or return $e->die_event;
1007 if ($xact->balance_owed > 0) {
1008 # it's positive and needs to be adjusted
1009 # (it either started positive, or we rebilled it positive)
1010 my $billings = $e->search_money_billing([
1015 order_by => {mb => 'amount desc'},
1017 flesh_fields => {mb => ['adjustments']},
1021 my @billings_to_zero = grep { !$U->is_true($_->voided) or !_is_fully_adjusted($_) } @$billings;
1022 $CC->adjust_bills_to_zero($e, \@billings_to_zero, "System: MANUAL ADJUSTMENT");
1025 push(@modified, $xact->id);
1027 # now we see if we can close the transaction
1028 # same logic as make_payments();
1029 my $close_xact_fail = $CC->maybe_close_xact($e, $xact_id);
1030 if ($close_xact_fail) {
1031 return $close_xact_fail->{evt};
1040 __PACKAGE__->register_method(
1041 method => 'edit_bill_note',
1042 api_name => 'open-ils.circ.money.billing.note.edit',
1044 Edits the note for a bill
1045 @param authtoken Login session key
1046 @param note The replacement note for the bills we're editing
1047 @param billid Id for the bill to edit the note of. This parameter may be repeated to reference other bills.
1048 @return 1 on success, Event on error
1051 sub edit_bill_note {
1052 my( $s, $c, $authtoken, $note, @billids ) = @_;
1054 my $e = new_editor( authtoken => $authtoken, xact => 1 );
1055 return $e->die_event unless $e->checkauth;
1056 return $e->die_event unless $e->allowed('UPDATE_BILL_NOTE');
1058 for my $billid (@billids) {
1060 my $bill = $e->retrieve_money_billing($billid)
1061 or return $e->die_event;
1064 # FIXME: Does this get audited? Need some way so that the original creator of the bill does not get credit/blame for the new note.
1066 $e->update_money_billing($bill) or return $e->die_event;
1073 __PACKAGE__->register_method(
1074 method => 'edit_payment_note',
1075 api_name => 'open-ils.circ.money.payment.note.edit',
1077 Edits the note for a payment
1078 @param authtoken Login session key
1079 @param note The replacement note for the payments we're editing
1080 @param paymentid Id for the payment to edit the note of. This parameter may be repeated to reference other payments.
1081 @return 1 on success, Event on error
1084 sub edit_payment_note {
1085 my( $s, $c, $authtoken, $note, @paymentids ) = @_;
1087 my $e = new_editor( authtoken => $authtoken, xact => 1 );
1088 return $e->die_event unless $e->checkauth;
1089 return $e->die_event unless $e->allowed('UPDATE_PAYMENT_NOTE');
1091 for my $paymentid (@paymentids) {
1093 my $payment = $e->retrieve_money_payment($paymentid)
1094 or return $e->die_event;
1096 $payment->note($note);
1097 # FIXME: Does this get audited? Need some way so that the original taker of the payment does not get credit/blame for the new note.
1099 $e->update_money_payment($payment) or return $e->die_event;
1107 __PACKAGE__->register_method (
1108 method => 'fetch_mbts',
1110 api_name => 'open-ils.circ.money.billable_xact_summary.retrieve'
1113 my( $self, $conn, $auth, $id) = @_;
1115 my $e = new_editor(xact => 1, authtoken=>$auth);
1116 return $e->event unless $e->checkauth;
1117 my ($mbts) = $U->fetch_mbts($id, $e);
1119 my $user = $e->retrieve_actor_user($mbts->usr)
1120 or return $e->die_event;
1122 return $e->die_event unless $e->allowed('VIEW_TRANSACTION', $user->home_ou);
1128 __PACKAGE__->register_method(
1129 method => 'desk_payments',
1130 api_name => 'open-ils.circ.money.org_unit.desk_payments'
1133 my( $self, $conn, $auth, $org, $start_date, $end_date ) = @_;
1134 my $e = new_editor(authtoken=>$auth);
1135 return $e->event unless $e->checkauth;
1136 return $e->event unless $e->allowed('VIEW_TRANSACTION', $org);
1137 my $data = $U->storagereq(
1138 'open-ils.storage.money.org_unit.desk_payments.atomic',
1139 $org, $start_date, $end_date );
1141 $_->workstation( $_->workstation->name ) for(@$data);
1146 __PACKAGE__->register_method(
1147 method => 'user_payments',
1148 api_name => 'open-ils.circ.money.org_unit.user_payments'
1152 my( $self, $conn, $auth, $org, $start_date, $end_date ) = @_;
1153 my $e = new_editor(authtoken=>$auth);
1154 return $e->event unless $e->checkauth;
1155 return $e->event unless $e->allowed('VIEW_TRANSACTION', $org);
1156 my $data = $U->storagereq(
1157 'open-ils.storage.money.org_unit.user_payments.atomic',
1158 $org, $start_date, $end_date );
1161 $e->retrieve_actor_card($_->usr->card)->barcode);
1163 $e->retrieve_actor_org_unit($_->usr->home_ou)->shortname);
1169 __PACKAGE__->register_method(
1170 method => 'retrieve_credit_payable_balance',
1171 api_name => 'open-ils.circ.credit.payable_balance.retrieve',
1174 desc => q/Returns the total amount the patron can pay via credit card/,
1176 { desc => 'Authentication token', type => 'string' },
1177 { desc => 'User id', type => 'number' }
1179 return => { desc => 'The ID of the new provider' }
1183 sub retrieve_credit_payable_balance {
1184 my ( $self, $conn, $auth, $user_id ) = @_;
1185 my $e = new_editor(authtoken => $auth);
1186 return $e->event unless $e->checkauth;
1188 my $user = $e->retrieve_actor_user($user_id)
1189 or return $e->event;
1191 if($e->requestor->id != $user_id) {
1192 return $e->event unless $e->allowed('VIEW_USER_TRANSACTIONS', $user->home_ou)
1195 my $circ_orgs = $e->json_query({
1196 "select" => {circ => ["circ_lib"]},
1198 "where" => {usr => $user_id, xact_finish => undef},
1202 my $groc_orgs = $e->json_query({
1203 "select" => {mg => ["billing_location"]},
1205 "where" => {usr => $user_id, xact_finish => undef},
1210 for my $org ( @$circ_orgs, @$groc_orgs ) {
1211 my $o = $org->{billing_location};
1212 $o = $org->{circ_lib} unless $o;
1213 next if $hash{$o}; # was $hash{$org}, but that doesn't make sense. $org is a hashref and $o gets added in the next line.
1214 $hash{$o} = $U->ou_ancestor_setting_value($o, 'credit.payments.allow', $e);
1217 my @credit_orgs = map { $hash{$_} ? ($_) : () } keys %hash;
1218 $logger->debug("credit: relevant orgs that allow credit payments => @credit_orgs");
1220 my $xact_summaries =
1221 OpenILS::Application::AppUtils->simplereq('open-ils.actor',
1222 'open-ils.actor.user.transactions.have_charge', $auth, $user_id);
1226 for my $xact (@$xact_summaries) {
1228 # make two lists and grab them in batch XXX
1229 if ( $xact->xact_type eq 'circulation' ) {
1230 my $circ = $e->retrieve_action_circulation($xact->id) or return $e->event;
1231 next unless grep { $_ == $circ->circ_lib } @credit_orgs;
1233 } elsif ($xact->xact_type eq 'grocery') {
1234 my $bill = $e->retrieve_money_grocery($xact->id) or return $e->event;
1235 next unless grep { $_ == $bill->billing_location } @credit_orgs;
1236 } elsif ($xact->xact_type eq 'reservation') {
1237 my $bill = $e->retrieve_booking_reservation($xact->id) or return $e->event;
1238 next unless grep { $_ == $bill->pickup_lib } @credit_orgs;
1240 $sum += $xact->balance_owed();
1247 __PACKAGE__->register_method(
1248 method => "retrieve_statement",
1250 api_name => "open-ils.circ.money.statement.retrieve",
1251 notes => "Returns an organized summary of a billable transaction, including all bills, payments, adjustments, and voids."
1257 return $parser->parse_datetime(clean_ISO8601($ts))->epoch;
1260 my %_statement_sort = (
1262 'account_adjustment' => 1,
1267 sub retrieve_statement {
1268 my ( $self, $client, $auth, $xact_id ) = @_;
1270 my $e = new_editor(authtoken=>$auth);
1271 return $e->event unless $e->checkauth;
1272 return $e->event unless $e->allowed('VIEW_TRANSACTION');
1274 # XXX: move this lookup login into a DB query?
1277 # collect all payments/adjustments
1278 my $payments = $e->search_money_payment({ xact => $xact_id });
1279 foreach my $payment (@$payments) {
1280 my $type = $payment->payment_type;
1281 $type = 'payment' if $type ne 'account_adjustment';
1282 push(@line_prep, [$type, _to_epoch($payment->payment_ts), $payment->payment_ts, $payment->id, $payment]);
1285 # collect all billings
1286 my $billings = $e->search_money_billing({ xact => $xact_id });
1287 foreach my $billing (@$billings) {
1288 if ($U->is_true($billing->voided)){
1289 push(@line_prep, ['void', _to_epoch($billing->void_time), $billing->void_time, $billing->id, $billing]); # voids get two entries, one to represent the bill event, one for the void event
1291 push(@line_prep, ['billing', _to_epoch($billing->billing_ts), $billing->billing_ts, $billing->id, $billing]);
1294 # order every event by timestamp, then bills/adjustments/voids/payments order, then id
1295 my @ordered_line_prep = sort {
1298 $_statement_sort{$a->[0]} <=> $_statement_sort{$b->[0]}
1303 # let's start building the statement structure
1304 my (@lines, %current_line, $running_balance);
1305 foreach my $event (@ordered_line_prep) {
1306 my $obj = $event->[4];
1307 my $type = $event->[0];
1308 my $ts = $event->[2];
1309 my $billing_type = $type =~ /billing|void/ ? $obj->billing_type : ''; # TODO: get non-legacy billing type
1310 my $note = $obj->note || '';
1311 # last line should be void information, try to isolate it
1312 if ($type eq 'billing' and $obj->voided) {
1314 } elsif ($type eq 'void') {
1315 $note = (split(/\n/, $note))[-1];
1318 # if we have new details, start a new line
1319 if ($current_line{amount} and (
1320 $type ne $current_line{type}
1321 or ($note ne $current_line{note})
1322 or ($billing_type ne $current_line{billing_type})
1325 push(@lines, {%current_line}); # push a copy of the hash, not the real thing
1328 if (!$current_line{type}) {
1329 $current_line{type} = $type;
1330 $current_line{billing_type} = $billing_type;
1331 $current_line{note} = $note;
1333 if (!$current_line{start_date}) {
1334 $current_line{start_date} = $ts;
1335 } elsif ($ts ne $current_line{start_date}) {
1336 $current_line{end_date} = $ts;
1338 $current_line{amount} += $obj->amount;
1339 if ($current_line{details}) {
1340 push(@{$current_line{details}}, $obj);
1342 $current_line{details} = [$obj];
1345 push(@lines, {%current_line}); # push last one on
1347 # get/update totals, format notes
1351 account_adjustment => 0,
1354 foreach my $line (@lines) {
1355 $totals{$line->{type}} += $line->{amount};
1356 if ($line->{type} eq 'billing') {
1357 $running_balance += $line->{amount};
1358 } else { # not a billing; balance goes down for everything else
1359 $running_balance -= $line->{amount};
1361 $line->{running_balance} = $running_balance;
1362 $line->{note} = $line->{note} ? [split(/\n/, $line->{note})] : [];
1365 my $xact = $e->retrieve_money_billable_transaction([
1369 mbt => [qw/circulation grocery/],
1370 circ => [qw/target_copy/],
1371 acp => [qw/call_number location status age_protect total_circ_count/],
1372 acn => [qw/record prefix suffix/],
1373 bre => [qw/wide_display_entry/]
1375 select => {bre => ['id']}
1380 my $billing_location;
1382 if ($xact->circulation) {
1383 $billing_location = $xact->circulation->circ_lib;
1384 my $copy = $xact->circulation->target_copy;
1385 if ($copy->call_number->id == -1) {
1386 $title = $copy->dummy_title;
1388 $title_id = $copy->call_number->record->id;
1389 $title = OpenSRF::Utils::JSON->JSON2perl(
1390 $copy->call_number->record->wide_display_entry->title);
1393 $billing_location = $xact->grocery->billing_location;
1394 $title = $xact->grocery->note;
1398 xact_id => $xact_id,
1401 title_id => $title_id,
1402 billing_location => $billing_location,
1404 balance_due => $totals{billing} - ($totals{payment} + $totals{account_adjustment} + $totals{void}),
1405 billing_total => $totals{billing},
1406 credit_total => $totals{payment} + $totals{account_adjustment},
1407 payment_total => $totals{payment},
1408 account_adjustment_total => $totals{account_adjustment},
1409 void_total => $totals{void}