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