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