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