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;
32 use OpenILS::Const qw/:const/;
34 sub get_processor_settings {
37 my $processor = lc shift;
39 # Get the names of every credit processor setting for our given processor.
40 # They're a little different per processor.
41 my $setting_names = $e->json_query({
42 select => {coust => ["name"]},
43 from => {coust => {}},
44 where => {name => {like => "credit.processor.${processor}.%"}}
45 }) or return $e->die_event;
47 # Make keys for a hash we're going to build out of the last dot-delimited
48 # component of each setting name.
49 ($_->{key} = $_->{name}) =~ s/.+\.(\w+)$/$1/ for @$setting_names;
51 # Return a hash with those short keys, and for values the value of
52 # the corresponding OU setting within our scope.
55 $_->{key} => $U->ou_ancestor_setting_value($org_unit, $_->{name})
60 # process_stripe_or_bop_payment()
61 # This is a helper method to make_payments() below (specifically,
62 # the credit-card part). It's the first point in the Perl code where
63 # we need to care about the distinction between Stripe and the
64 # Paypal/PayflowPro/AuthorizeNet kinds of processors (the latter group
65 # uses B::OP and handles payment card info, whereas Stripe doesn't use
66 # B::OP and doesn't require us to know anything about the payment card
69 # Return an event in all cases. That means a success returns a SUCCESS
71 sub process_stripe_or_bop_payment {
72 my ($e, $user_id, $this_ou, $total_paid, $cc_args) = @_;
74 # A few stanzas to determine which processor we're using and whether we're
75 # really adequately set up for it.
76 if (!$cc_args->{processor}) {
77 if (!($cc_args->{processor} =
78 $U->ou_ancestor_setting_value(
79 $this_ou, 'credit.processor.default'
83 return OpenILS::Event->new('CREDIT_PROCESSOR_NOT_SPECIFIED');
87 # Make sure the configured credit processor has a safe/correct name.
88 return OpenILS::Event->new('CREDIT_PROCESSOR_NOT_ALLOWED')
89 unless $cc_args->{processor} =~ /^[a-z0-9_\-]+$/i;
91 # Get the settings for the processor and make sure they're serviceable.
92 my $psettings = get_processor_settings($e, $this_ou, $cc_args->{processor});
93 return $psettings if defined $U->event_code($psettings);
94 return OpenILS::Event->new('CREDIT_PROCESSOR_NOT_ENABLED')
95 unless $psettings->{enabled};
97 # Now we branch. Stripe is one thing, and everything else is another.
99 if ($cc_args->{processor} eq 'Stripe') { # Stripe
100 my $stripe = Business::Stripe->new(-api_key => $psettings->{secretkey});
101 $stripe->charges_create(
102 amount => int($total_paid * 100.0), # Stripe takes amount in pennies
103 card => $cc_args->{stripe_token},
104 description => $cc_args->{note}
107 if ($stripe->success) {
108 $logger->info("Stripe payment succeeded");
109 return OpenILS::Event->new(
110 "SUCCESS", payload => {
111 map { $_ => $stripe->success->{$_} } qw(
112 invoice customer balance_transaction id created card
117 $logger->info("Stripe payment failed");
118 return OpenILS::Event->new(
119 "CREDIT_PROCESSOR_DECLINED_TRANSACTION",
120 payload => $stripe->error # XXX what happens if this contains
121 # JSON::backportPP::* objects?
125 } else { # B::OP style (Paypal/PayflowPro/AuthorizeNet)
126 return OpenILS::Event->new('BAD_PARAMS', note => 'Need CC number')
127 unless $cc_args->{number};
129 return OpenILS::Application::Circ::CreditCard::process_payment({
130 "processor" => $cc_args->{processor},
131 "desc" => $cc_args->{note},
132 "amount" => $total_paid,
133 "patron_id" => $user_id,
134 "cc" => $cc_args->{number},
135 "expiration" => sprintf(
137 $cc_args->{expire_month},
138 $cc_args->{expire_year}
141 "first_name" => $cc_args->{billing_first},
142 "last_name" => $cc_args->{billing_last},
143 "address" => $cc_args->{billing_address},
144 "city" => $cc_args->{billing_city},
145 "state" => $cc_args->{billing_state},
146 "zip" => $cc_args->{billing_zip},
147 "cvv2" => $cc_args->{cvv2},
154 __PACKAGE__->register_method(
155 method => "make_payments",
156 api_name => "open-ils.circ.money.payment",
158 desc => q/Create payments for a given user and set of transactions,
159 login must have CREATE_PAYMENT privileges.
160 If any payments fail, all are reverted back./,
162 {desc => 'Authtoken', type => 'string'},
163 {desc => q/Arguments Hash, supporting the following params:
170 where_process 1 to use processor, !1 for out-of-band
171 approval_code (for out-of-band payment)
172 type (for out-of-band payment)
173 number (for call to payment processor)
174 stripe_token (for call to Stripe payment processor)
175 expire_month (for call to payment processor)
176 expire_year (for call to payment processor)
177 billing_first (for out-of-band payments and for call to payment processor)
178 billing_last (for out-of-band payments and for call to payment processor)
179 billing_address (for call to payment processor)
180 billing_city (for call to payment processor)
181 billing_state (for call to payment processor)
182 billing_zip (for call to payment processor)
183 note (if payments->{note} is blank, use this)
193 desc => q/Last user transaction ID. This is the actor.usr.last_xact_id value/,
199 q{Array of payment IDs on success, event on failure. Event possibilities include:
201 Bad parameters were given to this API method itself.
204 The last user transaction ID does not match the ID in the database. This means
205 the user object has been updated since the last retrieval. The client should
206 be instructed to reload the user object and related transactions before attempting
208 REFUND_EXCEEDS_BALANCE
209 REFUND_EXCEEDS_DESK_PAYMENTS
210 CREDIT_PROCESSOR_NOT_SPECIFIED
211 Evergreen has not been set up to process CC payments.
212 CREDIT_PROCESSOR_NOT_ALLOWED
213 Evergreen has been incorrectly setup for CC payments.
214 CREDIT_PROCESSOR_NOT_ENABLED
215 Evergreen has been set up for CC payments, but an admin
216 has not explicitly enabled them.
217 CREDIT_PROCESSOR_BAD_PARAMS
218 Evergreen has been incorrectly setup for CC payments;
219 specifically, the login and/or password for the CC
220 processor weren't provided.
221 CREDIT_PROCESSOR_INVALID_CC_NUMBER
222 You have supplied a credit card number that Evergreen
223 has judged to be invalid even before attempting to contact
224 the payment processor.
225 CREDIT_PROCESSOR_DECLINED_TRANSACTION
226 We contacted the CC processor to attempt the charge, but
228 The error_message field of the event payload will
229 contain the payment processor's response. This
230 typically includes a message in plain English intended
231 for human consumption. In PayPal's case, the message
232 is preceded by an integer, a colon, and a space, so
233 a caller might take the 2nd match from /^(\d+: )?(.+)$/
234 to present to the user.
235 The payload also contains other fields from the payment
236 processor, but these are generally not user-friendly
238 CREDIT_PROCESSOR_SUCCESS_WO_RECORD
239 A payment was processed successfully, but couldn't be
240 recorded in Evergreen. This is _bad bad bad_, as it means
241 somebody made a payment but isn't getting credit for it.
242 See errors in the system log if this happens. Info from
243 the credit card transaction will also be available in the
244 event payload, although this probably won't be suitable for
245 staff client/OPAC display.
252 my($self, $client, $auth, $payments, $last_xact_id) = @_;
254 my $e = new_editor(authtoken => $auth, xact => 1);
255 return $e->die_event unless $e->checkauth;
257 my $type = $payments->{payment_type};
258 my $user_id = $payments->{userid};
259 my $credit = $payments->{patron_credit} || 0;
260 my $drawer = $e->requestor->wsid;
261 my $note = $payments->{note};
262 my $cc_args = $payments->{cc_args};
263 my $check_number = $payments->{check_number};
265 my $this_ou = $e->requestor->ws_ou || $e->requestor->home_ou;
269 # unless/until determined by payment processor API
270 my ($approval_code, $cc_processor, $cc_type, $cc_order_number) = (undef,undef,undef, undef);
272 my $patron = $e->retrieve_actor_user($user_id) or return $e->die_event;
274 if($patron->last_xact_id ne $last_xact_id) {
276 return OpenILS::Event->new('INVALID_USER_XACT_ID');
279 # A user is allowed to make credit card payments on his/her own behalf
280 # All other scenarious require permission
281 unless($type eq 'credit_card_payment' and $user_id == $e->requestor->id) {
282 return $e->die_event unless $e->allowed('CREATE_PAYMENT', $patron->home_ou);
285 # first collect the transactions and make sure the transaction
286 # user matches the requested user
289 # We rewrite the payments array for sanity's sake, to avoid more
290 # than one payment per transaction per call, which is not legitimate
291 # but has been seen in the wild coming from the staff client. This
292 # is presumably a staff client (xulrunner) bug.
293 my @unique_xact_payments;
294 for my $pay (@{$payments->{payments}}) {
295 my $xact_id = $pay->[0];
296 if (exists($xacts{$xact_id})) {
298 return OpenILS::Event->new('MULTIPLE_PAYMENTS_FOR_XACT');
301 my $xact = $e->retrieve_money_billable_transaction_summary($xact_id)
302 or return $e->die_event;
304 if($xact->usr != $user_id) {
306 return OpenILS::Event->new('BAD_PARAMS', note => q/user does not match transaction/);
309 $xacts{$xact_id} = $xact;
310 push @unique_xact_payments, $pay;
312 $payments->{payments} = \@unique_xact_payments;
316 for my $pay (@{$payments->{payments}}) {
317 my $transid = $pay->[0];
318 my $amount = $pay->[1];
319 $amount =~ s/\$//og; # just to be safe
320 my $trans = $xacts{$transid};
322 $total_paid += $amount;
324 my $org_id = $U->xact_org($transid, $e);
326 if (!$orgs{$org_id}) {
329 # patron credit has to be allowed at all orgs receiving payment
330 if ($type eq 'credit_payment' and $U->ou_ancestor_setting_value(
331 $org_id, 'circ.disable_patron_credit', $e)) {
333 return OpenILS::Event->new('PATRON_CREDIT_DISABLED');
337 # A negative payment is a refund.
340 # Negative credit card payments are not allowed
341 if($type eq 'credit_card_payment') {
343 return OpenILS::Event->new(
345 note => q/Negative credit card payments not allowed/
349 # If the refund causes the transaction balance to exceed 0 dollars,
350 # we are in effect loaning the patron money. This is not allowed.
351 if( ($trans->balance_owed - $amount) > 0 ) {
353 return OpenILS::Event->new('REFUND_EXCEEDS_BALANCE');
356 # Otherwise, make sure the refund does not exceed desk payments
357 # This is also not allowed
359 my $desk_payments = $e->search_money_desk_payment({xact => $transid, voided => 'f'});
360 $desk_total += $_->amount for @$desk_payments;
362 if( (-$amount) > $desk_total ) {
364 return OpenILS::Event->new(
365 'REFUND_EXCEEDS_DESK_PAYMENTS',
366 payload => { allowed_refund => $desk_total, submitted_refund => -$amount } );
370 my $payobj = "Fieldmapper::money::$type";
371 $payobj = $payobj->new;
373 $payobj->amount($amount);
374 $payobj->amount_collected($amount);
375 $payobj->xact($transid);
376 $payobj->note($note);
377 if ((not $payobj->note) and ($type eq 'credit_card_payment')) {
378 $payobj->note($cc_args->{note});
381 if ($payobj->has_field('accepting_usr')) { $payobj->accepting_usr($e->requestor->id); }
382 if ($payobj->has_field('cash_drawer')) { $payobj->cash_drawer($drawer); }
383 if ($payobj->has_field('cc_type')) { $payobj->cc_type($cc_args->{type}); }
384 if ($payobj->has_field('check_number')) { $payobj->check_number($check_number); }
386 # Store the last 4 digits of the CC number
387 if ($payobj->has_field('cc_number')) {
388 $payobj->cc_number(substr($cc_args->{number}, -4));
390 if ($payobj->has_field('expire_month')) { $payobj->expire_month($cc_args->{expire_month}); $logger->info("LFW XXX expire_month is $cc_args->{expire_month}"); }
391 if ($payobj->has_field('expire_year')) { $payobj->expire_year($cc_args->{expire_year}); }
393 # Note: It is important not to set approval_code
394 # on the fieldmapper object yet.
396 push(@payment_objs, $payobj);
398 } # all payment objects have been created and inserted.
400 #### NO WRITES TO THE DB ABOVE THIS LINE -- THEY'LL ONLY BE DISCARDED ###
403 # After we try to externally process a credit card (if desired), we'll
404 # open a new transaction. We cannot leave one open while credit card
405 # processing might be happening, as it can easily time out the database
410 if($type eq 'credit_card_payment') {
411 $approval_code = $cc_args->{approval_code};
412 # If an approval code was not given, we'll need
413 # to call to the payment processor ourselves.
414 if ($cc_args->{where_process} == 1) {
415 my $response = process_stripe_or_bop_payment(
416 $e, $user_id, $this_ou, $total_paid, $cc_args
419 if ($U->event_code($response)) { # non-success (success is 0)
421 "Credit card payment for user $user_id failed: " .
422 $response->{textcode} . " " .
423 ($response->{payload}->{error_message} ||
424 $response->{payload}{message})
428 # We need to save this for later in case there's a failure on
429 # the EG side to store the processor's result.
431 $cc_payload = $response->{"payload"}; # also used way later
434 no warnings 'uninitialized';
435 $cc_type = $cc_payload->{card_type};
436 $approval_code = $cc_payload->{authorization} ||
438 $cc_processor = $cc_payload->{processor} ||
439 $cc_args->{processor};
440 $cc_order_number = $cc_payload->{order_number} ||
441 $cc_payload->{invoice};
443 $logger->info("Credit card payment for user $user_id succeeded");
446 return OpenILS::Event->new(
447 'BAD_PARAMS', note => 'Need approval code'
448 ) if not $cc_args->{approval_code};
452 ### RE-OPEN TRANSACTION HERE ###
456 # create payment records
457 my $create_money_method = "create_money_" . $type;
458 for my $payment (@payment_objs) {
459 # update the transaction if it's done
460 my $amount = $payment->amount;
461 my $transid = $payment->xact;
462 my $trans = $xacts{$transid};
463 # making payment with existing patron credit.
464 $credit -= $amount if $type eq 'credit_payment';
465 if( (my $cred = ($trans->balance_owed - $amount)) <= 0 ) {
466 # Any overpay on this transaction goes directly into patron
470 my $circ = $e->retrieve_action_circulation(
475 flesh_fields => {circ => ['target_copy','billings']}
478 ); # Flesh the copy, so we can monkey with the status if
481 # Whether or not we close the transaction. We definitely
482 # close if no circulation transaction is present,
483 # otherwise we check if the circulation is in a state that
484 # allows itself to be closed.
485 if (!$circ || OpenILS::Application::Circ::CircCommon->can_close_circ($e, $circ)) {
486 $trans = $e->retrieve_money_billable_transaction($transid);
487 $trans->xact_finish("now");
488 if (!$e->update_money_billable_transaction($trans)) {
489 return _recording_failure(
490 $e, "update_money_billable_transaction() failed",
491 $payment, $cc_payload
495 # If we have a circ, we need to check if the copy
496 # status is lost or long overdue. If it is then we
497 # check org_unit_settings for the copy owning library
498 # and adjust and possibly adjust copy status to lost
501 # We need the copy to check settings and to possibly
503 my $copy = $circ->target_copy();
504 # Library where we'll check settings.
505 my $check_lib = $copy->circ_lib();
507 # check the copy status
508 if (($copy->status() == OILS_COPY_STATUS_LOST || $copy->status() == OILS_COPY_STATUS_LONG_OVERDUE)
509 && $U->is_true($U->ou_ancestor_setting_value($check_lib, 'circ.use_lost_paid_copy_status', $e))) {
510 $copy->status(OILS_COPY_STATUS_LOST_AND_PAID);
511 if (!$e->update_asset_copy($copy)) {
512 return _recording_failure(
513 $e, "update_asset_copy_failed()",
514 $payment, $cc_payload
522 # Urgh, clean up this mega-function one day.
523 if ($cc_processor eq 'Stripe' and $approval_code and $cc_payload) {
524 $payment->expire_month($cc_payload->{card}{exp_month});
525 $payment->expire_year($cc_payload->{card}{exp_year});
526 $payment->cc_number($cc_payload->{card}{last4});
529 $payment->approval_code($approval_code) if $approval_code;
530 $payment->cc_order_number($cc_order_number) if $cc_order_number;
531 $payment->cc_type($cc_type) if $cc_type;
532 $payment->cc_processor($cc_processor) if $cc_processor;
533 $payment->cc_first_name($cc_args->{'billing_first'}) if $cc_args->{'billing_first'};
534 $payment->cc_last_name($cc_args->{'billing_last'}) if $cc_args->{'billing_last'};
535 if (!$e->$create_money_method($payment)) {
536 return _recording_failure(
537 $e, "$create_money_method failed", $payment, $cc_payload
541 push(@payment_ids, $payment->id);
544 my $evt = _update_patron_credit($e, $patron, $credit);
546 return _recording_failure(
547 $e, "_update_patron_credit() failed", undef, $cc_payload
551 for my $org_id (keys %orgs) {
552 # calculate penalties for each of the affected orgs
553 $evt = OpenILS::Utils::Penalty->calculate_penalties(
554 $e, $user_id, $org_id
557 return _recording_failure(
558 $e, "calculate_penalties() failed", undef, $cc_payload
563 # update the user to create a new last_xact_id
564 $e->update_actor_user($patron) or return $e->die_event;
565 $patron = $e->retrieve_actor_user($patron) or return $e->die_event;
568 # update the cached user object if a user is making a payment toward
569 # his/her own account
570 $U->simplereq('open-ils.auth', 'open-ils.auth.session.reset_timeout', $auth, 1)
571 if $user_id == $e->requestor->id;
573 return {last_xact_id => $patron->last_xact_id, payments => \@payment_ids};
576 sub _recording_failure {
577 my ($e, $msg, $payment, $payload) = @_;
579 if ($payload) { # If the payment processor already accepted a payment:
580 $logger->error($msg);
581 $logger->error("Payment processor payload: " . Dumper($payload));
582 # payment shouldn't contain CC number
583 $logger->error("Payment: " . Dumper($payment)) if $payment;
587 return new OpenILS::Event(
588 "CREDIT_PROCESSOR_SUCCESS_WO_RECORD",
589 "payload" => $payload
591 } else { # Otherwise, the problem is somewhat less severe:
593 $logger->warn("Payment: " . Dumper($payment)) if $payment;
594 return $e->die_event;
598 sub _update_patron_credit {
599 my($e, $patron, $credit) = @_;
600 return undef if $credit == 0;
601 $patron->credit_forward_balance($patron->credit_forward_balance + $credit);
602 return OpenILS::Event->new('NEGATIVE_PATRON_BALANCE') if $patron->credit_forward_balance < 0;
603 $e->update_actor_user($patron) or return $e->die_event;
608 __PACKAGE__->register_method(
609 method => "retrieve_payments",
610 api_name => "open-ils.circ.money.payment.retrieve.all_",
611 notes => "Returns a list of payments attached to a given transaction"
613 sub retrieve_payments {
614 my( $self, $client, $login, $transid ) = @_;
617 $apputils->checksesperm($login, 'VIEW_TRANSACTION');
620 # XXX the logic here is wrong.. we need to check the owner of the transaction
621 # to make sure the requestor has access
623 # XXX grab the view, for each object in the view, grab the real object
625 return $apputils->simplereq(
627 'open-ils.cstore.direct.money.payment.search.atomic', { xact => $transid } );
631 __PACKAGE__->register_method(
632 method => "retrieve_payments2",
634 api_name => "open-ils.circ.money.payment.retrieve.all",
635 notes => "Returns a list of payments attached to a given transaction"
638 sub retrieve_payments2 {
639 my( $self, $client, $login, $transid ) = @_;
641 my $e = new_editor(authtoken=>$login);
642 return $e->event unless $e->checkauth;
643 return $e->event unless $e->allowed('VIEW_TRANSACTION');
646 my $pmnts = $e->search_money_payment({ xact => $transid });
648 my $type = $_->payment_type;
649 my $meth = "retrieve_money_$type";
650 my $p = $e->$meth($_->id) or return $e->event;
651 $p->payment_type($type);
652 $p->cash_drawer($e->retrieve_actor_workstation($p->cash_drawer))
653 if $p->has_field('cash_drawer');
654 push( @payments, $p );
660 __PACKAGE__->register_method(
661 method => "format_payment_receipt",
662 api_name => "open-ils.circ.money.payment_receipt.print",
664 desc => 'Returns a printable receipt for the specified payments',
666 { desc => 'Authentication token', type => 'string'},
667 { desc => 'Payment ID or array of payment IDs', type => 'number' },
670 desc => q/An action_trigger.event object or error event./,
675 __PACKAGE__->register_method(
676 method => "format_payment_receipt",
677 api_name => "open-ils.circ.money.payment_receipt.email",
679 desc => 'Emails a receipt for the specified payments to the user associated with the first payment',
681 { desc => 'Authentication token', type => 'string'},
682 { desc => 'Payment ID or array of payment IDs', type => 'number' },
685 desc => q/Undefined on success, otherwise an error event./,
691 sub format_payment_receipt {
692 my($self, $conn, $auth, $mp_id) = @_;
695 if (ref $mp_id ne 'ARRAY') {
696 $mp_ids = [ $mp_id ];
701 my $for_print = ($self->api_name =~ /print/);
702 my $for_email = ($self->api_name =~ /email/);
704 # manually use xact (i.e. authoritative) so we can kill the cstore
705 # connection before sending the action/trigger request. This prevents our cstore
706 # backend from sitting idle while A/T (which uses its own transactions) runs.
707 my $e = new_editor(xact => 1, authtoken => $auth);
708 return $e->die_event unless $e->checkauth;
711 for my $id (@$mp_ids) {
713 my $payment = $e->retrieve_money_payment([
721 ]) or return $e->die_event;
723 return $e->die_event unless
724 $e->requestor->id == $payment->xact->usr->id or
725 $e->allowed('VIEW_TRANSACTION', $payment->xact->usr->home_ou);
727 push @$payments, $payment;
734 return $U->fire_object_event(undef, 'money.format.payment_receipt.print', $payments, $$payments[0]->xact->usr->home_ou);
736 } elsif ($for_email) {
738 for my $p (@$payments) {
739 $U->create_events_for_hook('money.format.payment_receipt.email', $p, $p->xact->usr->home_ou, undef, undef, 1);
746 __PACKAGE__->register_method(
747 method => "create_grocery_bill",
748 api_name => "open-ils.circ.money.grocery.create",
750 Creates a new grocery transaction using the transaction object provided
751 PARAMS: (login_session, money.grocery (mg) object)
754 sub create_grocery_bill {
755 my( $self, $client, $login, $transaction ) = @_;
757 my( $staff, $evt ) = $apputils->checkses($login);
759 $evt = $apputils->check_perms($staff->id,
760 $transaction->billing_location, 'CREATE_TRANSACTION' );
764 $logger->activity("Creating grocery bill " . Dumper($transaction) );
766 $transaction->clear_id;
767 my $session = $apputils->start_db_session;
768 $apputils->set_audit_info($session, $login, $staff->id, $staff->wsid);
769 my $transid = $session->request(
770 'open-ils.storage.direct.money.grocery.create', $transaction)->gather(1);
772 throw OpenSRF::EX ("Error creating new money.grocery") unless defined $transid;
774 $logger->debug("Created new grocery transaction $transid");
776 $apputils->commit_db_session($session);
778 my $e = new_editor(xact=>1);
779 $evt = $U->check_open_xact($e, $transid);
787 __PACKAGE__->register_method(
788 method => 'fetch_reservation',
789 api_name => 'open-ils.circ.booking.reservation.retrieve'
791 sub fetch_reservation {
792 my( $self, $conn, $auth, $id ) = @_;
793 my $e = new_editor(authtoken=>$auth);
794 return $e->event unless $e->checkauth;
795 return $e->event unless $e->allowed('VIEW_TRANSACTION'); # eh.. basically the same permission
796 my $g = $e->retrieve_booking_reservation($id)
801 __PACKAGE__->register_method(
802 method => 'fetch_grocery',
803 api_name => 'open-ils.circ.money.grocery.retrieve'
806 my( $self, $conn, $auth, $id ) = @_;
807 my $e = new_editor(authtoken=>$auth);
808 return $e->event unless $e->checkauth;
809 return $e->event unless $e->allowed('VIEW_TRANSACTION'); # eh.. basically the same permission
810 my $g = $e->retrieve_money_grocery($id)
816 __PACKAGE__->register_method(
817 method => "billing_items",
818 api_name => "open-ils.circ.money.billing.retrieve.all",
821 desc => 'Returns a list of billing items for the given transaction ID. ' .
822 'If the operator is not the owner of the transaction, the VIEW_TRANSACTION permission is required.',
824 { desc => 'Authentication token', type => 'string'},
825 { desc => 'Transaction ID', type => 'number'}
828 desc => 'Transaction object, event on error'
834 my( $self, $client, $login, $transid ) = @_;
836 my( $trans, $evt ) = $U->fetch_billable_xact($transid);
840 ($staff, $evt ) = $apputils->checkses($login);
843 if($staff->id ne $trans->usr) {
844 $evt = $U->check_perms($staff->id, $staff->home_ou, 'VIEW_TRANSACTION');
848 return $apputils->simplereq( 'open-ils.cstore',
849 'open-ils.cstore.direct.money.billing.search.atomic', { xact => $transid } )
853 __PACKAGE__->register_method(
854 method => "billing_items_create",
855 api_name => "open-ils.circ.money.billing.create",
857 Creates a new billing line item
858 PARAMS( login, bill_object (mb) )
861 sub billing_items_create {
862 my( $self, $client, $login, $billing ) = @_;
864 my $e = new_editor(authtoken => $login, xact => 1);
865 return $e->die_event unless $e->checkauth;
866 return $e->die_event unless $e->allowed('CREATE_BILL');
868 my $xact = $e->retrieve_money_billable_transaction($billing->xact)
869 or return $e->die_event;
871 # if the transaction was closed, re-open it
872 if($xact->xact_finish) {
873 $xact->clear_xact_finish;
874 $e->update_money_billable_transaction($xact)
875 or return $e->die_event;
878 my $amt = $billing->amount;
880 $billing->amount($amt);
882 $e->create_money_billing($billing) or return $e->die_event;
883 my $evt = OpenILS::Utils::Penalty->calculate_penalties($e, $xact->usr, $U->xact_org($xact->id,$e));
886 $evt = $U->check_open_xact($e, $xact->id, $xact);
895 __PACKAGE__->register_method(
896 method => 'void_bill',
897 api_name => 'open-ils.circ.money.billing.void',
900 @param authtoken Login session key
901 @param billid Id for the bill to void. This parameter may be repeated to reference other bills.
902 @return 1 on success, Event on error
906 my( $s, $c, $authtoken, @billids ) = @_;
908 my $e = new_editor( authtoken => $authtoken, xact => 1 );
909 return $e->die_event unless $e->checkauth;
910 return $e->die_event unless $e->allowed('VOID_BILLING');
913 for my $billid (@billids) {
915 my $bill = $e->retrieve_money_billing($billid)
916 or return $e->die_event;
918 my $xact = $e->retrieve_money_billable_transaction($bill->xact)
919 or return $e->die_event;
921 if($U->is_true($bill->voided)) {
923 return OpenILS::Event->new('BILL_ALREADY_VOIDED', payload => $bill);
926 my $org = $U->xact_org($bill->xact, $e);
927 $users{$xact->usr} = {} unless $users{$xact->usr};
928 $users{$xact->usr}->{$org} = 1;
931 $bill->voider($e->requestor->id);
932 $bill->void_time('now');
934 $e->update_money_billing($bill) or return $e->die_event;
935 my $evt = $U->check_open_xact($e, $bill->xact, $xact);
939 # calculate penalties for all user/org combinations
940 for my $user_id (keys %users) {
941 for my $org_id (keys %{$users{$user_id}}) {
942 OpenILS::Utils::Penalty->calculate_penalties($e, $user_id, $org_id);
950 __PACKAGE__->register_method(
951 method => 'edit_bill_note',
952 api_name => 'open-ils.circ.money.billing.note.edit',
954 Edits the note for a bill
955 @param authtoken Login session key
956 @param note The replacement note for the bills we're editing
957 @param billid Id for the bill to edit the note of. This parameter may be repeated to reference other bills.
958 @return 1 on success, Event on error
962 my( $s, $c, $authtoken, $note, @billids ) = @_;
964 my $e = new_editor( authtoken => $authtoken, xact => 1 );
965 return $e->die_event unless $e->checkauth;
966 return $e->die_event unless $e->allowed('UPDATE_BILL_NOTE');
968 for my $billid (@billids) {
970 my $bill = $e->retrieve_money_billing($billid)
971 or return $e->die_event;
974 # 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.
976 $e->update_money_billing($bill) or return $e->die_event;
983 __PACKAGE__->register_method(
984 method => 'edit_payment_note',
985 api_name => 'open-ils.circ.money.payment.note.edit',
987 Edits the note for a payment
988 @param authtoken Login session key
989 @param note The replacement note for the payments we're editing
990 @param paymentid Id for the payment to edit the note of. This parameter may be repeated to reference other payments.
991 @return 1 on success, Event on error
994 sub edit_payment_note {
995 my( $s, $c, $authtoken, $note, @paymentids ) = @_;
997 my $e = new_editor( authtoken => $authtoken, xact => 1 );
998 return $e->die_event unless $e->checkauth;
999 return $e->die_event unless $e->allowed('UPDATE_PAYMENT_NOTE');
1001 for my $paymentid (@paymentids) {
1003 my $payment = $e->retrieve_money_payment($paymentid)
1004 or return $e->die_event;
1006 $payment->note($note);
1007 # 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.
1009 $e->update_money_payment($payment) or return $e->die_event;
1017 __PACKAGE__->register_method (
1018 method => 'fetch_mbts',
1020 api_name => 'open-ils.circ.money.billable_xact_summary.retrieve'
1023 my( $self, $conn, $auth, $id) = @_;
1025 my $e = new_editor(xact => 1, authtoken=>$auth);
1026 return $e->event unless $e->checkauth;
1027 my ($mbts) = $U->fetch_mbts($id, $e);
1029 my $user = $e->retrieve_actor_user($mbts->usr)
1030 or return $e->die_event;
1032 return $e->die_event unless $e->allowed('VIEW_TRANSACTION', $user->home_ou);
1038 __PACKAGE__->register_method(
1039 method => 'desk_payments',
1040 api_name => 'open-ils.circ.money.org_unit.desk_payments'
1043 my( $self, $conn, $auth, $org, $start_date, $end_date ) = @_;
1044 my $e = new_editor(authtoken=>$auth);
1045 return $e->event unless $e->checkauth;
1046 return $e->event unless $e->allowed('VIEW_TRANSACTION', $org);
1047 my $data = $U->storagereq(
1048 'open-ils.storage.money.org_unit.desk_payments.atomic',
1049 $org, $start_date, $end_date );
1051 $_->workstation( $_->workstation->name ) for(@$data);
1056 __PACKAGE__->register_method(
1057 method => 'user_payments',
1058 api_name => 'open-ils.circ.money.org_unit.user_payments'
1062 my( $self, $conn, $auth, $org, $start_date, $end_date ) = @_;
1063 my $e = new_editor(authtoken=>$auth);
1064 return $e->event unless $e->checkauth;
1065 return $e->event unless $e->allowed('VIEW_TRANSACTION', $org);
1066 my $data = $U->storagereq(
1067 'open-ils.storage.money.org_unit.user_payments.atomic',
1068 $org, $start_date, $end_date );
1071 $e->retrieve_actor_card($_->usr->card)->barcode);
1073 $e->retrieve_actor_org_unit($_->usr->home_ou)->shortname);
1079 __PACKAGE__->register_method(
1080 method => 'retrieve_credit_payable_balance',
1081 api_name => 'open-ils.circ.credit.payable_balance.retrieve',
1084 desc => q/Returns the total amount the patron can pay via credit card/,
1086 { desc => 'Authentication token', type => 'string' },
1087 { desc => 'User id', type => 'number' }
1089 return => { desc => 'The ID of the new provider' }
1093 sub retrieve_credit_payable_balance {
1094 my ( $self, $conn, $auth, $user_id ) = @_;
1095 my $e = new_editor(authtoken => $auth);
1096 return $e->event unless $e->checkauth;
1098 my $user = $e->retrieve_actor_user($user_id)
1099 or return $e->event;
1101 if($e->requestor->id != $user_id) {
1102 return $e->event unless $e->allowed('VIEW_USER_TRANSACTIONS', $user->home_ou)
1105 my $circ_orgs = $e->json_query({
1106 "select" => {circ => ["circ_lib"]},
1108 "where" => {usr => $user_id, xact_finish => undef},
1112 my $groc_orgs = $e->json_query({
1113 "select" => {mg => ["billing_location"]},
1115 "where" => {usr => $user_id, xact_finish => undef},
1120 for my $org ( @$circ_orgs, @$groc_orgs ) {
1121 my $o = $org->{billing_location};
1122 $o = $org->{circ_lib} unless $o;
1123 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.
1124 $hash{$o} = $U->ou_ancestor_setting_value($o, 'credit.payments.allow', $e);
1127 my @credit_orgs = map { $hash{$_} ? ($_) : () } keys %hash;
1128 $logger->debug("credit: relevant orgs that allow credit payments => @credit_orgs");
1130 my $xact_summaries =
1131 OpenILS::Application::AppUtils->simplereq('open-ils.actor',
1132 'open-ils.actor.user.transactions.have_charge', $auth, $user_id);
1136 for my $xact (@$xact_summaries) {
1138 # make two lists and grab them in batch XXX
1139 if ( $xact->xact_type eq 'circulation' ) {
1140 my $circ = $e->retrieve_action_circulation($xact->id) or return $e->event;
1141 next unless grep { $_ == $circ->circ_lib } @credit_orgs;
1143 } elsif ($xact->xact_type eq 'grocery') {
1144 my $bill = $e->retrieve_money_grocery($xact->id) or return $e->event;
1145 next unless grep { $_ == $bill->billing_location } @credit_orgs;
1146 } elsif ($xact->xact_type eq 'reservation') {
1147 my $bill = $e->retrieve_booking_reservation($xact->id) or return $e->event;
1148 next unless grep { $_ == $bill->pickup_lib } @credit_orgs;
1150 $sum += $xact->balance_owed();