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);
29 use OpenSRF::Utils::Logger qw/:logger/;
30 use OpenILS::Utils::CStoreEditor qw/:funcs/;
31 use OpenILS::Utils::Penalty;
33 $Data::Dumper::Indent = 0;
34 use OpenILS::Const qw/:const/;
35 use OpenILS::Utils::DateTime qw/:datetime/;
36 use DateTime::Format::ISO8601;
37 my $parser = DateTime::Format::ISO8601->new;
39 sub get_processor_settings {
42 my $processor = lc shift;
44 # Get the names of every credit processor setting for our given processor.
45 # They're a little different per processor.
46 my $setting_names = $e->json_query({
47 select => {coust => ["name"]},
48 from => {coust => {}},
49 where => {name => {like => "credit.processor.${processor}.%"}}
50 }) or return $e->die_event;
52 # Make keys for a hash we're going to build out of the last dot-delimited
53 # component of each setting name.
54 ($_->{key} = $_->{name}) =~ s/.+\.(\w+)$/$1/ for @$setting_names;
56 # Return a hash with those short keys, and for values the value of
57 # the corresponding OU setting within our scope.
60 $_->{key} => $U->ou_ancestor_setting_value($org_unit, $_->{name})
65 # process_stripe_or_bop_payment()
66 # This is a helper method to make_payments() below (specifically,
67 # the credit-card part). It's the first point in the Perl code where
68 # we need to care about the distinction between Stripe and the
69 # Paypal/PayflowPro/AuthorizeNet kinds of processors (the latter group
70 # uses B::OP and handles payment card info, whereas Stripe doesn't use
71 # B::OP and doesn't require us to know anything about the payment card
74 # Return an event in all cases. That means a success returns a SUCCESS
76 sub process_stripe_or_bop_payment {
77 my ($e, $user_id, $this_ou, $total_paid, $cc_args) = @_;
79 # A few stanzas to determine which processor we're using and whether we're
80 # really adequately set up for it.
81 if (!$cc_args->{processor}) {
82 if (!($cc_args->{processor} =
83 $U->ou_ancestor_setting_value(
84 $this_ou, 'credit.processor.default'
88 return OpenILS::Event->new('CREDIT_PROCESSOR_NOT_SPECIFIED');
92 # Make sure the configured credit processor has a safe/correct name.
93 return OpenILS::Event->new('CREDIT_PROCESSOR_NOT_ALLOWED')
94 unless $cc_args->{processor} =~ /^[a-z0-9_\-]+$/i;
96 # Get the settings for the processor and make sure they're serviceable.
97 my $psettings = get_processor_settings($e, $this_ou, $cc_args->{processor});
98 return $psettings if defined $U->event_code($psettings);
99 return OpenILS::Event->new('CREDIT_PROCESSOR_NOT_ENABLED')
100 unless $psettings->{enabled};
102 # Now we branch. Stripe is one thing, and everything else is another.
104 if ($cc_args->{processor} eq 'Stripe') { # Stripe
105 my $stripe = Business::Stripe->new(-api_key => $psettings->{secretkey});
106 $stripe->api('post','payment_intents/' . $cc_args->{stripe_payment_intent});
107 if ($stripe->success) {
108 $logger->debug('Stripe payment intent retrieved');
109 my $intent = $stripe->success;
110 if ($intent->{status} eq 'succeeded') {
111 $logger->info('Stripe payment succeeded');
112 return OpenILS::Event->new(
113 'SUCCESS', payload => {
114 invoice => $intent->{invoice},
115 customer => $intent->{customer},
116 balance_transaction => 'N/A',
118 created => $intent->{created},
123 $logger->info('Stripe payment failed');
124 return OpenILS::Event->new(
125 'CREDIT_PROCESSOR_DECLINED_TRANSACTION',
126 payload => $intent->{last_payment_error}
130 $logger->debug('Stripe payment intent not retrieved');
131 $logger->info('Stripe payment failed');
132 return OpenILS::Event->new(
133 "CREDIT_PROCESSOR_DECLINED_TRANSACTION",
134 payload => $stripe->error # XXX what happens if this contains
135 # JSON::backportPP::* objects?
139 } else { # B::OP style (Paypal/PayflowPro/AuthorizeNet)
140 return OpenILS::Event->new('BAD_PARAMS', note => 'Need CC number')
141 unless $cc_args->{number};
143 return OpenILS::Application::Circ::CreditCard::process_payment({
144 "processor" => $cc_args->{processor},
145 "desc" => $cc_args->{note},
146 "amount" => $total_paid,
147 "patron_id" => $user_id,
148 "cc" => $cc_args->{number},
149 "expiration" => sprintf(
151 $cc_args->{expire_month},
152 $cc_args->{expire_year}
155 "first_name" => $cc_args->{billing_first},
156 "last_name" => $cc_args->{billing_last},
157 "address" => $cc_args->{billing_address},
158 "city" => $cc_args->{billing_city},
159 "state" => $cc_args->{billing_state},
160 "zip" => $cc_args->{billing_zip},
161 "cvv2" => $cc_args->{cvv2},
168 __PACKAGE__->register_method(
169 method => "make_payments",
170 api_name => "open-ils.circ.money.payment",
172 desc => q/Create payments for a given user and set of transactions,
173 login must have CREATE_PAYMENT privileges.
174 If any payments fail, all are reverted back./,
176 {desc => 'Authtoken', type => 'string'},
177 {desc => q/Arguments Hash, supporting the following params:
184 where_process 1 to use processor, !1 for out-of-band
185 approval_code (for out-of-band payment)
186 type (for out-of-band payment)
187 number (for call to payment processor)
188 stripe_token (for call to Stripe payment processor)
189 expire_month (for call to payment processor)
190 expire_year (for call to payment processor)
191 billing_first (for out-of-band payments and for call to payment processor)
192 billing_last (for out-of-band payments and for call to payment processor)
193 billing_address (for call to payment processor)
194 billing_city (for call to payment processor)
195 billing_state (for call to payment processor)
196 billing_zip (for call to payment processor)
197 note (if payments->{note} is blank, use this)
207 desc => q/Last user transaction ID. This is the actor.usr.last_xact_id value/,
213 q{Array of payment IDs on success, event on failure. Event possibilities include:
215 Bad parameters were given to this API method itself.
218 The last user transaction ID does not match the ID in the database. This means
219 the user object has been updated since the last retrieval. The client should
220 be instructed to reload the user object and related transactions before attempting
222 REFUND_EXCEEDS_BALANCE
223 REFUND_EXCEEDS_DESK_PAYMENTS
224 CREDIT_PROCESSOR_NOT_SPECIFIED
225 Evergreen has not been set up to process CC payments.
226 CREDIT_PROCESSOR_NOT_ALLOWED
227 Evergreen has been incorrectly setup for CC payments.
228 CREDIT_PROCESSOR_NOT_ENABLED
229 Evergreen has been set up for CC payments, but an admin
230 has not explicitly enabled them.
231 CREDIT_PROCESSOR_BAD_PARAMS
232 Evergreen has been incorrectly setup for CC payments;
233 specifically, the login and/or password for the CC
234 processor weren't provided.
235 CREDIT_PROCESSOR_INVALID_CC_NUMBER
236 You have supplied a credit card number that Evergreen
237 has judged to be invalid even before attempting to contact
238 the payment processor.
239 CREDIT_PROCESSOR_DECLINED_TRANSACTION
240 We contacted the CC processor to attempt the charge, but
242 The error_message field of the event payload will
243 contain the payment processor's response. This
244 typically includes a message in plain English intended
245 for human consumption. In PayPal's case, the message
246 is preceded by an integer, a colon, and a space, so
247 a caller might take the 2nd match from /^(\d+: )?(.+)$/
248 to present to the user.
249 The payload also contains other fields from the payment
250 processor, but these are generally not user-friendly
252 CREDIT_PROCESSOR_SUCCESS_WO_RECORD
253 A payment was processed successfully, but couldn't be
254 recorded in Evergreen. This is _bad bad bad_, as it means
255 somebody made a payment but isn't getting credit for it.
256 See errors in the system log if this happens. Info from
257 the credit card transaction will also be available in the
258 event payload, although this probably won't be suitable for
259 staff client/OPAC display.
266 my($self, $client, $auth, $payments, $last_xact_id) = @_;
268 my $e = new_editor(authtoken => $auth, xact => 1);
269 return $e->die_event unless $e->checkauth;
271 my $type = $payments->{payment_type};
272 my $user_id = $payments->{userid};
273 my $credit = $payments->{patron_credit} || 0;
274 my $drawer = $e->requestor->wsid;
275 my $note = $payments->{note};
276 my $cc_args = $payments->{cc_args};
277 my $check_number = $payments->{check_number};
279 my $this_ou = $e->requestor->ws_ou || $e->requestor->home_ou;
283 # unless/until determined by payment processor API
284 my ($approval_code, $cc_processor, $cc_order_number) = (undef,undef,undef, undef);
286 my $patron = $e->retrieve_actor_user($user_id) or return $e->die_event;
288 if($patron->last_xact_id ne $last_xact_id) {
290 return OpenILS::Event->new('INVALID_USER_XACT_ID');
293 # A user is allowed to make credit card payments on his/her own behalf
294 # All other scenarious require permission
295 unless($type eq 'credit_card_payment' and $user_id == $e->requestor->id) {
296 return $e->die_event unless $e->allowed('CREATE_PAYMENT', $patron->home_ou);
299 # first collect the transactions and make sure the transaction
300 # user matches the requested user
303 # We rewrite the payments array for sanity's sake, to avoid more
304 # than one payment per transaction per call, which is not legitimate
305 # but has been seen in the wild coming from the staff client. This
306 # is presumably a staff client (xulrunner) bug.
307 my @unique_xact_payments;
308 for my $pay (@{$payments->{payments}}) {
309 my $xact_id = $pay->[0];
310 if (exists($xacts{$xact_id})) {
312 return OpenILS::Event->new('MULTIPLE_PAYMENTS_FOR_XACT');
315 my $xact = $e->retrieve_money_billable_transaction_summary($xact_id)
316 or return $e->die_event;
318 if($xact->usr != $user_id) {
320 return OpenILS::Event->new('BAD_PARAMS', note => q/user does not match transaction/);
323 $xacts{$xact_id} = $xact;
324 push @unique_xact_payments, $pay;
326 $payments->{payments} = \@unique_xact_payments;
330 for my $pay (@{$payments->{payments}}) {
331 my $transid = $pay->[0];
332 my $amount = $pay->[1];
333 $amount =~ s/\$//og; # just to be safe
334 my $trans = $xacts{$transid};
336 # add amounts as integers
337 $total_paid += (100 * $amount);
339 my $org_id = $U->xact_org($transid, $e);
341 if (!$orgs{$org_id}) {
344 # patron credit has to be allowed at all orgs receiving payment
345 if ($type eq 'credit_payment' and $U->ou_ancestor_setting_value(
346 $org_id, 'circ.disable_patron_credit', $e)) {
348 return OpenILS::Event->new('PATRON_CREDIT_DISABLED');
352 # A negative payment is a refund.
355 # Negative credit card payments are not allowed
356 if($type eq 'credit_card_payment') {
358 return OpenILS::Event->new(
360 note => q/Negative credit card payments not allowed/
364 # If the refund causes the transaction balance to exceed 0 dollars,
365 # we are in effect loaning the patron money. This is not allowed.
366 if( ($trans->balance_owed - $amount) > 0 ) {
368 return OpenILS::Event->new('REFUND_EXCEEDS_BALANCE');
371 # Otherwise, make sure the refund does not exceed desk payments
372 # This is also not allowed
374 my $desk_payments = $e->search_money_desk_payment({xact => $transid, voided => 'f'});
375 $desk_total += $_->amount for @$desk_payments;
377 if( (-$amount) > $desk_total ) {
379 return OpenILS::Event->new(
380 'REFUND_EXCEEDS_DESK_PAYMENTS',
381 payload => { allowed_refund => $desk_total, submitted_refund => -$amount } );
385 my $payobj = "Fieldmapper::money::$type";
386 $payobj = $payobj->new;
388 $payobj->amount($amount);
389 $payobj->amount_collected($amount);
390 $payobj->xact($transid);
391 $payobj->note($note);
392 if ((not $payobj->note) and ($type eq 'credit_card_payment')) {
393 $payobj->note($cc_args->{note});
396 if ($payobj->has_field('accepting_usr')) { $payobj->accepting_usr($e->requestor->id); }
397 if ($payobj->has_field('cash_drawer')) { $payobj->cash_drawer($drawer); }
398 if ($payobj->has_field('check_number')) { $payobj->check_number($check_number); }
400 # Store the last 4 digits of the CC number
401 if ($payobj->has_field('cc_number')) {
402 $payobj->cc_number(substr($cc_args->{number}, -4));
405 # Note: It is important not to set approval_code
406 # on the fieldmapper object yet.
408 push(@payment_objs, $payobj);
410 } # all payment objects have been created and inserted.
412 # return to decimal format, forcing X.YY format for consistency.
413 $total_paid = sprintf("%.2f", $total_paid / 100);
415 #### NO WRITES TO THE DB ABOVE THIS LINE -- THEY'LL ONLY BE DISCARDED ###
418 # After we try to externally process a credit card (if desired), we'll
419 # open a new transaction. We cannot leave one open while credit card
420 # processing might be happening, as it can easily time out the database
425 if($type eq 'credit_card_payment') {
426 $approval_code = $cc_args->{approval_code};
427 # If an approval code was not given, we'll need
428 # to call to the payment processor ourselves.
429 if ($cc_args->{where_process} == 1) {
430 my $response = process_stripe_or_bop_payment(
431 $e, $user_id, $this_ou, $total_paid, $cc_args
434 if ($U->event_code($response)) { # non-success (success is 0)
436 "Credit card payment for user $user_id failed: " .
437 $response->{textcode} . " " .
438 ($response->{payload}->{error_message} ||
439 $response->{payload}{message})
443 # We need to save this for later in case there's a failure on
444 # the EG side to store the processor's result.
446 $cc_payload = $response->{"payload"}; # also used way later
449 no warnings 'uninitialized';
450 $approval_code = $cc_payload->{authorization} ||
452 $cc_processor = $cc_payload->{processor} ||
453 $cc_args->{processor};
454 $cc_order_number = $cc_payload->{order_number} ||
455 $cc_payload->{invoice};
457 $logger->info("Credit card payment for user $user_id succeeded");
460 return OpenILS::Event->new(
461 'BAD_PARAMS', note => 'Need approval code'
462 ) if not $cc_args->{approval_code};
466 ### RE-OPEN TRANSACTION HERE ###
470 # create payment records
471 my $create_money_method = "create_money_" . $type;
472 for my $payment (@payment_objs) {
473 # update the transaction if it's done
474 my $amount = $payment->amount;
475 my $transid = $payment->xact;
476 my $trans = $xacts{$transid};
477 # making payment with existing patron credit.
478 $credit -= $amount if $type eq 'credit_payment';
479 if( (my $cred = ($trans->balance_owed - $amount)) <= 0 ) {
480 # Any overpay on this transaction goes directly into patron
485 # Attempt to close the transaction.
486 my $close_xact_fail = $CC->maybe_close_xact($e, $transid);
487 if ($close_xact_fail) {
488 return _recording_failure(
489 $e, $close_xact_fail->{message},
490 $payment, $cc_payload
495 # Urgh, clean up this mega-function one day.
496 if ($cc_processor eq 'Stripe' and $approval_code and $cc_payload) {
497 $payment->cc_number($cc_payload->{card}); # not actually available :)
500 $payment->approval_code($approval_code) if $approval_code;
501 $payment->cc_order_number($cc_order_number) if $cc_order_number;
502 $payment->cc_processor($cc_processor) if $cc_processor;
503 if (!$e->$create_money_method($payment)) {
504 return _recording_failure(
505 $e, "$create_money_method failed", $payment, $cc_payload
509 push(@payment_ids, $payment->id);
512 my $evt = _update_patron_credit($e, $patron, $credit);
514 return _recording_failure(
515 $e, "_update_patron_credit() failed", undef, $cc_payload
519 for my $org_id (keys %orgs) {
520 # calculate penalties for each of the affected orgs
521 $evt = OpenILS::Utils::Penalty->calculate_penalties(
522 $e, $user_id, $org_id
525 return _recording_failure(
526 $e, "calculate_penalties() failed", undef, $cc_payload
531 # update the user to create a new last_xact_id
532 $e->update_actor_user($patron) or return $e->die_event;
533 $patron = $e->retrieve_actor_user($patron) or return $e->die_event;
536 # update the cached user object if a user is making a payment toward
537 # his/her own account
538 $U->simplereq('open-ils.auth', 'open-ils.auth.session.reset_timeout', $auth, 1)
539 if $user_id == $e->requestor->id;
541 return {last_xact_id => $patron->last_xact_id, payments => \@payment_ids};
544 sub _recording_failure {
545 my ($e, $msg, $payment, $payload) = @_;
547 if ($payload) { # If the payment processor already accepted a payment:
548 $logger->error($msg);
549 $logger->error("Payment processor payload: " . Dumper($payload));
550 # payment shouldn't contain CC number
551 $logger->error("Payment: " . Dumper($payment)) if $payment;
555 return new OpenILS::Event(
556 "CREDIT_PROCESSOR_SUCCESS_WO_RECORD",
557 "payload" => $payload
559 } else { # Otherwise, the problem is somewhat less severe:
561 $logger->warn("Payment: " . Dumper($payment)) if $payment;
562 return $e->die_event;
566 sub _update_patron_credit {
567 my($e, $patron, $credit) = @_;
568 return undef if $credit == 0;
569 $patron->credit_forward_balance($patron->credit_forward_balance + $credit);
570 return OpenILS::Event->new('NEGATIVE_PATRON_BALANCE') if $patron->credit_forward_balance < 0;
571 $e->update_actor_user($patron) or return $e->die_event;
576 __PACKAGE__->register_method(
577 method => "retrieve_payments",
578 api_name => "open-ils.circ.money.payment.retrieve.all_",
579 notes => "Returns a list of payments attached to a given transaction"
581 sub retrieve_payments {
582 my( $self, $client, $login, $transid ) = @_;
585 $apputils->checksesperm($login, 'VIEW_TRANSACTION');
588 # XXX the logic here is wrong.. we need to check the owner of the transaction
589 # to make sure the requestor has access
591 # XXX grab the view, for each object in the view, grab the real object
593 return $apputils->simplereq(
595 'open-ils.cstore.direct.money.payment.search.atomic', { xact => $transid } );
599 __PACKAGE__->register_method(
600 method => "retrieve_payments2",
602 api_name => "open-ils.circ.money.payment.retrieve.all",
603 notes => "Returns a list of payments attached to a given transaction"
606 sub retrieve_payments2 {
607 my( $self, $client, $login, $transid ) = @_;
609 my $e = new_editor(authtoken=>$login);
610 return $e->event unless $e->checkauth;
611 return $e->event unless $e->allowed('VIEW_TRANSACTION');
614 my $pmnts = $e->search_money_payment({ xact => $transid });
616 my $type = $_->payment_type;
617 my $meth = "retrieve_money_$type";
618 my $p = $e->$meth($_->id) or return $e->event;
619 $p->payment_type($type);
620 $p->cash_drawer($e->retrieve_actor_workstation($p->cash_drawer))
621 if $p->has_field('cash_drawer');
622 push( @payments, $p );
628 __PACKAGE__->register_method(
629 method => "format_payment_receipt",
630 api_name => "open-ils.circ.money.payment_receipt.print",
632 desc => 'Returns a printable receipt for the specified payments',
634 { desc => 'Authentication token', type => 'string'},
635 { desc => 'Payment ID or array of payment IDs', type => 'number' },
638 desc => q/An action_trigger.event object or error event./,
643 __PACKAGE__->register_method(
644 method => "format_payment_receipt",
645 api_name => "open-ils.circ.money.payment_receipt.email",
647 desc => 'Emails a receipt for the specified payments to the user associated with the first payment',
649 { desc => 'Authentication token', type => 'string'},
650 { desc => 'Payment ID or array of payment IDs', type => 'number' },
653 desc => q/Undefined on success, otherwise an error event./,
659 sub format_payment_receipt {
660 my($self, $conn, $auth, $mp_id) = @_;
663 if (ref $mp_id ne 'ARRAY') {
664 $mp_ids = [ $mp_id ];
669 my $for_print = ($self->api_name =~ /print/);
670 my $for_email = ($self->api_name =~ /email/);
672 # manually use xact (i.e. authoritative) so we can kill the cstore
673 # connection before sending the action/trigger request. This prevents our cstore
674 # backend from sitting idle while A/T (which uses its own transactions) runs.
675 my $e = new_editor(xact => 1, authtoken => $auth);
676 return $e->die_event unless $e->checkauth;
679 for my $id (@$mp_ids) {
681 my $payment = $e->retrieve_money_payment([
689 ]) or return $e->die_event;
691 return $e->die_event unless
692 $e->requestor->id == $payment->xact->usr->id or
693 $e->allowed('VIEW_TRANSACTION', $payment->xact->usr->home_ou);
695 push @$payments, $payment;
702 return $U->fire_object_event(undef, 'money.format.payment_receipt.print', $payments, $$payments[0]->xact->usr->home_ou);
704 } elsif ($for_email) {
706 for my $p (@$payments) {
707 $U->create_events_for_hook('money.format.payment_receipt.email', $p, $p->xact->usr->home_ou, undef, undef, 1);
714 __PACKAGE__->register_method(
715 method => "create_grocery_bill",
716 api_name => "open-ils.circ.money.grocery.create",
718 Creates a new grocery transaction using the transaction object provided
719 PARAMS: (login_session, money.grocery (mg) object)
722 sub create_grocery_bill {
723 my( $self, $client, $login, $transaction ) = @_;
725 my( $staff, $evt ) = $apputils->checkses($login);
727 $evt = $apputils->check_perms($staff->id,
728 $transaction->billing_location, 'CREATE_TRANSACTION' );
732 $logger->activity("Creating grocery bill " . Dumper($transaction) );
734 $transaction->clear_id;
735 my $session = $apputils->start_db_session;
736 $apputils->set_audit_info($session, $login, $staff->id, $staff->wsid);
737 my $transid = $session->request(
738 'open-ils.storage.direct.money.grocery.create', $transaction)->gather(1);
740 throw OpenSRF::EX ("Error creating new money.grocery") unless defined $transid;
742 $logger->debug("Created new grocery transaction $transid");
744 $apputils->commit_db_session($session);
746 my $e = new_editor(xact=>1);
747 $evt = $U->check_open_xact($e, $transid);
755 __PACKAGE__->register_method(
756 method => 'fetch_reservation',
757 api_name => 'open-ils.circ.booking.reservation.retrieve'
759 sub fetch_reservation {
760 my( $self, $conn, $auth, $id ) = @_;
761 my $e = new_editor(authtoken=>$auth);
762 return $e->event unless $e->checkauth;
763 return $e->event unless $e->allowed('VIEW_TRANSACTION'); # eh.. basically the same permission
764 my $g = $e->retrieve_booking_reservation($id)
769 __PACKAGE__->register_method(
770 method => 'fetch_grocery',
771 api_name => 'open-ils.circ.money.grocery.retrieve'
774 my( $self, $conn, $auth, $id ) = @_;
775 my $e = new_editor(authtoken=>$auth);
776 return $e->event unless $e->checkauth;
777 return $e->event unless $e->allowed('VIEW_TRANSACTION'); # eh.. basically the same permission
778 my $g = $e->retrieve_money_grocery($id)
784 __PACKAGE__->register_method(
785 method => "billing_items",
786 api_name => "open-ils.circ.money.billing.retrieve.all",
789 desc => 'Returns a list of billing items for the given transaction ID. ' .
790 'If the operator is not the owner of the transaction, the VIEW_TRANSACTION permission is required.',
792 { desc => 'Authentication token', type => 'string'},
793 { desc => 'Transaction ID', type => 'number'}
796 desc => 'Transaction object, event on error'
802 my( $self, $client, $login, $transid ) = @_;
804 my( $trans, $evt ) = $U->fetch_billable_xact($transid);
808 ($staff, $evt ) = $apputils->checkses($login);
811 if($staff->id ne $trans->usr) {
812 $evt = $U->check_perms($staff->id, $staff->home_ou, 'VIEW_TRANSACTION');
816 return $apputils->simplereq( 'open-ils.cstore',
817 'open-ils.cstore.direct.money.billing.search.atomic', { xact => $transid } )
821 __PACKAGE__->register_method(
822 method => "billing_items_create",
823 api_name => "open-ils.circ.money.billing.create",
825 Creates a new billing line item
826 PARAMS( login, bill_object (mb) )
829 sub billing_items_create {
830 my( $self, $client, $login, $billing ) = @_;
832 my $e = new_editor(authtoken => $login, xact => 1);
833 return $e->die_event unless $e->checkauth;
834 return $e->die_event unless $e->allowed('CREATE_BILL');
836 my $xact = $e->retrieve_money_billable_transaction($billing->xact)
837 or return $e->die_event;
839 # if the transaction was closed, re-open it
840 if($xact->xact_finish) {
841 $xact->clear_xact_finish;
842 $e->update_money_billable_transaction($xact)
843 or return $e->die_event;
846 my $amt = $billing->amount;
848 $billing->amount($amt);
850 $e->create_money_billing($billing) or return $e->die_event;
851 my $evt = OpenILS::Utils::Penalty->calculate_penalties($e, $xact->usr, $U->xact_org($xact->id,$e));
854 $evt = $U->check_open_xact($e, $xact->id, $xact);
863 __PACKAGE__->register_method(
864 method => 'void_bill',
865 api_name => 'open-ils.circ.money.billing.void',
868 @param authtoken Login session key
869 @param billid Id for the bill to void. This parameter may be repeated to reference other bills.
870 @return 1 on success, Event on error
874 my( $s, $c, $authtoken, @billids ) = @_;
875 my $editor = new_editor(authtoken=>$authtoken, xact=>1);
876 return $editor->die_event unless $editor->checkauth;
877 return $editor->die_event unless $editor->allowed('VOID_BILLING');
878 my $rv = $CC->void_bills($editor, \@billids);
879 if (ref($rv) eq 'HASH') {
883 # We should have gotten 1.
890 __PACKAGE__->register_method(
891 method => 'adjust_bills_to_zero_manual',
892 api_name => 'open-ils.circ.money.billable_xact.adjust_to_zero',
895 Given a list of billable transactions, manipulate the
896 transaction using account adjustments to result in a
900 {desc => 'Authtoken', type => 'string'},
901 {desc => 'Array of transaction IDs', type => 'array'}
904 desc => q/Array of IDs for each transaction updated,
913 my $xact_id = $xact->id;
914 # the plan: rebill voided billings until we get a positive balance
916 # step 1: get the voided/adjusted billings
917 my $billings = $e->search_money_billing([
922 order_by => {mb => 'amount desc'},
924 flesh_fields => {mb => ['adjustments']},
927 my @billings = grep { $U->is_true($_->voided) or @{$_->adjustments} } @$billings;
929 my $xact_balance = $xact->balance_owed;
930 $logger->debug("rebilling for xact $xact_id with balance $xact_balance");
932 my $rebill_amount = 0;
934 # step 2: generate new bills just like the old ones
935 for my $billing (@billings) {
937 if ($U->is_true($billing->voided)) {
938 $amount = $billing->amount;
939 } else { # adjusted billing
940 map { $amount = $U->fpsum($amount, $_->amount) } @{$billing->adjustments};
942 my $evt = $CC->create_bill(
946 $billing->billing_type,
948 "System: MANUAL ADJUSTMENT, BILLING #".$billing->id." REINSTATED\n(PREV: ".$billing->note.")",
949 $billing->period_start(),
950 $billing->period_end()
953 $rebill_amount += $billing->amount;
955 # if we have a postive (or zero) balance now, stop
956 last if ($xact_balance + $rebill_amount >= 0);
960 sub _is_fully_adjusted {
964 map { $amount_adj = $U->fpsum($amount_adj, $_->amount) } @{$billing->adjustments};
966 return $billing->amount == $amount_adj;
969 sub adjust_bills_to_zero_manual {
970 my ($self, $client, $auth, $xact_ids) = @_;
972 my $e = new_editor(xact => 1, authtoken => $auth);
973 return $e->die_event unless $e->checkauth;
975 # in case a bare ID is passed
976 $xact_ids = [$xact_ids] unless ref $xact_ids;
979 for my $xact_id (@$xact_ids) {
982 $e->retrieve_money_billable_transaction_summary([
984 {flesh => 1, flesh_fields => {mbts => ['usr']}}
985 ]) or return $e->die_event;
987 if ($xact->balance_owed == 0) {
988 # zero already, all done
992 return $e->die_event unless
993 $e->allowed('ADJUST_BILLS', $xact->usr->home_ou);
995 if ($xact->balance_owed < 0) {
996 my $evt = _rebill_xact($e, $xact);
998 # refetch xact to get new balance
1000 $e->retrieve_money_billable_transaction_summary([
1002 {flesh => 1, flesh_fields => {mbts => ['usr']}}
1003 ]) or return $e->die_event;
1006 if ($xact->balance_owed > 0) {
1007 # it's positive and needs to be adjusted
1008 # (it either started positive, or we rebilled it positive)
1009 my $billings = $e->search_money_billing([
1014 order_by => {mb => 'amount desc'},
1016 flesh_fields => {mb => ['adjustments']},
1020 my @billings_to_zero = grep { !$U->is_true($_->voided) or !_is_fully_adjusted($_) } @$billings;
1021 $CC->adjust_bills_to_zero($e, \@billings_to_zero, "System: MANUAL ADJUSTMENT");
1024 push(@modified, $xact->id);
1026 # now we see if we can close the transaction
1027 # same logic as make_payments();
1028 my $close_xact_fail = $CC->maybe_close_xact($e, $xact_id);
1029 if ($close_xact_fail) {
1030 return $close_xact_fail->{evt};
1039 __PACKAGE__->register_method(
1040 method => 'edit_bill_note',
1041 api_name => 'open-ils.circ.money.billing.note.edit',
1043 Edits the note for a bill
1044 @param authtoken Login session key
1045 @param note The replacement note for the bills we're editing
1046 @param billid Id for the bill to edit the note of. This parameter may be repeated to reference other bills.
1047 @return 1 on success, Event on error
1050 sub edit_bill_note {
1051 my( $s, $c, $authtoken, $note, @billids ) = @_;
1053 my $e = new_editor( authtoken => $authtoken, xact => 1 );
1054 return $e->die_event unless $e->checkauth;
1055 return $e->die_event unless $e->allowed('UPDATE_BILL_NOTE');
1057 for my $billid (@billids) {
1059 my $bill = $e->retrieve_money_billing($billid)
1060 or return $e->die_event;
1063 # 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.
1065 $e->update_money_billing($bill) or return $e->die_event;
1072 __PACKAGE__->register_method(
1073 method => 'edit_payment_note',
1074 api_name => 'open-ils.circ.money.payment.note.edit',
1076 Edits the note for a payment
1077 @param authtoken Login session key
1078 @param note The replacement note for the payments we're editing
1079 @param paymentid Id for the payment to edit the note of. This parameter may be repeated to reference other payments.
1080 @return 1 on success, Event on error
1083 sub edit_payment_note {
1084 my( $s, $c, $authtoken, $note, @paymentids ) = @_;
1086 my $e = new_editor( authtoken => $authtoken, xact => 1 );
1087 return $e->die_event unless $e->checkauth;
1088 return $e->die_event unless $e->allowed('UPDATE_PAYMENT_NOTE');
1090 for my $paymentid (@paymentids) {
1092 my $payment = $e->retrieve_money_payment($paymentid)
1093 or return $e->die_event;
1095 $payment->note($note);
1096 # 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.
1098 $e->update_money_payment($payment) or return $e->die_event;
1106 __PACKAGE__->register_method (
1107 method => 'fetch_mbts',
1109 api_name => 'open-ils.circ.money.billable_xact_summary.retrieve'
1112 my( $self, $conn, $auth, $id) = @_;
1114 my $e = new_editor(xact => 1, authtoken=>$auth);
1115 return $e->event unless $e->checkauth;
1116 my ($mbts) = $U->fetch_mbts($id, $e);
1118 my $user = $e->retrieve_actor_user($mbts->usr)
1119 or return $e->die_event;
1121 return $e->die_event unless $e->allowed('VIEW_TRANSACTION', $user->home_ou);
1127 __PACKAGE__->register_method(
1128 method => 'desk_payments',
1129 api_name => 'open-ils.circ.money.org_unit.desk_payments'
1132 my( $self, $conn, $auth, $org, $start_date, $end_date ) = @_;
1133 my $e = new_editor(authtoken=>$auth);
1134 return $e->event unless $e->checkauth;
1135 return $e->event unless $e->allowed('VIEW_TRANSACTION', $org);
1136 my $data = $U->storagereq(
1137 'open-ils.storage.money.org_unit.desk_payments.atomic',
1138 $org, $start_date, $end_date );
1140 $_->workstation( $_->workstation->name ) for(@$data);
1145 __PACKAGE__->register_method(
1146 method => 'user_payments',
1147 api_name => 'open-ils.circ.money.org_unit.user_payments'
1151 my( $self, $conn, $auth, $org, $start_date, $end_date ) = @_;
1152 my $e = new_editor(authtoken=>$auth);
1153 return $e->event unless $e->checkauth;
1154 return $e->event unless $e->allowed('VIEW_TRANSACTION', $org);
1155 my $data = $U->storagereq(
1156 'open-ils.storage.money.org_unit.user_payments.atomic',
1157 $org, $start_date, $end_date );
1160 $e->retrieve_actor_card($_->usr->card)->barcode);
1162 $e->retrieve_actor_org_unit($_->usr->home_ou)->shortname);
1168 __PACKAGE__->register_method(
1169 method => 'retrieve_credit_payable_balance',
1170 api_name => 'open-ils.circ.credit.payable_balance.retrieve',
1173 desc => q/Returns the total amount the patron can pay via credit card/,
1175 { desc => 'Authentication token', type => 'string' },
1176 { desc => 'User id', type => 'number' }
1178 return => { desc => 'The ID of the new provider' }
1182 sub retrieve_credit_payable_balance {
1183 my ( $self, $conn, $auth, $user_id ) = @_;
1184 my $e = new_editor(authtoken => $auth);
1185 return $e->event unless $e->checkauth;
1187 my $user = $e->retrieve_actor_user($user_id)
1188 or return $e->event;
1190 if($e->requestor->id != $user_id) {
1191 return $e->event unless $e->allowed('VIEW_USER_TRANSACTIONS', $user->home_ou)
1194 my $circ_orgs = $e->json_query({
1195 "select" => {circ => ["circ_lib"]},
1197 "where" => {usr => $user_id, xact_finish => undef},
1201 my $groc_orgs = $e->json_query({
1202 "select" => {mg => ["billing_location"]},
1204 "where" => {usr => $user_id, xact_finish => undef},
1209 for my $org ( @$circ_orgs, @$groc_orgs ) {
1210 my $o = $org->{billing_location};
1211 $o = $org->{circ_lib} unless $o;
1212 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.
1213 $hash{$o} = $U->ou_ancestor_setting_value($o, 'credit.payments.allow', $e);
1216 my @credit_orgs = map { $hash{$_} ? ($_) : () } keys %hash;
1217 $logger->debug("credit: relevant orgs that allow credit payments => @credit_orgs");
1219 my $xact_summaries =
1220 OpenILS::Application::AppUtils->simplereq('open-ils.actor',
1221 'open-ils.actor.user.transactions.have_charge', $auth, $user_id);
1225 for my $xact (@$xact_summaries) {
1227 # make two lists and grab them in batch XXX
1228 if ( $xact->xact_type eq 'circulation' ) {
1229 my $circ = $e->retrieve_action_circulation($xact->id) or return $e->event;
1230 next unless grep { $_ == $circ->circ_lib } @credit_orgs;
1232 } elsif ($xact->xact_type eq 'grocery') {
1233 my $bill = $e->retrieve_money_grocery($xact->id) or return $e->event;
1234 next unless grep { $_ == $bill->billing_location } @credit_orgs;
1235 } elsif ($xact->xact_type eq 'reservation') {
1236 my $bill = $e->retrieve_booking_reservation($xact->id) or return $e->event;
1237 next unless grep { $_ == $bill->pickup_lib } @credit_orgs;
1239 $sum += $xact->balance_owed();
1246 __PACKAGE__->register_method(
1247 method => "retrieve_statement",
1249 api_name => "open-ils.circ.money.statement.retrieve",
1250 notes => "Returns an organized summary of a billable transaction, including all bills, payments, adjustments, and voids."
1256 return $parser->parse_datetime(clean_ISO8601($ts))->epoch;
1259 my %_statement_sort = (
1261 'account_adjustment' => 1,
1266 sub retrieve_statement {
1267 my ( $self, $client, $auth, $xact_id ) = @_;
1269 my $e = new_editor(authtoken=>$auth);
1270 return $e->event unless $e->checkauth;
1271 return $e->event unless $e->allowed('VIEW_TRANSACTION');
1273 # XXX: move this lookup login into a DB query?
1276 # collect all payments/adjustments
1277 my $payments = $e->search_money_payment({ xact => $xact_id });
1278 foreach my $payment (@$payments) {
1279 my $type = $payment->payment_type;
1280 $type = 'payment' if $type ne 'account_adjustment';
1281 push(@line_prep, [$type, _to_epoch($payment->payment_ts), $payment->payment_ts, $payment->id, $payment]);
1284 # collect all billings
1285 my $billings = $e->search_money_billing({ xact => $xact_id });
1286 foreach my $billing (@$billings) {
1287 if ($U->is_true($billing->voided)){
1288 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
1290 push(@line_prep, ['billing', _to_epoch($billing->billing_ts), $billing->billing_ts, $billing->id, $billing]);
1293 # order every event by timestamp, then bills/adjustments/voids/payments order, then id
1294 my @ordered_line_prep = sort {
1297 $_statement_sort{$a->[0]} <=> $_statement_sort{$b->[0]}
1302 # let's start building the statement structure
1303 my (@lines, %current_line, $running_balance);
1304 foreach my $event (@ordered_line_prep) {
1305 my $obj = $event->[4];
1306 my $type = $event->[0];
1307 my $ts = $event->[2];
1308 my $billing_type = $type =~ /billing|void/ ? $obj->billing_type : ''; # TODO: get non-legacy billing type
1309 my $note = $obj->note || '';
1310 # last line should be void information, try to isolate it
1311 if ($type eq 'billing' and $obj->voided) {
1313 } elsif ($type eq 'void') {
1314 $note = (split(/\n/, $note))[-1];
1317 # if we have new details, start a new line
1318 if ($current_line{amount} and (
1319 $type ne $current_line{type}
1320 or ($note ne $current_line{note})
1321 or ($billing_type ne $current_line{billing_type})
1324 push(@lines, {%current_line}); # push a copy of the hash, not the real thing
1327 if (!$current_line{type}) {
1328 $current_line{type} = $type;
1329 $current_line{billing_type} = $billing_type;
1330 $current_line{note} = $note;
1332 if (!$current_line{start_date}) {
1333 $current_line{start_date} = $ts;
1334 } elsif ($ts ne $current_line{start_date}) {
1335 $current_line{end_date} = $ts;
1337 $current_line{amount} += $obj->amount;
1338 if ($current_line{details}) {
1339 push(@{$current_line{details}}, $obj);
1341 $current_line{details} = [$obj];
1344 push(@lines, {%current_line}); # push last one on
1346 # get/update totals, format notes
1350 account_adjustment => 0,
1353 foreach my $line (@lines) {
1354 $totals{$line->{type}} += $line->{amount};
1355 if ($line->{type} eq 'billing') {
1356 $running_balance += $line->{amount};
1357 } else { # not a billing; balance goes down for everything else
1358 $running_balance -= $line->{amount};
1360 $line->{running_balance} = $running_balance;
1361 $line->{note} = $line->{note} ? [split(/\n/, $line->{note})] : [];
1365 xact_id => $xact_id,
1367 balance_due => $totals{billing} - ($totals{payment} + $totals{account_adjustment} + $totals{void}),
1368 billing_total => $totals{billing},
1369 credit_total => $totals{payment} + $totals{account_adjustment},
1370 payment_total => $totals{payment},
1371 account_adjustment_total => $totals{account_adjustment},
1372 void_total => $totals{void}