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
484 my $circ = $e->retrieve_action_circulation(
489 flesh_fields => {circ => ['target_copy','billings']}
492 ); # Flesh the copy, so we can monkey with the status if
495 # Whether or not we close the transaction. We definitely
496 # close if no circulation transaction is present,
497 # otherwise we check if the circulation is in a state that
498 # allows itself to be closed.
499 if (!$circ || $CC->can_close_circ($e, $circ)) {
500 $trans = $e->retrieve_money_billable_transaction($transid);
501 $trans->xact_finish("now");
502 if (!$e->update_money_billable_transaction($trans)) {
503 return _recording_failure(
504 $e, "update_money_billable_transaction() failed",
505 $payment, $cc_payload
509 # If we have a circ, we need to check if the copy
510 # status is lost or long overdue. If it is then we
511 # check org_unit_settings for the copy owning library
512 # and adjust and possibly adjust copy status to lost
514 if ($circ && ($circ->stop_fines eq 'LOST' || $circ->stop_fines eq 'LONGOVERDUE')) {
515 # We need the copy to check settings and to possibly
517 my $copy = $circ->target_copy();
518 # Library where we'll check settings.
519 my $check_lib = $copy->circ_lib();
521 # check the copy status
522 if (($copy->status() == OILS_COPY_STATUS_LOST || $copy->status() == OILS_COPY_STATUS_LONG_OVERDUE)
523 && $U->is_true($U->ou_ancestor_setting_value($check_lib, 'circ.use_lost_paid_copy_status', $e))) {
524 $copy->status(OILS_COPY_STATUS_LOST_AND_PAID);
525 if (!$e->update_asset_copy($copy)) {
526 return _recording_failure(
527 $e, "update_asset_copy_failed()",
528 $payment, $cc_payload
536 # Urgh, clean up this mega-function one day.
537 if ($cc_processor eq 'Stripe' and $approval_code and $cc_payload) {
538 $payment->cc_number($cc_payload->{card}); # not actually available :)
541 $payment->approval_code($approval_code) if $approval_code;
542 $payment->cc_order_number($cc_order_number) if $cc_order_number;
543 $payment->cc_processor($cc_processor) if $cc_processor;
544 if (!$e->$create_money_method($payment)) {
545 return _recording_failure(
546 $e, "$create_money_method failed", $payment, $cc_payload
550 push(@payment_ids, $payment->id);
553 my $evt = _update_patron_credit($e, $patron, $credit);
555 return _recording_failure(
556 $e, "_update_patron_credit() failed", undef, $cc_payload
560 for my $org_id (keys %orgs) {
561 # calculate penalties for each of the affected orgs
562 $evt = OpenILS::Utils::Penalty->calculate_penalties(
563 $e, $user_id, $org_id
566 return _recording_failure(
567 $e, "calculate_penalties() failed", undef, $cc_payload
572 # update the user to create a new last_xact_id
573 $e->update_actor_user($patron) or return $e->die_event;
574 $patron = $e->retrieve_actor_user($patron) or return $e->die_event;
577 # update the cached user object if a user is making a payment toward
578 # his/her own account
579 $U->simplereq('open-ils.auth', 'open-ils.auth.session.reset_timeout', $auth, 1)
580 if $user_id == $e->requestor->id;
582 return {last_xact_id => $patron->last_xact_id, payments => \@payment_ids};
585 sub _recording_failure {
586 my ($e, $msg, $payment, $payload) = @_;
588 if ($payload) { # If the payment processor already accepted a payment:
589 $logger->error($msg);
590 $logger->error("Payment processor payload: " . Dumper($payload));
591 # payment shouldn't contain CC number
592 $logger->error("Payment: " . Dumper($payment)) if $payment;
596 return new OpenILS::Event(
597 "CREDIT_PROCESSOR_SUCCESS_WO_RECORD",
598 "payload" => $payload
600 } else { # Otherwise, the problem is somewhat less severe:
602 $logger->warn("Payment: " . Dumper($payment)) if $payment;
603 return $e->die_event;
607 sub _update_patron_credit {
608 my($e, $patron, $credit) = @_;
609 return undef if $credit == 0;
610 $patron->credit_forward_balance($patron->credit_forward_balance + $credit);
611 return OpenILS::Event->new('NEGATIVE_PATRON_BALANCE') if $patron->credit_forward_balance < 0;
612 $e->update_actor_user($patron) or return $e->die_event;
617 __PACKAGE__->register_method(
618 method => "retrieve_payments",
619 api_name => "open-ils.circ.money.payment.retrieve.all_",
620 notes => "Returns a list of payments attached to a given transaction"
622 sub retrieve_payments {
623 my( $self, $client, $login, $transid ) = @_;
626 $apputils->checksesperm($login, 'VIEW_TRANSACTION');
629 # XXX the logic here is wrong.. we need to check the owner of the transaction
630 # to make sure the requestor has access
632 # XXX grab the view, for each object in the view, grab the real object
634 return $apputils->simplereq(
636 'open-ils.cstore.direct.money.payment.search.atomic', { xact => $transid } );
640 __PACKAGE__->register_method(
641 method => "retrieve_payments2",
643 api_name => "open-ils.circ.money.payment.retrieve.all",
644 notes => "Returns a list of payments attached to a given transaction"
647 sub retrieve_payments2 {
648 my( $self, $client, $login, $transid ) = @_;
650 my $e = new_editor(authtoken=>$login);
651 return $e->event unless $e->checkauth;
652 return $e->event unless $e->allowed('VIEW_TRANSACTION');
655 my $pmnts = $e->search_money_payment({ xact => $transid });
657 my $type = $_->payment_type;
658 my $meth = "retrieve_money_$type";
659 my $p = $e->$meth($_->id) or return $e->event;
660 $p->payment_type($type);
661 $p->cash_drawer($e->retrieve_actor_workstation($p->cash_drawer))
662 if $p->has_field('cash_drawer');
663 push( @payments, $p );
669 __PACKAGE__->register_method(
670 method => "format_payment_receipt",
671 api_name => "open-ils.circ.money.payment_receipt.print",
673 desc => 'Returns a printable receipt for the specified payments',
675 { desc => 'Authentication token', type => 'string'},
676 { desc => 'Payment ID or array of payment IDs', type => 'number' },
679 desc => q/An action_trigger.event object or error event./,
684 __PACKAGE__->register_method(
685 method => "format_payment_receipt",
686 api_name => "open-ils.circ.money.payment_receipt.email",
688 desc => 'Emails a receipt for the specified payments to the user associated with the first payment',
690 { desc => 'Authentication token', type => 'string'},
691 { desc => 'Payment ID or array of payment IDs', type => 'number' },
694 desc => q/Undefined on success, otherwise an error event./,
700 sub format_payment_receipt {
701 my($self, $conn, $auth, $mp_id) = @_;
704 if (ref $mp_id ne 'ARRAY') {
705 $mp_ids = [ $mp_id ];
710 my $for_print = ($self->api_name =~ /print/);
711 my $for_email = ($self->api_name =~ /email/);
713 # manually use xact (i.e. authoritative) so we can kill the cstore
714 # connection before sending the action/trigger request. This prevents our cstore
715 # backend from sitting idle while A/T (which uses its own transactions) runs.
716 my $e = new_editor(xact => 1, authtoken => $auth);
717 return $e->die_event unless $e->checkauth;
720 for my $id (@$mp_ids) {
722 my $payment = $e->retrieve_money_payment([
730 ]) or return $e->die_event;
732 return $e->die_event unless
733 $e->requestor->id == $payment->xact->usr->id or
734 $e->allowed('VIEW_TRANSACTION', $payment->xact->usr->home_ou);
736 push @$payments, $payment;
743 return $U->fire_object_event(undef, 'money.format.payment_receipt.print', $payments, $$payments[0]->xact->usr->home_ou);
745 } elsif ($for_email) {
747 for my $p (@$payments) {
748 $U->create_events_for_hook('money.format.payment_receipt.email', $p, $p->xact->usr->home_ou, undef, undef, 1);
755 __PACKAGE__->register_method(
756 method => "create_grocery_bill",
757 api_name => "open-ils.circ.money.grocery.create",
759 Creates a new grocery transaction using the transaction object provided
760 PARAMS: (login_session, money.grocery (mg) object)
763 sub create_grocery_bill {
764 my( $self, $client, $login, $transaction ) = @_;
766 my( $staff, $evt ) = $apputils->checkses($login);
768 $evt = $apputils->check_perms($staff->id,
769 $transaction->billing_location, 'CREATE_TRANSACTION' );
773 $logger->activity("Creating grocery bill " . Dumper($transaction) );
775 $transaction->clear_id;
776 my $session = $apputils->start_db_session;
777 $apputils->set_audit_info($session, $login, $staff->id, $staff->wsid);
778 my $transid = $session->request(
779 'open-ils.storage.direct.money.grocery.create', $transaction)->gather(1);
781 throw OpenSRF::EX ("Error creating new money.grocery") unless defined $transid;
783 $logger->debug("Created new grocery transaction $transid");
785 $apputils->commit_db_session($session);
787 my $e = new_editor(xact=>1);
788 $evt = $U->check_open_xact($e, $transid);
796 __PACKAGE__->register_method(
797 method => 'fetch_reservation',
798 api_name => 'open-ils.circ.booking.reservation.retrieve'
800 sub fetch_reservation {
801 my( $self, $conn, $auth, $id ) = @_;
802 my $e = new_editor(authtoken=>$auth);
803 return $e->event unless $e->checkauth;
804 return $e->event unless $e->allowed('VIEW_TRANSACTION'); # eh.. basically the same permission
805 my $g = $e->retrieve_booking_reservation($id)
810 __PACKAGE__->register_method(
811 method => 'fetch_grocery',
812 api_name => 'open-ils.circ.money.grocery.retrieve'
815 my( $self, $conn, $auth, $id ) = @_;
816 my $e = new_editor(authtoken=>$auth);
817 return $e->event unless $e->checkauth;
818 return $e->event unless $e->allowed('VIEW_TRANSACTION'); # eh.. basically the same permission
819 my $g = $e->retrieve_money_grocery($id)
825 __PACKAGE__->register_method(
826 method => "billing_items",
827 api_name => "open-ils.circ.money.billing.retrieve.all",
830 desc => 'Returns a list of billing items for the given transaction ID. ' .
831 'If the operator is not the owner of the transaction, the VIEW_TRANSACTION permission is required.',
833 { desc => 'Authentication token', type => 'string'},
834 { desc => 'Transaction ID', type => 'number'}
837 desc => 'Transaction object, event on error'
843 my( $self, $client, $login, $transid ) = @_;
845 my( $trans, $evt ) = $U->fetch_billable_xact($transid);
849 ($staff, $evt ) = $apputils->checkses($login);
852 if($staff->id ne $trans->usr) {
853 $evt = $U->check_perms($staff->id, $staff->home_ou, 'VIEW_TRANSACTION');
857 return $apputils->simplereq( 'open-ils.cstore',
858 'open-ils.cstore.direct.money.billing.search.atomic', { xact => $transid } )
862 __PACKAGE__->register_method(
863 method => "billing_items_create",
864 api_name => "open-ils.circ.money.billing.create",
866 Creates a new billing line item
867 PARAMS( login, bill_object (mb) )
870 sub billing_items_create {
871 my( $self, $client, $login, $billing ) = @_;
873 my $e = new_editor(authtoken => $login, xact => 1);
874 return $e->die_event unless $e->checkauth;
875 return $e->die_event unless $e->allowed('CREATE_BILL');
877 my $xact = $e->retrieve_money_billable_transaction($billing->xact)
878 or return $e->die_event;
880 # if the transaction was closed, re-open it
881 if($xact->xact_finish) {
882 $xact->clear_xact_finish;
883 $e->update_money_billable_transaction($xact)
884 or return $e->die_event;
887 my $amt = $billing->amount;
889 $billing->amount($amt);
891 $e->create_money_billing($billing) or return $e->die_event;
892 my $evt = OpenILS::Utils::Penalty->calculate_penalties($e, $xact->usr, $U->xact_org($xact->id,$e));
895 $evt = $U->check_open_xact($e, $xact->id, $xact);
904 __PACKAGE__->register_method(
905 method => 'void_bill',
906 api_name => 'open-ils.circ.money.billing.void',
909 @param authtoken Login session key
910 @param billid Id for the bill to void. This parameter may be repeated to reference other bills.
911 @return 1 on success, Event on error
915 my( $s, $c, $authtoken, @billids ) = @_;
916 my $editor = new_editor(authtoken=>$authtoken, xact=>1);
917 return $editor->die_event unless $editor->checkauth;
918 return $editor->die_event unless $editor->allowed('VOID_BILLING');
919 my $rv = $CC->void_bills($editor, \@billids);
920 if (ref($rv) eq 'HASH') {
924 # We should have gotten 1.
931 __PACKAGE__->register_method(
932 method => 'adjust_bills_to_zero_manual',
933 api_name => 'open-ils.circ.money.billable_xact.adjust_to_zero',
936 Given a list of billable transactions, manipulate the
937 transaction using account adjustments to result in a
941 {desc => 'Authtoken', type => 'string'},
942 {desc => 'Array of transaction IDs', type => 'array'}
945 desc => q/Array of IDs for each transaction updated,
954 my $xact_id = $xact->id;
955 # the plan: rebill voided billings until we get a positive balance
957 # step 1: get the voided/adjusted billings
958 my $billings = $e->search_money_billing([
963 order_by => {mb => 'amount desc'},
965 flesh_fields => {mb => ['adjustments']},
968 my @billings = grep { $U->is_true($_->voided) or @{$_->adjustments} } @$billings;
970 my $xact_balance = $xact->balance_owed;
971 $logger->debug("rebilling for xact $xact_id with balance $xact_balance");
973 my $rebill_amount = 0;
975 # step 2: generate new bills just like the old ones
976 for my $billing (@billings) {
978 if ($U->is_true($billing->voided)) {
979 $amount = $billing->amount;
980 } else { # adjusted billing
981 map { $amount = $U->fpsum($amount, $_->amount) } @{$billing->adjustments};
983 my $evt = $CC->create_bill(
987 $billing->billing_type,
989 "System: MANUAL ADJUSTMENT, BILLING #".$billing->id." REINSTATED\n(PREV: ".$billing->note.")",
990 $billing->period_start(),
991 $billing->period_end()
994 $rebill_amount += $billing->amount;
996 # if we have a postive (or zero) balance now, stop
997 last if ($xact_balance + $rebill_amount >= 0);
1001 sub _is_fully_adjusted {
1005 map { $amount_adj = $U->fpsum($amount_adj, $_->amount) } @{$billing->adjustments};
1007 return $billing->amount == $amount_adj;
1010 sub adjust_bills_to_zero_manual {
1011 my ($self, $client, $auth, $xact_ids) = @_;
1013 my $e = new_editor(xact => 1, authtoken => $auth);
1014 return $e->die_event unless $e->checkauth;
1016 # in case a bare ID is passed
1017 $xact_ids = [$xact_ids] unless ref $xact_ids;
1020 for my $xact_id (@$xact_ids) {
1023 $e->retrieve_money_billable_transaction_summary([
1025 {flesh => 1, flesh_fields => {mbts => ['usr']}}
1026 ]) or return $e->die_event;
1028 if ($xact->balance_owed == 0) {
1029 # zero already, all done
1033 return $e->die_event unless
1034 $e->allowed('ADJUST_BILLS', $xact->usr->home_ou);
1036 if ($xact->balance_owed < 0) {
1037 my $evt = _rebill_xact($e, $xact);
1038 return $evt if $evt;
1039 # refetch xact to get new balance
1041 $e->retrieve_money_billable_transaction_summary([
1043 {flesh => 1, flesh_fields => {mbts => ['usr']}}
1044 ]) or return $e->die_event;
1047 if ($xact->balance_owed > 0) {
1048 # it's positive and needs to be adjusted
1049 # (it either started positive, or we rebilled it positive)
1050 my $billings = $e->search_money_billing([
1055 order_by => {mb => 'amount desc'},
1057 flesh_fields => {mb => ['adjustments']},
1061 my @billings_to_zero = grep { !$U->is_true($_->voided) or !_is_fully_adjusted($_) } @$billings;
1062 $CC->adjust_bills_to_zero($e, \@billings_to_zero, "System: MANUAL ADJUSTMENT");
1065 push(@modified, $xact->id);
1067 # now we see if we can close the transaction
1068 # same logic as make_payments();
1069 my $circ = $e->retrieve_action_circulation($xact_id);
1070 if (!$circ or $CC->can_close_circ($e, $circ)) {
1071 # we don't check to see if the xact is already closed. since the
1072 # xact had a negative balance, it should not have been closed, so
1073 # assume 'now' is the correct close time regardless.
1074 my $trans = $e->retrieve_money_billable_transaction($xact_id);
1075 $trans->xact_finish("now");
1076 $e->update_money_billable_transaction($trans) or return $e->die_event;
1085 __PACKAGE__->register_method(
1086 method => 'edit_bill_note',
1087 api_name => 'open-ils.circ.money.billing.note.edit',
1089 Edits the note for a bill
1090 @param authtoken Login session key
1091 @param note The replacement note for the bills we're editing
1092 @param billid Id for the bill to edit the note of. This parameter may be repeated to reference other bills.
1093 @return 1 on success, Event on error
1096 sub edit_bill_note {
1097 my( $s, $c, $authtoken, $note, @billids ) = @_;
1099 my $e = new_editor( authtoken => $authtoken, xact => 1 );
1100 return $e->die_event unless $e->checkauth;
1101 return $e->die_event unless $e->allowed('UPDATE_BILL_NOTE');
1103 for my $billid (@billids) {
1105 my $bill = $e->retrieve_money_billing($billid)
1106 or return $e->die_event;
1109 # 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.
1111 $e->update_money_billing($bill) or return $e->die_event;
1118 __PACKAGE__->register_method(
1119 method => 'edit_payment_note',
1120 api_name => 'open-ils.circ.money.payment.note.edit',
1122 Edits the note for a payment
1123 @param authtoken Login session key
1124 @param note The replacement note for the payments we're editing
1125 @param paymentid Id for the payment to edit the note of. This parameter may be repeated to reference other payments.
1126 @return 1 on success, Event on error
1129 sub edit_payment_note {
1130 my( $s, $c, $authtoken, $note, @paymentids ) = @_;
1132 my $e = new_editor( authtoken => $authtoken, xact => 1 );
1133 return $e->die_event unless $e->checkauth;
1134 return $e->die_event unless $e->allowed('UPDATE_PAYMENT_NOTE');
1136 for my $paymentid (@paymentids) {
1138 my $payment = $e->retrieve_money_payment($paymentid)
1139 or return $e->die_event;
1141 $payment->note($note);
1142 # 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.
1144 $e->update_money_payment($payment) or return $e->die_event;
1152 __PACKAGE__->register_method (
1153 method => 'fetch_mbts',
1155 api_name => 'open-ils.circ.money.billable_xact_summary.retrieve'
1158 my( $self, $conn, $auth, $id) = @_;
1160 my $e = new_editor(xact => 1, authtoken=>$auth);
1161 return $e->event unless $e->checkauth;
1162 my ($mbts) = $U->fetch_mbts($id, $e);
1164 my $user = $e->retrieve_actor_user($mbts->usr)
1165 or return $e->die_event;
1167 return $e->die_event unless $e->allowed('VIEW_TRANSACTION', $user->home_ou);
1173 __PACKAGE__->register_method(
1174 method => 'desk_payments',
1175 api_name => 'open-ils.circ.money.org_unit.desk_payments'
1178 my( $self, $conn, $auth, $org, $start_date, $end_date ) = @_;
1179 my $e = new_editor(authtoken=>$auth);
1180 return $e->event unless $e->checkauth;
1181 return $e->event unless $e->allowed('VIEW_TRANSACTION', $org);
1182 my $data = $U->storagereq(
1183 'open-ils.storage.money.org_unit.desk_payments.atomic',
1184 $org, $start_date, $end_date );
1186 $_->workstation( $_->workstation->name ) for(@$data);
1191 __PACKAGE__->register_method(
1192 method => 'user_payments',
1193 api_name => 'open-ils.circ.money.org_unit.user_payments'
1197 my( $self, $conn, $auth, $org, $start_date, $end_date ) = @_;
1198 my $e = new_editor(authtoken=>$auth);
1199 return $e->event unless $e->checkauth;
1200 return $e->event unless $e->allowed('VIEW_TRANSACTION', $org);
1201 my $data = $U->storagereq(
1202 'open-ils.storage.money.org_unit.user_payments.atomic',
1203 $org, $start_date, $end_date );
1206 $e->retrieve_actor_card($_->usr->card)->barcode);
1208 $e->retrieve_actor_org_unit($_->usr->home_ou)->shortname);
1214 __PACKAGE__->register_method(
1215 method => 'retrieve_credit_payable_balance',
1216 api_name => 'open-ils.circ.credit.payable_balance.retrieve',
1219 desc => q/Returns the total amount the patron can pay via credit card/,
1221 { desc => 'Authentication token', type => 'string' },
1222 { desc => 'User id', type => 'number' }
1224 return => { desc => 'The ID of the new provider' }
1228 sub retrieve_credit_payable_balance {
1229 my ( $self, $conn, $auth, $user_id ) = @_;
1230 my $e = new_editor(authtoken => $auth);
1231 return $e->event unless $e->checkauth;
1233 my $user = $e->retrieve_actor_user($user_id)
1234 or return $e->event;
1236 if($e->requestor->id != $user_id) {
1237 return $e->event unless $e->allowed('VIEW_USER_TRANSACTIONS', $user->home_ou)
1240 my $circ_orgs = $e->json_query({
1241 "select" => {circ => ["circ_lib"]},
1243 "where" => {usr => $user_id, xact_finish => undef},
1247 my $groc_orgs = $e->json_query({
1248 "select" => {mg => ["billing_location"]},
1250 "where" => {usr => $user_id, xact_finish => undef},
1255 for my $org ( @$circ_orgs, @$groc_orgs ) {
1256 my $o = $org->{billing_location};
1257 $o = $org->{circ_lib} unless $o;
1258 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.
1259 $hash{$o} = $U->ou_ancestor_setting_value($o, 'credit.payments.allow', $e);
1262 my @credit_orgs = map { $hash{$_} ? ($_) : () } keys %hash;
1263 $logger->debug("credit: relevant orgs that allow credit payments => @credit_orgs");
1265 my $xact_summaries =
1266 OpenILS::Application::AppUtils->simplereq('open-ils.actor',
1267 'open-ils.actor.user.transactions.have_charge', $auth, $user_id);
1271 for my $xact (@$xact_summaries) {
1273 # make two lists and grab them in batch XXX
1274 if ( $xact->xact_type eq 'circulation' ) {
1275 my $circ = $e->retrieve_action_circulation($xact->id) or return $e->event;
1276 next unless grep { $_ == $circ->circ_lib } @credit_orgs;
1278 } elsif ($xact->xact_type eq 'grocery') {
1279 my $bill = $e->retrieve_money_grocery($xact->id) or return $e->event;
1280 next unless grep { $_ == $bill->billing_location } @credit_orgs;
1281 } elsif ($xact->xact_type eq 'reservation') {
1282 my $bill = $e->retrieve_booking_reservation($xact->id) or return $e->event;
1283 next unless grep { $_ == $bill->pickup_lib } @credit_orgs;
1285 $sum += $xact->balance_owed();
1292 __PACKAGE__->register_method(
1293 method => "retrieve_statement",
1295 api_name => "open-ils.circ.money.statement.retrieve",
1296 notes => "Returns an organized summary of a billable transaction, including all bills, payments, adjustments, and voids."
1302 return $parser->parse_datetime(clean_ISO8601($ts))->epoch;
1305 my %_statement_sort = (
1307 'account_adjustment' => 1,
1312 sub retrieve_statement {
1313 my ( $self, $client, $auth, $xact_id ) = @_;
1315 my $e = new_editor(authtoken=>$auth);
1316 return $e->event unless $e->checkauth;
1317 return $e->event unless $e->allowed('VIEW_TRANSACTION');
1319 # XXX: move this lookup login into a DB query?
1322 # collect all payments/adjustments
1323 my $payments = $e->search_money_payment({ xact => $xact_id });
1324 foreach my $payment (@$payments) {
1325 my $type = $payment->payment_type;
1326 $type = 'payment' if $type ne 'account_adjustment';
1327 push(@line_prep, [$type, _to_epoch($payment->payment_ts), $payment->payment_ts, $payment->id, $payment]);
1330 # collect all billings
1331 my $billings = $e->search_money_billing({ xact => $xact_id });
1332 foreach my $billing (@$billings) {
1333 if ($U->is_true($billing->voided)){
1334 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
1336 push(@line_prep, ['billing', _to_epoch($billing->billing_ts), $billing->billing_ts, $billing->id, $billing]);
1339 # order every event by timestamp, then bills/adjustments/voids/payments order, then id
1340 my @ordered_line_prep = sort {
1343 $_statement_sort{$a->[0]} <=> $_statement_sort{$b->[0]}
1348 # let's start building the statement structure
1349 my (@lines, %current_line, $running_balance);
1350 foreach my $event (@ordered_line_prep) {
1351 my $obj = $event->[4];
1352 my $type = $event->[0];
1353 my $ts = $event->[2];
1354 my $billing_type = $type =~ /billing|void/ ? $obj->billing_type : ''; # TODO: get non-legacy billing type
1355 my $note = $obj->note || '';
1356 # last line should be void information, try to isolate it
1357 if ($type eq 'billing' and $obj->voided) {
1359 } elsif ($type eq 'void') {
1360 $note = (split(/\n/, $note))[-1];
1363 # if we have new details, start a new line
1364 if ($current_line{amount} and (
1365 $type ne $current_line{type}
1366 or ($note ne $current_line{note})
1367 or ($billing_type ne $current_line{billing_type})
1370 push(@lines, {%current_line}); # push a copy of the hash, not the real thing
1373 if (!$current_line{type}) {
1374 $current_line{type} = $type;
1375 $current_line{billing_type} = $billing_type;
1376 $current_line{note} = $note;
1378 if (!$current_line{start_date}) {
1379 $current_line{start_date} = $ts;
1380 } elsif ($ts ne $current_line{start_date}) {
1381 $current_line{end_date} = $ts;
1383 $current_line{amount} += $obj->amount;
1384 if ($current_line{details}) {
1385 push(@{$current_line{details}}, $obj);
1387 $current_line{details} = [$obj];
1390 push(@lines, {%current_line}); # push last one on
1392 # get/update totals, format notes
1396 account_adjustment => 0,
1399 foreach my $line (@lines) {
1400 $totals{$line->{type}} += $line->{amount};
1401 if ($line->{type} eq 'billing') {
1402 $running_balance += $line->{amount};
1403 } else { # not a billing; balance goes down for everything else
1404 $running_balance -= $line->{amount};
1406 $line->{running_balance} = $running_balance;
1407 $line->{note} = $line->{note} ? [split(/\n/, $line->{note})] : [];
1411 xact_id => $xact_id,
1413 balance_due => $totals{billing} - ($totals{payment} + $totals{account_adjustment} + $totals{void}),
1414 billing_total => $totals{billing},
1415 credit_total => $totals{payment} + $totals{account_adjustment},
1416 payment_total => $totals{payment},
1417 account_adjustment_total => $totals{account_adjustment},
1418 void_total => $totals{void}