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