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 __PACKAGE__->register_method(
32 method => "make_payments",
33 api_name => "open-ils.circ.money.payment",
35 desc => q/Create payments for a given user and set of transactions,
36 login must have CREATE_PAYMENT privileges.
37 If any payments fail, all are reverted back./,
39 {desc => 'Authtoken', type => 'string'},
40 {desc => q/Arguments Hash, supporting the following params:
47 where_process 1 to use processor, !1 for out-of-band
48 approval_code (for out-of-band payment)
49 type (for out-of-band payment)
50 number (for call to payment processor)
51 expire_month (for call to payment processor)
52 expire_year (for call to payment processor)
53 note (if payments->{note} is blank, use this)
66 my($self, $client, $auth, $payments) = @_;
68 my $e = new_editor(authtoken => $auth, xact => 1);
69 return $e->die_event unless $e->checkauth;
71 my $type = $payments->{payment_type};
72 my $user_id = $payments->{userid};
73 my $credit = $payments->{patron_credit} || 0;
74 my $drawer = $e->requestor->wsid;
75 my $note = $payments->{note};
76 my $cc_args = $payments->{cc_args};
77 my $check_number = $payments->{check_number};
79 my $this_ou = $e->requestor->ws_ou;
82 # unless/until determined by payment processor API
83 my ($approval_code, $cc_processor, $cc_type) = (undef,undef,undef);
85 my $patron = $e->retrieve_actor_user($user_id) or return $e->die_event;
87 # A user is allowed to make credit card payments on his/her own behalf
88 # All other scenarious require permission
89 unless($type eq 'credit_card_payment' and $user_id == $e->requestor->id) {
90 return $e->die_event unless $e->allowed('CREATE_PAYMENT', $patron->home_ou);
93 # first collect the transactions and make sure the transaction
94 # user matches the requested user
96 for my $pay (@{$payments->{payments}}) {
97 my $xact_id = $pay->[0];
98 my $xact = $e->retrieve_money_billable_transaction_summary($xact_id)
99 or return $e->die_event;
101 if($xact->usr != $user_id) {
103 return OpenILS::Event->new('BAD_PARAMS', note => q/user does not match transaction/);
106 $xacts{$xact_id} = $xact;
111 for my $pay (@{$payments->{payments}}) {
112 my $transid = $pay->[0];
113 my $amount = $pay->[1];
114 $amount =~ s/\$//og; # just to be safe
115 my $trans = $xacts{$transid};
117 $total_paid += $amount;
119 $orgs{$U->xact_org($transid, $e)} = 1;
121 # A negative payment is a refund.
124 # Negative credit card payments are not allowed
125 if($type eq 'credit_card_payment') {
127 return OpenILS::Event->new(
129 note => q/Negative credit card payments not allowed/
133 # If the refund causes the transaction balance to exceed 0 dollars,
134 # we are in effect loaning the patron money. This is not allowed.
135 if( ($trans->balance_owed - $amount) > 0 ) {
137 return OpenILS::Event->new('REFUND_EXCEEDS_BALANCE');
140 # Otherwise, make sure the refund does not exceed desk payments
141 # This is also not allowed
143 my $desk_payments = $e->search_money_desk_payment({xact => $transid, voided => 'f'});
144 $desk_total += $_->amount for @$desk_payments;
146 if( (-$amount) > $desk_total ) {
148 return OpenILS::Event->new(
149 'REFUND_EXCEEDS_DESK_PAYMENTS',
150 payload => { allowed_refund => $desk_total, submitted_refund => -$amount } );
154 my $payobj = "Fieldmapper::money::$type";
155 $payobj = $payobj->new;
157 $payobj->amount($amount);
158 $payobj->amount_collected($amount);
159 $payobj->xact($transid);
160 $payobj->note($note);
161 if ((not $payobj->note) and ($type eq 'credit_card_payment')) {
162 $payobj->note($cc_args->{note});
165 if ($payobj->has_field('accepting_usr')) { $payobj->accepting_usr($e->requestor->id); }
166 if ($payobj->has_field('cash_drawer')) { $payobj->cash_drawer($drawer); }
167 if ($payobj->has_field('cc_type')) { $payobj->cc_type($cc_args->{type}); }
168 if ($payobj->has_field('check_number')) { $payobj->check_number($check_number); }
170 # Store the last 4 digits of the CC number
171 if ($payobj->has_field('cc_number')) {
172 $payobj->cc_number(substr($cc_args->{number}, -4));
174 if ($payobj->has_field('expire_month')) { $payobj->expire_month($cc_args->{expire_month}); }
175 if ($payobj->has_field('expire_year')) { $payobj->expire_year($cc_args->{expire_year}); }
177 # Note: It is important not to set approval_code
178 # on the fieldmapper object yet.
180 push(@payment_objs, $payobj);
182 } # all payment objects have been created and inserted.
184 #### NO WRITES TO THE DB ABOVE THIS LINE -- THEY'LL ONLY BE DISCARDED ###
187 # After we try to externally process a credit card (if desired), we'll
188 # open a new transaction. We cannot leave one open while credit card
189 # processing might be happening, as it can easily time out the database
191 if($type eq 'credit_card_payment') {
192 $approval_code = $cc_args->{approval_code};
193 # If an approval code was not given, we'll need
194 # to call to the payment processor ourselves.
195 if ($cc_args->{where_process} == 1) {
196 return OpenILS::Event->new('BAD_PARAMS', note => 'Need CC number')
197 if not $cc_args->{number};
198 my $response = $apputils->simplereq(
200 'open-ils.credit.process',
202 "desc" => $cc_args->{note},
203 "amount" => $total_paid,
204 "patron_id" => $user_id,
205 "cc" => $cc_args->{number},
206 "expiration" => sprintf(
208 $cc_args->{expire_month},
209 $cc_args->{expire_year}
215 if (exists $response->{ilsevent}) {
218 if ($response->{statusCode} != 200) {
219 $logger->info("Credit card payment for user $user_id " .
220 "failed with message: " . $response->{statusText});
221 return OpenILS::Event->new(
222 'CREDIT_PROCESSOR_DECLINED_TRANSACTION',
223 note => $response->{statusText}
226 $approval_code = $response->{approvalCode};
227 $cc_type = $response->{cardType};
228 $cc_processor = $response->{processor};
229 $logger->info("Credit card payment processing for " .
230 "user $user_id succeeded");
233 return OpenILS::Event->new(
234 'BAD_PARAMS', note => 'Need approval code'
235 ) if not $cc_args->{approval_code};
239 ### RE-OPEN TRANSACTION HERE ###
242 # create payment records
243 my $create_money_method = "create_money_" . $type;
244 for my $payment (@payment_objs) {
245 # update the transaction if it's done
246 my $amount = $payment->amount;
247 my $transid = $payment->xact;
248 my $trans = $xacts{$transid};
249 if( (my $cred = ($trans->balance_owed - $amount)) <= 0 ) {
250 # Any overpay on this transaction goes directly into patron
251 # credit making payment with existing patron credit.
252 $credit -= $amount if $type eq 'credit_payment';
256 my $circ = $e->retrieve_action_circulation($transid);
258 if(!$circ || $circ->stop_fines) {
259 # If this is a circulation, we can't close the transaction
260 # unless stop_fines is set.
261 $trans = $e->retrieve_money_billable_transaction($transid);
262 $trans->xact_finish("now");
263 if (!$e->update_money_billable_transaction($trans)) {
264 $logger->warn("update_money_billable_transaction() " .
267 return OpenILS::Event->new(
268 'CREDIT_PROCESSOR_SUCCESS_WO_RECORD',
269 note => 'update_money_billable_transaction() failed'
275 $payment->approval_code($approval_code) if $approval_code;
276 $payment->cc_type($cc_type) if $cc_type;
277 $payment->cc_processor($cc_processor) if $cc_processor;
278 if (!$e->$create_money_method($payment)) {
279 $logger->warn("$create_money_method failed: " .
280 Dumper($payment)); # won't contain CC number.
282 return OpenILS::Event->new(
283 'CREDIT_PROCESSOR_SUCCESS_WO_RECORD',
284 note => "$create_money_method failed"
289 my $evt = _update_patron_credit($e, $patron, $credit);
291 $logger->warn("_update_patron_credit() failed");
293 return OpenILS::Event->new(
294 'CREDIT_PROCESSOR_SUCCESS_WO_RECORD',
295 note => "_update_patron_credit() failed"
299 for my $org_id (keys %orgs) {
300 # calculate penalties for each of the affected orgs
301 $evt = OpenILS::Utils::Penalty->calculate_penalties(
302 $e, $user_id, $org_id
306 "OpenILS::Utils::Penalty::calculate_penalties() failed"
309 return OpenILS::Event->new(
310 'CREDIT_PROCESSOR_SUCCESS_WO_RECORD',
311 note => "OpenILS::Utils::Penalty::calculate_penalties() failed"
320 sub _update_patron_credit {
321 my($e, $patron, $credit) = @_;
322 return undef if $credit == 0;
323 $patron->credit_forward_balance($patron->credit_forward_balance + $credit);
324 return OpenILS::Event->new('NEGATIVE_PATRON_BALANCE') if $patron->credit_forward_balance < 0;
325 $e->update_actor_user($patron) or return $e->die_event;
330 __PACKAGE__->register_method(
331 method => "retrieve_payments",
332 api_name => "open-ils.circ.money.payment.retrieve.all_",
333 notes => "Returns a list of payments attached to a given transaction"
335 sub retrieve_payments {
336 my( $self, $client, $login, $transid ) = @_;
339 $apputils->checksesperm($login, 'VIEW_TRANSACTION');
342 # XXX the logic here is wrong.. we need to check the owner of the transaction
343 # to make sure the requestor has access
345 # XXX grab the view, for each object in the view, grab the real object
347 return $apputils->simplereq(
349 'open-ils.cstore.direct.money.payment.search.atomic', { xact => $transid } );
353 __PACKAGE__->register_method(
354 method => "retrieve_payments2",
356 api_name => "open-ils.circ.money.payment.retrieve.all",
357 notes => "Returns a list of payments attached to a given transaction"
360 sub retrieve_payments2 {
361 my( $self, $client, $login, $transid ) = @_;
363 my $e = new_editor(authtoken=>$login);
364 return $e->event unless $e->checkauth;
365 return $e->event unless $e->allowed('VIEW_TRANSACTION');
368 my $pmnts = $e->search_money_payment({ xact => $transid });
370 my $type = $_->payment_type;
371 my $meth = "retrieve_money_$type";
372 my $p = $e->$meth($_->id) or return $e->event;
373 $p->payment_type($type);
374 $p->cash_drawer($e->retrieve_actor_workstation($p->cash_drawer))
375 if $p->has_field('cash_drawer');
376 push( @payments, $p );
383 __PACKAGE__->register_method(
384 method => "create_grocery_bill",
385 api_name => "open-ils.circ.money.grocery.create",
387 Creates a new grocery transaction using the transaction object provided
388 PARAMS: (login_session, money.grocery (mg) object)
391 sub create_grocery_bill {
392 my( $self, $client, $login, $transaction ) = @_;
394 my( $staff, $evt ) = $apputils->checkses($login);
396 $evt = $apputils->check_perms($staff->id,
397 $transaction->billing_location, 'CREATE_TRANSACTION' );
401 $logger->activity("Creating grocery bill " . Dumper($transaction) );
403 $transaction->clear_id;
404 my $session = $apputils->start_db_session;
405 my $transid = $session->request(
406 'open-ils.storage.direct.money.grocery.create', $transaction)->gather(1);
408 throw OpenSRF::EX ("Error creating new money.grocery") unless defined $transid;
410 $logger->debug("Created new grocery transaction $transid");
412 $apputils->commit_db_session($session);
414 my $e = new_editor(xact=>1);
415 $evt = _check_open_xact($e, $transid);
423 __PACKAGE__->register_method(
424 method => 'fetch_grocery',
425 api_name => 'open-ils.circ.money.grocery.retrieve'
428 my( $self, $conn, $auth, $id ) = @_;
429 my $e = new_editor(authtoken=>$auth);
430 return $e->event unless $e->checkauth;
431 return $e->event unless $e->allowed('VIEW_TRANSACTION'); # eh.. basically the same permission
432 my $g = $e->retrieve_money_grocery($id)
438 __PACKAGE__->register_method(
439 method => "billing_items",
441 api_name => "open-ils.circ.money.billing.retrieve.all",
443 Returns a list of billing items for the given transaction.
444 PARAMS( login, transaction_id )
448 my( $self, $client, $login, $transid ) = @_;
450 my( $trans, $evt ) = $U->fetch_billable_xact($transid);
454 ($staff, $evt ) = $apputils->checkses($login);
457 if($staff->id ne $trans->usr) {
458 $evt = $U->check_perms($staff->id, $staff->home_ou, 'VIEW_TRANSACTION');
462 return $apputils->simplereq( 'open-ils.cstore',
463 'open-ils.cstore.direct.money.billing.search.atomic', { xact => $transid } )
467 __PACKAGE__->register_method(
468 method => "billing_items_create",
469 api_name => "open-ils.circ.money.billing.create",
471 Creates a new billing line item
472 PARAMS( login, bill_object (mb) )
475 sub billing_items_create {
476 my( $self, $client, $login, $billing ) = @_;
478 my $e = new_editor(authtoken => $login, xact => 1);
479 return $e->die_event unless $e->checkauth;
480 return $e->die_event unless $e->allowed('CREATE_BILL');
482 my $xact = $e->retrieve_money_billable_transaction($billing->xact)
483 or return $e->die_event;
485 # if the transaction was closed, re-open it
486 if($xact->xact_finish) {
487 $xact->clear_xact_finish;
488 $e->update_money_billable_transaction($xact)
489 or return $e->die_event;
492 my $amt = $billing->amount;
494 $billing->amount($amt);
496 $e->create_money_billing($billing) or return $e->die_event;
497 my $evt = OpenILS::Utils::Penalty->calculate_penalties($e, $xact->usr, $U->xact_org($xact->id));
505 __PACKAGE__->register_method(
506 method => 'void_bill',
507 api_name => 'open-ils.circ.money.billing.void',
510 @param authtoken Login session key
511 @param billid Id for the bill to void. This parameter may be repeated to reference other bills.
512 @return 1 on success, Event on error
516 my( $s, $c, $authtoken, @billids ) = @_;
518 my $e = new_editor( authtoken => $authtoken, xact => 1 );
519 return $e->die_event unless $e->checkauth;
520 return $e->die_event unless $e->allowed('VOID_BILLING');
523 for my $billid (@billids) {
525 my $bill = $e->retrieve_money_billing($billid)
526 or return $e->die_event;
528 my $xact = $e->retrieve_money_billable_transaction($bill->xact)
529 or return $e->die_event;
531 if($U->is_true($bill->voided)) {
533 return OpenILS::Event->new('BILL_ALREADY_VOIDED', payload => $bill);
536 my $org = $U->xact_org($bill->xact, $e);
537 $users{$xact->usr} = {} unless $users{$xact->usr};
538 $users{$xact->usr}->{$org} = 1;
541 $bill->voider($e->requestor->id);
542 $bill->void_time('now');
544 $e->update_money_billing($bill) or return $e->die_event;
545 my $evt = _check_open_xact($e, $bill->xact, $xact);
549 # calculate penalties for all user/org combinations
550 for my $user_id (keys %users) {
551 for my $org_id (keys %{$users{$user_id}}) {
552 OpenILS::Utils::Penalty->calculate_penalties($e, $user_id, $org_id);
560 __PACKAGE__->register_method(
561 method => 'edit_bill_note',
562 api_name => 'open-ils.circ.money.billing.note.edit',
564 Edits the note for a bill
565 @param authtoken Login session key
566 @param note The replacement note for the bills we're editing
567 @param billid Id for the bill to edit the note of. This parameter may be repeated to reference other bills.
568 @return 1 on success, Event on error
572 my( $s, $c, $authtoken, $note, @billids ) = @_;
574 my $e = new_editor( authtoken => $authtoken, xact => 1 );
575 return $e->die_event unless $e->checkauth;
576 return $e->die_event unless $e->allowed('UPDATE_BILL_NOTE');
578 for my $billid (@billids) {
580 my $bill = $e->retrieve_money_billing($billid)
581 or return $e->die_event;
584 # 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.
586 $e->update_money_billing($bill) or return $e->die_event;
593 __PACKAGE__->register_method(
594 method => 'edit_payment_note',
595 api_name => 'open-ils.circ.money.payment.note.edit',
597 Edits the note for a payment
598 @param authtoken Login session key
599 @param note The replacement note for the payments we're editing
600 @param paymentid Id for the payment to edit the note of. This parameter may be repeated to reference other payments.
601 @return 1 on success, Event on error
604 sub edit_payment_note {
605 my( $s, $c, $authtoken, $note, @paymentids ) = @_;
607 my $e = new_editor( authtoken => $authtoken, xact => 1 );
608 return $e->die_event unless $e->checkauth;
609 return $e->die_event unless $e->allowed('UPDATE_PAYMENT_NOTE');
611 for my $paymentid (@paymentids) {
613 my $payment = $e->retrieve_money_payment($paymentid)
614 or return $e->die_event;
616 $payment->note($note);
617 # 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.
619 $e->update_money_payment($payment) or return $e->die_event;
626 sub _check_open_xact {
627 my( $editor, $xactid, $xact ) = @_;
629 # Grab the transaction
630 $xact ||= $editor->retrieve_money_billable_transaction($xactid);
631 return $editor->event unless $xact;
632 $xactid ||= $xact->id;
634 # grab the summary and see how much is owed on this transaction
635 my ($summary) = $U->fetch_mbts($xactid, $editor);
637 # grab the circulation if it is a circ;
638 my $circ = $editor->retrieve_action_circulation($xactid);
640 # If nothing is owed on the transaction but it is still open
641 # and this transaction is not an open circulation, close it
643 ( $summary->balance_owed == 0 and ! $xact->xact_finish ) and
644 ( !$circ or $circ->stop_fines )) {
646 $logger->info("closing transaction ".$xact->id. ' becauase balance_owed == 0');
647 $xact->xact_finish('now');
648 $editor->update_money_billable_transaction($xact)
649 or return $editor->event;
653 # If money is owed or a refund is due on the xact and xact_finish
654 # is set, clear it (to reopen the xact) and update
655 if( $summary->balance_owed != 0 and $xact->xact_finish ) {
656 $logger->info("re-opening transaction ".$xact->id. ' becauase balance_owed != 0');
657 $xact->clear_xact_finish;
658 $editor->update_money_billable_transaction($xact)
659 or return $editor->event;
666 __PACKAGE__->register_method (
667 method => 'fetch_mbts',
669 api_name => 'open-ils.circ.money.billable_xact_summary.retrieve'
672 my( $self, $conn, $auth, $id) = @_;
674 my $e = new_editor(xact => 1, authtoken=>$auth);
675 return $e->event unless $e->checkauth;
676 my ($mbts) = $U->fetch_mbts($id, $e);
678 my $user = $e->retrieve_actor_user($mbts->usr)
679 or return $e->die_event;
681 return $e->die_event unless $e->allowed('VIEW_TRANSACTION', $user->home_ou);
687 __PACKAGE__->register_method(
688 method => 'desk_payments',
689 api_name => 'open-ils.circ.money.org_unit.desk_payments'
692 my( $self, $conn, $auth, $org, $start_date, $end_date ) = @_;
693 my $e = new_editor(authtoken=>$auth);
694 return $e->event unless $e->checkauth;
695 return $e->event unless $e->allowed('VIEW_TRANSACTION', $org);
696 my $data = $U->storagereq(
697 'open-ils.storage.money.org_unit.desk_payments.atomic',
698 $org, $start_date, $end_date );
700 $_->workstation( $_->workstation->name ) for(@$data);
705 __PACKAGE__->register_method(
706 method => 'user_payments',
707 api_name => 'open-ils.circ.money.org_unit.user_payments'
711 my( $self, $conn, $auth, $org, $start_date, $end_date ) = @_;
712 my $e = new_editor(authtoken=>$auth);
713 return $e->event unless $e->checkauth;
714 return $e->event unless $e->allowed('VIEW_TRANSACTION', $org);
715 my $data = $U->storagereq(
716 'open-ils.storage.money.org_unit.user_payments.atomic',
717 $org, $start_date, $end_date );
720 $e->retrieve_actor_card($_->usr->card)->barcode);
722 $e->retrieve_actor_org_unit($_->usr->home_ou)->shortname);