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 "processor" => $cc_args->{processor},
130 "desc" => $cc_args->{note},
131 "amount" => $total_paid,
132 "patron_id" => $user_id,
133 "cc" => $cc_args->{number},
134 "expiration" => sprintf(
136 $cc_args->{expire_month},
137 $cc_args->{expire_year}
140 "first_name" => $cc_args->{billing_first},
141 "last_name" => $cc_args->{billing_last},
142 "address" => $cc_args->{billing_address},
143 "city" => $cc_args->{billing_city},
144 "state" => $cc_args->{billing_state},
145 "zip" => $cc_args->{billing_zip},
146 "cvv2" => $cc_args->{cvv2},
153 __PACKAGE__->register_method(
154 method => "make_payments",
155 api_name => "open-ils.circ.money.payment",
157 desc => q/Create payments for a given user and set of transactions,
158 login must have CREATE_PAYMENT privileges.
159 If any payments fail, all are reverted back./,
161 {desc => 'Authtoken', type => 'string'},
162 {desc => q/Arguments Hash, supporting the following params:
169 where_process 1 to use processor, !1 for out-of-band
170 approval_code (for out-of-band payment)
171 type (for out-of-band payment)
172 number (for call to payment processor)
173 stripe_token (for call to Stripe payment processor)
174 expire_month (for call to payment processor)
175 expire_year (for call to payment processor)
176 billing_first (for out-of-band payments and for call to payment processor)
177 billing_last (for out-of-band payments and for call to payment processor)
178 billing_address (for call to payment processor)
179 billing_city (for call to payment processor)
180 billing_state (for call to payment processor)
181 billing_zip (for call to payment processor)
182 note (if payments->{note} is blank, use this)
192 desc => q/Last user transaction ID. This is the actor.usr.last_xact_id value/,
198 q{Array of payment IDs on success, event on failure. Event possibilities include:
200 Bad parameters were given to this API method itself.
203 The last user transaction ID does not match the ID in the database. This means
204 the user object has been updated since the last retrieval. The client should
205 be instructed to reload the user object and related transactions before attempting
207 REFUND_EXCEEDS_BALANCE
208 REFUND_EXCEEDS_DESK_PAYMENTS
209 CREDIT_PROCESSOR_NOT_SPECIFIED
210 Evergreen has not been set up to process CC payments.
211 CREDIT_PROCESSOR_NOT_ALLOWED
212 Evergreen has been incorrectly setup for CC payments.
213 CREDIT_PROCESSOR_NOT_ENABLED
214 Evergreen has been set up for CC payments, but an admin
215 has not explicitly enabled them.
216 CREDIT_PROCESSOR_BAD_PARAMS
217 Evergreen has been incorrectly setup for CC payments;
218 specifically, the login and/or password for the CC
219 processor weren't provided.
220 CREDIT_PROCESSOR_INVALID_CC_NUMBER
221 You have supplied a credit card number that Evergreen
222 has judged to be invalid even before attempting to contact
223 the payment processor.
224 CREDIT_PROCESSOR_DECLINED_TRANSACTION
225 We contacted the CC processor to attempt the charge, but
227 The error_message field of the event payload will
228 contain the payment processor's response. This
229 typically includes a message in plain English intended
230 for human consumption. In PayPal's case, the message
231 is preceded by an integer, a colon, and a space, so
232 a caller might take the 2nd match from /^(\d+: )?(.+)$/
233 to present to the user.
234 The payload also contains other fields from the payment
235 processor, but these are generally not user-friendly
237 CREDIT_PROCESSOR_SUCCESS_WO_RECORD
238 A payment was processed successfully, but couldn't be
239 recorded in Evergreen. This is _bad bad bad_, as it means
240 somebody made a payment but isn't getting credit for it.
241 See errors in the system log if this happens. Info from
242 the credit card transaction will also be available in the
243 event payload, although this probably won't be suitable for
244 staff client/OPAC display.
251 my($self, $client, $auth, $payments, $last_xact_id) = @_;
253 my $e = new_editor(authtoken => $auth, xact => 1);
254 return $e->die_event unless $e->checkauth;
256 my $type = $payments->{payment_type};
257 my $user_id = $payments->{userid};
258 my $credit = $payments->{patron_credit} || 0;
259 my $drawer = $e->requestor->wsid;
260 my $note = $payments->{note};
261 my $cc_args = $payments->{cc_args};
262 my $check_number = $payments->{check_number};
264 my $this_ou = $e->requestor->ws_ou || $e->requestor->home_ou;
268 # unless/until determined by payment processor API
269 my ($approval_code, $cc_processor, $cc_type, $cc_order_number) = (undef,undef,undef, undef);
271 my $patron = $e->retrieve_actor_user($user_id) or return $e->die_event;
273 if($patron->last_xact_id ne $last_xact_id) {
275 return OpenILS::Event->new('INVALID_USER_XACT_ID');
278 # A user is allowed to make credit card payments on his/her own behalf
279 # All other scenarious require permission
280 unless($type eq 'credit_card_payment' and $user_id == $e->requestor->id) {
281 return $e->die_event unless $e->allowed('CREATE_PAYMENT', $patron->home_ou);
284 # first collect the transactions and make sure the transaction
285 # user matches the requested user
288 # We rewrite the payments array for sanity's sake, to avoid more
289 # than one payment per transaction per call, which is not legitimate
290 # but has been seen in the wild coming from the staff client. This
291 # is presumably a staff client (xulrunner) bug.
292 my @unique_xact_payments;
293 for my $pay (@{$payments->{payments}}) {
294 my $xact_id = $pay->[0];
295 if (exists($xacts{$xact_id})) {
297 return OpenILS::Event->new('MULTIPLE_PAYMENTS_FOR_XACT');
300 my $xact = $e->retrieve_money_billable_transaction_summary($xact_id)
301 or return $e->die_event;
303 if($xact->usr != $user_id) {
305 return OpenILS::Event->new('BAD_PARAMS', note => q/user does not match transaction/);
308 $xacts{$xact_id} = $xact;
309 push @unique_xact_payments, $pay;
311 $payments->{payments} = \@unique_xact_payments;
315 for my $pay (@{$payments->{payments}}) {
316 my $transid = $pay->[0];
317 my $amount = $pay->[1];
318 $amount =~ s/\$//og; # just to be safe
319 my $trans = $xacts{$transid};
321 $total_paid += $amount;
323 my $org_id = $U->xact_org($transid, $e);
325 if (!$orgs{$org_id}) {
328 # patron credit has to be allowed at all orgs receiving payment
329 if ($type eq 'credit_payment' and $U->ou_ancestor_setting_value(
330 $org_id, 'circ.disable_patron_credit', $e)) {
332 return OpenILS::Event->new('PATRON_CREDIT_DISABLED');
336 # A negative payment is a refund.
339 # Negative credit card payments are not allowed
340 if($type eq 'credit_card_payment') {
342 return OpenILS::Event->new(
344 note => q/Negative credit card payments not allowed/
348 # If the refund causes the transaction balance to exceed 0 dollars,
349 # we are in effect loaning the patron money. This is not allowed.
350 if( ($trans->balance_owed - $amount) > 0 ) {
352 return OpenILS::Event->new('REFUND_EXCEEDS_BALANCE');
355 # Otherwise, make sure the refund does not exceed desk payments
356 # This is also not allowed
358 my $desk_payments = $e->search_money_desk_payment({xact => $transid, voided => 'f'});
359 $desk_total += $_->amount for @$desk_payments;
361 if( (-$amount) > $desk_total ) {
363 return OpenILS::Event->new(
364 'REFUND_EXCEEDS_DESK_PAYMENTS',
365 payload => { allowed_refund => $desk_total, submitted_refund => -$amount } );
369 my $payobj = "Fieldmapper::money::$type";
370 $payobj = $payobj->new;
372 $payobj->amount($amount);
373 $payobj->amount_collected($amount);
374 $payobj->xact($transid);
375 $payobj->note($note);
376 if ((not $payobj->note) and ($type eq 'credit_card_payment')) {
377 $payobj->note($cc_args->{note});
380 if ($payobj->has_field('accepting_usr')) { $payobj->accepting_usr($e->requestor->id); }
381 if ($payobj->has_field('cash_drawer')) { $payobj->cash_drawer($drawer); }
382 if ($payobj->has_field('cc_type')) { $payobj->cc_type($cc_args->{type}); }
383 if ($payobj->has_field('check_number')) { $payobj->check_number($check_number); }
385 # Store the last 4 digits of the CC number
386 if ($payobj->has_field('cc_number')) {
387 $payobj->cc_number(substr($cc_args->{number}, -4));
389 if ($payobj->has_field('expire_month')) { $payobj->expire_month($cc_args->{expire_month}); $logger->info("LFW XXX expire_month is $cc_args->{expire_month}"); }
390 if ($payobj->has_field('expire_year')) { $payobj->expire_year($cc_args->{expire_year}); }
392 # Note: It is important not to set approval_code
393 # on the fieldmapper object yet.
395 push(@payment_objs, $payobj);
397 } # all payment objects have been created and inserted.
399 #### NO WRITES TO THE DB ABOVE THIS LINE -- THEY'LL ONLY BE DISCARDED ###
402 # After we try to externally process a credit card (if desired), we'll
403 # open a new transaction. We cannot leave one open while credit card
404 # processing might be happening, as it can easily time out the database
409 if($type eq 'credit_card_payment') {
410 $approval_code = $cc_args->{approval_code};
411 # If an approval code was not given, we'll need
412 # to call to the payment processor ourselves.
413 if ($cc_args->{where_process} == 1) {
414 my $response = process_stripe_or_bop_payment(
415 $e, $user_id, $this_ou, $total_paid, $cc_args
418 if ($U->event_code($response)) { # non-success (success is 0)
420 "Credit card payment for user $user_id failed: " .
421 $response->{textcode} . " " .
422 ($response->{payload}->{error_message} ||
423 $response->{payload}{message})
427 # We need to save this for later in case there's a failure on
428 # the EG side to store the processor's result.
430 $cc_payload = $response->{"payload"}; # also used way later
433 no warnings 'uninitialized';
434 $cc_type = $cc_payload->{card_type};
435 $approval_code = $cc_payload->{authorization} ||
437 $cc_processor = $cc_payload->{processor} ||
438 $cc_args->{processor};
439 $cc_order_number = $cc_payload->{order_number} ||
440 $cc_payload->{invoice};
442 $logger->info("Credit card payment for user $user_id succeeded");
445 return OpenILS::Event->new(
446 'BAD_PARAMS', note => 'Need approval code'
447 ) if not $cc_args->{approval_code};
451 ### RE-OPEN TRANSACTION HERE ###
455 # create payment records
456 my $create_money_method = "create_money_" . $type;
457 for my $payment (@payment_objs) {
458 # update the transaction if it's done
459 my $amount = $payment->amount;
460 my $transid = $payment->xact;
461 my $trans = $xacts{$transid};
462 # making payment with existing patron credit.
463 $credit -= $amount if $type eq 'credit_payment';
464 if( (my $cred = ($trans->balance_owed - $amount)) <= 0 ) {
465 # Any overpay on this transaction goes directly into patron
469 my $circ = $e->retrieve_action_circulation($transid);
471 # Whether or not we close the transaction. We definitely
472 # close is no circulation transaction is present,
473 # otherwise we check if the circulation is in a state that
474 # allows itself to be closed.
475 if (!$circ || OpenILS::Application::Circ::CircCommon->can_close_circ($e, $circ)) {
476 $trans = $e->retrieve_money_billable_transaction($transid);
477 $trans->xact_finish("now");
478 if (!$e->update_money_billable_transaction($trans)) {
479 return _recording_failure(
480 $e, "update_money_billable_transaction() failed",
481 $payment, $cc_payload
487 # Urgh, clean up this mega-function one day.
488 if ($cc_processor eq 'Stripe' and $approval_code and $cc_payload) {
489 $payment->expire_month($cc_payload->{card}{exp_month});
490 $payment->expire_year($cc_payload->{card}{exp_year});
491 $payment->cc_number($cc_payload->{card}{last4});
494 $payment->approval_code($approval_code) if $approval_code;
495 $payment->cc_order_number($cc_order_number) if $cc_order_number;
496 $payment->cc_type($cc_type) if $cc_type;
497 $payment->cc_processor($cc_processor) if $cc_processor;
498 $payment->cc_first_name($cc_args->{'billing_first'}) if $cc_args->{'billing_first'};
499 $payment->cc_last_name($cc_args->{'billing_last'}) if $cc_args->{'billing_last'};
500 if (!$e->$create_money_method($payment)) {
501 return _recording_failure(
502 $e, "$create_money_method failed", $payment, $cc_payload
506 push(@payment_ids, $payment->id);
509 my $evt = _update_patron_credit($e, $patron, $credit);
511 return _recording_failure(
512 $e, "_update_patron_credit() failed", undef, $cc_payload
516 for my $org_id (keys %orgs) {
517 # calculate penalties for each of the affected orgs
518 $evt = OpenILS::Utils::Penalty->calculate_penalties(
519 $e, $user_id, $org_id
522 return _recording_failure(
523 $e, "calculate_penalties() failed", undef, $cc_payload
528 # update the user to create a new last_xact_id
529 $e->update_actor_user($patron) or return $e->die_event;
530 $patron = $e->retrieve_actor_user($patron) or return $e->die_event;
533 # update the cached user object if a user is making a payment toward
534 # his/her own account
535 $U->simplereq('open-ils.auth', 'open-ils.auth.session.reset_timeout', $auth, 1)
536 if $user_id == $e->requestor->id;
538 return {last_xact_id => $patron->last_xact_id, payments => \@payment_ids};
541 sub _recording_failure {
542 my ($e, $msg, $payment, $payload) = @_;
544 if ($payload) { # If the payment processor already accepted a payment:
545 $logger->error($msg);
546 $logger->error("Payment processor payload: " . Dumper($payload));
547 # payment shouldn't contain CC number
548 $logger->error("Payment: " . Dumper($payment)) if $payment;
552 return new OpenILS::Event(
553 "CREDIT_PROCESSOR_SUCCESS_WO_RECORD",
554 "payload" => $payload
556 } else { # Otherwise, the problem is somewhat less severe:
558 $logger->warn("Payment: " . Dumper($payment)) if $payment;
559 return $e->die_event;
563 sub _update_patron_credit {
564 my($e, $patron, $credit) = @_;
565 return undef if $credit == 0;
566 $patron->credit_forward_balance($patron->credit_forward_balance + $credit);
567 return OpenILS::Event->new('NEGATIVE_PATRON_BALANCE') if $patron->credit_forward_balance < 0;
568 $e->update_actor_user($patron) or return $e->die_event;
573 __PACKAGE__->register_method(
574 method => "retrieve_payments",
575 api_name => "open-ils.circ.money.payment.retrieve.all_",
576 notes => "Returns a list of payments attached to a given transaction"
578 sub retrieve_payments {
579 my( $self, $client, $login, $transid ) = @_;
582 $apputils->checksesperm($login, 'VIEW_TRANSACTION');
585 # XXX the logic here is wrong.. we need to check the owner of the transaction
586 # to make sure the requestor has access
588 # XXX grab the view, for each object in the view, grab the real object
590 return $apputils->simplereq(
592 'open-ils.cstore.direct.money.payment.search.atomic', { xact => $transid } );
596 __PACKAGE__->register_method(
597 method => "retrieve_payments2",
599 api_name => "open-ils.circ.money.payment.retrieve.all",
600 notes => "Returns a list of payments attached to a given transaction"
603 sub retrieve_payments2 {
604 my( $self, $client, $login, $transid ) = @_;
606 my $e = new_editor(authtoken=>$login);
607 return $e->event unless $e->checkauth;
608 return $e->event unless $e->allowed('VIEW_TRANSACTION');
611 my $pmnts = $e->search_money_payment({ xact => $transid });
613 my $type = $_->payment_type;
614 my $meth = "retrieve_money_$type";
615 my $p = $e->$meth($_->id) or return $e->event;
616 $p->payment_type($type);
617 $p->cash_drawer($e->retrieve_actor_workstation($p->cash_drawer))
618 if $p->has_field('cash_drawer');
619 push( @payments, $p );
625 __PACKAGE__->register_method(
626 method => "format_payment_receipt",
627 api_name => "open-ils.circ.money.payment_receipt.print",
629 desc => 'Returns a printable receipt for the specified payments',
631 { desc => 'Authentication token', type => 'string'},
632 { desc => 'Payment ID or array of payment IDs', type => 'number' },
635 desc => q/An action_trigger.event object or error event./,
640 __PACKAGE__->register_method(
641 method => "format_payment_receipt",
642 api_name => "open-ils.circ.money.payment_receipt.email",
644 desc => 'Emails a receipt for the specified payments to the user associated with the first payment',
646 { desc => 'Authentication token', type => 'string'},
647 { desc => 'Payment ID or array of payment IDs', type => 'number' },
650 desc => q/Undefined on success, otherwise an error event./,
656 sub format_payment_receipt {
657 my($self, $conn, $auth, $mp_id) = @_;
660 if (ref $mp_id ne 'ARRAY') {
661 $mp_ids = [ $mp_id ];
666 my $for_print = ($self->api_name =~ /print/);
667 my $for_email = ($self->api_name =~ /email/);
669 # manually use xact (i.e. authoritative) so we can kill the cstore
670 # connection before sending the action/trigger request. This prevents our cstore
671 # backend from sitting idle while A/T (which uses its own transactions) runs.
672 my $e = new_editor(xact => 1, authtoken => $auth);
673 return $e->die_event unless $e->checkauth;
676 for my $id (@$mp_ids) {
678 my $payment = $e->retrieve_money_payment([
686 ]) or return $e->die_event;
688 return $e->die_event unless
689 $e->requestor->id == $payment->xact->usr->id or
690 $e->allowed('VIEW_TRANSACTION', $payment->xact->usr->home_ou);
692 push @$payments, $payment;
699 return $U->fire_object_event(undef, 'money.format.payment_receipt.print', $payments, $$payments[0]->xact->usr->home_ou);
701 } elsif ($for_email) {
703 for my $p (@$payments) {
704 $U->create_events_for_hook('money.format.payment_receipt.email', $p, $p->xact->usr->home_ou, undef, undef, 1);
711 __PACKAGE__->register_method(
712 method => "create_grocery_bill",
713 api_name => "open-ils.circ.money.grocery.create",
715 Creates a new grocery transaction using the transaction object provided
716 PARAMS: (login_session, money.grocery (mg) object)
719 sub create_grocery_bill {
720 my( $self, $client, $login, $transaction ) = @_;
722 my( $staff, $evt ) = $apputils->checkses($login);
724 $evt = $apputils->check_perms($staff->id,
725 $transaction->billing_location, 'CREATE_TRANSACTION' );
729 $logger->activity("Creating grocery bill " . Dumper($transaction) );
731 $transaction->clear_id;
732 my $session = $apputils->start_db_session;
733 $apputils->set_audit_info($session, $login, $staff->id, $staff->wsid);
734 my $transid = $session->request(
735 'open-ils.storage.direct.money.grocery.create', $transaction)->gather(1);
737 throw OpenSRF::EX ("Error creating new money.grocery") unless defined $transid;
739 $logger->debug("Created new grocery transaction $transid");
741 $apputils->commit_db_session($session);
743 my $e = new_editor(xact=>1);
744 $evt = $U->check_open_xact($e, $transid);
752 __PACKAGE__->register_method(
753 method => 'fetch_reservation',
754 api_name => 'open-ils.circ.booking.reservation.retrieve'
756 sub fetch_reservation {
757 my( $self, $conn, $auth, $id ) = @_;
758 my $e = new_editor(authtoken=>$auth);
759 return $e->event unless $e->checkauth;
760 return $e->event unless $e->allowed('VIEW_TRANSACTION'); # eh.. basically the same permission
761 my $g = $e->retrieve_booking_reservation($id)
766 __PACKAGE__->register_method(
767 method => 'fetch_grocery',
768 api_name => 'open-ils.circ.money.grocery.retrieve'
771 my( $self, $conn, $auth, $id ) = @_;
772 my $e = new_editor(authtoken=>$auth);
773 return $e->event unless $e->checkauth;
774 return $e->event unless $e->allowed('VIEW_TRANSACTION'); # eh.. basically the same permission
775 my $g = $e->retrieve_money_grocery($id)
781 __PACKAGE__->register_method(
782 method => "billing_items",
783 api_name => "open-ils.circ.money.billing.retrieve.all",
786 desc => 'Returns a list of billing items for the given transaction ID. ' .
787 'If the operator is not the owner of the transaction, the VIEW_TRANSACTION permission is required.',
789 { desc => 'Authentication token', type => 'string'},
790 { desc => 'Transaction ID', type => 'number'}
793 desc => 'Transaction object, event on error'
799 my( $self, $client, $login, $transid ) = @_;
801 my( $trans, $evt ) = $U->fetch_billable_xact($transid);
805 ($staff, $evt ) = $apputils->checkses($login);
808 if($staff->id ne $trans->usr) {
809 $evt = $U->check_perms($staff->id, $staff->home_ou, 'VIEW_TRANSACTION');
813 return $apputils->simplereq( 'open-ils.cstore',
814 'open-ils.cstore.direct.money.billing.search.atomic', { xact => $transid } )
818 __PACKAGE__->register_method(
819 method => "billing_items_create",
820 api_name => "open-ils.circ.money.billing.create",
822 Creates a new billing line item
823 PARAMS( login, bill_object (mb) )
826 sub billing_items_create {
827 my( $self, $client, $login, $billing ) = @_;
829 my $e = new_editor(authtoken => $login, xact => 1);
830 return $e->die_event unless $e->checkauth;
831 return $e->die_event unless $e->allowed('CREATE_BILL');
833 my $xact = $e->retrieve_money_billable_transaction($billing->xact)
834 or return $e->die_event;
836 # if the transaction was closed, re-open it
837 if($xact->xact_finish) {
838 $xact->clear_xact_finish;
839 $e->update_money_billable_transaction($xact)
840 or return $e->die_event;
843 my $amt = $billing->amount;
845 $billing->amount($amt);
847 $e->create_money_billing($billing) or return $e->die_event;
848 my $evt = OpenILS::Utils::Penalty->calculate_penalties($e, $xact->usr, $U->xact_org($xact->id,$e));
851 $evt = $U->check_open_xact($e, $xact->id, $xact);
860 __PACKAGE__->register_method(
861 method => 'void_bill',
862 api_name => 'open-ils.circ.money.billing.void',
865 @param authtoken Login session key
866 @param billid Id for the bill to void. This parameter may be repeated to reference other bills.
867 @return 1 on success, Event on error
871 my( $s, $c, $authtoken, @billids ) = @_;
873 my $e = new_editor( authtoken => $authtoken, xact => 1 );
874 return $e->die_event unless $e->checkauth;
875 return $e->die_event unless $e->allowed('VOID_BILLING');
878 for my $billid (@billids) {
880 my $bill = $e->retrieve_money_billing($billid)
881 or return $e->die_event;
883 my $xact = $e->retrieve_money_billable_transaction($bill->xact)
884 or return $e->die_event;
886 if($U->is_true($bill->voided)) {
888 return OpenILS::Event->new('BILL_ALREADY_VOIDED', payload => $bill);
891 my $org = $U->xact_org($bill->xact, $e);
892 $users{$xact->usr} = {} unless $users{$xact->usr};
893 $users{$xact->usr}->{$org} = 1;
896 $bill->voider($e->requestor->id);
897 $bill->void_time('now');
899 $e->update_money_billing($bill) or return $e->die_event;
900 my $evt = $U->check_open_xact($e, $bill->xact, $xact);
904 # calculate penalties for all user/org combinations
905 for my $user_id (keys %users) {
906 for my $org_id (keys %{$users{$user_id}}) {
907 OpenILS::Utils::Penalty->calculate_penalties($e, $user_id, $org_id);
915 __PACKAGE__->register_method(
916 method => 'edit_bill_note',
917 api_name => 'open-ils.circ.money.billing.note.edit',
919 Edits the note for a bill
920 @param authtoken Login session key
921 @param note The replacement note for the bills we're editing
922 @param billid Id for the bill to edit the note of. This parameter may be repeated to reference other bills.
923 @return 1 on success, Event on error
927 my( $s, $c, $authtoken, $note, @billids ) = @_;
929 my $e = new_editor( authtoken => $authtoken, xact => 1 );
930 return $e->die_event unless $e->checkauth;
931 return $e->die_event unless $e->allowed('UPDATE_BILL_NOTE');
933 for my $billid (@billids) {
935 my $bill = $e->retrieve_money_billing($billid)
936 or return $e->die_event;
939 # 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.
941 $e->update_money_billing($bill) or return $e->die_event;
948 __PACKAGE__->register_method(
949 method => 'edit_payment_note',
950 api_name => 'open-ils.circ.money.payment.note.edit',
952 Edits the note for a payment
953 @param authtoken Login session key
954 @param note The replacement note for the payments we're editing
955 @param paymentid Id for the payment to edit the note of. This parameter may be repeated to reference other payments.
956 @return 1 on success, Event on error
959 sub edit_payment_note {
960 my( $s, $c, $authtoken, $note, @paymentids ) = @_;
962 my $e = new_editor( authtoken => $authtoken, xact => 1 );
963 return $e->die_event unless $e->checkauth;
964 return $e->die_event unless $e->allowed('UPDATE_PAYMENT_NOTE');
966 for my $paymentid (@paymentids) {
968 my $payment = $e->retrieve_money_payment($paymentid)
969 or return $e->die_event;
971 $payment->note($note);
972 # 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.
974 $e->update_money_payment($payment) or return $e->die_event;
982 __PACKAGE__->register_method (
983 method => 'fetch_mbts',
985 api_name => 'open-ils.circ.money.billable_xact_summary.retrieve'
988 my( $self, $conn, $auth, $id) = @_;
990 my $e = new_editor(xact => 1, authtoken=>$auth);
991 return $e->event unless $e->checkauth;
992 my ($mbts) = $U->fetch_mbts($id, $e);
994 my $user = $e->retrieve_actor_user($mbts->usr)
995 or return $e->die_event;
997 return $e->die_event unless $e->allowed('VIEW_TRANSACTION', $user->home_ou);
1003 __PACKAGE__->register_method(
1004 method => 'desk_payments',
1005 api_name => 'open-ils.circ.money.org_unit.desk_payments'
1008 my( $self, $conn, $auth, $org, $start_date, $end_date ) = @_;
1009 my $e = new_editor(authtoken=>$auth);
1010 return $e->event unless $e->checkauth;
1011 return $e->event unless $e->allowed('VIEW_TRANSACTION', $org);
1012 my $data = $U->storagereq(
1013 'open-ils.storage.money.org_unit.desk_payments.atomic',
1014 $org, $start_date, $end_date );
1016 $_->workstation( $_->workstation->name ) for(@$data);
1021 __PACKAGE__->register_method(
1022 method => 'user_payments',
1023 api_name => 'open-ils.circ.money.org_unit.user_payments'
1027 my( $self, $conn, $auth, $org, $start_date, $end_date ) = @_;
1028 my $e = new_editor(authtoken=>$auth);
1029 return $e->event unless $e->checkauth;
1030 return $e->event unless $e->allowed('VIEW_TRANSACTION', $org);
1031 my $data = $U->storagereq(
1032 'open-ils.storage.money.org_unit.user_payments.atomic',
1033 $org, $start_date, $end_date );
1036 $e->retrieve_actor_card($_->usr->card)->barcode);
1038 $e->retrieve_actor_org_unit($_->usr->home_ou)->shortname);
1044 __PACKAGE__->register_method(
1045 method => 'retrieve_credit_payable_balance',
1046 api_name => 'open-ils.circ.credit.payable_balance.retrieve',
1049 desc => q/Returns the total amount the patron can pay via credit card/,
1051 { desc => 'Authentication token', type => 'string' },
1052 { desc => 'User id', type => 'number' }
1054 return => { desc => 'The ID of the new provider' }
1058 sub retrieve_credit_payable_balance {
1059 my ( $self, $conn, $auth, $user_id ) = @_;
1060 my $e = new_editor(authtoken => $auth);
1061 return $e->event unless $e->checkauth;
1063 my $user = $e->retrieve_actor_user($user_id)
1064 or return $e->event;
1066 if($e->requestor->id != $user_id) {
1067 return $e->event unless $e->allowed('VIEW_USER_TRANSACTIONS', $user->home_ou)
1070 my $circ_orgs = $e->json_query({
1071 "select" => {circ => ["circ_lib"]},
1073 "where" => {usr => $user_id, xact_finish => undef},
1077 my $groc_orgs = $e->json_query({
1078 "select" => {mg => ["billing_location"]},
1080 "where" => {usr => $user_id, xact_finish => undef},
1085 for my $org ( @$circ_orgs, @$groc_orgs ) {
1086 my $o = $org->{billing_location};
1087 $o = $org->{circ_lib} unless $o;
1088 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.
1089 $hash{$o} = $U->ou_ancestor_setting_value($o, 'credit.payments.allow', $e);
1092 my @credit_orgs = map { $hash{$_} ? ($_) : () } keys %hash;
1093 $logger->debug("credit: relevant orgs that allow credit payments => @credit_orgs");
1095 my $xact_summaries =
1096 OpenILS::Application::AppUtils->simplereq('open-ils.actor',
1097 'open-ils.actor.user.transactions.have_charge', $auth, $user_id);
1101 for my $xact (@$xact_summaries) {
1103 # make two lists and grab them in batch XXX
1104 if ( $xact->xact_type eq 'circulation' ) {
1105 my $circ = $e->retrieve_action_circulation($xact->id) or return $e->event;
1106 next unless grep { $_ == $circ->circ_lib } @credit_orgs;
1108 } elsif ($xact->xact_type eq 'grocery') {
1109 my $bill = $e->retrieve_money_grocery($xact->id) or return $e->event;
1110 next unless grep { $_ == $bill->billing_location } @credit_orgs;
1111 } elsif ($xact->xact_type eq 'reservation') {
1112 my $bill = $e->retrieve_booking_reservation($xact->id) or return $e->event;
1113 next unless grep { $_ == $bill->pickup_lib } @credit_orgs;
1115 $sum += $xact->balance_owed();