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