1 # ---------------------------------------------------------------
2 # Copyright (C) 2005 Georgia Public Library Service
3 # Bill Erickson <billserickson@gmail.com>
5 # This program is free software; you can redistribute it and/or
6 # modify it under the terms of the GNU General Public License
7 # as published by the Free Software Foundation; either version 2
8 # of the License, or (at your option) any later version.
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU General Public License for more details.
14 # ---------------------------------------------------------------
16 package OpenILS::Application::Circ::Money;
17 use base qw/OpenILS::Application/;
18 use strict; use warnings;
19 use OpenILS::Application::AppUtils;
20 use OpenILS::Application::Circ::CircCommon;
21 my $apputils = "OpenILS::Application::AppUtils";
22 my $U = "OpenILS::Application::AppUtils";
23 my $CC = "OpenILS::Application::Circ::CircCommon";
25 use OpenSRF::EX qw(:try);
29 use OpenSRF::Utils::Logger qw/:logger/;
30 use OpenILS::Utils::CStoreEditor qw/:funcs/;
31 use OpenILS::Utils::Penalty;
33 $Data::Dumper::Indent = 0;
34 use OpenILS::Const qw/:const/;
36 sub get_processor_settings {
39 my $processor = lc shift;
41 # Get the names of every credit processor setting for our given processor.
42 # They're a little different per processor.
43 my $setting_names = $e->json_query({
44 select => {coust => ["name"]},
45 from => {coust => {}},
46 where => {name => {like => "credit.processor.${processor}.%"}}
47 }) or return $e->die_event;
49 # Make keys for a hash we're going to build out of the last dot-delimited
50 # component of each setting name.
51 ($_->{key} = $_->{name}) =~ s/.+\.(\w+)$/$1/ for @$setting_names;
53 # Return a hash with those short keys, and for values the value of
54 # the corresponding OU setting within our scope.
57 $_->{key} => $U->ou_ancestor_setting_value($org_unit, $_->{name})
62 # process_stripe_or_bop_payment()
63 # This is a helper method to make_payments() below (specifically,
64 # the credit-card part). It's the first point in the Perl code where
65 # we need to care about the distinction between Stripe and the
66 # Paypal/PayflowPro/AuthorizeNet kinds of processors (the latter group
67 # uses B::OP and handles payment card info, whereas Stripe doesn't use
68 # B::OP and doesn't require us to know anything about the payment card
71 # Return an event in all cases. That means a success returns a SUCCESS
73 sub process_stripe_or_bop_payment {
74 my ($e, $user_id, $this_ou, $total_paid, $cc_args) = @_;
76 # A few stanzas to determine which processor we're using and whether we're
77 # really adequately set up for it.
78 if (!$cc_args->{processor}) {
79 if (!($cc_args->{processor} =
80 $U->ou_ancestor_setting_value(
81 $this_ou, 'credit.processor.default'
85 return OpenILS::Event->new('CREDIT_PROCESSOR_NOT_SPECIFIED');
89 # Make sure the configured credit processor has a safe/correct name.
90 return OpenILS::Event->new('CREDIT_PROCESSOR_NOT_ALLOWED')
91 unless $cc_args->{processor} =~ /^[a-z0-9_\-]+$/i;
93 # Get the settings for the processor and make sure they're serviceable.
94 my $psettings = get_processor_settings($e, $this_ou, $cc_args->{processor});
95 return $psettings if defined $U->event_code($psettings);
96 return OpenILS::Event->new('CREDIT_PROCESSOR_NOT_ENABLED')
97 unless $psettings->{enabled};
99 # Now we branch. Stripe is one thing, and everything else is another.
101 if ($cc_args->{processor} eq 'Stripe') { # Stripe
102 my $stripe = Business::Stripe->new(-api_key => $psettings->{secretkey});
103 $stripe->charges_create(
104 amount => int($total_paid * 100.0), # Stripe takes amount in pennies
105 card => $cc_args->{stripe_token},
106 description => $cc_args->{note}
109 if ($stripe->success) {
110 $logger->info("Stripe payment succeeded");
111 return OpenILS::Event->new(
112 "SUCCESS", payload => {
113 map { $_ => $stripe->success->{$_} } qw(
114 invoice customer balance_transaction id created card
119 $logger->info("Stripe payment failed");
120 return OpenILS::Event->new(
121 "CREDIT_PROCESSOR_DECLINED_TRANSACTION",
122 payload => $stripe->error # XXX what happens if this contains
123 # JSON::backportPP::* objects?
127 } else { # B::OP style (Paypal/PayflowPro/AuthorizeNet)
128 return OpenILS::Event->new('BAD_PARAMS', note => 'Need CC number')
129 unless $cc_args->{number};
131 return OpenILS::Application::Circ::CreditCard::process_payment({
132 "processor" => $cc_args->{processor},
133 "desc" => $cc_args->{note},
134 "amount" => $total_paid,
135 "patron_id" => $user_id,
136 "cc" => $cc_args->{number},
137 "expiration" => sprintf(
139 $cc_args->{expire_month},
140 $cc_args->{expire_year}
143 "first_name" => $cc_args->{billing_first},
144 "last_name" => $cc_args->{billing_last},
145 "address" => $cc_args->{billing_address},
146 "city" => $cc_args->{billing_city},
147 "state" => $cc_args->{billing_state},
148 "zip" => $cc_args->{billing_zip},
149 "cvv2" => $cc_args->{cvv2},
156 __PACKAGE__->register_method(
157 method => "make_payments",
158 api_name => "open-ils.circ.money.payment",
160 desc => q/Create payments for a given user and set of transactions,
161 login must have CREATE_PAYMENT privileges.
162 If any payments fail, all are reverted back./,
164 {desc => 'Authtoken', type => 'string'},
165 {desc => q/Arguments Hash, supporting the following params:
172 where_process 1 to use processor, !1 for out-of-band
173 approval_code (for out-of-band payment)
174 type (for out-of-band payment)
175 number (for call to payment processor)
176 stripe_token (for call to Stripe payment processor)
177 expire_month (for call to payment processor)
178 expire_year (for call to payment processor)
179 billing_first (for out-of-band payments and for call to payment processor)
180 billing_last (for out-of-band payments and for call to payment processor)
181 billing_address (for call to payment processor)
182 billing_city (for call to payment processor)
183 billing_state (for call to payment processor)
184 billing_zip (for call to payment processor)
185 note (if payments->{note} is blank, use this)
195 desc => q/Last user transaction ID. This is the actor.usr.last_xact_id value/,
201 q{Array of payment IDs on success, event on failure. Event possibilities include:
203 Bad parameters were given to this API method itself.
206 The last user transaction ID does not match the ID in the database. This means
207 the user object has been updated since the last retrieval. The client should
208 be instructed to reload the user object and related transactions before attempting
210 REFUND_EXCEEDS_BALANCE
211 REFUND_EXCEEDS_DESK_PAYMENTS
212 CREDIT_PROCESSOR_NOT_SPECIFIED
213 Evergreen has not been set up to process CC payments.
214 CREDIT_PROCESSOR_NOT_ALLOWED
215 Evergreen has been incorrectly setup for CC payments.
216 CREDIT_PROCESSOR_NOT_ENABLED
217 Evergreen has been set up for CC payments, but an admin
218 has not explicitly enabled them.
219 CREDIT_PROCESSOR_BAD_PARAMS
220 Evergreen has been incorrectly setup for CC payments;
221 specifically, the login and/or password for the CC
222 processor weren't provided.
223 CREDIT_PROCESSOR_INVALID_CC_NUMBER
224 You have supplied a credit card number that Evergreen
225 has judged to be invalid even before attempting to contact
226 the payment processor.
227 CREDIT_PROCESSOR_DECLINED_TRANSACTION
228 We contacted the CC processor to attempt the charge, but
230 The error_message field of the event payload will
231 contain the payment processor's response. This
232 typically includes a message in plain English intended
233 for human consumption. In PayPal's case, the message
234 is preceded by an integer, a colon, and a space, so
235 a caller might take the 2nd match from /^(\d+: )?(.+)$/
236 to present to the user.
237 The payload also contains other fields from the payment
238 processor, but these are generally not user-friendly
240 CREDIT_PROCESSOR_SUCCESS_WO_RECORD
241 A payment was processed successfully, but couldn't be
242 recorded in Evergreen. This is _bad bad bad_, as it means
243 somebody made a payment but isn't getting credit for it.
244 See errors in the system log if this happens. Info from
245 the credit card transaction will also be available in the
246 event payload, although this probably won't be suitable for
247 staff client/OPAC display.
254 my($self, $client, $auth, $payments, $last_xact_id) = @_;
256 my $e = new_editor(authtoken => $auth, xact => 1);
257 return $e->die_event unless $e->checkauth;
259 my $type = $payments->{payment_type};
260 my $user_id = $payments->{userid};
261 my $credit = $payments->{patron_credit} || 0;
262 my $drawer = $e->requestor->wsid;
263 my $note = $payments->{note};
264 my $cc_args = $payments->{cc_args};
265 my $check_number = $payments->{check_number};
267 my $this_ou = $e->requestor->ws_ou || $e->requestor->home_ou;
271 # unless/until determined by payment processor API
272 my ($approval_code, $cc_processor, $cc_type, $cc_order_number) = (undef,undef,undef, undef);
274 my $patron = $e->retrieve_actor_user($user_id) or return $e->die_event;
276 if($patron->last_xact_id ne $last_xact_id) {
278 return OpenILS::Event->new('INVALID_USER_XACT_ID');
281 # A user is allowed to make credit card payments on his/her own behalf
282 # All other scenarious require permission
283 unless($type eq 'credit_card_payment' and $user_id == $e->requestor->id) {
284 return $e->die_event unless $e->allowed('CREATE_PAYMENT', $patron->home_ou);
287 # first collect the transactions and make sure the transaction
288 # user matches the requested user
291 # We rewrite the payments array for sanity's sake, to avoid more
292 # than one payment per transaction per call, which is not legitimate
293 # but has been seen in the wild coming from the staff client. This
294 # is presumably a staff client (xulrunner) bug.
295 my @unique_xact_payments;
296 for my $pay (@{$payments->{payments}}) {
297 my $xact_id = $pay->[0];
298 if (exists($xacts{$xact_id})) {
300 return OpenILS::Event->new('MULTIPLE_PAYMENTS_FOR_XACT');
303 my $xact = $e->retrieve_money_billable_transaction_summary($xact_id)
304 or return $e->die_event;
306 if($xact->usr != $user_id) {
308 return OpenILS::Event->new('BAD_PARAMS', note => q/user does not match transaction/);
311 $xacts{$xact_id} = $xact;
312 push @unique_xact_payments, $pay;
314 $payments->{payments} = \@unique_xact_payments;
318 for my $pay (@{$payments->{payments}}) {
319 my $transid = $pay->[0];
320 my $amount = $pay->[1];
321 $amount =~ s/\$//og; # just to be safe
322 my $trans = $xacts{$transid};
324 $total_paid += $amount;
326 my $org_id = $U->xact_org($transid, $e);
328 if (!$orgs{$org_id}) {
331 # patron credit has to be allowed at all orgs receiving payment
332 if ($type eq 'credit_payment' and $U->ou_ancestor_setting_value(
333 $org_id, 'circ.disable_patron_credit', $e)) {
335 return OpenILS::Event->new('PATRON_CREDIT_DISABLED');
339 # A negative payment is a refund.
342 # Negative credit card payments are not allowed
343 if($type eq 'credit_card_payment') {
345 return OpenILS::Event->new(
347 note => q/Negative credit card payments not allowed/
351 # If the refund causes the transaction balance to exceed 0 dollars,
352 # we are in effect loaning the patron money. This is not allowed.
353 if( ($trans->balance_owed - $amount) > 0 ) {
355 return OpenILS::Event->new('REFUND_EXCEEDS_BALANCE');
358 # Otherwise, make sure the refund does not exceed desk payments
359 # This is also not allowed
361 my $desk_payments = $e->search_money_desk_payment({xact => $transid, voided => 'f'});
362 $desk_total += $_->amount for @$desk_payments;
364 if( (-$amount) > $desk_total ) {
366 return OpenILS::Event->new(
367 'REFUND_EXCEEDS_DESK_PAYMENTS',
368 payload => { allowed_refund => $desk_total, submitted_refund => -$amount } );
372 my $payobj = "Fieldmapper::money::$type";
373 $payobj = $payobj->new;
375 $payobj->amount($amount);
376 $payobj->amount_collected($amount);
377 $payobj->xact($transid);
378 $payobj->note($note);
379 if ((not $payobj->note) and ($type eq 'credit_card_payment')) {
380 $payobj->note($cc_args->{note});
383 if ($payobj->has_field('accepting_usr')) { $payobj->accepting_usr($e->requestor->id); }
384 if ($payobj->has_field('cash_drawer')) { $payobj->cash_drawer($drawer); }
385 if ($payobj->has_field('cc_type')) { $payobj->cc_type($cc_args->{type}); }
386 if ($payobj->has_field('check_number')) { $payobj->check_number($check_number); }
388 # Store the last 4 digits of the CC number
389 if ($payobj->has_field('cc_number')) {
390 $payobj->cc_number(substr($cc_args->{number}, -4));
392 if ($payobj->has_field('expire_month')) { $payobj->expire_month($cc_args->{expire_month}); $logger->info("LFW XXX expire_month is $cc_args->{expire_month}"); }
393 if ($payobj->has_field('expire_year')) { $payobj->expire_year($cc_args->{expire_year}); }
395 # Note: It is important not to set approval_code
396 # on the fieldmapper object yet.
398 push(@payment_objs, $payobj);
400 } # all payment objects have been created and inserted.
402 #### NO WRITES TO THE DB ABOVE THIS LINE -- THEY'LL ONLY BE DISCARDED ###
405 # After we try to externally process a credit card (if desired), we'll
406 # open a new transaction. We cannot leave one open while credit card
407 # processing might be happening, as it can easily time out the database
412 if($type eq 'credit_card_payment') {
413 $approval_code = $cc_args->{approval_code};
414 # If an approval code was not given, we'll need
415 # to call to the payment processor ourselves.
416 if ($cc_args->{where_process} == 1) {
417 my $response = process_stripe_or_bop_payment(
418 $e, $user_id, $this_ou, $total_paid, $cc_args
421 if ($U->event_code($response)) { # non-success (success is 0)
423 "Credit card payment for user $user_id failed: " .
424 $response->{textcode} . " " .
425 ($response->{payload}->{error_message} ||
426 $response->{payload}{message})
430 # We need to save this for later in case there's a failure on
431 # the EG side to store the processor's result.
433 $cc_payload = $response->{"payload"}; # also used way later
436 no warnings 'uninitialized';
437 $cc_type = $cc_payload->{card_type};
438 $approval_code = $cc_payload->{authorization} ||
440 $cc_processor = $cc_payload->{processor} ||
441 $cc_args->{processor};
442 $cc_order_number = $cc_payload->{order_number} ||
443 $cc_payload->{invoice};
445 $logger->info("Credit card payment for user $user_id succeeded");
448 return OpenILS::Event->new(
449 'BAD_PARAMS', note => 'Need approval code'
450 ) if not $cc_args->{approval_code};
454 ### RE-OPEN TRANSACTION HERE ###
458 # create payment records
459 my $create_money_method = "create_money_" . $type;
460 for my $payment (@payment_objs) {
461 # update the transaction if it's done
462 my $amount = $payment->amount;
463 my $transid = $payment->xact;
464 my $trans = $xacts{$transid};
465 # making payment with existing patron credit.
466 $credit -= $amount if $type eq 'credit_payment';
467 if( (my $cred = ($trans->balance_owed - $amount)) <= 0 ) {
468 # Any overpay on this transaction goes directly into patron
472 my $circ = $e->retrieve_action_circulation(
477 flesh_fields => {circ => ['target_copy','billings']}
480 ); # Flesh the copy, so we can monkey with the status if
483 # Whether or not we close the transaction. We definitely
484 # close if no circulation transaction is present,
485 # otherwise we check if the circulation is in a state that
486 # allows itself to be closed.
487 if (!$circ || $CC->can_close_circ($e, $circ)) {
488 $trans = $e->retrieve_money_billable_transaction($transid);
489 $trans->xact_finish("now");
490 if (!$e->update_money_billable_transaction($trans)) {
491 return _recording_failure(
492 $e, "update_money_billable_transaction() failed",
493 $payment, $cc_payload
497 # If we have a circ, we need to check if the copy
498 # status is lost or long overdue. If it is then we
499 # check org_unit_settings for the copy owning library
500 # and adjust and possibly adjust copy status to lost
502 if ($circ && ($circ->stop_fines eq 'LOST' || $circ->stop_fines eq 'LONGOVERDUE')) {
503 # We need the copy to check settings and to possibly
505 my $copy = $circ->target_copy();
506 # Library where we'll check settings.
507 my $check_lib = $copy->circ_lib();
509 # check the copy status
510 if (($copy->status() == OILS_COPY_STATUS_LOST || $copy->status() == OILS_COPY_STATUS_LONG_OVERDUE)
511 && $U->is_true($U->ou_ancestor_setting_value($check_lib, 'circ.use_lost_paid_copy_status', $e))) {
512 $copy->status(OILS_COPY_STATUS_LOST_AND_PAID);
513 if (!$e->update_asset_copy($copy)) {
514 return _recording_failure(
515 $e, "update_asset_copy_failed()",
516 $payment, $cc_payload
524 # Urgh, clean up this mega-function one day.
525 if ($cc_processor eq 'Stripe' and $approval_code and $cc_payload) {
526 $payment->expire_month($cc_payload->{card}{exp_month});
527 $payment->expire_year($cc_payload->{card}{exp_year});
528 $payment->cc_number($cc_payload->{card}{last4});
531 $payment->approval_code($approval_code) if $approval_code;
532 $payment->cc_order_number($cc_order_number) if $cc_order_number;
533 $payment->cc_type($cc_type) if $cc_type;
534 $payment->cc_processor($cc_processor) if $cc_processor;
535 $payment->cc_first_name($cc_args->{'billing_first'}) if $cc_args->{'billing_first'};
536 $payment->cc_last_name($cc_args->{'billing_last'}) if $cc_args->{'billing_last'};
537 if (!$e->$create_money_method($payment)) {
538 return _recording_failure(
539 $e, "$create_money_method failed", $payment, $cc_payload
543 push(@payment_ids, $payment->id);
546 my $evt = _update_patron_credit($e, $patron, $credit);
548 return _recording_failure(
549 $e, "_update_patron_credit() failed", undef, $cc_payload
553 for my $org_id (keys %orgs) {
554 # calculate penalties for each of the affected orgs
555 $evt = OpenILS::Utils::Penalty->calculate_penalties(
556 $e, $user_id, $org_id
559 return _recording_failure(
560 $e, "calculate_penalties() failed", undef, $cc_payload
565 # update the user to create a new last_xact_id
566 $e->update_actor_user($patron) or return $e->die_event;
567 $patron = $e->retrieve_actor_user($patron) or return $e->die_event;
570 # update the cached user object if a user is making a payment toward
571 # his/her own account
572 $U->simplereq('open-ils.auth', 'open-ils.auth.session.reset_timeout', $auth, 1)
573 if $user_id == $e->requestor->id;
575 return {last_xact_id => $patron->last_xact_id, payments => \@payment_ids};
578 sub _recording_failure {
579 my ($e, $msg, $payment, $payload) = @_;
581 if ($payload) { # If the payment processor already accepted a payment:
582 $logger->error($msg);
583 $logger->error("Payment processor payload: " . Dumper($payload));
584 # payment shouldn't contain CC number
585 $logger->error("Payment: " . Dumper($payment)) if $payment;
589 return new OpenILS::Event(
590 "CREDIT_PROCESSOR_SUCCESS_WO_RECORD",
591 "payload" => $payload
593 } else { # Otherwise, the problem is somewhat less severe:
595 $logger->warn("Payment: " . Dumper($payment)) if $payment;
596 return $e->die_event;
600 sub _update_patron_credit {
601 my($e, $patron, $credit) = @_;
602 return undef if $credit == 0;
603 $patron->credit_forward_balance($patron->credit_forward_balance + $credit);
604 return OpenILS::Event->new('NEGATIVE_PATRON_BALANCE') if $patron->credit_forward_balance < 0;
605 $e->update_actor_user($patron) or return $e->die_event;
610 __PACKAGE__->register_method(
611 method => "retrieve_payments",
612 api_name => "open-ils.circ.money.payment.retrieve.all_",
613 notes => "Returns a list of payments attached to a given transaction"
615 sub retrieve_payments {
616 my( $self, $client, $login, $transid ) = @_;
619 $apputils->checksesperm($login, 'VIEW_TRANSACTION');
622 # XXX the logic here is wrong.. we need to check the owner of the transaction
623 # to make sure the requestor has access
625 # XXX grab the view, for each object in the view, grab the real object
627 return $apputils->simplereq(
629 'open-ils.cstore.direct.money.payment.search.atomic', { xact => $transid } );
633 __PACKAGE__->register_method(
634 method => "retrieve_payments2",
636 api_name => "open-ils.circ.money.payment.retrieve.all",
637 notes => "Returns a list of payments attached to a given transaction"
640 sub retrieve_payments2 {
641 my( $self, $client, $login, $transid ) = @_;
643 my $e = new_editor(authtoken=>$login);
644 return $e->event unless $e->checkauth;
645 return $e->event unless $e->allowed('VIEW_TRANSACTION');
648 my $pmnts = $e->search_money_payment({ xact => $transid });
650 my $type = $_->payment_type;
651 my $meth = "retrieve_money_$type";
652 my $p = $e->$meth($_->id) or return $e->event;
653 $p->payment_type($type);
654 $p->cash_drawer($e->retrieve_actor_workstation($p->cash_drawer))
655 if $p->has_field('cash_drawer');
656 push( @payments, $p );
662 __PACKAGE__->register_method(
663 method => "format_payment_receipt",
664 api_name => "open-ils.circ.money.payment_receipt.print",
666 desc => 'Returns a printable receipt for the specified payments',
668 { desc => 'Authentication token', type => 'string'},
669 { desc => 'Payment ID or array of payment IDs', type => 'number' },
672 desc => q/An action_trigger.event object or error event./,
677 __PACKAGE__->register_method(
678 method => "format_payment_receipt",
679 api_name => "open-ils.circ.money.payment_receipt.email",
681 desc => 'Emails a receipt for the specified payments to the user associated with the first payment',
683 { desc => 'Authentication token', type => 'string'},
684 { desc => 'Payment ID or array of payment IDs', type => 'number' },
687 desc => q/Undefined on success, otherwise an error event./,
693 sub format_payment_receipt {
694 my($self, $conn, $auth, $mp_id) = @_;
697 if (ref $mp_id ne 'ARRAY') {
698 $mp_ids = [ $mp_id ];
703 my $for_print = ($self->api_name =~ /print/);
704 my $for_email = ($self->api_name =~ /email/);
706 # manually use xact (i.e. authoritative) so we can kill the cstore
707 # connection before sending the action/trigger request. This prevents our cstore
708 # backend from sitting idle while A/T (which uses its own transactions) runs.
709 my $e = new_editor(xact => 1, authtoken => $auth);
710 return $e->die_event unless $e->checkauth;
713 for my $id (@$mp_ids) {
715 my $payment = $e->retrieve_money_payment([
723 ]) or return $e->die_event;
725 return $e->die_event unless
726 $e->requestor->id == $payment->xact->usr->id or
727 $e->allowed('VIEW_TRANSACTION', $payment->xact->usr->home_ou);
729 push @$payments, $payment;
736 return $U->fire_object_event(undef, 'money.format.payment_receipt.print', $payments, $$payments[0]->xact->usr->home_ou);
738 } elsif ($for_email) {
740 for my $p (@$payments) {
741 $U->create_events_for_hook('money.format.payment_receipt.email', $p, $p->xact->usr->home_ou, undef, undef, 1);
748 __PACKAGE__->register_method(
749 method => "create_grocery_bill",
750 api_name => "open-ils.circ.money.grocery.create",
752 Creates a new grocery transaction using the transaction object provided
753 PARAMS: (login_session, money.grocery (mg) object)
756 sub create_grocery_bill {
757 my( $self, $client, $login, $transaction ) = @_;
759 my( $staff, $evt ) = $apputils->checkses($login);
761 $evt = $apputils->check_perms($staff->id,
762 $transaction->billing_location, 'CREATE_TRANSACTION' );
766 $logger->activity("Creating grocery bill " . Dumper($transaction) );
768 $transaction->clear_id;
769 my $session = $apputils->start_db_session;
770 $apputils->set_audit_info($session, $login, $staff->id, $staff->wsid);
771 my $transid = $session->request(
772 'open-ils.storage.direct.money.grocery.create', $transaction)->gather(1);
774 throw OpenSRF::EX ("Error creating new money.grocery") unless defined $transid;
776 $logger->debug("Created new grocery transaction $transid");
778 $apputils->commit_db_session($session);
780 my $e = new_editor(xact=>1);
781 $evt = $U->check_open_xact($e, $transid);
789 __PACKAGE__->register_method(
790 method => 'fetch_reservation',
791 api_name => 'open-ils.circ.booking.reservation.retrieve'
793 sub fetch_reservation {
794 my( $self, $conn, $auth, $id ) = @_;
795 my $e = new_editor(authtoken=>$auth);
796 return $e->event unless $e->checkauth;
797 return $e->event unless $e->allowed('VIEW_TRANSACTION'); # eh.. basically the same permission
798 my $g = $e->retrieve_booking_reservation($id)
803 __PACKAGE__->register_method(
804 method => 'fetch_grocery',
805 api_name => 'open-ils.circ.money.grocery.retrieve'
808 my( $self, $conn, $auth, $id ) = @_;
809 my $e = new_editor(authtoken=>$auth);
810 return $e->event unless $e->checkauth;
811 return $e->event unless $e->allowed('VIEW_TRANSACTION'); # eh.. basically the same permission
812 my $g = $e->retrieve_money_grocery($id)
818 __PACKAGE__->register_method(
819 method => "billing_items",
820 api_name => "open-ils.circ.money.billing.retrieve.all",
823 desc => 'Returns a list of billing items for the given transaction ID. ' .
824 'If the operator is not the owner of the transaction, the VIEW_TRANSACTION permission is required.',
826 { desc => 'Authentication token', type => 'string'},
827 { desc => 'Transaction ID', type => 'number'}
830 desc => 'Transaction object, event on error'
836 my( $self, $client, $login, $transid ) = @_;
838 my( $trans, $evt ) = $U->fetch_billable_xact($transid);
842 ($staff, $evt ) = $apputils->checkses($login);
845 if($staff->id ne $trans->usr) {
846 $evt = $U->check_perms($staff->id, $staff->home_ou, 'VIEW_TRANSACTION');
850 return $apputils->simplereq( 'open-ils.cstore',
851 'open-ils.cstore.direct.money.billing.search.atomic', { xact => $transid } )
855 __PACKAGE__->register_method(
856 method => "billing_items_create",
857 api_name => "open-ils.circ.money.billing.create",
859 Creates a new billing line item
860 PARAMS( login, bill_object (mb) )
863 sub billing_items_create {
864 my( $self, $client, $login, $billing ) = @_;
866 my $e = new_editor(authtoken => $login, xact => 1);
867 return $e->die_event unless $e->checkauth;
868 return $e->die_event unless $e->allowed('CREATE_BILL');
870 my $xact = $e->retrieve_money_billable_transaction($billing->xact)
871 or return $e->die_event;
873 # if the transaction was closed, re-open it
874 if($xact->xact_finish) {
875 $xact->clear_xact_finish;
876 $e->update_money_billable_transaction($xact)
877 or return $e->die_event;
880 my $amt = $billing->amount;
882 $billing->amount($amt);
884 $e->create_money_billing($billing) or return $e->die_event;
885 my $evt = OpenILS::Utils::Penalty->calculate_penalties($e, $xact->usr, $U->xact_org($xact->id,$e));
888 $evt = $U->check_open_xact($e, $xact->id, $xact);
897 __PACKAGE__->register_method(
898 method => 'void_bill',
899 api_name => 'open-ils.circ.money.billing.void',
902 @param authtoken Login session key
903 @param billid Id for the bill to void. This parameter may be repeated to reference other bills.
904 @return 1 on success, Event on error
908 my( $s, $c, $authtoken, @billids ) = @_;
909 my $editor = new_editor(authtoken=>$authtoken, xact=>1);
910 return $editor->die_event unless $editor->checkauth;
911 return $editor->die_event unless $editor->allowed('VOID_BILLING');
912 my $rv = $CC->void_bills($editor, \@billids);
913 if (ref($rv) eq 'HASH') {
917 # We should have gotten 1.
924 __PACKAGE__->register_method(
925 method => 'adjust_bills_to_zero_manual',
926 api_name => 'open-ils.circ.money.billable_xact.adjust_to_zero',
929 Given a list of billable transactions, manipulate the
930 transaction using account adjustments to result in a
934 {desc => 'Authtoken', type => 'string'},
935 {desc => 'Array of transaction IDs', type => 'array'}
938 desc => q/Array of IDs for each transaction updated,
947 my $xact_id = $xact->id;
948 # the plan: rebill voided billings until we get a positive balance
950 # step 1: get the voided/adjusted billings
951 my $billings = $e->search_money_billing([
956 order_by => {mb => 'amount desc'},
958 flesh_fields => {mb => ['adjustments']},
961 my @billings = grep { $U->is_true($_->voided) or @{$_->adjustments} } @$billings;
963 my $xact_balance = $xact->balance_owed;
964 $logger->debug("rebilling for xact $xact_id with balance $xact_balance");
966 my $rebill_amount = 0;
968 # step 2: generate new bills just like the old ones
969 for my $billing (@billings) {
971 if ($U->is_true($billing->voided)) {
972 $amount = $billing->amount;
973 } else { # adjusted billing
974 map { $amount = $U->fpadd($amount, $_->amount) } @{$billing->adjustments};
976 my $evt = $CC->create_bill(
980 $billing->billing_type,
982 "System: MANUAL ADJUSTMENT, BILLING #".$billing->id." REINSTATED\n(PREV: ".$billing->note.")",
983 $billing->billing_ts()
986 $rebill_amount += $billing->amount;
988 # if we have a postive (or zero) balance now, stop
989 last if $rebill_amount >= $xact_balance;
993 sub _is_fully_adjusted {
997 map { $amount_adj = $U->fpadd($amount_adj, $_->amount) } @{$billing->adjustments};
999 return $billing->amount == $amount_adj;
1002 sub adjust_bills_to_zero_manual {
1003 my ($self, $client, $auth, $xact_ids) = @_;
1005 my $e = new_editor(xact => 1, authtoken => $auth);
1006 return $e->die_event unless $e->checkauth;
1008 # in case a bare ID is passed
1009 $xact_ids = [$xact_ids] unless ref $xact_ids;
1012 for my $xact_id (@$xact_ids) {
1015 $e->retrieve_money_billable_transaction_summary([
1017 {flesh => 1, flesh_fields => {mbts => ['usr']}}
1018 ]) or return $e->die_event;
1020 return $e->die_event unless
1021 $e->allowed('ADJUST_BILLS', $xact->usr->home_ou);
1023 if ($xact->balance_owed < 0) {
1024 my $evt = _rebill_xact($e, $xact);
1025 return $evt if $evt;
1026 # refetch xact to get new balance
1028 $e->retrieve_money_billable_transaction_summary([
1030 {flesh => 1, flesh_fields => {mbts => ['usr']}}
1031 ]) or return $e->die_event;
1034 my $billings = $e->search_money_billing([
1039 order_by => {mb => 'amount desc'},
1041 flesh_fields => {mb => ['adjustments']},
1045 if ($xact->balance_owed == 0) {
1046 # if was zero, or we rebilled it to zero
1049 # it's positive and needs to be adjusted
1050 my @billings_to_zero = grep { !$U->is_true($_->voided) or !_is_fully_adjusted($_) } @$billings;
1051 $CC->adjust_bills_to_zero($e, \@billings_to_zero, "System: MANUAL ADJUSTMENT");
1054 push(@modified, $xact->id);
1056 # now we see if we can close the transaction
1057 # same logic as make_payments();
1058 my $circ = $e->retrieve_action_circulation($xact_id);
1059 if ($circ and !$CC->can_close_circ($e, $circ)) {
1060 # we don't check to see if the xact is already closed. since the
1061 # xact had a negative balance, it should not have been closed, so
1062 # assume 'now' is the correct close time regardless.
1063 my $trans = $e->retrieve_money_billable_transaction($xact_id);
1064 $trans->xact_finish("now");
1065 $e->update_money_billable_transaction($trans) or return $e->die_event;
1074 __PACKAGE__->register_method(
1075 method => 'edit_bill_note',
1076 api_name => 'open-ils.circ.money.billing.note.edit',
1078 Edits the note for a bill
1079 @param authtoken Login session key
1080 @param note The replacement note for the bills we're editing
1081 @param billid Id for the bill to edit the note of. This parameter may be repeated to reference other bills.
1082 @return 1 on success, Event on error
1085 sub edit_bill_note {
1086 my( $s, $c, $authtoken, $note, @billids ) = @_;
1088 my $e = new_editor( authtoken => $authtoken, xact => 1 );
1089 return $e->die_event unless $e->checkauth;
1090 return $e->die_event unless $e->allowed('UPDATE_BILL_NOTE');
1092 for my $billid (@billids) {
1094 my $bill = $e->retrieve_money_billing($billid)
1095 or return $e->die_event;
1098 # 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.
1100 $e->update_money_billing($bill) or return $e->die_event;
1107 __PACKAGE__->register_method(
1108 method => 'edit_payment_note',
1109 api_name => 'open-ils.circ.money.payment.note.edit',
1111 Edits the note for a payment
1112 @param authtoken Login session key
1113 @param note The replacement note for the payments we're editing
1114 @param paymentid Id for the payment to edit the note of. This parameter may be repeated to reference other payments.
1115 @return 1 on success, Event on error
1118 sub edit_payment_note {
1119 my( $s, $c, $authtoken, $note, @paymentids ) = @_;
1121 my $e = new_editor( authtoken => $authtoken, xact => 1 );
1122 return $e->die_event unless $e->checkauth;
1123 return $e->die_event unless $e->allowed('UPDATE_PAYMENT_NOTE');
1125 for my $paymentid (@paymentids) {
1127 my $payment = $e->retrieve_money_payment($paymentid)
1128 or return $e->die_event;
1130 $payment->note($note);
1131 # 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.
1133 $e->update_money_payment($payment) or return $e->die_event;
1141 __PACKAGE__->register_method (
1142 method => 'fetch_mbts',
1144 api_name => 'open-ils.circ.money.billable_xact_summary.retrieve'
1147 my( $self, $conn, $auth, $id) = @_;
1149 my $e = new_editor(xact => 1, authtoken=>$auth);
1150 return $e->event unless $e->checkauth;
1151 my ($mbts) = $U->fetch_mbts($id, $e);
1153 my $user = $e->retrieve_actor_user($mbts->usr)
1154 or return $e->die_event;
1156 return $e->die_event unless $e->allowed('VIEW_TRANSACTION', $user->home_ou);
1162 __PACKAGE__->register_method(
1163 method => 'desk_payments',
1164 api_name => 'open-ils.circ.money.org_unit.desk_payments'
1167 my( $self, $conn, $auth, $org, $start_date, $end_date ) = @_;
1168 my $e = new_editor(authtoken=>$auth);
1169 return $e->event unless $e->checkauth;
1170 return $e->event unless $e->allowed('VIEW_TRANSACTION', $org);
1171 my $data = $U->storagereq(
1172 'open-ils.storage.money.org_unit.desk_payments.atomic',
1173 $org, $start_date, $end_date );
1175 $_->workstation( $_->workstation->name ) for(@$data);
1180 __PACKAGE__->register_method(
1181 method => 'user_payments',
1182 api_name => 'open-ils.circ.money.org_unit.user_payments'
1186 my( $self, $conn, $auth, $org, $start_date, $end_date ) = @_;
1187 my $e = new_editor(authtoken=>$auth);
1188 return $e->event unless $e->checkauth;
1189 return $e->event unless $e->allowed('VIEW_TRANSACTION', $org);
1190 my $data = $U->storagereq(
1191 'open-ils.storage.money.org_unit.user_payments.atomic',
1192 $org, $start_date, $end_date );
1195 $e->retrieve_actor_card($_->usr->card)->barcode);
1197 $e->retrieve_actor_org_unit($_->usr->home_ou)->shortname);
1203 __PACKAGE__->register_method(
1204 method => 'retrieve_credit_payable_balance',
1205 api_name => 'open-ils.circ.credit.payable_balance.retrieve',
1208 desc => q/Returns the total amount the patron can pay via credit card/,
1210 { desc => 'Authentication token', type => 'string' },
1211 { desc => 'User id', type => 'number' }
1213 return => { desc => 'The ID of the new provider' }
1217 sub retrieve_credit_payable_balance {
1218 my ( $self, $conn, $auth, $user_id ) = @_;
1219 my $e = new_editor(authtoken => $auth);
1220 return $e->event unless $e->checkauth;
1222 my $user = $e->retrieve_actor_user($user_id)
1223 or return $e->event;
1225 if($e->requestor->id != $user_id) {
1226 return $e->event unless $e->allowed('VIEW_USER_TRANSACTIONS', $user->home_ou)
1229 my $circ_orgs = $e->json_query({
1230 "select" => {circ => ["circ_lib"]},
1232 "where" => {usr => $user_id, xact_finish => undef},
1236 my $groc_orgs = $e->json_query({
1237 "select" => {mg => ["billing_location"]},
1239 "where" => {usr => $user_id, xact_finish => undef},
1244 for my $org ( @$circ_orgs, @$groc_orgs ) {
1245 my $o = $org->{billing_location};
1246 $o = $org->{circ_lib} unless $o;
1247 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.
1248 $hash{$o} = $U->ou_ancestor_setting_value($o, 'credit.payments.allow', $e);
1251 my @credit_orgs = map { $hash{$_} ? ($_) : () } keys %hash;
1252 $logger->debug("credit: relevant orgs that allow credit payments => @credit_orgs");
1254 my $xact_summaries =
1255 OpenILS::Application::AppUtils->simplereq('open-ils.actor',
1256 'open-ils.actor.user.transactions.have_charge', $auth, $user_id);
1260 for my $xact (@$xact_summaries) {
1262 # make two lists and grab them in batch XXX
1263 if ( $xact->xact_type eq 'circulation' ) {
1264 my $circ = $e->retrieve_action_circulation($xact->id) or return $e->event;
1265 next unless grep { $_ == $circ->circ_lib } @credit_orgs;
1267 } elsif ($xact->xact_type eq 'grocery') {
1268 my $bill = $e->retrieve_money_grocery($xact->id) or return $e->event;
1269 next unless grep { $_ == $bill->billing_location } @credit_orgs;
1270 } elsif ($xact->xact_type eq 'reservation') {
1271 my $bill = $e->retrieve_booking_reservation($xact->id) or return $e->event;
1272 next unless grep { $_ == $bill->pickup_lib } @credit_orgs;
1274 $sum += $xact->balance_owed();