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 my $apputils = "OpenILS::Application::AppUtils";
21 my $U = "OpenILS::Application::AppUtils";
23 use OpenSRF::EX qw(:try);
27 use OpenSRF::Utils::Logger qw/:logger/;
28 use OpenILS::Utils::CStoreEditor qw/:funcs/;
29 use OpenILS::Utils::Penalty;
31 $Data::Dumper::Indent = 0;
33 sub get_processor_settings {
36 my $processor = lc shift;
38 # Get the names of every credit processor setting for our given processor.
39 # They're a little different per processor.
40 my $setting_names = $e->json_query({
41 select => {coust => ["name"]},
42 from => {coust => {}},
43 where => {name => {like => "credit.processor.${processor}.%"}}
44 }) or return $e->die_event;
46 # Make keys for a hash we're going to build out of the last dot-delimited
47 # component of each setting name.
48 ($_->{key} = $_->{name}) =~ s/.+\.(\w+)$/$1/ for @$setting_names;
50 # Return a hash with those short keys, and for values the value of
51 # the corresponding OU setting within our scope.
54 $_->{key} => $U->ou_ancestor_setting_value($org_unit, $_->{name})
59 # process_stripe_or_bop_payment()
60 # This is a helper method to make_payments() below (specifically,
61 # the credit-card part). It's the first point in the Perl code where
62 # we need to care about the distinction between Stripe and the
63 # Paypal/PayflowPro/AuthorizeNet kinds of processors (the latter group
64 # uses B::OP and handles payment card info, whereas Stripe doesn't use
65 # B::OP and doesn't require us to know anything about the payment card
68 # Return an event in all cases. That means a success returns a SUCCESS
70 sub process_stripe_or_bop_payment {
71 my ($e, $user_id, $this_ou, $total_paid, $cc_args) = @_;
73 # A few stanzas to determine which processor we're using and whether we're
74 # really adequately set up for it.
75 if (!$cc_args->{processor}) {
76 if (!($cc_args->{processor} =
77 $U->ou_ancestor_setting_value(
78 $this_ou, 'credit.processor.default'
82 return OpenILS::Event->new('CREDIT_PROCESSOR_NOT_SPECIFIED');
86 # Make sure the configured credit processor has a safe/correct name.
87 return OpenILS::Event->new('CREDIT_PROCESSOR_NOT_ALLOWED')
88 unless $cc_args->{processor} =~ /^[a-z0-9_\-]+$/i;
90 # Get the settings for the processor and make sure they're serviceable.
91 my $psettings = get_processor_settings($e, $this_ou, $cc_args->{processor});
92 return $psettings if defined $U->event_code($psettings);
93 return OpenILS::Event->new('CREDIT_PROCESSOR_NOT_ENABLED')
94 unless $psettings->{enabled};
96 # Now we branch. Stripe is one thing, and everything else is another.
98 if ($cc_args->{processor} eq 'Stripe') { # Stripe
99 my $stripe = Business::Stripe->new(-api_key => $psettings->{secretkey});
100 $stripe->charges_create(
101 amount => int($total_paid * 100.0), # Stripe takes amount in pennies
102 card => $cc_args->{stripe_token},
103 description => $cc_args->{note}
106 if ($stripe->success) {
107 $logger->info("Stripe payment succeeded");
108 return OpenILS::Event->new(
109 "SUCCESS", payload => {
110 map { $_ => $stripe->success->{$_} } qw(
111 invoice customer balance_transaction id created card
116 $logger->info("Stripe payment failed");
117 return OpenILS::Event->new(
118 "CREDIT_PROCESSOR_DECLINED_TRANSACTION",
119 payload => $stripe->error # XXX what happens if this contains
120 # JSON::backportPP::* objects?
124 } else { # B::OP style (Paypal/PayflowPro/AuthorizeNet)
125 return OpenILS::Event->new('BAD_PARAMS', note => 'Need CC number')
126 unless $cc_args->{number};
128 return OpenILS::Application::Circ::CreditCard::process_payment({
129 "desc" => $cc_args->{note},
130 "amount" => $total_paid,
131 "patron_id" => $user_id,
132 "cc" => $cc_args->{number},
133 "expiration" => sprintf(
135 $cc_args->{expire_month},
136 $cc_args->{expire_year}
139 "first_name" => $cc_args->{billing_first},
140 "last_name" => $cc_args->{billing_last},
141 "address" => $cc_args->{billing_address},
142 "city" => $cc_args->{billing_city},
143 "state" => $cc_args->{billing_state},
144 "zip" => $cc_args->{billing_zip},
145 "cvv2" => $cc_args->{cvv2},
152 __PACKAGE__->register_method(
153 method => "make_payments",
154 api_name => "open-ils.circ.money.payment",
156 desc => q/Create payments for a given user and set of transactions,
157 login must have CREATE_PAYMENT privileges.
158 If any payments fail, all are reverted back./,
160 {desc => 'Authtoken', type => 'string'},
161 {desc => q/Arguments Hash, supporting the following params:
168 where_process 1 to use processor, !1 for out-of-band
169 approval_code (for out-of-band payment)
170 type (for out-of-band payment)
171 number (for call to payment processor)
172 stripe_token (for call to Stripe payment processor)
173 expire_month (for call to payment processor)
174 expire_year (for call to payment processor)
175 billing_first (for out-of-band payments and for call to payment processor)
176 billing_last (for out-of-band payments and for call to payment processor)
177 billing_address (for call to payment processor)
178 billing_city (for call to payment processor)
179 billing_state (for call to payment processor)
180 billing_zip (for call to payment processor)
181 note (if payments->{note} is blank, use this)
191 desc => q/Last user transaction ID. This is the actor.usr.last_xact_id value/,
197 q{Array of payment IDs on success, event on failure. Event possibilities include:
199 Bad parameters were given to this API method itself.
202 The last user transaction ID does not match the ID in the database. This means
203 the user object has been updated since the last retrieval. The client should
204 be instructed to reload the user object and related transactions before attempting
206 REFUND_EXCEEDS_BALANCE
207 REFUND_EXCEEDS_DESK_PAYMENTS
208 CREDIT_PROCESSOR_NOT_SPECIFIED
209 Evergreen has not been set up to process CC payments.
210 CREDIT_PROCESSOR_NOT_ALLOWED
211 Evergreen has been incorrectly setup for CC payments.
212 CREDIT_PROCESSOR_NOT_ENABLED
213 Evergreen has been set up for CC payments, but an admin
214 has not explicitly enabled them.
215 CREDIT_PROCESSOR_BAD_PARAMS
216 Evergreen has been incorrectly setup for CC payments;
217 specifically, the login and/or password for the CC
218 processor weren't provided.
219 CREDIT_PROCESSOR_INVALID_CC_NUMBER
220 You have supplied a credit card number that Evergreen
221 has judged to be invalid even before attempting to contact
222 the payment processor.
223 CREDIT_PROCESSOR_DECLINED_TRANSACTION
224 We contacted the CC processor to attempt the charge, but
226 The error_message field of the event payload will
227 contain the payment processor's response. This
228 typically includes a message in plain English intended
229 for human consumption. In PayPal's case, the message
230 is preceded by an integer, a colon, and a space, so
231 a caller might take the 2nd match from /^(\d+: )?(.+)$/
232 to present to the user.
233 The payload also contains other fields from the payment
234 processor, but these are generally not user-friendly
236 CREDIT_PROCESSOR_SUCCESS_WO_RECORD
237 A payment was processed successfully, but couldn't be
238 recorded in Evergreen. This is _bad bad bad_, as it means
239 somebody made a payment but isn't getting credit for it.
240 See errors in the system log if this happens. Info from
241 the credit card transaction will also be available in the
242 event payload, although this probably won't be suitable for
243 staff client/OPAC display.
250 my($self, $client, $auth, $payments, $last_xact_id) = @_;
252 my $e = new_editor(authtoken => $auth, xact => 1);
253 return $e->die_event unless $e->checkauth;
255 my $type = $payments->{payment_type};
256 my $user_id = $payments->{userid};
257 my $credit = $payments->{patron_credit} || 0;
258 my $drawer = $e->requestor->wsid;
259 my $note = $payments->{note};
260 my $cc_args = $payments->{cc_args};
261 my $check_number = $payments->{check_number};
263 my $this_ou = $e->requestor->ws_ou || $e->requestor->home_ou;
267 # unless/until determined by payment processor API
268 my ($approval_code, $cc_processor, $cc_type, $cc_order_number) = (undef,undef,undef, undef);
270 my $patron = $e->retrieve_actor_user($user_id) or return $e->die_event;
272 if($patron->last_xact_id ne $last_xact_id) {
274 return OpenILS::Event->new('INVALID_USER_XACT_ID');
277 # A user is allowed to make credit card payments on his/her own behalf
278 # All other scenarious require permission
279 unless($type eq 'credit_card_payment' and $user_id == $e->requestor->id) {
280 return $e->die_event unless $e->allowed('CREATE_PAYMENT', $patron->home_ou);
283 # first collect the transactions and make sure the transaction
284 # user matches the requested user
287 # We rewrite the payments array for sanity's sake, to avoid more
288 # than one payment per transaction per call, which is not legitimate
289 # but has been seen in the wild coming from the staff client. This
290 # is presumably a staff client (xulrunner) bug.
291 my @unique_xact_payments;
292 for my $pay (@{$payments->{payments}}) {
293 my $xact_id = $pay->[0];
294 if (exists($xacts{$xact_id})) {
296 return OpenILS::Event->new('MULTIPLE_PAYMENTS_FOR_XACT');
299 my $xact = $e->retrieve_money_billable_transaction_summary($xact_id)
300 or return $e->die_event;
302 if($xact->usr != $user_id) {
304 return OpenILS::Event->new('BAD_PARAMS', note => q/user does not match transaction/);
307 $xacts{$xact_id} = $xact;
308 push @unique_xact_payments, $pay;
310 $payments->{payments} = \@unique_xact_payments;
314 for my $pay (@{$payments->{payments}}) {
315 my $transid = $pay->[0];
316 my $amount = $pay->[1];
317 $amount =~ s/\$//og; # just to be safe
318 my $trans = $xacts{$transid};
320 $total_paid += $amount;
322 my $org_id = $U->xact_org($transid, $e);
324 if (!$orgs{$org_id}) {
327 # patron credit has to be allowed at all orgs receiving payment
328 if ($type eq 'credit_payment' and $U->ou_ancestor_setting_value(
329 $org_id, 'circ.disable_patron_credit', $e)) {
331 return OpenILS::Event->new('PATRON_CREDIT_DISABLED');
335 # A negative payment is a refund.
338 # Negative credit card payments are not allowed
339 if($type eq 'credit_card_payment') {
341 return OpenILS::Event->new(
343 note => q/Negative credit card payments not allowed/
347 # If the refund causes the transaction balance to exceed 0 dollars,
348 # we are in effect loaning the patron money. This is not allowed.
349 if( ($trans->balance_owed - $amount) > 0 ) {
351 return OpenILS::Event->new('REFUND_EXCEEDS_BALANCE');
354 # Otherwise, make sure the refund does not exceed desk payments
355 # This is also not allowed
357 my $desk_payments = $e->search_money_desk_payment({xact => $transid, voided => 'f'});
358 $desk_total += $_->amount for @$desk_payments;
360 if( (-$amount) > $desk_total ) {
362 return OpenILS::Event->new(
363 'REFUND_EXCEEDS_DESK_PAYMENTS',
364 payload => { allowed_refund => $desk_total, submitted_refund => -$amount } );
368 my $payobj = "Fieldmapper::money::$type";
369 $payobj = $payobj->new;
371 $payobj->amount($amount);
372 $payobj->amount_collected($amount);
373 $payobj->xact($transid);
374 $payobj->note($note);
375 if ((not $payobj->note) and ($type eq 'credit_card_payment')) {
376 $payobj->note($cc_args->{note});
379 if ($payobj->has_field('accepting_usr')) { $payobj->accepting_usr($e->requestor->id); }
380 if ($payobj->has_field('cash_drawer')) { $payobj->cash_drawer($drawer); }
381 if ($payobj->has_field('cc_type')) { $payobj->cc_type($cc_args->{type}); }
382 if ($payobj->has_field('check_number')) { $payobj->check_number($check_number); }
384 # Store the last 4 digits of the CC number
385 if ($payobj->has_field('cc_number')) {
386 $payobj->cc_number(substr($cc_args->{number}, -4));
388 if ($payobj->has_field('expire_month')) { $payobj->expire_month($cc_args->{expire_month}); $logger->info("LFW XXX expire_month is $cc_args->{expire_month}"); }
389 if ($payobj->has_field('expire_year')) { $payobj->expire_year($cc_args->{expire_year}); }
391 # Note: It is important not to set approval_code
392 # on the fieldmapper object yet.
394 push(@payment_objs, $payobj);
396 } # all payment objects have been created and inserted.
398 #### NO WRITES TO THE DB ABOVE THIS LINE -- THEY'LL ONLY BE DISCARDED ###
401 # After we try to externally process a credit card (if desired), we'll
402 # open a new transaction. We cannot leave one open while credit card
403 # processing might be happening, as it can easily time out the database
408 if($type eq 'credit_card_payment') {
409 $approval_code = $cc_args->{approval_code};
410 # If an approval code was not given, we'll need
411 # to call to the payment processor ourselves.
412 if ($cc_args->{where_process} == 1) {
413 my $response = process_stripe_or_bop_payment(
414 $e, $user_id, $this_ou, $total_paid, $cc_args
417 if ($U->event_code($response)) { # non-success (success is 0)
419 "Credit card payment for user $user_id failed: " .
420 $response->{textcode} . " " .
421 ($response->{payload}->{error_message} ||
422 $response->{payload}{message})
426 # We need to save this for later in case there's a failure on
427 # the EG side to store the processor's result.
429 $cc_payload = $response->{"payload"}; # also used way later
432 no warnings 'uninitialized';
433 $cc_type = $cc_payload->{card_type};
434 $approval_code = $cc_payload->{authorization} ||
436 $cc_processor = $cc_payload->{processor} ||
437 $cc_args->{processor};
438 $cc_order_number = $cc_payload->{order_number} ||
439 $cc_payload->{invoice};
441 $logger->info("Credit card payment for user $user_id succeeded");
444 return OpenILS::Event->new(
445 'BAD_PARAMS', note => 'Need approval code'
446 ) if not $cc_args->{approval_code};
450 ### RE-OPEN TRANSACTION HERE ###
454 # create payment records
455 my $create_money_method = "create_money_" . $type;
456 for my $payment (@payment_objs) {
457 # update the transaction if it's done
458 my $amount = $payment->amount;
459 my $transid = $payment->xact;
460 my $trans = $xacts{$transid};
461 # making payment with existing patron credit.
462 $credit -= $amount if $type eq 'credit_payment';
463 if( (my $cred = ($trans->balance_owed - $amount)) <= 0 ) {
464 # Any overpay on this transaction goes directly into patron
468 my $circ = $e->retrieve_action_circulation($transid);
470 # Whether or not we close the transaction. We definitely
471 # close is no circulation transaction is present,
472 # otherwise we check if the circulation is in a state that
473 # allows itself to be closed.
474 if (!$circ || OpenILS::Application::Circ::CircCommon->can_close_circ($e, $circ)) {
475 $trans = $e->retrieve_money_billable_transaction($transid);
476 $trans->xact_finish("now");
477 if (!$e->update_money_billable_transaction($trans)) {
478 return _recording_failure(
479 $e, "update_money_billable_transaction() failed",
480 $payment, $cc_payload
486 # Urgh, clean up this mega-function one day.
487 if ($cc_processor eq 'Stripe' and $approval_code and $cc_payload) {
488 $payment->expire_month($cc_payload->{card}{exp_month});
489 $payment->expire_year($cc_payload->{card}{exp_year});
490 $payment->cc_number($cc_payload->{card}{last4});
493 $payment->approval_code($approval_code) if $approval_code;
494 $payment->cc_order_number($cc_order_number) if $cc_order_number;
495 $payment->cc_type($cc_type) if $cc_type;
496 $payment->cc_processor($cc_processor) if $cc_processor;
497 $payment->cc_first_name($cc_args->{'billing_first'}) if $cc_args->{'billing_first'};
498 $payment->cc_last_name($cc_args->{'billing_last'}) if $cc_args->{'billing_last'};
499 if (!$e->$create_money_method($payment)) {
500 return _recording_failure(
501 $e, "$create_money_method failed", $payment, $cc_payload
505 push(@payment_ids, $payment->id);
508 my $evt = _update_patron_credit($e, $patron, $credit);
510 return _recording_failure(
511 $e, "_update_patron_credit() failed", undef, $cc_payload
515 for my $org_id (keys %orgs) {
516 # calculate penalties for each of the affected orgs
517 $evt = OpenILS::Utils::Penalty->calculate_penalties(
518 $e, $user_id, $org_id
521 return _recording_failure(
522 $e, "calculate_penalties() failed", undef, $cc_payload
527 # update the user to create a new last_xact_id
528 $e->update_actor_user($patron) or return $e->die_event;
529 $patron = $e->retrieve_actor_user($patron) or return $e->die_event;
532 # update the cached user object if a user is making a payment toward
533 # his/her own account
534 $U->simplereq('open-ils.auth', 'open-ils.auth.session.reset_timeout', $auth, 1)
535 if $user_id == $e->requestor->id;
537 return {last_xact_id => $patron->last_xact_id, payments => \@payment_ids};
540 sub _recording_failure {
541 my ($e, $msg, $payment, $payload) = @_;
543 if ($payload) { # If the payment processor already accepted a payment:
544 $logger->error($msg);
545 $logger->error("Payment processor payload: " . Dumper($payload));
546 # payment shouldn't contain CC number
547 $logger->error("Payment: " . Dumper($payment)) if $payment;
551 return new OpenILS::Event(
552 "CREDIT_PROCESSOR_SUCCESS_WO_RECORD",
553 "payload" => $payload
555 } else { # Otherwise, the problem is somewhat less severe:
557 $logger->warn("Payment: " . Dumper($payment)) if $payment;
558 return $e->die_event;
562 sub _update_patron_credit {
563 my($e, $patron, $credit) = @_;
564 return undef if $credit == 0;
565 $patron->credit_forward_balance($patron->credit_forward_balance + $credit);
566 return OpenILS::Event->new('NEGATIVE_PATRON_BALANCE') if $patron->credit_forward_balance < 0;
567 $e->update_actor_user($patron) or return $e->die_event;
572 __PACKAGE__->register_method(
573 method => "retrieve_payments",
574 api_name => "open-ils.circ.money.payment.retrieve.all_",
575 notes => "Returns a list of payments attached to a given transaction"
577 sub retrieve_payments {
578 my( $self, $client, $login, $transid ) = @_;
581 $apputils->checksesperm($login, 'VIEW_TRANSACTION');
584 # XXX the logic here is wrong.. we need to check the owner of the transaction
585 # to make sure the requestor has access
587 # XXX grab the view, for each object in the view, grab the real object
589 return $apputils->simplereq(
591 'open-ils.cstore.direct.money.payment.search.atomic', { xact => $transid } );
595 __PACKAGE__->register_method(
596 method => "retrieve_payments2",
598 api_name => "open-ils.circ.money.payment.retrieve.all",
599 notes => "Returns a list of payments attached to a given transaction"
602 sub retrieve_payments2 {
603 my( $self, $client, $login, $transid ) = @_;
605 my $e = new_editor(authtoken=>$login);
606 return $e->event unless $e->checkauth;
607 return $e->event unless $e->allowed('VIEW_TRANSACTION');
610 my $pmnts = $e->search_money_payment({ xact => $transid });
612 my $type = $_->payment_type;
613 my $meth = "retrieve_money_$type";
614 my $p = $e->$meth($_->id) or return $e->event;
615 $p->payment_type($type);
616 $p->cash_drawer($e->retrieve_actor_workstation($p->cash_drawer))
617 if $p->has_field('cash_drawer');
618 push( @payments, $p );
624 __PACKAGE__->register_method(
625 method => "format_payment_receipt",
626 api_name => "open-ils.circ.money.payment_receipt.print",
628 desc => 'Returns a printable receipt for the specified payments',
630 { desc => 'Authentication token', type => 'string'},
631 { desc => 'Payment ID or array of payment IDs', type => 'number' },
634 desc => q/An action_trigger.event object or error event./,
639 __PACKAGE__->register_method(
640 method => "format_payment_receipt",
641 api_name => "open-ils.circ.money.payment_receipt.email",
643 desc => 'Emails a receipt for the specified payments to the user associated with the first payment',
645 { desc => 'Authentication token', type => 'string'},
646 { desc => 'Payment ID or array of payment IDs', type => 'number' },
649 desc => q/Undefined on success, otherwise an error event./,
655 sub format_payment_receipt {
656 my($self, $conn, $auth, $mp_id) = @_;
659 if (ref $mp_id ne 'ARRAY') {
660 $mp_ids = [ $mp_id ];
665 my $for_print = ($self->api_name =~ /print/);
666 my $for_email = ($self->api_name =~ /email/);
668 # manually use xact (i.e. authoritative) so we can kill the cstore
669 # connection before sending the action/trigger request. This prevents our cstore
670 # backend from sitting idle while A/T (which uses its own transactions) runs.
671 my $e = new_editor(xact => 1, authtoken => $auth);
672 return $e->die_event unless $e->checkauth;
675 for my $id (@$mp_ids) {
677 my $payment = $e->retrieve_money_payment([
685 ]) or return $e->die_event;
687 return $e->die_event unless
688 $e->requestor->id == $payment->xact->usr->id or
689 $e->allowed('VIEW_TRANSACTION', $payment->xact->usr->home_ou);
691 push @$payments, $payment;
698 return $U->fire_object_event(undef, 'money.format.payment_receipt.print', $payments, $$payments[0]->xact->usr->home_ou);
700 } elsif ($for_email) {
702 for my $p (@$payments) {
703 $U->create_events_for_hook('money.format.payment_receipt.email', $p, $p->xact->usr->home_ou, undef, undef, 1);
710 __PACKAGE__->register_method(
711 method => "create_grocery_bill",
712 api_name => "open-ils.circ.money.grocery.create",
714 Creates a new grocery transaction using the transaction object provided
715 PARAMS: (login_session, money.grocery (mg) object)
718 sub create_grocery_bill {
719 my( $self, $client, $login, $transaction ) = @_;
721 my( $staff, $evt ) = $apputils->checkses($login);
723 $evt = $apputils->check_perms($staff->id,
724 $transaction->billing_location, 'CREATE_TRANSACTION' );
728 $logger->activity("Creating grocery bill " . Dumper($transaction) );
730 $transaction->clear_id;
731 my $session = $apputils->start_db_session;
732 $apputils->set_audit_info($session, $login, $staff->id, $staff->wsid);
733 my $transid = $session->request(
734 'open-ils.storage.direct.money.grocery.create', $transaction)->gather(1);
736 throw OpenSRF::EX ("Error creating new money.grocery") unless defined $transid;
738 $logger->debug("Created new grocery transaction $transid");
740 $apputils->commit_db_session($session);
742 my $e = new_editor(xact=>1);
743 $evt = $U->check_open_xact($e, $transid);
751 __PACKAGE__->register_method(
752 method => 'fetch_reservation',
753 api_name => 'open-ils.circ.booking.reservation.retrieve'
755 sub fetch_reservation {
756 my( $self, $conn, $auth, $id ) = @_;
757 my $e = new_editor(authtoken=>$auth);
758 return $e->event unless $e->checkauth;
759 return $e->event unless $e->allowed('VIEW_TRANSACTION'); # eh.. basically the same permission
760 my $g = $e->retrieve_booking_reservation($id)
765 __PACKAGE__->register_method(
766 method => 'fetch_grocery',
767 api_name => 'open-ils.circ.money.grocery.retrieve'
770 my( $self, $conn, $auth, $id ) = @_;
771 my $e = new_editor(authtoken=>$auth);
772 return $e->event unless $e->checkauth;
773 return $e->event unless $e->allowed('VIEW_TRANSACTION'); # eh.. basically the same permission
774 my $g = $e->retrieve_money_grocery($id)
780 __PACKAGE__->register_method(
781 method => "billing_items",
782 api_name => "open-ils.circ.money.billing.retrieve.all",
785 desc => 'Returns a list of billing items for the given transaction ID. ' .
786 'If the operator is not the owner of the transaction, the VIEW_TRANSACTION permission is required.',
788 { desc => 'Authentication token', type => 'string'},
789 { desc => 'Transaction ID', type => 'number'}
792 desc => 'Transaction object, event on error'
798 my( $self, $client, $login, $transid ) = @_;
800 my( $trans, $evt ) = $U->fetch_billable_xact($transid);
804 ($staff, $evt ) = $apputils->checkses($login);
807 if($staff->id ne $trans->usr) {
808 $evt = $U->check_perms($staff->id, $staff->home_ou, 'VIEW_TRANSACTION');
812 return $apputils->simplereq( 'open-ils.cstore',
813 'open-ils.cstore.direct.money.billing.search.atomic', { xact => $transid } )
817 __PACKAGE__->register_method(
818 method => "billing_items_create",
819 api_name => "open-ils.circ.money.billing.create",
821 Creates a new billing line item
822 PARAMS( login, bill_object (mb) )
825 sub billing_items_create {
826 my( $self, $client, $login, $billing ) = @_;
828 my $e = new_editor(authtoken => $login, xact => 1);
829 return $e->die_event unless $e->checkauth;
830 return $e->die_event unless $e->allowed('CREATE_BILL');
832 my $xact = $e->retrieve_money_billable_transaction($billing->xact)
833 or return $e->die_event;
835 # if the transaction was closed, re-open it
836 if($xact->xact_finish) {
837 $xact->clear_xact_finish;
838 $e->update_money_billable_transaction($xact)
839 or return $e->die_event;
842 my $amt = $billing->amount;
844 $billing->amount($amt);
846 $e->create_money_billing($billing) or return $e->die_event;
847 my $evt = OpenILS::Utils::Penalty->calculate_penalties($e, $xact->usr, $U->xact_org($xact->id,$e));
850 $evt = $U->check_open_xact($e, $xact->id, $xact);
859 __PACKAGE__->register_method(
860 method => 'void_bill',
861 api_name => 'open-ils.circ.money.billing.void',
864 @param authtoken Login session key
865 @param billid Id for the bill to void. This parameter may be repeated to reference other bills.
866 @return 1 on success, Event on error
870 my( $s, $c, $authtoken, @billids ) = @_;
872 my $e = new_editor( authtoken => $authtoken, xact => 1 );
873 return $e->die_event unless $e->checkauth;
874 return $e->die_event unless $e->allowed('VOID_BILLING');
877 for my $billid (@billids) {
879 my $bill = $e->retrieve_money_billing($billid)
880 or return $e->die_event;
882 my $xact = $e->retrieve_money_billable_transaction($bill->xact)
883 or return $e->die_event;
885 if($U->is_true($bill->voided)) {
887 return OpenILS::Event->new('BILL_ALREADY_VOIDED', payload => $bill);
890 my $org = $U->xact_org($bill->xact, $e);
891 $users{$xact->usr} = {} unless $users{$xact->usr};
892 $users{$xact->usr}->{$org} = 1;
895 $bill->voider($e->requestor->id);
896 $bill->void_time('now');
898 $e->update_money_billing($bill) or return $e->die_event;
899 my $evt = $U->check_open_xact($e, $bill->xact, $xact);
903 # calculate penalties for all user/org combinations
904 for my $user_id (keys %users) {
905 for my $org_id (keys %{$users{$user_id}}) {
906 OpenILS::Utils::Penalty->calculate_penalties($e, $user_id, $org_id);
914 __PACKAGE__->register_method(
915 method => 'edit_bill_note',
916 api_name => 'open-ils.circ.money.billing.note.edit',
918 Edits the note for a bill
919 @param authtoken Login session key
920 @param note The replacement note for the bills we're editing
921 @param billid Id for the bill to edit the note of. This parameter may be repeated to reference other bills.
922 @return 1 on success, Event on error
926 my( $s, $c, $authtoken, $note, @billids ) = @_;
928 my $e = new_editor( authtoken => $authtoken, xact => 1 );
929 return $e->die_event unless $e->checkauth;
930 return $e->die_event unless $e->allowed('UPDATE_BILL_NOTE');
932 for my $billid (@billids) {
934 my $bill = $e->retrieve_money_billing($billid)
935 or return $e->die_event;
938 # 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.
940 $e->update_money_billing($bill) or return $e->die_event;
947 __PACKAGE__->register_method(
948 method => 'edit_payment_note',
949 api_name => 'open-ils.circ.money.payment.note.edit',
951 Edits the note for a payment
952 @param authtoken Login session key
953 @param note The replacement note for the payments we're editing
954 @param paymentid Id for the payment to edit the note of. This parameter may be repeated to reference other payments.
955 @return 1 on success, Event on error
958 sub edit_payment_note {
959 my( $s, $c, $authtoken, $note, @paymentids ) = @_;
961 my $e = new_editor( authtoken => $authtoken, xact => 1 );
962 return $e->die_event unless $e->checkauth;
963 return $e->die_event unless $e->allowed('UPDATE_PAYMENT_NOTE');
965 for my $paymentid (@paymentids) {
967 my $payment = $e->retrieve_money_payment($paymentid)
968 or return $e->die_event;
970 $payment->note($note);
971 # 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.
973 $e->update_money_payment($payment) or return $e->die_event;
981 __PACKAGE__->register_method (
982 method => 'fetch_mbts',
984 api_name => 'open-ils.circ.money.billable_xact_summary.retrieve'
987 my( $self, $conn, $auth, $id) = @_;
989 my $e = new_editor(xact => 1, authtoken=>$auth);
990 return $e->event unless $e->checkauth;
991 my ($mbts) = $U->fetch_mbts($id, $e);
993 my $user = $e->retrieve_actor_user($mbts->usr)
994 or return $e->die_event;
996 return $e->die_event unless $e->allowed('VIEW_TRANSACTION', $user->home_ou);
1002 __PACKAGE__->register_method(
1003 method => 'desk_payments',
1004 api_name => 'open-ils.circ.money.org_unit.desk_payments'
1007 my( $self, $conn, $auth, $org, $start_date, $end_date ) = @_;
1008 my $e = new_editor(authtoken=>$auth);
1009 return $e->event unless $e->checkauth;
1010 return $e->event unless $e->allowed('VIEW_TRANSACTION', $org);
1011 my $data = $U->storagereq(
1012 'open-ils.storage.money.org_unit.desk_payments.atomic',
1013 $org, $start_date, $end_date );
1015 $_->workstation( $_->workstation->name ) for(@$data);
1020 __PACKAGE__->register_method(
1021 method => 'user_payments',
1022 api_name => 'open-ils.circ.money.org_unit.user_payments'
1026 my( $self, $conn, $auth, $org, $start_date, $end_date ) = @_;
1027 my $e = new_editor(authtoken=>$auth);
1028 return $e->event unless $e->checkauth;
1029 return $e->event unless $e->allowed('VIEW_TRANSACTION', $org);
1030 my $data = $U->storagereq(
1031 'open-ils.storage.money.org_unit.user_payments.atomic',
1032 $org, $start_date, $end_date );
1035 $e->retrieve_actor_card($_->usr->card)->barcode);
1037 $e->retrieve_actor_org_unit($_->usr->home_ou)->shortname);
1043 __PACKAGE__->register_method(
1044 method => 'retrieve_credit_payable_balance',
1045 api_name => 'open-ils.circ.credit.payable_balance.retrieve',
1048 desc => q/Returns the total amount the patron can pay via credit card/,
1050 { desc => 'Authentication token', type => 'string' },
1051 { desc => 'User id', type => 'number' }
1053 return => { desc => 'The ID of the new provider' }
1057 sub retrieve_credit_payable_balance {
1058 my ( $self, $conn, $auth, $user_id ) = @_;
1059 my $e = new_editor(authtoken => $auth);
1060 return $e->event unless $e->checkauth;
1062 my $user = $e->retrieve_actor_user($user_id)
1063 or return $e->event;
1065 if($e->requestor->id != $user_id) {
1066 return $e->event unless $e->allowed('VIEW_USER_TRANSACTIONS', $user->home_ou)
1069 my $circ_orgs = $e->json_query({
1070 "select" => {circ => ["circ_lib"]},
1072 "where" => {usr => $user_id, xact_finish => undef},
1076 my $groc_orgs = $e->json_query({
1077 "select" => {mg => ["billing_location"]},
1079 "where" => {usr => $user_id, xact_finish => undef},
1084 for my $org ( @$circ_orgs, @$groc_orgs ) {
1085 my $o = $org->{billing_location};
1086 $o = $org->{circ_lib} unless $o;
1087 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.
1088 $hash{$o} = $U->ou_ancestor_setting_value($o, 'credit.payments.allow', $e);
1091 my @credit_orgs = map { $hash{$_} ? ($_) : () } keys %hash;
1092 $logger->debug("credit: relevant orgs that allow credit payments => @credit_orgs");
1094 my $xact_summaries =
1095 OpenILS::Application::AppUtils->simplereq('open-ils.actor',
1096 'open-ils.actor.user.transactions.have_charge', $auth, $user_id);
1100 for my $xact (@$xact_summaries) {
1102 # make two lists and grab them in batch XXX
1103 if ( $xact->xact_type eq 'circulation' ) {
1104 my $circ = $e->retrieve_action_circulation($xact->id) or return $e->event;
1105 next unless grep { $_ == $circ->circ_lib } @credit_orgs;
1107 } elsif ($xact->xact_type eq 'grocery') {
1108 my $bill = $e->retrieve_money_grocery($xact->id) or return $e->event;
1109 next unless grep { $_ == $bill->billing_location } @credit_orgs;
1110 } elsif ($xact->xact_type eq 'reservation') {
1111 my $bill = $e->retrieve_booking_reservation($xact->id) or return $e->event;
1112 next unless grep { $_ == $bill->pickup_lib } @credit_orgs;
1114 $sum += $xact->balance_owed();