]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/src/perlmods/OpenILS/Application/Circ/Money.pm
Better documentation of payment outcomes, particularly credit card declines
[working/Evergreen.git] / Open-ILS / src / perlmods / OpenILS / Application / Circ / Money.pm
1 # ---------------------------------------------------------------
2 # Copyright (C) 2005  Georgia Public Library Service 
3 # Bill Erickson <billserickson@gmail.com>
4
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.
9
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 # ---------------------------------------------------------------
15
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";
22
23 use OpenSRF::EX qw(:try);
24 use OpenILS::Perm;
25 use Data::Dumper;
26 use OpenILS::Event;
27 use OpenSRF::Utils::Logger qw/:logger/;
28 use OpenILS::Utils::CStoreEditor qw/:funcs/;
29 use OpenILS::Utils::Penalty;
30
31 __PACKAGE__->register_method(
32     method => "make_payments",
33     api_name => "open-ils.circ.money.payment",
34     signature => {
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./,
38         params => [
39             {desc => 'Authtoken', type => 'string'},
40             {desc => q/Arguments Hash, supporting the following params:
41                 { 
42                     payment_type
43                     userid
44                     patron_credit
45                     note
46                     cc_args: {
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                         billing_first   (for call to payment processor)
54                         billing_last    (for call to payment processor)
55                         billing_address (for call to payment processor)
56                         billing_city    (for call to payment processor)
57                         billing_state   (for call to payment processor)
58                         billing_zip     (for call to payment processor)
59                         note            (if payments->{note} is blank, use this)
60                     },
61                     check_number
62                     payments: [ 
63                         [trans_id, amt], 
64                         [...]
65                     ], 
66                 }/, type => 'hash'
67             },
68         ],
69         "return" => {
70             "desc" =>
71                 q{1 on success, event on failure.  Event possibilities include:
72                 BAD_PARAMS
73                     Bad parameters were given to this API method itself.
74                     See note field.
75                 REFUND_EXCEEDS_BALANCE
76                 REFUND_EXCEEDS_DESK_PAYMENTS
77                 CREDIT_PROCESSOR_NOT_SPECIFIED
78                     Evergreen has not been set up to process CC payments.
79                 CREDIT_PROCESSOR_NOT_ALLOWED
80                     Evergreen has been incorrectly setup for CC payments.
81                 CREDIT_PROCESSOR_NOT_ENABLED
82                     Evergreen has been set up for CC payments, but an admin
83                     has not explicitly enabled them.
84                 CREDIT_PROCESSOR_BAD_PARAMS
85                     Evergreen has been incorrectly setup for CC payments;
86                     specifically, the login and/or password for the CC
87                     processor weren't provided.
88                 CREDIT_PROCESSOR_INVALID_CC_NUMBER
89                     You have supplied a credit card number that Evergreen
90                     has judged to be invalid even before attempting to contact
91                     the payment processor.
92                 CREDIT_PROCESSOR_DECLINED_TRANSACTION
93                     We contacted the CC processor to attempt the charge, but
94                     they declined it.
95                         The error_message field of the event payload will
96                         contain the payment processor's response.  This
97                         typically includes a message in plain English intended
98                         for human consumption.  In PayPal's case, the message
99                         is preceded by an integer, a colon, and a space, so
100                         a caller might take the 2nd match from /^(\d+: )?(.+)$/
101                         to present to the user.
102                         The payload also contains other fields from the payment
103                         processor, but these are generally not user-friendly
104                         strings.
105                 CREDIT_PROCESSOR_SUCCESS_WO_RECORD
106                     A payment was processed successfully, but couldn't be
107                     recorded in Evergreen.  This is _bad bad bad_, as it means
108                     somebody made a payment but isn't getting credit for it.
109                     See note field for more info.
110 },
111             "type" => "number"
112         }
113     }
114 );
115 sub make_payments {
116     my($self, $client, $auth, $payments) = @_;
117
118     my $e = new_editor(authtoken => $auth, xact => 1);
119     return $e->die_event unless $e->checkauth;
120
121     my $type = $payments->{payment_type};
122     my $user_id = $payments->{userid};
123     my $credit = $payments->{patron_credit} || 0;
124     my $drawer = $e->requestor->wsid;
125     my $note = $payments->{note};
126     my $cc_args = $payments->{cc_args};
127     my $check_number = $payments->{check_number};
128     my $total_paid = 0;
129     my $this_ou = $e->requestor->ws_ou;
130     my %orgs;
131
132     # unless/until determined by payment processor API
133     my ($approval_code, $cc_processor, $cc_type) = (undef,undef,undef);
134
135     my $patron = $e->retrieve_actor_user($user_id) or return $e->die_event;
136
137     # A user is allowed to make credit card payments on his/her own behalf
138     # All other scenarious require permission
139     unless($type eq 'credit_card_payment' and $user_id == $e->requestor->id) {
140         return $e->die_event unless $e->allowed('CREATE_PAYMENT', $patron->home_ou);
141     }
142
143     # first collect the transactions and make sure the transaction
144     # user matches the requested user
145     my %xacts;
146     for my $pay (@{$payments->{payments}}) {
147         my $xact_id = $pay->[0];
148         my $xact = $e->retrieve_money_billable_transaction_summary($xact_id)
149             or return $e->die_event;
150         
151         if($xact->usr != $user_id) {
152             $e->rollback;
153             return OpenILS::Event->new('BAD_PARAMS', note => q/user does not match transaction/);
154         }
155
156         $xacts{$xact_id} = $xact;
157     }
158
159     my @payment_objs;
160
161     for my $pay (@{$payments->{payments}}) {
162         my $transid = $pay->[0];
163         my $amount = $pay->[1];
164         $amount =~ s/\$//og; # just to be safe
165         my $trans = $xacts{$transid};
166
167         $total_paid += $amount;
168
169         $orgs{$U->xact_org($transid, $e)} = 1;
170
171         # A negative payment is a refund.  
172         if( $amount < 0 ) {
173
174             # Negative credit card payments are not allowed
175             if($type eq 'credit_card_payment') {
176                 $e->rollback;
177                 return OpenILS::Event->new(
178                     'BAD_PARAMS', 
179                     note => q/Negative credit card payments not allowed/
180                 );
181             }
182
183             # If the refund causes the transaction balance to exceed 0 dollars, 
184             # we are in effect loaning the patron money.  This is not allowed.
185             if( ($trans->balance_owed - $amount) > 0 ) {
186                 $e->rollback;
187                 return OpenILS::Event->new('REFUND_EXCEEDS_BALANCE');
188             }
189
190             # Otherwise, make sure the refund does not exceed desk payments
191             # This is also not allowed
192             my $desk_total = 0;
193             my $desk_payments = $e->search_money_desk_payment({xact => $transid, voided => 'f'});
194             $desk_total += $_->amount for @$desk_payments;
195
196             if( (-$amount) > $desk_total ) {
197                 $e->rollback;
198                 return OpenILS::Event->new(
199                     'REFUND_EXCEEDS_DESK_PAYMENTS', 
200                     payload => { allowed_refund => $desk_total, submitted_refund => -$amount } );
201             }
202         }
203
204         my $payobj = "Fieldmapper::money::$type";
205         $payobj = $payobj->new;
206
207         $payobj->amount($amount);
208         $payobj->amount_collected($amount);
209         $payobj->xact($transid);
210         $payobj->note($note);
211         if ((not $payobj->note) and ($type eq 'credit_card_payment')) {
212             $payobj->note($cc_args->{note});
213         }
214
215         if ($payobj->has_field('accepting_usr')) { $payobj->accepting_usr($e->requestor->id); }
216         if ($payobj->has_field('cash_drawer')) { $payobj->cash_drawer($drawer); }
217         if ($payobj->has_field('cc_type')) { $payobj->cc_type($cc_args->{type}); }
218         if ($payobj->has_field('check_number')) { $payobj->check_number($check_number); }
219
220         # Store the last 4 digits of the CC number
221         if ($payobj->has_field('cc_number')) {
222             $payobj->cc_number(substr($cc_args->{number}, -4));
223         }
224         if ($payobj->has_field('expire_month')) { $payobj->expire_month($cc_args->{expire_month}); }
225         if ($payobj->has_field('expire_year')) { $payobj->expire_year($cc_args->{expire_year}); }
226         
227         # Note: It is important not to set approval_code
228         # on the fieldmapper object yet.
229
230         push(@payment_objs, $payobj);
231
232     } # all payment objects have been created and inserted. 
233
234     #### NO WRITES TO THE DB ABOVE THIS LINE -- THEY'LL ONLY BE DISCARDED  ###
235     $e->rollback;
236
237     # After we try to externally process a credit card (if desired), we'll
238     # open a new transaction.  We cannot leave one open while credit card
239     # processing might be happening, as it can easily time out the database
240     # transaction.
241     if($type eq 'credit_card_payment') {
242         $approval_code = $cc_args->{approval_code};
243         # If an approval code was not given, we'll need
244         # to call to the payment processor ourselves.
245         if ($cc_args->{where_process} == 1) {
246             return OpenILS::Event->new('BAD_PARAMS', note => 'Need CC number')
247                 if not $cc_args->{number};
248             my $response =
249                 OpenILS::Application::Circ::CreditCard::process_payment({
250                     "desc" => $cc_args->{note},
251                     "amount" => $total_paid,
252                     "patron_id" => $user_id,
253                     "cc" => $cc_args->{number},
254                     "expiration" => sprintf(
255                         "%02d-%04d",
256                         $cc_args->{expire_month},
257                         $cc_args->{expire_year}
258                     ),
259                     "ou" => $this_ou,
260                     "first_name" => $cc_args->{billing_first},
261                     "last_name" => $cc_args->{billing_last},
262                     "address" => $cc_args->{billing_address},
263                     "city" => $cc_args->{billing_city},
264                     "state" => $cc_args->{billing_state},
265                     "zip" => $cc_args->{billing_zip},
266                 });
267
268             if ($U->event_code($response)) { # non-success
269                 $logger->info(
270                     "Credit card payment for user $user_id failed: " .
271                     $response->{"textcode"} . " " .
272                     $response->{"payload"}->{"error_message"}
273                 );
274
275                 return $response;
276             } else {
277                 $approval_code = $response->{"payload"}->{"authorization"};
278                 $cc_type = $response->{"payload"}->{"card_type"};
279                 $cc_processor = $response->{"payload"}->{"processor"};
280                 $logger->info(
281                     "Credit card payment for user $user_id succeeded"
282                 );
283             }
284         } else {
285             return OpenILS::Event->new(
286                 'BAD_PARAMS', note => 'Need approval code'
287             ) if not $cc_args->{approval_code};
288         }
289     }
290
291     ### RE-OPEN TRANSACTION HERE ###
292     $e->xact_begin;
293
294     # create payment records
295     my $create_money_method = "create_money_" . $type;
296     for my $payment (@payment_objs) {
297         # update the transaction if it's done
298         my $amount = $payment->amount;
299         my $transid = $payment->xact;
300         my $trans = $xacts{$transid};
301         if( (my $cred = ($trans->balance_owed - $amount)) <= 0 ) {
302             # Any overpay on this transaction goes directly into patron
303             # credit making payment with existing patron credit.
304             $credit -= $amount if $type eq 'credit_payment';
305
306             $cred = -$cred;
307             $credit += $cred;
308             my $circ = $e->retrieve_action_circulation($transid);
309
310             if(!$circ || $circ->stop_fines) {
311                 # If this is a circulation, we can't close the transaction
312                 # unless stop_fines is set.
313                 $trans = $e->retrieve_money_billable_transaction($transid);
314                 $trans->xact_finish("now");
315                 if (!$e->update_money_billable_transaction($trans)) {
316                     $logger->warn("update_money_billable_transaction() " .
317                         "failed");
318                     $e->rollback;
319                     return OpenILS::Event->new(
320                         'CREDIT_PROCESSOR_SUCCESS_WO_RECORD',
321                         note => 'update_money_billable_transaction() failed'
322                     );
323                 }
324             }
325         }
326
327         $payment->approval_code($approval_code) if $approval_code;
328         $payment->cc_type($cc_type) if $cc_type;
329         $payment->cc_processor($cc_processor) if $cc_processor;
330         if (!$e->$create_money_method($payment)) {
331             $logger->warn("$create_money_method failed: " .
332                 Dumper($payment)); # won't contain CC number.
333             $e->rollback;
334             return OpenILS::Event->new(
335                 'CREDIT_PROCESSOR_SUCCESS_WO_RECORD',
336                 note => "$create_money_method failed"
337             );
338         }
339     }
340
341     my $evt = _update_patron_credit($e, $patron, $credit);
342     if ($evt) {
343         $logger->warn("_update_patron_credit() failed");
344         $e->rollback;
345         return OpenILS::Event->new(
346             'CREDIT_PROCESSOR_SUCCESS_WO_RECORD',
347             note => "_update_patron_credit() failed"
348         );
349     }
350
351     for my $org_id (keys %orgs) {
352         # calculate penalties for each of the affected orgs
353         $evt = OpenILS::Utils::Penalty->calculate_penalties(
354             $e, $user_id, $org_id
355         );
356         if ($evt) {
357             $logger->warn(
358                 "OpenILS::Utils::Penalty::calculate_penalties() failed"
359             );
360             $e->rollback;
361             return OpenILS::Event->new(
362                 'CREDIT_PROCESSOR_SUCCESS_WO_RECORD',
363                 note => "OpenILS::Utils::Penalty::calculate_penalties() failed"
364             );
365         }
366     }
367
368     $e->commit;
369     return 1;
370 }
371
372 sub _update_patron_credit {
373     my($e, $patron, $credit) = @_;
374     return undef if $credit == 0;
375     $patron->credit_forward_balance($patron->credit_forward_balance + $credit);
376     return OpenILS::Event->new('NEGATIVE_PATRON_BALANCE') if $patron->credit_forward_balance < 0;
377     $e->update_actor_user($patron) or return $e->die_event;
378     return undef;
379 }
380
381
382 __PACKAGE__->register_method(
383     method    => "retrieve_payments",
384     api_name    => "open-ils.circ.money.payment.retrieve.all_",
385     notes        => "Returns a list of payments attached to a given transaction"
386     );
387 sub retrieve_payments {
388     my( $self, $client, $login, $transid ) = @_;
389
390     my( $staff, $evt ) =  
391         $apputils->checksesperm($login, 'VIEW_TRANSACTION');
392     return $evt if $evt;
393
394     # XXX the logic here is wrong.. we need to check the owner of the transaction
395     # to make sure the requestor has access
396
397     # XXX grab the view, for each object in the view, grab the real object
398
399     return $apputils->simplereq(
400         'open-ils.cstore',
401         'open-ils.cstore.direct.money.payment.search.atomic', { xact => $transid } );
402 }
403
404
405 __PACKAGE__->register_method(
406     method    => "retrieve_payments2",
407     authoritative => 1,
408     api_name    => "open-ils.circ.money.payment.retrieve.all",
409     notes        => "Returns a list of payments attached to a given transaction"
410     );
411     
412 sub retrieve_payments2 {
413     my( $self, $client, $login, $transid ) = @_;
414
415     my $e = new_editor(authtoken=>$login);
416     return $e->event unless $e->checkauth;
417     return $e->event unless $e->allowed('VIEW_TRANSACTION');
418
419     my @payments;
420     my $pmnts = $e->search_money_payment({ xact => $transid });
421     for( @$pmnts ) {
422         my $type = $_->payment_type;
423         my $meth = "retrieve_money_$type";
424         my $p = $e->$meth($_->id) or return $e->event;
425         $p->payment_type($type);
426         $p->cash_drawer($e->retrieve_actor_workstation($p->cash_drawer))
427             if $p->has_field('cash_drawer');
428         push( @payments, $p );
429     }
430
431     return \@payments;
432 }
433
434 __PACKAGE__->register_method(
435     method    => "format_payment_receipt",
436     api_name  => "open-ils.circ.money.payment_receipt.print",
437     signature => {
438         desc   => 'Returns a printable receipt for the specified payments',
439         params => [
440             { desc => 'Authentication token',  type => 'string'},
441             { desc => 'Payment ID or array of payment IDs', type => 'number' },
442         ],
443         return => {
444             desc => q/An action_trigger.event object or error event./,
445             type => 'object',
446         }
447     }
448 );
449 __PACKAGE__->register_method(
450     method    => "format_payment_receipt",
451     api_name  => "open-ils.circ.money.payment_receipt.email",
452     signature => {
453         desc   => 'Emails a receipt for the specified payments to the user associated with the first payment',
454         params => [
455             { desc => 'Authentication token',  type => 'string'},
456             { desc => 'Payment ID or array of payment IDs', type => 'number' },
457         ],
458         return => {
459             desc => q/Undefined on success, otherwise an error event./,
460             type => 'object',
461         }
462     }
463 );
464
465 sub format_payment_receipt {
466     my($self, $conn, $auth, $mp_id) = @_;
467
468     my $mp_ids;
469     if (ref $mp_id ne 'ARRAY') {
470         $mp_ids = [ $mp_id ];
471     } else {
472         $mp_ids = $mp_id;
473     }
474
475     my $for_print = ($self->api_name =~ /print/);
476     my $for_email = ($self->api_name =~ /email/);
477     my $e = new_editor(authtoken => $auth);
478     return $e->event unless $e->checkauth;
479
480     my $payments = [];
481     for my $id (@$mp_ids) {
482
483         my $payment = $e->retrieve_money_payment([
484             $id,
485             {   flesh => 2,
486                 flesh_fields => {
487                     mp => ['xact'],
488                     mbt => ['usr']
489                 }
490             }
491         ]) or return OpenILS::Event->new('MP_NOT_FOUND');
492
493         return $e->event unless $e->allowed('VIEW_TRANSACTION', $payment->xact->usr->home_ou); 
494
495         push @$payments, $payment;
496     }
497
498     if ($for_print) {
499
500         return $U->fire_object_event(undef, 'money.format.payment_receipt.print', $payments, $$payments[0]->xact->usr->home_ou);
501
502     } elsif ($for_email) {
503
504         for my $p (@$payments) {
505             $U->create_events_for_hook('money.format.payment_receipt.email', $p, $p->xact->usr->home_ou, undef, undef, 1);
506         }
507     }
508
509     return undef;
510 }
511
512 __PACKAGE__->register_method(
513     method    => "create_grocery_bill",
514     api_name    => "open-ils.circ.money.grocery.create",
515     notes        => <<"    NOTE");
516     Creates a new grocery transaction using the transaction object provided
517     PARAMS: (login_session, money.grocery (mg) object)
518     NOTE
519
520 sub create_grocery_bill {
521     my( $self, $client, $login, $transaction ) = @_;
522
523     my( $staff, $evt ) = $apputils->checkses($login);
524     return $evt if $evt;
525     $evt = $apputils->check_perms($staff->id, 
526         $transaction->billing_location, 'CREATE_TRANSACTION' );
527     return $evt if $evt;
528
529
530     $logger->activity("Creating grocery bill " . Dumper($transaction) );
531
532     $transaction->clear_id;
533     my $session = $apputils->start_db_session;
534     my $transid = $session->request(
535         'open-ils.storage.direct.money.grocery.create', $transaction)->gather(1);
536
537     throw OpenSRF::EX ("Error creating new money.grocery") unless defined $transid;
538
539     $logger->debug("Created new grocery transaction $transid");
540     
541     $apputils->commit_db_session($session);
542
543     my $e = new_editor(xact=>1);
544     $evt = _check_open_xact($e, $transid);
545     return $evt if $evt;
546     $e->commit;
547
548     return $transid;
549 }
550
551
552 __PACKAGE__->register_method(
553     method => 'fetch_reservation',
554     api_name => 'open-ils.circ.booking.reservation.retrieve'
555 );
556 sub fetch_reservation {
557     my( $self, $conn, $auth, $id ) = @_;
558     my $e = new_editor(authtoken=>$auth);
559     return $e->event unless $e->checkauth;
560     return $e->event unless $e->allowed('VIEW_TRANSACTION'); # eh.. basically the same permission
561     my $g = $e->retrieve_booking_reservation($id)
562         or return $e->event;
563     return $g;
564 }
565
566 __PACKAGE__->register_method(
567     method   => 'fetch_grocery',
568     api_name => 'open-ils.circ.money.grocery.retrieve'
569 );
570 sub fetch_grocery {
571     my( $self, $conn, $auth, $id ) = @_;
572     my $e = new_editor(authtoken=>$auth);
573     return $e->event unless $e->checkauth;
574     return $e->event unless $e->allowed('VIEW_TRANSACTION'); # eh.. basically the same permission
575     my $g = $e->retrieve_money_grocery($id)
576         or return $e->event;
577     return $g;
578 }
579
580
581 __PACKAGE__->register_method(
582     method        => "billing_items",
583     api_name      => "open-ils.circ.money.billing.retrieve.all",
584     authoritative => 1,
585     signature     => {
586         desc   => 'Returns a list of billing items for the given transaction ID.  ' .
587                   'If the operator is not the owner of the transaction, the VIEW_TRANSACTION permission is required.',
588         params => [
589             { desc => 'Authentication token', type => 'string'},
590             { desc => 'Transaction ID',       type => 'number'}
591         ],
592         return => {
593             desc => 'Transaction object, event on error'
594         },
595     }
596 );
597
598 sub billing_items {
599     my( $self, $client, $login, $transid ) = @_;
600
601     my( $trans, $evt ) = $U->fetch_billable_xact($transid);
602     return $evt if $evt;
603
604     my $staff;
605     ($staff, $evt ) = $apputils->checkses($login);
606     return $evt if $evt;
607
608     if($staff->id ne $trans->usr) {
609         $evt = $U->check_perms($staff->id, $staff->home_ou, 'VIEW_TRANSACTION');
610         return $evt if $evt;
611     }
612     
613     return $apputils->simplereq( 'open-ils.cstore',
614         'open-ils.cstore.direct.money.billing.search.atomic', { xact => $transid } )
615 }
616
617
618 __PACKAGE__->register_method(
619     method   => "billing_items_create",
620     api_name => "open-ils.circ.money.billing.create",
621     notes    => <<"    NOTE");
622     Creates a new billing line item
623     PARAMS( login, bill_object (mb) )
624     NOTE
625
626 sub billing_items_create {
627     my( $self, $client, $login, $billing ) = @_;
628
629     my $e = new_editor(authtoken => $login, xact => 1);
630     return $e->die_event unless $e->checkauth;
631     return $e->die_event unless $e->allowed('CREATE_BILL');
632
633     my $xact = $e->retrieve_money_billable_transaction($billing->xact)
634         or return $e->die_event;
635
636     # if the transaction was closed, re-open it
637     if($xact->xact_finish) {
638         $xact->clear_xact_finish;
639         $e->update_money_billable_transaction($xact)
640             or return $e->die_event;
641     }
642
643     my $amt = $billing->amount;
644     $amt =~ s/\$//og;
645     $billing->amount($amt);
646
647     $e->create_money_billing($billing) or return $e->die_event;
648     my $evt = OpenILS::Utils::Penalty->calculate_penalties($e, $xact->usr, $U->xact_org($xact->id));
649     return $evt if $evt;
650     $e->commit;
651
652     return $billing->id;
653 }
654
655
656 __PACKAGE__->register_method(
657     method        =>    'void_bill',
658     api_name        => 'open-ils.circ.money.billing.void',
659     signature    => q/
660         Voids a bill
661         @param authtoken Login session key
662         @param billid Id for the bill to void.  This parameter may be repeated to reference other bills.
663         @return 1 on success, Event on error
664     /
665 );
666 sub void_bill {
667     my( $s, $c, $authtoken, @billids ) = @_;
668
669     my $e = new_editor( authtoken => $authtoken, xact => 1 );
670     return $e->die_event unless $e->checkauth;
671     return $e->die_event unless $e->allowed('VOID_BILLING');
672
673     my %users;
674     for my $billid (@billids) {
675
676         my $bill = $e->retrieve_money_billing($billid)
677             or return $e->die_event;
678
679         my $xact = $e->retrieve_money_billable_transaction($bill->xact)
680             or return $e->die_event;
681
682         if($U->is_true($bill->voided)) {
683             $e->rollback;
684             return OpenILS::Event->new('BILL_ALREADY_VOIDED', payload => $bill);
685         }
686
687         my $org = $U->xact_org($bill->xact, $e);
688         $users{$xact->usr} = {} unless $users{$xact->usr};
689         $users{$xact->usr}->{$org} = 1;
690
691         $bill->voided('t');
692         $bill->voider($e->requestor->id);
693         $bill->void_time('now');
694     
695         $e->update_money_billing($bill) or return $e->die_event;
696         my $evt = _check_open_xact($e, $bill->xact, $xact);
697         return $evt if $evt;
698     }
699
700     # calculate penalties for all user/org combinations
701     for my $user_id (keys %users) {
702         for my $org_id (keys %{$users{$user_id}}) {
703             OpenILS::Utils::Penalty->calculate_penalties($e, $user_id, $org_id);
704         }
705     }
706     $e->commit;
707     return 1;
708 }
709
710
711 __PACKAGE__->register_method(
712     method        =>    'edit_bill_note',
713     api_name        => 'open-ils.circ.money.billing.note.edit',
714     signature    => q/
715         Edits the note for a bill
716         @param authtoken Login session key
717         @param note The replacement note for the bills we're editing
718         @param billid Id for the bill to edit the note of.  This parameter may be repeated to reference other bills.
719         @return 1 on success, Event on error
720     /
721 );
722 sub edit_bill_note {
723     my( $s, $c, $authtoken, $note, @billids ) = @_;
724
725     my $e = new_editor( authtoken => $authtoken, xact => 1 );
726     return $e->die_event unless $e->checkauth;
727     return $e->die_event unless $e->allowed('UPDATE_BILL_NOTE');
728
729     for my $billid (@billids) {
730
731         my $bill = $e->retrieve_money_billing($billid)
732             or return $e->die_event;
733
734         $bill->note($note);
735         # 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.
736     
737         $e->update_money_billing($bill) or return $e->die_event;
738     }
739     $e->commit;
740     return 1;
741 }
742
743
744 __PACKAGE__->register_method(
745     method        =>    'edit_payment_note',
746     api_name        => 'open-ils.circ.money.payment.note.edit',
747     signature    => q/
748         Edits the note for a payment
749         @param authtoken Login session key
750         @param note The replacement note for the payments we're editing
751         @param paymentid Id for the payment to edit the note of.  This parameter may be repeated to reference other payments.
752         @return 1 on success, Event on error
753     /
754 );
755 sub edit_payment_note {
756     my( $s, $c, $authtoken, $note, @paymentids ) = @_;
757
758     my $e = new_editor( authtoken => $authtoken, xact => 1 );
759     return $e->die_event unless $e->checkauth;
760     return $e->die_event unless $e->allowed('UPDATE_PAYMENT_NOTE');
761
762     for my $paymentid (@paymentids) {
763
764         my $payment = $e->retrieve_money_payment($paymentid)
765             or return $e->die_event;
766
767         $payment->note($note);
768         # 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.
769     
770         $e->update_money_payment($payment) or return $e->die_event;
771     }
772
773     $e->commit;
774     return 1;
775 }
776
777 sub _check_open_xact {
778     my( $editor, $xactid, $xact ) = @_;
779
780     # Grab the transaction
781     $xact ||= $editor->retrieve_money_billable_transaction($xactid);
782     return $editor->event unless $xact;
783     $xactid ||= $xact->id;
784
785     # grab the summary and see how much is owed on this transaction
786     my ($summary) = $U->fetch_mbts($xactid, $editor);
787
788     # grab the circulation if it is a circ;
789     my $circ = $editor->retrieve_action_circulation($xactid);
790
791     # If nothing is owed on the transaction but it is still open
792     # and this transaction is not an open circulation, close it
793     if( 
794         ( $summary->balance_owed == 0 and ! $xact->xact_finish ) and
795         ( !$circ or $circ->stop_fines )) {
796
797         $logger->info("closing transaction ".$xact->id. ' becauase balance_owed == 0');
798         $xact->xact_finish('now');
799         $editor->update_money_billable_transaction($xact)
800             or return $editor->event;
801         return undef;
802     }
803
804     # If money is owed or a refund is due on the xact and xact_finish
805     # is set, clear it (to reopen the xact) and update
806     if( $summary->balance_owed != 0 and $xact->xact_finish ) {
807         $logger->info("re-opening transaction ".$xact->id. ' becauase balance_owed != 0');
808         $xact->clear_xact_finish;
809         $editor->update_money_billable_transaction($xact)
810             or return $editor->event;
811         return undef;
812     }
813     return undef;
814 }
815
816
817 __PACKAGE__->register_method (
818     method => 'fetch_mbts',
819     authoritative => 1,
820     api_name => 'open-ils.circ.money.billable_xact_summary.retrieve'
821 );
822 sub fetch_mbts {
823     my( $self, $conn, $auth, $id) = @_;
824
825     my $e = new_editor(xact => 1, authtoken=>$auth);
826     return $e->event unless $e->checkauth;
827     my ($mbts) = $U->fetch_mbts($id, $e);
828
829     my $user = $e->retrieve_actor_user($mbts->usr)
830         or return $e->die_event;
831
832     return $e->die_event unless $e->allowed('VIEW_TRANSACTION', $user->home_ou);
833     $e->rollback;
834     return $mbts
835 }
836
837
838 __PACKAGE__->register_method(
839     method => 'desk_payments',
840     api_name => 'open-ils.circ.money.org_unit.desk_payments'
841 );
842 sub desk_payments {
843     my( $self, $conn, $auth, $org, $start_date, $end_date ) = @_;
844     my $e = new_editor(authtoken=>$auth);
845     return $e->event unless $e->checkauth;
846     return $e->event unless $e->allowed('VIEW_TRANSACTION', $org);
847     my $data = $U->storagereq(
848         'open-ils.storage.money.org_unit.desk_payments.atomic',
849         $org, $start_date, $end_date );
850
851     $_->workstation( $_->workstation->name ) for(@$data);
852     return $data;
853 }
854
855
856 __PACKAGE__->register_method(
857     method => 'user_payments',
858     api_name => 'open-ils.circ.money.org_unit.user_payments'
859 );
860
861 sub user_payments {
862     my( $self, $conn, $auth, $org, $start_date, $end_date ) = @_;
863     my $e = new_editor(authtoken=>$auth);
864     return $e->event unless $e->checkauth;
865     return $e->event unless $e->allowed('VIEW_TRANSACTION', $org);
866     my $data = $U->storagereq(
867         'open-ils.storage.money.org_unit.user_payments.atomic',
868         $org, $start_date, $end_date );
869     for(@$data) {
870         $_->usr->card(
871             $e->retrieve_actor_card($_->usr->card)->barcode);
872         $_->usr->home_ou(
873             $e->retrieve_actor_org_unit($_->usr->home_ou)->shortname);
874     }
875     return $data;
876 }
877
878
879 __PACKAGE__->register_method(
880     method    => 'retrieve_credit_payable_balance',
881     api_name  => 'open-ils.circ.credit.payable_balance.retrieve',
882     authoritative => 1,
883     signature => {
884         desc   => q/Returns the total amount the patron can pay via credit card/,
885         params => [
886             { desc => 'Authentication token', type => 'string' },
887             { desc => 'User id', type => 'number' }
888         ],
889         return => { desc => 'The ID of the new provider' }
890     }
891 );
892
893 sub retrieve_credit_payable_balance {
894     my ( $self, $conn, $auth, $user_id ) = @_;
895     my $e = new_editor(authtoken => $auth);
896     return $e->event unless $e->checkauth;
897
898     my $user = $e->retrieve_actor_user($user_id) 
899         or return $e->event;
900
901     if($e->requestor->id != $user_id) {
902         return $e->event unless $e->allowed('VIEW_USER_TRANSACTIONS', $user->home_ou)
903     }
904
905     my $circ_orgs = $e->json_query({
906         "select" => {circ => ["circ_lib"]},
907         from     => "circ",
908         "where"  => {usr => $user_id, xact_finish => undef},
909         distinct => 1
910     });
911
912     my $groc_orgs = $e->json_query({
913         "select" => {mg => ["billing_location"]},
914         from     => "mg",
915         "where"  => {usr => $user_id, xact_finish => undef},
916         distinct => 1
917     });
918
919     my %hash;
920     for my $org ( @$circ_orgs, @$groc_orgs ) {
921         my $o = $org->{billing_location};
922         $o = $org->{circ_lib} unless $o;
923         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.
924         $hash{$o} = $U->ou_ancestor_setting_value($o, 'credit.payments.allow', $e);
925     }
926
927     my @credit_orgs = map { $hash{$_} ? ($_) : () } keys %hash;
928     $logger->debug("credit: relevant orgs that allow credit payments => @credit_orgs");
929
930     my $xact_summaries =
931       OpenILS::Application::AppUtils->simplereq('open-ils.actor',
932         'open-ils.actor.user.transactions.have_charge', $auth, $user_id);
933
934     my $sum = 0.0;
935
936     for my $xact (@$xact_summaries) {
937
938         # make two lists and grab them in batch XXX
939         if ( $xact->xact_type eq 'circulation' ) {
940             my $circ = $e->retrieve_action_circulation($xact->id) or return $e->event;
941             next unless grep { $_ == $circ->circ_lib } @credit_orgs;
942
943         } elsif ($xact->xact_type eq 'grocery') {
944             my $bill = $e->retrieve_money_grocery($xact->id) or return $e->event;
945             next unless grep { $_ == $bill->billing_location } @credit_orgs;
946         } elsif ($xact->xact_type eq 'reservation') {
947             my $bill = $e->retrieve_booking_reservation($xact->id) or return $e->event;
948             next unless grep { $_ == $bill->pickup_lib } @credit_orgs;
949         }
950         $sum += $xact->balance_owed();
951     }
952
953     return $sum;
954 }
955
956
957 1;