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;
43 sub get_processor_settings {
46 my $processor = lc shift;
48 # Get the names of every credit processor setting for our given processor.
49 # They're a little different per processor.
50 my $setting_names = $e->json_query({
51 select => {coust => ["name"]},
52 from => {coust => {}},
53 where => {name => {like => "credit.processor.${processor}.%"}}
54 }) or return $e->die_event;
56 # Make keys for a hash we're going to build out of the last dot-delimited
57 # component of each setting name.
58 ($_->{key} = $_->{name}) =~ s/.+\.(\w+)$/$1/ for @$setting_names;
60 # Return a hash with those short keys, and for values the value of
61 # the corresponding OU setting within our scope.
64 $_->{key} => $U->ou_ancestor_setting_value($org_unit, $_->{name})
69 # process_stripe_or_bop_payment()
70 # This is a helper method to make_payments() below (specifically,
71 # the credit-card part). It's the first point in the Perl code where
72 # we need to care about the distinction between Stripe and the
73 # Paypal/PayflowPro/AuthorizeNet kinds of processors (the latter group
74 # uses B::OP and handles payment card info, whereas Stripe doesn't use
75 # B::OP and doesn't require us to know anything about the payment card
78 # Return an event in all cases. That means a success returns a SUCCESS
80 sub process_stripe_or_bop_payment {
81 my ($e, $user_id, $this_ou, $total_paid, $cc_args) = @_;
83 # A few stanzas to determine which processor we're using and whether we're
84 # really adequately set up for it.
85 if (!$cc_args->{processor}) {
86 if (!($cc_args->{processor} =
87 $U->ou_ancestor_setting_value(
88 $this_ou, 'credit.processor.default'
92 return OpenILS::Event->new('CREDIT_PROCESSOR_NOT_SPECIFIED');
96 # Make sure the configured credit processor has a safe/correct name.
97 return OpenILS::Event->new('CREDIT_PROCESSOR_NOT_ALLOWED')
98 unless $cc_args->{processor} =~ /^[a-z0-9_\-]+$/i;
100 # Get the settings for the processor and make sure they're serviceable.
101 my $psettings = get_processor_settings($e, $this_ou, $cc_args->{processor});
102 return $psettings if defined $U->event_code($psettings);
103 return OpenILS::Event->new('CREDIT_PROCESSOR_NOT_ENABLED')
104 unless $psettings->{enabled};
106 # Now we branch. Stripe is one thing, and everything else is another.
107 # TODO: rename/refactor these methods, we're layering in Smartpay as well
109 if ($cc_args->{processor} eq 'Stripe') { # Stripe
110 my $stripe = Business::Stripe->new(-api_key => $psettings->{secretkey});
111 $stripe->api('post','payment_intents/' . $cc_args->{stripe_payment_intent});
112 if ($stripe->success) {
113 $logger->debug('Stripe payment intent retrieved');
114 my $intent = $stripe->success;
115 if ($intent->{status} eq 'succeeded') {
116 $logger->info('Stripe payment succeeded');
117 return OpenILS::Event->new(
118 'SUCCESS', payload => {
119 invoice => $intent->{invoice},
120 customer => $intent->{customer},
121 balance_transaction => 'N/A',
123 created => $intent->{created},
128 $logger->info('Stripe payment failed');
129 return OpenILS::Event->new(
130 'CREDIT_PROCESSOR_DECLINED_TRANSACTION',
131 payload => $intent->{last_payment_error}
135 $logger->debug('Stripe payment intent not retrieved');
136 $logger->info('Stripe payment failed');
137 return OpenILS::Event->new(
138 "CREDIT_PROCESSOR_DECLINED_TRANSACTION",
139 payload => $stripe->error # XXX what happens if this contains
140 # JSON::backportPP::* objects?
144 } elsif ($cc_args->{processor} eq 'SmartPAY') { # SmartPAY
145 my $smartpay_secret = $cc_args->{smartpay_secret};
146 my $smartpay_session = $cc_args->{smartpay_session};
147 if ($smartpay_secret =~ /^smartpay/) {
148 my $cache = OpenSRF::Utils::Cache->new('global');
149 my $secret_data = $cache->get_cache( $smartpay_secret );
150 $logger->debug("SmartPAY secret_data: " . Dumper($secret_data));
151 my $sessionA = $secret_data->{session_key};
152 my $sessionB = $smartpay_session;
153 if ($sessionA =~ /([A-Za-z0-9]+)/) {
156 if ($sessionB =~ /([A-Za-z0-9]+)/) {
159 if ($sessionA ne $sessionB) {
160 $logger->info("SmartPAY payment failed: session_key mismatch: <$sessionA> vs <$sessionB>");
161 return OpenILS::Event->new(
162 'CREDIT_PROCESSOR_DECLINED_TRANSACTION',
163 payload => { 'result' => 'session_key mismatch' }
166 if ($cc_args->{smartpay_result} == 1) {
167 $logger->info('SmartPAY payment succeeded');
168 return OpenILS::Event->new(
169 'SUCCESS', payload => {
172 balance_transaction => 'N/A',
179 $logger->info('SmartPAY payment failed: ' . $cc_args->{smartpay_result});
180 return OpenILS::Event->new(
181 'CREDIT_PROCESSOR_DECLINED_TRANSACTION',
182 payload => { 'result' => $cc_args->{Result} }
186 $logger->info('SmartPAY payment failed: secret key malformed');
187 return OpenILS::Event->new(
188 'CREDIT_PROCESSOR_DECLINED_TRANSACTION',
189 payload => { 'result' => 'secret key malformed' }
192 } else { # B::OP style (Paypal/PayflowPro/AuthorizeNet)
193 return OpenILS::Event->new('BAD_PARAMS', note => 'Need CC number')
194 unless $cc_args->{number};
196 return OpenILS::Application::Circ::CreditCard::process_payment({
197 "processor" => $cc_args->{processor},
198 "desc" => $cc_args->{note},
199 "amount" => $total_paid,
200 "patron_id" => $user_id,
201 "cc" => $cc_args->{number},
202 "expiration" => sprintf(
204 $cc_args->{expire_month},
205 $cc_args->{expire_year}
208 "first_name" => $cc_args->{billing_first},
209 "last_name" => $cc_args->{billing_last},
210 "address" => $cc_args->{billing_address},
211 "city" => $cc_args->{billing_city},
212 "state" => $cc_args->{billing_state},
213 "zip" => $cc_args->{billing_zip},
214 "cvv2" => $cc_args->{cvv2},
221 __PACKAGE__->register_method(
222 method => "make_payments",
223 api_name => "open-ils.circ.money.payment",
225 desc => q/Create payments for a given user and set of transactions,
226 login must have CREATE_PAYMENT privileges.
227 If any payments fail, all are reverted back./,
229 {desc => 'Authtoken', type => 'string'},
230 {desc => q/Arguments Hash, supporting the following params:
237 where_process 1 to use processor, !1 for out-of-band
238 approval_code (for out-of-band payment)
239 type (for out-of-band payment)
240 number (for call to payment processor)
241 stripe_token (for call to Stripe payment processor)
242 expire_month (for call to payment processor)
243 expire_year (for call to payment processor)
244 billing_first (for out-of-band payments and for call to payment processor)
245 billing_last (for out-of-band payments and for call to payment processor)
246 billing_address (for call to payment processor)
247 billing_city (for call to payment processor)
248 billing_state (for call to payment processor)
249 billing_zip (for call to payment processor)
250 note (if payments->{note} is blank, use this)
260 desc => q/Last user transaction ID. This is the actor.usr.last_xact_id value/,
266 q{Array of payment IDs on success, event on failure. Event possibilities include:
268 Bad parameters were given to this API method itself.
271 The last user transaction ID does not match the ID in the database. This means
272 the user object has been updated since the last retrieval. The client should
273 be instructed to reload the user object and related transactions before attempting
275 REFUND_EXCEEDS_BALANCE
276 REFUND_EXCEEDS_DESK_PAYMENTS
277 CREDIT_PROCESSOR_NOT_SPECIFIED
278 Evergreen has not been set up to process CC payments.
279 CREDIT_PROCESSOR_NOT_ALLOWED
280 Evergreen has been incorrectly setup for CC payments.
281 CREDIT_PROCESSOR_NOT_ENABLED
282 Evergreen has been set up for CC payments, but an admin
283 has not explicitly enabled them.
284 CREDIT_PROCESSOR_BAD_PARAMS
285 Evergreen has been incorrectly setup for CC payments;
286 specifically, the login and/or password for the CC
287 processor weren't provided.
288 CREDIT_PROCESSOR_INVALID_CC_NUMBER
289 You have supplied a credit card number that Evergreen
290 has judged to be invalid even before attempting to contact
291 the payment processor.
292 CREDIT_PROCESSOR_DECLINED_TRANSACTION
293 We contacted the CC processor to attempt the charge, but
295 The error_message field of the event payload will
296 contain the payment processor's response. This
297 typically includes a message in plain English intended
298 for human consumption. In PayPal's case, the message
299 is preceded by an integer, a colon, and a space, so
300 a caller might take the 2nd match from /^(\d+: )?(.+)$/
301 to present to the user.
302 The payload also contains other fields from the payment
303 processor, but these are generally not user-friendly
305 CREDIT_PROCESSOR_SUCCESS_WO_RECORD
306 A payment was processed successfully, but couldn't be
307 recorded in Evergreen. This is _bad bad bad_, as it means
308 somebody made a payment but isn't getting credit for it.
309 See errors in the system log if this happens. Info from
310 the credit card transaction will also be available in the
311 event payload, although this probably won't be suitable for
312 staff client/OPAC display.
319 my($self, $client, $auth, $payments, $last_xact_id) = @_;
321 my $e = new_editor(authtoken => $auth, xact => 1);
322 return $e->die_event unless $e->checkauth;
324 my $type = $payments->{payment_type};
325 my $user_id = $payments->{userid};
326 my $credit = $payments->{patron_credit} || 0;
327 my $drawer = $e->requestor->wsid;
328 my $note = $payments->{note};
329 my $cc_args = $payments->{cc_args};
330 my $check_number = $payments->{check_number};
332 my $this_ou = $e->requestor->ws_ou || $e->requestor->home_ou;
336 # unless/until determined by payment processor API
337 my ($approval_code, $cc_processor, $cc_order_number) = (undef,undef,undef, undef);
339 my $patron = $e->retrieve_actor_user($user_id) or return $e->die_event;
341 if($patron->last_xact_id ne $last_xact_id) {
343 return OpenILS::Event->new('INVALID_USER_XACT_ID');
346 # A user is allowed to make credit card payments on his/her own behalf
347 # All other scenarious require permission
348 unless($type eq 'credit_card_payment' and $user_id == $e->requestor->id) {
349 return $e->die_event unless $e->allowed('CREATE_PAYMENT', $patron->home_ou);
352 # first collect the transactions and make sure the transaction
353 # user matches the requested user
356 # We rewrite the payments array for sanity's sake, to avoid more
357 # than one payment per transaction per call, which is not legitimate
358 # but has been seen in the wild coming from the staff client. This
359 # is presumably a staff client (xulrunner) bug.
360 my @unique_xact_payments;
361 for my $pay (@{$payments->{payments}}) {
362 my $xact_id = $pay->[0];
363 if (exists($xacts{$xact_id})) {
365 return OpenILS::Event->new('MULTIPLE_PAYMENTS_FOR_XACT');
368 my $xact = $e->retrieve_money_billable_transaction_summary($xact_id)
369 or return $e->die_event;
371 if($xact->usr != $user_id) {
373 return OpenILS::Event->new('BAD_PARAMS', note => q/user does not match transaction/);
376 $xacts{$xact_id} = $xact;
377 push @unique_xact_payments, $pay;
379 $payments->{payments} = \@unique_xact_payments;
383 for my $pay (@{$payments->{payments}}) {
384 my $transid = $pay->[0];
385 my $amount = $pay->[1];
386 $amount =~ s/\$//og; # just to be safe
387 my $trans = $xacts{$transid};
389 # add amounts as integers
390 $total_paid += (100 * $amount);
392 my $org_id = $U->xact_org($transid, $e);
394 if (!$orgs{$org_id}) {
397 # patron credit has to be allowed at all orgs receiving payment
398 if ($type eq 'credit_payment' and $U->ou_ancestor_setting_value(
399 $org_id, 'circ.disable_patron_credit', $e)) {
401 return OpenILS::Event->new('PATRON_CREDIT_DISABLED');
405 # A negative payment is a refund.
408 # Negative credit card payments are not allowed
409 if($type eq 'credit_card_payment') {
411 return OpenILS::Event->new(
413 note => q/Negative credit card payments not allowed/
417 # If the refund causes the transaction balance to exceed 0 dollars,
418 # we are in effect loaning the patron money. This is not allowed.
419 if( ($trans->balance_owed - $amount) > 0 ) {
421 return OpenILS::Event->new('REFUND_EXCEEDS_BALANCE');
424 # Otherwise, make sure the refund does not exceed desk payments
425 # This is also not allowed
427 my $desk_payments = $e->search_money_desk_payment({xact => $transid, voided => 'f'});
428 $desk_total += $_->amount for @$desk_payments;
430 if( (-$amount) > $desk_total ) {
432 return OpenILS::Event->new(
433 'REFUND_EXCEEDS_DESK_PAYMENTS',
434 payload => { allowed_refund => $desk_total, submitted_refund => -$amount } );
438 my $payobj = "Fieldmapper::money::$type";
439 $payobj = $payobj->new;
441 $payobj->amount($amount);
442 $payobj->amount_collected($amount);
443 $payobj->xact($transid);
444 $payobj->note($note);
445 if ((not $payobj->note) and ($type eq 'credit_card_payment')) {
446 $payobj->note($cc_args->{note});
449 if ($payobj->has_field('accepting_usr')) { $payobj->accepting_usr($e->requestor->id); }
450 if ($payobj->has_field('cash_drawer')) { $payobj->cash_drawer($drawer); }
451 if ($payobj->has_field('check_number')) { $payobj->check_number($check_number); }
453 # Store the last 4 digits of the CC number
454 if ($payobj->has_field('cc_number')) {
455 $payobj->cc_number(substr($cc_args->{number}, -4));
458 # Note: It is important not to set approval_code
459 # on the fieldmapper object yet.
461 push(@payment_objs, $payobj);
463 } # all payment objects have been created and inserted.
465 # return to decimal format, forcing X.YY format for consistency.
466 $total_paid = sprintf("%.2f", $total_paid / 100);
468 #### NO WRITES TO THE DB ABOVE THIS LINE -- THEY'LL ONLY BE DISCARDED ###
471 # After we try to externally process a credit card (if desired), we'll
472 # open a new transaction. We cannot leave one open while credit card
473 # processing might be happening, as it can easily time out the database
478 if($type eq 'credit_card_payment') {
479 $approval_code = $cc_args->{approval_code};
480 # If an approval code was not given, we'll need
481 # to call to the payment processor ourselves.
482 if ($cc_args->{where_process} == 1) {
483 my $response = process_stripe_or_bop_payment(
484 $e, $user_id, $this_ou, $total_paid, $cc_args
487 if ($U->event_code($response)) { # non-success (success is 0)
489 "Credit card payment for user $user_id failed: " .
490 $response->{textcode} . " " .
491 ($response->{payload}->{error_message} ||
492 $response->{payload}{message})
496 # We need to save this for later in case there's a failure on
497 # the EG side to store the processor's result.
499 $cc_payload = $response->{"payload"}; # also used way later
502 no warnings 'uninitialized';
503 $approval_code = $cc_payload->{authorization} ||
505 $cc_processor = $cc_payload->{processor} ||
506 $cc_args->{processor};
507 $cc_order_number = $cc_payload->{order_number} ||
508 $cc_payload->{invoice};
510 $logger->info("Credit card payment for user $user_id succeeded");
513 return OpenILS::Event->new(
514 'BAD_PARAMS', note => 'Need approval code'
515 ) if not $cc_args->{approval_code};
519 ### RE-OPEN TRANSACTION HERE ###
523 # create payment records
524 my $create_money_method = "create_money_" . $type;
525 for my $payment (@payment_objs) {
526 # update the transaction if it's done
527 my $amount = $payment->amount;
528 my $transid = $payment->xact;
529 my $trans = $xacts{$transid};
530 # making payment with existing patron credit.
531 $credit -= $amount if $type eq 'credit_payment';
532 if( (my $cred = ($trans->balance_owed - $amount)) <= 0 ) {
533 # Any overpay on this transaction goes directly into patron
538 # Attempt to close the transaction.
539 my $close_xact_fail = $CC->maybe_close_xact($e, $transid);
540 if ($close_xact_fail) {
541 return _recording_failure(
542 $e, $close_xact_fail->{message},
543 $payment, $cc_payload
548 # Urgh, clean up this mega-function one day.
549 if ($cc_processor eq 'Stripe' and $approval_code and $cc_payload) {
550 $payment->cc_number($cc_payload->{card}); # not actually available :)
553 $payment->approval_code($approval_code) if $approval_code;
554 $payment->cc_order_number($cc_order_number) if $cc_order_number;
555 $payment->cc_processor($cc_processor) if $cc_processor;
556 if (!$e->$create_money_method($payment)) {
557 return _recording_failure(
558 $e, "$create_money_method failed", $payment, $cc_payload
562 push(@payment_ids, $payment->id);
565 my $evt = _update_patron_credit($e, $patron, $credit);
567 return _recording_failure(
568 $e, "_update_patron_credit() failed", undef, $cc_payload
572 for my $org_id (keys %orgs) {
573 # calculate penalties for each of the affected orgs
574 $evt = OpenILS::Utils::Penalty->calculate_penalties(
575 $e, $user_id, $org_id
578 return _recording_failure(
579 $e, "calculate_penalties() failed", undef, $cc_payload
584 # update the user to create a new last_xact_id
585 $e->update_actor_user($patron) or return $e->die_event;
586 $patron = $e->retrieve_actor_user($patron) or return $e->die_event;
589 # update the cached user object if a user is making a payment toward
590 # his/her own account
591 $U->simplereq('open-ils.auth', 'open-ils.auth.session.reset_timeout', $auth, 1)
592 if $user_id == $e->requestor->id;
594 return {last_xact_id => $patron->last_xact_id, payments => \@payment_ids};
597 sub _recording_failure {
598 my ($e, $msg, $payment, $payload) = @_;
600 if ($payload) { # If the payment processor already accepted a payment:
601 $logger->error($msg);
602 $logger->error("Payment processor payload: " . Dumper($payload));
603 # payment shouldn't contain CC number
604 $logger->error("Payment: " . Dumper($payment)) if $payment;
608 return new OpenILS::Event(
609 "CREDIT_PROCESSOR_SUCCESS_WO_RECORD",
610 "payload" => $payload
612 } else { # Otherwise, the problem is somewhat less severe:
614 $logger->warn("Payment: " . Dumper($payment)) if $payment;
615 return $e->die_event;
619 sub _update_patron_credit {
620 my($e, $patron, $credit) = @_;
621 return undef if $credit == 0;
622 $patron->credit_forward_balance($patron->credit_forward_balance + $credit);
623 return OpenILS::Event->new('NEGATIVE_PATRON_BALANCE') if $patron->credit_forward_balance < 0;
624 $e->update_actor_user($patron) or return $e->die_event;
629 __PACKAGE__->register_method(
630 method => "retrieve_payments",
631 api_name => "open-ils.circ.money.payment.retrieve.all_",
632 notes => "Returns a list of payments attached to a given transaction"
634 sub retrieve_payments {
635 my( $self, $client, $login, $transid ) = @_;
638 $apputils->checksesperm($login, 'VIEW_TRANSACTION');
641 # XXX the logic here is wrong.. we need to check the owner of the transaction
642 # to make sure the requestor has access
644 # XXX grab the view, for each object in the view, grab the real object
646 return $apputils->simplereq(
648 'open-ils.cstore.direct.money.payment.search.atomic', { xact => $transid } );
652 __PACKAGE__->register_method(
653 method => "retrieve_payments2",
655 api_name => "open-ils.circ.money.payment.retrieve.all",
656 notes => "Returns a list of payments attached to a given transaction"
659 sub retrieve_payments2 {
660 my( $self, $client, $login, $transid ) = @_;
662 my $e = new_editor(authtoken=>$login);
663 return $e->event unless $e->checkauth;
664 return $e->event unless $e->allowed('VIEW_TRANSACTION');
667 my $pmnts = $e->search_money_payment({ xact => $transid });
669 my $type = $_->payment_type;
670 my $meth = "retrieve_money_$type";
671 my $p = $e->$meth($_->id) or return $e->event;
672 $p->payment_type($type);
673 $p->cash_drawer($e->retrieve_actor_workstation($p->cash_drawer))
674 if $p->has_field('cash_drawer');
675 push( @payments, $p );
681 __PACKAGE__->register_method(
682 method => "format_payment_receipt",
683 api_name => "open-ils.circ.money.payment_receipt.print",
685 desc => 'Returns a printable receipt for the specified payments',
687 { desc => 'Authentication token', type => 'string'},
688 { desc => 'Payment ID or array of payment IDs', type => 'number' },
691 desc => q/An action_trigger.event object or error event./,
696 __PACKAGE__->register_method(
697 method => "format_payment_receipt",
698 api_name => "open-ils.circ.money.payment_receipt.email",
700 desc => 'Emails a receipt for the specified payments to the user associated with the first payment',
702 { desc => 'Authentication token', type => 'string'},
703 { desc => 'Payment ID or array of payment IDs', type => 'number' },
706 desc => q/Undefined on success, otherwise an error event./,
712 sub format_payment_receipt {
713 my($self, $conn, $auth, $mp_id) = @_;
716 if (ref $mp_id ne 'ARRAY') {
717 $mp_ids = [ $mp_id ];
722 my $for_print = ($self->api_name =~ /print/);
723 my $for_email = ($self->api_name =~ /email/);
725 # manually use xact (i.e. authoritative) so we can kill the cstore
726 # connection before sending the action/trigger request. This prevents our cstore
727 # backend from sitting idle while A/T (which uses its own transactions) runs.
728 my $e = new_editor(xact => 1, authtoken => $auth);
729 return $e->die_event unless $e->checkauth;
732 for my $id (@$mp_ids) {
734 my $payment = $e->retrieve_money_payment([
742 ]) or return $e->die_event;
744 return $e->die_event unless
745 $e->requestor->id == $payment->xact->usr->id or
746 $e->allowed('VIEW_TRANSACTION', $payment->xact->usr->home_ou);
748 push @$payments, $payment;
755 return $U->fire_object_event(undef, 'money.format.payment_receipt.print', $payments, $$payments[0]->xact->usr->home_ou);
757 } elsif ($for_email) {
759 for my $p (@$payments) {
760 $U->create_events_for_hook('money.format.payment_receipt.email', $p, $p->xact->usr->home_ou, undef, undef, 1);
767 __PACKAGE__->register_method(
768 method => "create_grocery_bill",
769 api_name => "open-ils.circ.money.grocery.create",
771 Creates a new grocery transaction using the transaction object provided
772 PARAMS: (login_session, money.grocery (mg) object)
775 sub create_grocery_bill {
776 my( $self, $client, $login, $transaction ) = @_;
778 my( $staff, $evt ) = $apputils->checkses($login);
780 $evt = $apputils->check_perms($staff->id,
781 $transaction->billing_location, 'CREATE_TRANSACTION' );
785 $logger->activity("Creating grocery bill " . Dumper($transaction) );
787 $transaction->clear_id;
788 my $session = $apputils->start_db_session;
789 $apputils->set_audit_info($session, $login, $staff->id, $staff->wsid);
790 my $transid = $session->request(
791 'open-ils.storage.direct.money.grocery.create', $transaction)->gather(1);
793 throw OpenSRF::EX ("Error creating new money.grocery") unless defined $transid;
795 $logger->debug("Created new grocery transaction $transid");
797 $apputils->commit_db_session($session);
799 my $e = new_editor(xact=>1);
800 $evt = $U->check_open_xact($e, $transid);
808 __PACKAGE__->register_method(
809 method => 'fetch_reservation',
810 api_name => 'open-ils.circ.booking.reservation.retrieve'
812 sub fetch_reservation {
813 my( $self, $conn, $auth, $id ) = @_;
814 my $e = new_editor(authtoken=>$auth);
815 return $e->event unless $e->checkauth;
816 return $e->event unless $e->allowed('VIEW_TRANSACTION'); # eh.. basically the same permission
817 my $g = $e->retrieve_booking_reservation($id)
822 __PACKAGE__->register_method(
823 method => 'fetch_grocery',
824 api_name => 'open-ils.circ.money.grocery.retrieve'
827 my( $self, $conn, $auth, $id ) = @_;
828 my $e = new_editor(authtoken=>$auth);
829 return $e->event unless $e->checkauth;
830 return $e->event unless $e->allowed('VIEW_TRANSACTION'); # eh.. basically the same permission
831 my $g = $e->retrieve_money_grocery($id)
837 __PACKAGE__->register_method(
838 method => "billing_items",
839 api_name => "open-ils.circ.money.billing.retrieve.all",
842 desc => 'Returns a list of billing items for the given transaction ID. ' .
843 'If the operator is not the owner of the transaction, the VIEW_TRANSACTION permission is required.',
845 { desc => 'Authentication token', type => 'string'},
846 { desc => 'Transaction ID', type => 'number'}
849 desc => 'Transaction object, event on error'
855 my( $self, $client, $login, $transid ) = @_;
857 my( $trans, $evt ) = $U->fetch_billable_xact($transid);
861 ($staff, $evt ) = $apputils->checkses($login);
864 if($staff->id ne $trans->usr) {
865 $evt = $U->check_perms($staff->id, $staff->home_ou, 'VIEW_TRANSACTION');
869 return $apputils->simplereq( 'open-ils.cstore',
870 'open-ils.cstore.direct.money.billing.search.atomic', { xact => $transid } )
874 __PACKAGE__->register_method(
875 method => "billing_items_create",
876 api_name => "open-ils.circ.money.billing.create",
878 Creates a new billing line item
879 PARAMS( login, bill_object (mb) )
882 sub billing_items_create {
883 my( $self, $client, $login, $billing ) = @_;
885 my $e = new_editor(authtoken => $login, xact => 1);
886 return $e->die_event unless $e->checkauth;
887 return $e->die_event unless $e->allowed('CREATE_BILL');
889 my $xact = $e->retrieve_money_billable_transaction($billing->xact)
890 or return $e->die_event;
892 # if the transaction was closed, re-open it
893 if($xact->xact_finish) {
894 $xact->clear_xact_finish;
895 $e->update_money_billable_transaction($xact)
896 or return $e->die_event;
899 my $amt = $billing->amount;
901 $billing->amount($amt);
903 $e->create_money_billing($billing) or return $e->die_event;
904 my $evt = OpenILS::Utils::Penalty->calculate_penalties($e, $xact->usr, $U->xact_org($xact->id,$e));
907 $evt = $U->check_open_xact($e, $xact->id, $xact);
916 __PACKAGE__->register_method(
917 method => 'void_bill',
918 api_name => 'open-ils.circ.money.billing.void',
921 @param authtoken Login session key
922 @param billid Id for the bill to void. This parameter may be repeated to reference other bills.
923 @return 1 on success, Event on error
927 my( $s, $c, $authtoken, @billids ) = @_;
928 my $editor = new_editor(authtoken=>$authtoken, xact=>1);
929 return $editor->die_event unless $editor->checkauth;
930 return $editor->die_event unless $editor->allowed('VOID_BILLING');
931 my $rv = $CC->void_bills($editor, \@billids);
932 if (ref($rv) eq 'HASH') {
936 # We should have gotten 1.
943 __PACKAGE__->register_method(
944 method => 'adjust_bills_to_zero_manual',
945 api_name => 'open-ils.circ.money.billable_xact.adjust_to_zero',
948 Given a list of billable transactions, manipulate the
949 transaction using account adjustments to result in a
953 {desc => 'Authtoken', type => 'string'},
954 {desc => 'Array of transaction IDs', type => 'array'}
957 desc => q/Array of IDs for each transaction updated,
966 my $xact_id = $xact->id;
967 # the plan: rebill voided billings until we get a positive balance
969 # step 1: get the voided/adjusted billings
970 my $billings = $e->search_money_billing([
975 order_by => {mb => 'amount desc'},
977 flesh_fields => {mb => ['adjustments']},
980 my @billings = grep { $U->is_true($_->voided) or @{$_->adjustments} } @$billings;
982 my $xact_balance = $xact->balance_owed;
983 $logger->debug("rebilling for xact $xact_id with balance $xact_balance");
985 my $rebill_amount = 0;
987 # step 2: generate new bills just like the old ones
988 for my $billing (@billings) {
990 if ($U->is_true($billing->voided)) {
991 $amount = $billing->amount;
992 } else { # adjusted billing
993 map { $amount = $U->fpsum($amount, $_->amount) } @{$billing->adjustments};
995 my $evt = $CC->create_bill(
999 $billing->billing_type,
1001 "System: MANUAL ADJUSTMENT, BILLING #".$billing->id." REINSTATED\n(PREV: ".$billing->note.")",
1002 $billing->period_start(),
1003 $billing->period_end()
1005 return $evt if $evt;
1006 $rebill_amount += $billing->amount;
1008 # if we have a postive (or zero) balance now, stop
1009 last if ($xact_balance + $rebill_amount >= 0);
1013 sub _is_fully_adjusted {
1017 map { $amount_adj = $U->fpsum($amount_adj, $_->amount) } @{$billing->adjustments};
1019 return $billing->amount == $amount_adj;
1022 sub adjust_bills_to_zero_manual {
1023 my ($self, $client, $auth, $xact_ids) = @_;
1025 my $e = new_editor(xact => 1, authtoken => $auth);
1026 return $e->die_event unless $e->checkauth;
1028 # in case a bare ID is passed
1029 $xact_ids = [$xact_ids] unless ref $xact_ids;
1032 for my $xact_id (@$xact_ids) {
1035 $e->retrieve_money_billable_transaction_summary([
1037 {flesh => 1, flesh_fields => {mbts => ['usr']}}
1038 ]) or return $e->die_event;
1040 if ($xact->balance_owed == 0) {
1041 # zero already, all done
1045 return $e->die_event unless
1046 $e->allowed('ADJUST_BILLS', $xact->usr->home_ou);
1048 if ($xact->balance_owed < 0) {
1049 my $evt = _rebill_xact($e, $xact);
1050 return $evt if $evt;
1051 # refetch xact to get new balance
1053 $e->retrieve_money_billable_transaction_summary([
1055 {flesh => 1, flesh_fields => {mbts => ['usr']}}
1056 ]) or return $e->die_event;
1059 if ($xact->balance_owed > 0) {
1060 # it's positive and needs to be adjusted
1061 # (it either started positive, or we rebilled it positive)
1062 my $billings = $e->search_money_billing([
1067 order_by => {mb => 'amount desc'},
1069 flesh_fields => {mb => ['adjustments']},
1073 my @billings_to_zero = grep { !$U->is_true($_->voided) or !_is_fully_adjusted($_) } @$billings;
1074 $CC->adjust_bills_to_zero($e, \@billings_to_zero, "System: MANUAL ADJUSTMENT");
1077 push(@modified, $xact->id);
1079 # now we see if we can close the transaction
1080 # same logic as make_payments();
1081 my $close_xact_fail = $CC->maybe_close_xact($e, $xact_id);
1082 if ($close_xact_fail) {
1083 return $close_xact_fail->{evt};
1092 __PACKAGE__->register_method(
1093 method => 'edit_bill_note',
1094 api_name => 'open-ils.circ.money.billing.note.edit',
1096 Edits the note for a bill
1097 @param authtoken Login session key
1098 @param note The replacement note for the bills we're editing
1099 @param billid Id for the bill to edit the note of. This parameter may be repeated to reference other bills.
1100 @return 1 on success, Event on error
1103 sub edit_bill_note {
1104 my( $s, $c, $authtoken, $note, @billids ) = @_;
1106 my $e = new_editor( authtoken => $authtoken, xact => 1 );
1107 return $e->die_event unless $e->checkauth;
1108 return $e->die_event unless $e->allowed('UPDATE_BILL_NOTE');
1110 for my $billid (@billids) {
1112 my $bill = $e->retrieve_money_billing($billid)
1113 or return $e->die_event;
1116 # 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.
1118 $e->update_money_billing($bill) or return $e->die_event;
1125 __PACKAGE__->register_method(
1126 method => 'edit_payment_note',
1127 api_name => 'open-ils.circ.money.payment.note.edit',
1129 Edits the note for a payment
1130 @param authtoken Login session key
1131 @param note The replacement note for the payments we're editing
1132 @param paymentid Id for the payment to edit the note of. This parameter may be repeated to reference other payments.
1133 @return 1 on success, Event on error
1136 sub edit_payment_note {
1137 my( $s, $c, $authtoken, $note, @paymentids ) = @_;
1139 my $e = new_editor( authtoken => $authtoken, xact => 1 );
1140 return $e->die_event unless $e->checkauth;
1141 return $e->die_event unless $e->allowed('UPDATE_PAYMENT_NOTE');
1143 for my $paymentid (@paymentids) {
1145 my $payment = $e->retrieve_money_payment($paymentid)
1146 or return $e->die_event;
1148 $payment->note($note);
1149 # 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.
1151 $e->update_money_payment($payment) or return $e->die_event;
1159 __PACKAGE__->register_method (
1160 method => 'fetch_mbts',
1162 api_name => 'open-ils.circ.money.billable_xact_summary.retrieve'
1165 my( $self, $conn, $auth, $id) = @_;
1167 my $e = new_editor(xact => 1, authtoken=>$auth);
1168 return $e->event unless $e->checkauth;
1169 my ($mbts) = $U->fetch_mbts($id, $e);
1171 my $user = $e->retrieve_actor_user($mbts->usr)
1172 or return $e->die_event;
1174 return $e->die_event unless $e->allowed('VIEW_TRANSACTION', $user->home_ou);
1180 __PACKAGE__->register_method(
1181 method => 'desk_payments',
1182 api_name => 'open-ils.circ.money.org_unit.desk_payments'
1185 my( $self, $conn, $auth, $org, $start_date, $end_date ) = @_;
1186 my $e = new_editor(authtoken=>$auth);
1187 return $e->event unless $e->checkauth;
1188 return $e->event unless $e->allowed('VIEW_TRANSACTION', $org);
1189 my $data = $U->storagereq(
1190 'open-ils.storage.money.org_unit.desk_payments.atomic',
1191 $org, $start_date, $end_date );
1193 $_->workstation( $_->workstation->name ) for(@$data);
1198 __PACKAGE__->register_method(
1199 method => 'user_payments',
1200 api_name => 'open-ils.circ.money.org_unit.user_payments'
1204 my( $self, $conn, $auth, $org, $start_date, $end_date ) = @_;
1205 my $e = new_editor(authtoken=>$auth);
1206 return $e->event unless $e->checkauth;
1207 return $e->event unless $e->allowed('VIEW_TRANSACTION', $org);
1208 my $data = $U->storagereq(
1209 'open-ils.storage.money.org_unit.user_payments.atomic',
1210 $org, $start_date, $end_date );
1213 $e->retrieve_actor_card($_->usr->card)->barcode);
1215 $e->retrieve_actor_org_unit($_->usr->home_ou)->shortname);
1221 __PACKAGE__->register_method(
1222 method => 'retrieve_credit_payable_balance',
1223 api_name => 'open-ils.circ.credit.payable_balance.retrieve',
1226 desc => q/Returns the total amount the patron can pay via credit card/,
1228 { desc => 'Authentication token', type => 'string' },
1229 { desc => 'User id', type => 'number' }
1231 return => { desc => 'The ID of the new provider' }
1235 sub retrieve_credit_payable_balance {
1236 my ( $self, $conn, $auth, $user_id ) = @_;
1237 my $e = new_editor(authtoken => $auth);
1238 return $e->event unless $e->checkauth;
1240 my $user = $e->retrieve_actor_user($user_id)
1241 or return $e->event;
1243 if($e->requestor->id != $user_id) {
1244 return $e->event unless $e->allowed('VIEW_USER_TRANSACTIONS', $user->home_ou)
1247 my $circ_orgs = $e->json_query({
1248 "select" => {circ => ["circ_lib"]},
1250 "where" => {usr => $user_id, xact_finish => undef},
1254 my $groc_orgs = $e->json_query({
1255 "select" => {mg => ["billing_location"]},
1257 "where" => {usr => $user_id, xact_finish => undef},
1262 for my $org ( @$circ_orgs, @$groc_orgs ) {
1263 my $o = $org->{billing_location};
1264 $o = $org->{circ_lib} unless $o;
1265 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.
1266 $hash{$o} = $U->ou_ancestor_setting_value($o, 'credit.payments.allow', $e);
1269 my @credit_orgs = map { $hash{$_} ? ($_) : () } keys %hash;
1270 $logger->debug("credit: relevant orgs that allow credit payments => @credit_orgs");
1272 my $xact_summaries =
1273 OpenILS::Application::AppUtils->simplereq('open-ils.actor',
1274 'open-ils.actor.user.transactions.have_charge', $auth, $user_id);
1278 for my $xact (@$xact_summaries) {
1280 # make two lists and grab them in batch XXX
1281 if ( $xact->xact_type eq 'circulation' ) {
1282 my $circ = $e->retrieve_action_circulation($xact->id) or return $e->event;
1283 next unless grep { $_ == $circ->circ_lib } @credit_orgs;
1285 } elsif ($xact->xact_type eq 'grocery') {
1286 my $bill = $e->retrieve_money_grocery($xact->id) or return $e->event;
1287 next unless grep { $_ == $bill->billing_location } @credit_orgs;
1288 } elsif ($xact->xact_type eq 'reservation') {
1289 my $bill = $e->retrieve_booking_reservation($xact->id) or return $e->event;
1290 next unless grep { $_ == $bill->pickup_lib } @credit_orgs;
1292 $sum += $xact->balance_owed();
1299 __PACKAGE__->register_method(
1300 method => "retrieve_statement",
1302 api_name => "open-ils.circ.money.statement.retrieve",
1303 notes => "Returns an organized summary of a billable transaction, including all bills, payments, adjustments, and voids."
1309 return $parser->parse_datetime(clean_ISO8601($ts))->epoch;
1312 my %_statement_sort = (
1314 'account_adjustment' => 1,
1319 sub retrieve_statement {
1320 my ( $self, $client, $auth, $xact_id ) = @_;
1322 my $e = new_editor(authtoken=>$auth);
1323 return $e->event unless $e->checkauth;
1324 return $e->event unless $e->allowed('VIEW_TRANSACTION');
1326 # XXX: move this lookup login into a DB query?
1329 # collect all payments/adjustments
1330 my $payments = $e->search_money_payment({ xact => $xact_id });
1331 foreach my $payment (@$payments) {
1332 my $type = $payment->payment_type;
1333 $type = 'payment' if $type ne 'account_adjustment';
1334 push(@line_prep, [$type, _to_epoch($payment->payment_ts), $payment->payment_ts, $payment->id, $payment]);
1337 # collect all billings
1338 my $billings = $e->search_money_billing({ xact => $xact_id });
1339 foreach my $billing (@$billings) {
1340 if ($U->is_true($billing->voided)){
1341 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
1343 push(@line_prep, ['billing', _to_epoch($billing->billing_ts), $billing->billing_ts, $billing->id, $billing]);
1346 # order every event by timestamp, then bills/adjustments/voids/payments order, then id
1347 my @ordered_line_prep = sort {
1350 $_statement_sort{$a->[0]} <=> $_statement_sort{$b->[0]}
1355 # let's start building the statement structure
1356 my (@lines, %current_line, $running_balance);
1357 foreach my $event (@ordered_line_prep) {
1358 my $obj = $event->[4];
1359 my $type = $event->[0];
1360 my $ts = $event->[2];
1361 my $billing_type = $type =~ /billing|void/ ? $obj->billing_type : ''; # TODO: get non-legacy billing type
1362 my $note = $obj->note || '';
1363 # last line should be void information, try to isolate it
1364 if ($type eq 'billing' and $obj->voided) {
1366 } elsif ($type eq 'void') {
1367 $note = (split(/\n/, $note))[-1];
1370 # if we have new details, start a new line
1371 if ($current_line{amount} and (
1372 $type ne $current_line{type}
1373 or ($note ne $current_line{note})
1374 or ($billing_type ne $current_line{billing_type})
1377 push(@lines, {%current_line}); # push a copy of the hash, not the real thing
1380 if (!$current_line{type}) {
1381 $current_line{type} = $type;
1382 $current_line{billing_type} = $billing_type;
1383 $current_line{note} = $note;
1385 if (!$current_line{start_date}) {
1386 $current_line{start_date} = $ts;
1387 } elsif ($ts ne $current_line{start_date}) {
1388 $current_line{end_date} = $ts;
1390 $current_line{amount} += $obj->amount;
1391 if ($current_line{details}) {
1392 push(@{$current_line{details}}, $obj);
1394 $current_line{details} = [$obj];
1397 push(@lines, {%current_line}); # push last one on
1399 # get/update totals, format notes
1403 account_adjustment => 0,
1406 foreach my $line (@lines) {
1407 $totals{$line->{type}} += $line->{amount};
1408 if ($line->{type} eq 'billing') {
1409 $running_balance += $line->{amount};
1410 } else { # not a billing; balance goes down for everything else
1411 $running_balance -= $line->{amount};
1413 $line->{running_balance} = $running_balance;
1414 $line->{note} = $line->{note} ? [split(/\n/, $line->{note})] : [];
1417 my $xact = $e->retrieve_money_billable_transaction([
1421 mbt => [qw/circulation grocery/],
1422 circ => [qw/target_copy/],
1423 acp => [qw/call_number location status age_protect total_circ_count/],
1424 acn => [qw/record prefix suffix/],
1425 bre => [qw/wide_display_entry/]
1427 select => {bre => ['id']}
1432 my $billing_location;
1434 if ($xact->circulation) {
1435 $billing_location = $xact->circulation->circ_lib;
1436 my $copy = $xact->circulation->target_copy;
1437 if ($copy->call_number->id == -1) {
1438 $title = $copy->dummy_title;
1440 $title_id = $copy->call_number->record->id;
1441 $title = OpenSRF::Utils::JSON->JSON2perl(
1442 $copy->call_number->record->wide_display_entry->title);
1445 $billing_location = $xact->grocery->billing_location;
1446 $title = $xact->grocery->note;
1450 xact_id => $xact_id,
1453 title_id => $title_id,
1454 billing_location => $billing_location,
1456 balance_due => $totals{billing} - ($totals{payment} + $totals{account_adjustment} + $totals{void}),
1457 billing_total => $totals{billing},
1458 credit_total => $totals{payment} + $totals{account_adjustment},
1459 payment_total => $totals{payment},
1460 account_adjustment_total => $totals{account_adjustment},
1461 void_total => $totals{void}