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