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