]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/src/perlmods/OpenILS/Application/Circ/Money.pm
return IDs of newly created payments on success
[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{Array of payment IDs 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     my @payment_ids;
303
304     # create payment records
305     my $create_money_method = "create_money_" . $type;
306     for my $payment (@payment_objs) {
307         # update the transaction if it's done
308         my $amount = $payment->amount;
309         my $transid = $payment->xact;
310         my $trans = $xacts{$transid};
311         if( (my $cred = ($trans->balance_owed - $amount)) <= 0 ) {
312             # Any overpay on this transaction goes directly into patron
313             # credit making payment with existing patron credit.
314             $credit -= $amount if $type eq 'credit_payment';
315
316             $cred = -$cred;
317             $credit += $cred;
318             my $circ = $e->retrieve_action_circulation($transid);
319
320             if(!$circ || $circ->stop_fines) {
321                 # If this is a circulation, we can't close the transaction
322                 # unless stop_fines is set.
323                 $trans = $e->retrieve_money_billable_transaction($transid);
324                 $trans->xact_finish("now");
325                 if (!$e->update_money_billable_transaction($trans)) {
326                     return _recording_failure(
327                         $e, "update_money_billable_transaction() failed",
328                         $payment, $cc_payload
329                     )
330                 }
331             }
332         }
333
334         $payment->approval_code($approval_code) if $approval_code;
335         $payment->cc_type($cc_type) if $cc_type;
336         $payment->cc_processor($cc_processor) if $cc_processor;
337         if (!$e->$create_money_method($payment)) {
338             return _recording_failure(
339                 $e, "$create_money_method failed", $payment, $cc_payload
340             );
341         }
342
343         push(@payment_ids, $payment->id);
344     }
345
346     my $evt = _update_patron_credit($e, $patron, $credit);
347     if ($evt) {
348         return _recording_failure(
349             $e, "_update_patron_credit() failed", undef, $cc_payload
350         );
351     }
352
353     for my $org_id (keys %orgs) {
354         # calculate penalties for each of the affected orgs
355         $evt = OpenILS::Utils::Penalty->calculate_penalties(
356             $e, $user_id, $org_id
357         );
358         if ($evt) {
359             return _recording_failure(
360                 $e, "calculate_penalties() failed", undef, $cc_payload
361             );
362         }
363     }
364
365     $e->commit;
366     return \@payment_ids;
367 }
368
369 sub _recording_failure {
370     my ($e, $msg, $payment, $payload) = @_;
371
372     if ($payload) { # If the payment processor already accepted a payment:
373         $logger->error($msg);
374         $logger->error("Payment processor payload: " . Dumper($payload));
375         # payment shouldn't contain CC number
376         $logger->error("Payment: " . Dumper($payment)) if $payment;
377
378         $e->rollback;
379
380         return new OpenILS::Event(
381             "CREDIT_PROCESSOR_SUCCESS_WO_RECORD",
382             "payload" => $payload
383         );
384     } else { # Otherwise, the problem is somewhat less severe:
385         $logger->warn($msg);
386         $logger->warn("Payment: " . Dumper($payment)) if $payment;
387         return $e->die_event;
388     }
389 }
390
391 sub _update_patron_credit {
392     my($e, $patron, $credit) = @_;
393     return undef if $credit == 0;
394     $patron->credit_forward_balance($patron->credit_forward_balance + $credit);
395     return OpenILS::Event->new('NEGATIVE_PATRON_BALANCE') if $patron->credit_forward_balance < 0;
396     $e->update_actor_user($patron) or return $e->die_event;
397     return undef;
398 }
399
400
401 __PACKAGE__->register_method(
402     method    => "retrieve_payments",
403     api_name    => "open-ils.circ.money.payment.retrieve.all_",
404     notes        => "Returns a list of payments attached to a given transaction"
405     );
406 sub retrieve_payments {
407     my( $self, $client, $login, $transid ) = @_;
408
409     my( $staff, $evt ) =  
410         $apputils->checksesperm($login, 'VIEW_TRANSACTION');
411     return $evt if $evt;
412
413     # XXX the logic here is wrong.. we need to check the owner of the transaction
414     # to make sure the requestor has access
415
416     # XXX grab the view, for each object in the view, grab the real object
417
418     return $apputils->simplereq(
419         'open-ils.cstore',
420         'open-ils.cstore.direct.money.payment.search.atomic', { xact => $transid } );
421 }
422
423
424 __PACKAGE__->register_method(
425     method    => "retrieve_payments2",
426     authoritative => 1,
427     api_name    => "open-ils.circ.money.payment.retrieve.all",
428     notes        => "Returns a list of payments attached to a given transaction"
429     );
430     
431 sub retrieve_payments2 {
432     my( $self, $client, $login, $transid ) = @_;
433
434     my $e = new_editor(authtoken=>$login);
435     return $e->event unless $e->checkauth;
436     return $e->event unless $e->allowed('VIEW_TRANSACTION');
437
438     my @payments;
439     my $pmnts = $e->search_money_payment({ xact => $transid });
440     for( @$pmnts ) {
441         my $type = $_->payment_type;
442         my $meth = "retrieve_money_$type";
443         my $p = $e->$meth($_->id) or return $e->event;
444         $p->payment_type($type);
445         $p->cash_drawer($e->retrieve_actor_workstation($p->cash_drawer))
446             if $p->has_field('cash_drawer');
447         push( @payments, $p );
448     }
449
450     return \@payments;
451 }
452
453 __PACKAGE__->register_method(
454     method    => "format_payment_receipt",
455     api_name  => "open-ils.circ.money.payment_receipt.print",
456     signature => {
457         desc   => 'Returns a printable receipt for the specified payments',
458         params => [
459             { desc => 'Authentication token',  type => 'string'},
460             { desc => 'Payment ID or array of payment IDs', type => 'number' },
461         ],
462         return => {
463             desc => q/An action_trigger.event object or error event./,
464             type => 'object',
465         }
466     }
467 );
468 __PACKAGE__->register_method(
469     method    => "format_payment_receipt",
470     api_name  => "open-ils.circ.money.payment_receipt.email",
471     signature => {
472         desc   => 'Emails a receipt for the specified payments to the user associated with the first payment',
473         params => [
474             { desc => 'Authentication token',  type => 'string'},
475             { desc => 'Payment ID or array of payment IDs', type => 'number' },
476         ],
477         return => {
478             desc => q/Undefined on success, otherwise an error event./,
479             type => 'object',
480         }
481     }
482 );
483
484 sub format_payment_receipt {
485     my($self, $conn, $auth, $mp_id) = @_;
486
487     my $mp_ids;
488     if (ref $mp_id ne 'ARRAY') {
489         $mp_ids = [ $mp_id ];
490     } else {
491         $mp_ids = $mp_id;
492     }
493
494     my $for_print = ($self->api_name =~ /print/);
495     my $for_email = ($self->api_name =~ /email/);
496     my $e = new_editor(authtoken => $auth);
497     return $e->event unless $e->checkauth;
498
499     my $payments = [];
500     for my $id (@$mp_ids) {
501
502         my $payment = $e->retrieve_money_payment([
503             $id,
504             {   flesh => 2,
505                 flesh_fields => {
506                     mp => ['xact'],
507                     mbt => ['usr']
508                 }
509             }
510         ]) or return OpenILS::Event->new('MP_NOT_FOUND');
511
512         return $e->event unless $e->allowed('VIEW_TRANSACTION', $payment->xact->usr->home_ou); 
513
514         push @$payments, $payment;
515     }
516
517     if ($for_print) {
518
519         return $U->fire_object_event(undef, 'money.format.payment_receipt.print', $payments, $$payments[0]->xact->usr->home_ou);
520
521     } elsif ($for_email) {
522
523         for my $p (@$payments) {
524             $U->create_events_for_hook('money.format.payment_receipt.email', $p, $p->xact->usr->home_ou, undef, undef, 1);
525         }
526     }
527
528     return undef;
529 }
530
531 __PACKAGE__->register_method(
532     method    => "create_grocery_bill",
533     api_name    => "open-ils.circ.money.grocery.create",
534     notes        => <<"    NOTE");
535     Creates a new grocery transaction using the transaction object provided
536     PARAMS: (login_session, money.grocery (mg) object)
537     NOTE
538
539 sub create_grocery_bill {
540     my( $self, $client, $login, $transaction ) = @_;
541
542     my( $staff, $evt ) = $apputils->checkses($login);
543     return $evt if $evt;
544     $evt = $apputils->check_perms($staff->id, 
545         $transaction->billing_location, 'CREATE_TRANSACTION' );
546     return $evt if $evt;
547
548
549     $logger->activity("Creating grocery bill " . Dumper($transaction) );
550
551     $transaction->clear_id;
552     my $session = $apputils->start_db_session;
553     my $transid = $session->request(
554         'open-ils.storage.direct.money.grocery.create', $transaction)->gather(1);
555
556     throw OpenSRF::EX ("Error creating new money.grocery") unless defined $transid;
557
558     $logger->debug("Created new grocery transaction $transid");
559     
560     $apputils->commit_db_session($session);
561
562     my $e = new_editor(xact=>1);
563     $evt = _check_open_xact($e, $transid);
564     return $evt if $evt;
565     $e->commit;
566
567     return $transid;
568 }
569
570
571 __PACKAGE__->register_method(
572     method => 'fetch_reservation',
573     api_name => 'open-ils.circ.booking.reservation.retrieve'
574 );
575 sub fetch_reservation {
576     my( $self, $conn, $auth, $id ) = @_;
577     my $e = new_editor(authtoken=>$auth);
578     return $e->event unless $e->checkauth;
579     return $e->event unless $e->allowed('VIEW_TRANSACTION'); # eh.. basically the same permission
580     my $g = $e->retrieve_booking_reservation($id)
581         or return $e->event;
582     return $g;
583 }
584
585 __PACKAGE__->register_method(
586     method   => 'fetch_grocery',
587     api_name => 'open-ils.circ.money.grocery.retrieve'
588 );
589 sub fetch_grocery {
590     my( $self, $conn, $auth, $id ) = @_;
591     my $e = new_editor(authtoken=>$auth);
592     return $e->event unless $e->checkauth;
593     return $e->event unless $e->allowed('VIEW_TRANSACTION'); # eh.. basically the same permission
594     my $g = $e->retrieve_money_grocery($id)
595         or return $e->event;
596     return $g;
597 }
598
599
600 __PACKAGE__->register_method(
601     method        => "billing_items",
602     api_name      => "open-ils.circ.money.billing.retrieve.all",
603     authoritative => 1,
604     signature     => {
605         desc   => 'Returns a list of billing items for the given transaction ID.  ' .
606                   'If the operator is not the owner of the transaction, the VIEW_TRANSACTION permission is required.',
607         params => [
608             { desc => 'Authentication token', type => 'string'},
609             { desc => 'Transaction ID',       type => 'number'}
610         ],
611         return => {
612             desc => 'Transaction object, event on error'
613         },
614     }
615 );
616
617 sub billing_items {
618     my( $self, $client, $login, $transid ) = @_;
619
620     my( $trans, $evt ) = $U->fetch_billable_xact($transid);
621     return $evt if $evt;
622
623     my $staff;
624     ($staff, $evt ) = $apputils->checkses($login);
625     return $evt if $evt;
626
627     if($staff->id ne $trans->usr) {
628         $evt = $U->check_perms($staff->id, $staff->home_ou, 'VIEW_TRANSACTION');
629         return $evt if $evt;
630     }
631     
632     return $apputils->simplereq( 'open-ils.cstore',
633         'open-ils.cstore.direct.money.billing.search.atomic', { xact => $transid } )
634 }
635
636
637 __PACKAGE__->register_method(
638     method   => "billing_items_create",
639     api_name => "open-ils.circ.money.billing.create",
640     notes    => <<"    NOTE");
641     Creates a new billing line item
642     PARAMS( login, bill_object (mb) )
643     NOTE
644
645 sub billing_items_create {
646     my( $self, $client, $login, $billing ) = @_;
647
648     my $e = new_editor(authtoken => $login, xact => 1);
649     return $e->die_event unless $e->checkauth;
650     return $e->die_event unless $e->allowed('CREATE_BILL');
651
652     my $xact = $e->retrieve_money_billable_transaction($billing->xact)
653         or return $e->die_event;
654
655     # if the transaction was closed, re-open it
656     if($xact->xact_finish) {
657         $xact->clear_xact_finish;
658         $e->update_money_billable_transaction($xact)
659             or return $e->die_event;
660     }
661
662     my $amt = $billing->amount;
663     $amt =~ s/\$//og;
664     $billing->amount($amt);
665
666     $e->create_money_billing($billing) or return $e->die_event;
667     my $evt = OpenILS::Utils::Penalty->calculate_penalties($e, $xact->usr, $U->xact_org($xact->id));
668     return $evt if $evt;
669     $e->commit;
670
671     return $billing->id;
672 }
673
674
675 __PACKAGE__->register_method(
676     method        =>    'void_bill',
677     api_name        => 'open-ils.circ.money.billing.void',
678     signature    => q/
679         Voids a bill
680         @param authtoken Login session key
681         @param billid Id for the bill to void.  This parameter may be repeated to reference other bills.
682         @return 1 on success, Event on error
683     /
684 );
685 sub void_bill {
686     my( $s, $c, $authtoken, @billids ) = @_;
687
688     my $e = new_editor( authtoken => $authtoken, xact => 1 );
689     return $e->die_event unless $e->checkauth;
690     return $e->die_event unless $e->allowed('VOID_BILLING');
691
692     my %users;
693     for my $billid (@billids) {
694
695         my $bill = $e->retrieve_money_billing($billid)
696             or return $e->die_event;
697
698         my $xact = $e->retrieve_money_billable_transaction($bill->xact)
699             or return $e->die_event;
700
701         if($U->is_true($bill->voided)) {
702             $e->rollback;
703             return OpenILS::Event->new('BILL_ALREADY_VOIDED', payload => $bill);
704         }
705
706         my $org = $U->xact_org($bill->xact, $e);
707         $users{$xact->usr} = {} unless $users{$xact->usr};
708         $users{$xact->usr}->{$org} = 1;
709
710         $bill->voided('t');
711         $bill->voider($e->requestor->id);
712         $bill->void_time('now');
713     
714         $e->update_money_billing($bill) or return $e->die_event;
715         my $evt = _check_open_xact($e, $bill->xact, $xact);
716         return $evt if $evt;
717     }
718
719     # calculate penalties for all user/org combinations
720     for my $user_id (keys %users) {
721         for my $org_id (keys %{$users{$user_id}}) {
722             OpenILS::Utils::Penalty->calculate_penalties($e, $user_id, $org_id);
723         }
724     }
725     $e->commit;
726     return 1;
727 }
728
729
730 __PACKAGE__->register_method(
731     method        =>    'edit_bill_note',
732     api_name        => 'open-ils.circ.money.billing.note.edit',
733     signature    => q/
734         Edits the note for a bill
735         @param authtoken Login session key
736         @param note The replacement note for the bills we're editing
737         @param billid Id for the bill to edit the note of.  This parameter may be repeated to reference other bills.
738         @return 1 on success, Event on error
739     /
740 );
741 sub edit_bill_note {
742     my( $s, $c, $authtoken, $note, @billids ) = @_;
743
744     my $e = new_editor( authtoken => $authtoken, xact => 1 );
745     return $e->die_event unless $e->checkauth;
746     return $e->die_event unless $e->allowed('UPDATE_BILL_NOTE');
747
748     for my $billid (@billids) {
749
750         my $bill = $e->retrieve_money_billing($billid)
751             or return $e->die_event;
752
753         $bill->note($note);
754         # 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.
755     
756         $e->update_money_billing($bill) or return $e->die_event;
757     }
758     $e->commit;
759     return 1;
760 }
761
762
763 __PACKAGE__->register_method(
764     method        =>    'edit_payment_note',
765     api_name        => 'open-ils.circ.money.payment.note.edit',
766     signature    => q/
767         Edits the note for a payment
768         @param authtoken Login session key
769         @param note The replacement note for the payments we're editing
770         @param paymentid Id for the payment to edit the note of.  This parameter may be repeated to reference other payments.
771         @return 1 on success, Event on error
772     /
773 );
774 sub edit_payment_note {
775     my( $s, $c, $authtoken, $note, @paymentids ) = @_;
776
777     my $e = new_editor( authtoken => $authtoken, xact => 1 );
778     return $e->die_event unless $e->checkauth;
779     return $e->die_event unless $e->allowed('UPDATE_PAYMENT_NOTE');
780
781     for my $paymentid (@paymentids) {
782
783         my $payment = $e->retrieve_money_payment($paymentid)
784             or return $e->die_event;
785
786         $payment->note($note);
787         # 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.
788     
789         $e->update_money_payment($payment) or return $e->die_event;
790     }
791
792     $e->commit;
793     return 1;
794 }
795
796 sub _check_open_xact {
797     my( $editor, $xactid, $xact ) = @_;
798
799     # Grab the transaction
800     $xact ||= $editor->retrieve_money_billable_transaction($xactid);
801     return $editor->event unless $xact;
802     $xactid ||= $xact->id;
803
804     # grab the summary and see how much is owed on this transaction
805     my ($summary) = $U->fetch_mbts($xactid, $editor);
806
807     # grab the circulation if it is a circ;
808     my $circ = $editor->retrieve_action_circulation($xactid);
809
810     # If nothing is owed on the transaction but it is still open
811     # and this transaction is not an open circulation, close it
812     if( 
813         ( $summary->balance_owed == 0 and ! $xact->xact_finish ) and
814         ( !$circ or $circ->stop_fines )) {
815
816         $logger->info("closing transaction ".$xact->id. ' becauase balance_owed == 0');
817         $xact->xact_finish('now');
818         $editor->update_money_billable_transaction($xact)
819             or return $editor->event;
820         return undef;
821     }
822
823     # If money is owed or a refund is due on the xact and xact_finish
824     # is set, clear it (to reopen the xact) and update
825     if( $summary->balance_owed != 0 and $xact->xact_finish ) {
826         $logger->info("re-opening transaction ".$xact->id. ' becauase balance_owed != 0');
827         $xact->clear_xact_finish;
828         $editor->update_money_billable_transaction($xact)
829             or return $editor->event;
830         return undef;
831     }
832     return undef;
833 }
834
835
836 __PACKAGE__->register_method (
837     method => 'fetch_mbts',
838     authoritative => 1,
839     api_name => 'open-ils.circ.money.billable_xact_summary.retrieve'
840 );
841 sub fetch_mbts {
842     my( $self, $conn, $auth, $id) = @_;
843
844     my $e = new_editor(xact => 1, authtoken=>$auth);
845     return $e->event unless $e->checkauth;
846     my ($mbts) = $U->fetch_mbts($id, $e);
847
848     my $user = $e->retrieve_actor_user($mbts->usr)
849         or return $e->die_event;
850
851     return $e->die_event unless $e->allowed('VIEW_TRANSACTION', $user->home_ou);
852     $e->rollback;
853     return $mbts
854 }
855
856
857 __PACKAGE__->register_method(
858     method => 'desk_payments',
859     api_name => 'open-ils.circ.money.org_unit.desk_payments'
860 );
861 sub desk_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.desk_payments.atomic',
868         $org, $start_date, $end_date );
869
870     $_->workstation( $_->workstation->name ) for(@$data);
871     return $data;
872 }
873
874
875 __PACKAGE__->register_method(
876     method => 'user_payments',
877     api_name => 'open-ils.circ.money.org_unit.user_payments'
878 );
879
880 sub user_payments {
881     my( $self, $conn, $auth, $org, $start_date, $end_date ) = @_;
882     my $e = new_editor(authtoken=>$auth);
883     return $e->event unless $e->checkauth;
884     return $e->event unless $e->allowed('VIEW_TRANSACTION', $org);
885     my $data = $U->storagereq(
886         'open-ils.storage.money.org_unit.user_payments.atomic',
887         $org, $start_date, $end_date );
888     for(@$data) {
889         $_->usr->card(
890             $e->retrieve_actor_card($_->usr->card)->barcode);
891         $_->usr->home_ou(
892             $e->retrieve_actor_org_unit($_->usr->home_ou)->shortname);
893     }
894     return $data;
895 }
896
897
898 __PACKAGE__->register_method(
899     method    => 'retrieve_credit_payable_balance',
900     api_name  => 'open-ils.circ.credit.payable_balance.retrieve',
901     authoritative => 1,
902     signature => {
903         desc   => q/Returns the total amount the patron can pay via credit card/,
904         params => [
905             { desc => 'Authentication token', type => 'string' },
906             { desc => 'User id', type => 'number' }
907         ],
908         return => { desc => 'The ID of the new provider' }
909     }
910 );
911
912 sub retrieve_credit_payable_balance {
913     my ( $self, $conn, $auth, $user_id ) = @_;
914     my $e = new_editor(authtoken => $auth);
915     return $e->event unless $e->checkauth;
916
917     my $user = $e->retrieve_actor_user($user_id) 
918         or return $e->event;
919
920     if($e->requestor->id != $user_id) {
921         return $e->event unless $e->allowed('VIEW_USER_TRANSACTIONS', $user->home_ou)
922     }
923
924     my $circ_orgs = $e->json_query({
925         "select" => {circ => ["circ_lib"]},
926         from     => "circ",
927         "where"  => {usr => $user_id, xact_finish => undef},
928         distinct => 1
929     });
930
931     my $groc_orgs = $e->json_query({
932         "select" => {mg => ["billing_location"]},
933         from     => "mg",
934         "where"  => {usr => $user_id, xact_finish => undef},
935         distinct => 1
936     });
937
938     my %hash;
939     for my $org ( @$circ_orgs, @$groc_orgs ) {
940         my $o = $org->{billing_location};
941         $o = $org->{circ_lib} unless $o;
942         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.
943         $hash{$o} = $U->ou_ancestor_setting_value($o, 'credit.payments.allow', $e);
944     }
945
946     my @credit_orgs = map { $hash{$_} ? ($_) : () } keys %hash;
947     $logger->debug("credit: relevant orgs that allow credit payments => @credit_orgs");
948
949     my $xact_summaries =
950       OpenILS::Application::AppUtils->simplereq('open-ils.actor',
951         'open-ils.actor.user.transactions.have_charge', $auth, $user_id);
952
953     my $sum = 0.0;
954
955     for my $xact (@$xact_summaries) {
956
957         # make two lists and grab them in batch XXX
958         if ( $xact->xact_type eq 'circulation' ) {
959             my $circ = $e->retrieve_action_circulation($xact->id) or return $e->event;
960             next unless grep { $_ == $circ->circ_lib } @credit_orgs;
961
962         } elsif ($xact->xact_type eq 'grocery') {
963             my $bill = $e->retrieve_money_grocery($xact->id) or return $e->event;
964             next unless grep { $_ == $bill->billing_location } @credit_orgs;
965         } elsif ($xact->xact_type eq 'reservation') {
966             my $bill = $e->retrieve_booking_reservation($xact->id) or return $e->event;
967             next unless grep { $_ == $bill->pickup_lib } @credit_orgs;
968         }
969         $sum += $xact->balance_owed();
970     }
971
972     return $sum;
973 }
974
975
976 1;