7cfc16821d01315ec75eee6858d29ecf1b55761a
[evergreen-equinox.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 use OpenILS::Application::Circ::CircCommon;
21 my $apputils = "OpenILS::Application::AppUtils";
22 my $U = "OpenILS::Application::AppUtils";
23 my $CC = "OpenILS::Application::Circ::CircCommon";
24
25 use OpenSRF::EX qw(:try);
26 use OpenSRF::Utils::JSON;
27 use OpenILS::Perm;
28 use Data::Dumper;
29 use OpenILS::Event;
30 use OpenSRF::Utils::Logger qw/:logger/;
31 use OpenILS::Utils::CStoreEditor qw/:funcs/;
32 use OpenILS::Utils::Penalty;
33 use Business::Stripe;
34 $Data::Dumper::Indent = 0;
35 use OpenILS::Const qw/:const/;
36 use OpenILS::Utils::DateTime qw/:datetime/;
37 use DateTime::Format::ISO8601;
38 my $parser = DateTime::Format::ISO8601->new;
39
40 sub get_processor_settings {
41     my $e = shift;
42     my $org_unit = shift;
43     my $processor = lc shift;
44
45     # Get the names of every credit processor setting for our given processor.
46     # They're a little different per processor.
47     my $setting_names = $e->json_query({
48         select => {coust => ["name"]},
49         from => {coust => {}},
50         where => {name => {like => "credit.processor.${processor}.%"}}
51     }) or return $e->die_event;
52
53     # Make keys for a hash we're going to build out of the last dot-delimited
54     # component of each setting name.
55     ($_->{key} = $_->{name}) =~ s/.+\.(\w+)$/$1/ for @$setting_names;
56
57     # Return a hash with those short keys, and for values the value of
58     # the corresponding OU setting within our scope.
59     return {
60         map {
61             $_->{key} => $U->ou_ancestor_setting_value($org_unit, $_->{name})
62         } @$setting_names
63     };
64 }
65
66 # process_stripe_or_bop_payment()
67 # This is a helper method to make_payments() below (specifically,
68 # the credit-card part). It's the first point in the Perl code where
69 # we need to care about the distinction between Stripe and the
70 # Paypal/PayflowPro/AuthorizeNet kinds of processors (the latter group
71 # uses B::OP and handles payment card info, whereas Stripe doesn't use
72 # B::OP and doesn't require us to know anything about the payment card
73 # info).
74 #
75 # Return an event in all cases.  That means a success returns a SUCCESS
76 # event.
77 sub process_stripe_or_bop_payment {
78     my ($e, $user_id, $this_ou, $total_paid, $cc_args) = @_;
79
80     # A few stanzas to determine which processor we're using and whether we're
81     # really adequately set up for it.
82     if (!$cc_args->{processor}) {
83         if (!($cc_args->{processor} =
84                 $U->ou_ancestor_setting_value(
85                     $this_ou, 'credit.processor.default'
86                 )
87             )
88         ) {
89             return OpenILS::Event->new('CREDIT_PROCESSOR_NOT_SPECIFIED');
90         }
91     }
92
93     # Make sure the configured credit processor has a safe/correct name.
94     return OpenILS::Event->new('CREDIT_PROCESSOR_NOT_ALLOWED')
95         unless $cc_args->{processor} =~ /^[a-z0-9_\-]+$/i;
96
97     # Get the settings for the processor and make sure they're serviceable.
98     my $psettings = get_processor_settings($e, $this_ou, $cc_args->{processor});
99     return $psettings if defined $U->event_code($psettings);
100     return OpenILS::Event->new('CREDIT_PROCESSOR_NOT_ENABLED')
101         unless $psettings->{enabled};
102
103     # Now we branch. Stripe is one thing, and everything else is another.
104
105     if ($cc_args->{processor} eq 'Stripe') { # Stripe
106         my $stripe = Business::Stripe->new(-api_key => $psettings->{secretkey});
107         $stripe->api('post','payment_intents/' . $cc_args->{stripe_payment_intent});
108         if ($stripe->success) {
109             $logger->debug('Stripe payment intent retrieved');
110             my $intent = $stripe->success;
111             if ($intent->{status} eq 'succeeded') {
112                 $logger->info('Stripe payment succeeded');
113                 return OpenILS::Event->new(
114                     'SUCCESS', payload => {
115                         invoice => $intent->{invoice},
116                         customer => $intent->{customer},
117                         balance_transaction => 'N/A',
118                         id => $intent->{id},
119                         created => $intent->{created},
120                         card => 'N/A'
121                     }
122                 );
123             } else {
124                 $logger->info('Stripe payment failed');
125                 return OpenILS::Event->new(
126                     'CREDIT_PROCESSOR_DECLINED_TRANSACTION',
127                     payload => $intent->{last_payment_error}
128                 );
129             }
130         } else {
131             $logger->debug('Stripe payment intent not retrieved');
132             $logger->info('Stripe payment failed');
133             return OpenILS::Event->new(
134                 "CREDIT_PROCESSOR_DECLINED_TRANSACTION",
135                 payload => $stripe->error  # XXX what happens if this contains
136                                            # JSON::backportPP::* objects?
137             );
138         }
139
140     } else { # B::OP style (Paypal/PayflowPro/AuthorizeNet)
141         return OpenILS::Event->new('BAD_PARAMS', note => 'Need CC number')
142             unless $cc_args->{number};
143
144         return OpenILS::Application::Circ::CreditCard::process_payment({
145             "processor" => $cc_args->{processor},
146             "desc" => $cc_args->{note},
147             "amount" => $total_paid,
148             "patron_id" => $user_id,
149             "cc" => $cc_args->{number},
150             "expiration" => sprintf(
151                 "%02d-%04d",
152                 $cc_args->{expire_month},
153                 $cc_args->{expire_year}
154             ),
155             "ou" => $this_ou,
156             "first_name" => $cc_args->{billing_first},
157             "last_name" => $cc_args->{billing_last},
158             "address" => $cc_args->{billing_address},
159             "city" => $cc_args->{billing_city},
160             "state" => $cc_args->{billing_state},
161             "zip" => $cc_args->{billing_zip},
162             "cvv2" => $cc_args->{cvv2},
163             %$psettings
164         });
165
166     }
167 }
168
169 __PACKAGE__->register_method(
170     method => "make_payments",
171     api_name => "open-ils.circ.money.payment",
172     signature => {
173         desc => q/Create payments for a given user and set of transactions,
174             login must have CREATE_PAYMENT privileges.
175             If any payments fail, all are reverted back./,
176         params => [
177             {desc => 'Authtoken', type => 'string'},
178             {desc => q/Arguments Hash, supporting the following params:
179                 { 
180                     payment_type
181                     userid
182                     patron_credit
183                     note
184                     cc_args: {
185                         where_process   1 to use processor, !1 for out-of-band
186                         approval_code   (for out-of-band payment)
187                         type            (for out-of-band payment)
188                         number          (for call to payment processor)
189                         stripe_token    (for call to Stripe payment processor)
190                         expire_month    (for call to payment processor)
191                         expire_year     (for call to payment processor)
192                         billing_first   (for out-of-band payments and for call to payment processor)
193                         billing_last    (for out-of-band payments and for call to payment processor)
194                         billing_address (for call to payment processor)
195                         billing_city    (for call to payment processor)
196                         billing_state   (for call to payment processor)
197                         billing_zip     (for call to payment processor)
198                         note            (if payments->{note} is blank, use this)
199                     },
200                     check_number
201                     payments: [ 
202                         [trans_id, amt], 
203                         [...]
204                     ], 
205                 }/, type => 'hash'
206             },
207             {
208                 desc => q/Last user transaction ID.  This is the actor.usr.last_xact_id value/, 
209                 type => 'string'
210             }
211         ],
212         "return" => {
213             "desc" =>
214                 q{Array of payment IDs on success, event on failure.  Event possibilities include:
215                 BAD_PARAMS
216                     Bad parameters were given to this API method itself.
217                     See note field.
218                 INVALID_USER_XACT_ID
219                     The last user transaction ID does not match the ID in the database.  This means
220                     the user object has been updated since the last retrieval.  The client should
221                     be instructed to reload the user object and related transactions before attempting
222                     another payment
223                 REFUND_EXCEEDS_BALANCE
224                 REFUND_EXCEEDS_DESK_PAYMENTS
225                 CREDIT_PROCESSOR_NOT_SPECIFIED
226                     Evergreen has not been set up to process CC payments.
227                 CREDIT_PROCESSOR_NOT_ALLOWED
228                     Evergreen has been incorrectly setup for CC payments.
229                 CREDIT_PROCESSOR_NOT_ENABLED
230                     Evergreen has been set up for CC payments, but an admin
231                     has not explicitly enabled them.
232                 CREDIT_PROCESSOR_BAD_PARAMS
233                     Evergreen has been incorrectly setup for CC payments;
234                     specifically, the login and/or password for the CC
235                     processor weren't provided.
236                 CREDIT_PROCESSOR_INVALID_CC_NUMBER
237                     You have supplied a credit card number that Evergreen
238                     has judged to be invalid even before attempting to contact
239                     the payment processor.
240                 CREDIT_PROCESSOR_DECLINED_TRANSACTION
241                     We contacted the CC processor to attempt the charge, but
242                     they declined it.
243                         The error_message field of the event payload will
244                         contain the payment processor's response.  This
245                         typically includes a message in plain English intended
246                         for human consumption.  In PayPal's case, the message
247                         is preceded by an integer, a colon, and a space, so
248                         a caller might take the 2nd match from /^(\d+: )?(.+)$/
249                         to present to the user.
250                         The payload also contains other fields from the payment
251                         processor, but these are generally not user-friendly
252                         strings.
253                 CREDIT_PROCESSOR_SUCCESS_WO_RECORD
254                     A payment was processed successfully, but couldn't be
255                     recorded in Evergreen.  This is _bad bad bad_, as it means
256                     somebody made a payment but isn't getting credit for it.
257                     See errors in the system log if this happens.  Info from
258                     the credit card transaction will also be available in the
259                     event payload, although this probably won't be suitable for
260                     staff client/OPAC display.
261 },
262             "type" => "number"
263         }
264     }
265 );
266 sub make_payments {
267     my($self, $client, $auth, $payments, $last_xact_id) = @_;
268
269     my $e = new_editor(authtoken => $auth, xact => 1);
270     return $e->die_event unless $e->checkauth;
271
272     my $type = $payments->{payment_type};
273     my $user_id = $payments->{userid};
274     my $credit = $payments->{patron_credit} || 0;
275     my $drawer = $e->requestor->wsid;
276     my $note = $payments->{note};
277     my $cc_args = $payments->{cc_args};
278     my $check_number = $payments->{check_number};
279     my $total_paid = 0;
280     my $this_ou = $e->requestor->ws_ou || $e->requestor->home_ou;
281     my %orgs;
282
283
284     # unless/until determined by payment processor API
285     my ($approval_code, $cc_processor, $cc_order_number) = (undef,undef,undef, undef);
286
287     my $patron = $e->retrieve_actor_user($user_id) or return $e->die_event;
288
289     if($patron->last_xact_id ne $last_xact_id) {
290         $e->rollback;
291         return OpenILS::Event->new('INVALID_USER_XACT_ID');
292     }
293
294     # A user is allowed to make credit card payments on his/her own behalf
295     # All other scenarious require permission
296     unless($type eq 'credit_card_payment' and $user_id == $e->requestor->id) {
297         return $e->die_event unless $e->allowed('CREATE_PAYMENT', $patron->home_ou);
298     }
299
300     # first collect the transactions and make sure the transaction
301     # user matches the requested user
302     my %xacts;
303
304     # We rewrite the payments array for sanity's sake, to avoid more
305     # than one payment per transaction per call, which is not legitimate
306     # but has been seen in the wild coming from the staff client.  This
307     # is presumably a staff client (xulrunner) bug.
308     my @unique_xact_payments;
309     for my $pay (@{$payments->{payments}}) {
310         my $xact_id = $pay->[0];
311         if (exists($xacts{$xact_id})) {
312             $e->rollback;
313             return OpenILS::Event->new('MULTIPLE_PAYMENTS_FOR_XACT');
314         }
315
316         my $xact = $e->retrieve_money_billable_transaction_summary($xact_id)
317             or return $e->die_event;
318         
319         if($xact->usr != $user_id) {
320             $e->rollback;
321             return OpenILS::Event->new('BAD_PARAMS', note => q/user does not match transaction/);
322         }
323
324         $xacts{$xact_id} = $xact;
325         push @unique_xact_payments, $pay;
326     }
327     $payments->{payments} = \@unique_xact_payments;
328
329     my @payment_objs;
330
331     for my $pay (@{$payments->{payments}}) {
332         my $transid = $pay->[0];
333         my $amount = $pay->[1];
334         $amount =~ s/\$//og; # just to be safe
335         my $trans = $xacts{$transid};
336
337         # add amounts as integers
338         $total_paid += (100 * $amount);
339
340         my $org_id = $U->xact_org($transid, $e);
341
342         if (!$orgs{$org_id}) {
343             $orgs{$org_id} = 1;
344
345             # patron credit has to be allowed at all orgs receiving payment
346             if ($type eq 'credit_payment' and $U->ou_ancestor_setting_value(
347                     $org_id, 'circ.disable_patron_credit', $e)) {
348                 $e->rollback;
349                 return OpenILS::Event->new('PATRON_CREDIT_DISABLED');
350             }
351         }
352
353         # A negative payment is a refund.  
354         if( $amount < 0 ) {
355
356             # Negative credit card payments are not allowed
357             if($type eq 'credit_card_payment') {
358                 $e->rollback;
359                 return OpenILS::Event->new(
360                     'BAD_PARAMS', 
361                     note => q/Negative credit card payments not allowed/
362                 );
363             }
364
365             # If the refund causes the transaction balance to exceed 0 dollars, 
366             # we are in effect loaning the patron money.  This is not allowed.
367             if( ($trans->balance_owed - $amount) > 0 ) {
368                 $e->rollback;
369                 return OpenILS::Event->new('REFUND_EXCEEDS_BALANCE');
370             }
371
372             # Otherwise, make sure the refund does not exceed desk payments
373             # This is also not allowed
374             my $desk_total = 0;
375             my $desk_payments = $e->search_money_desk_payment({xact => $transid, voided => 'f'});
376             $desk_total += $_->amount for @$desk_payments;
377
378             if( (-$amount) > $desk_total ) {
379                 $e->rollback;
380                 return OpenILS::Event->new(
381                     'REFUND_EXCEEDS_DESK_PAYMENTS', 
382                     payload => { allowed_refund => $desk_total, submitted_refund => -$amount } );
383             }
384         }
385
386         my $payobj = "Fieldmapper::money::$type";
387         $payobj = $payobj->new;
388
389         $payobj->amount($amount);
390         $payobj->amount_collected($amount);
391         $payobj->xact($transid);
392         $payobj->note($note);
393         if ((not $payobj->note) and ($type eq 'credit_card_payment')) {
394             $payobj->note($cc_args->{note});
395         }
396
397         if ($payobj->has_field('accepting_usr')) { $payobj->accepting_usr($e->requestor->id); }
398         if ($payobj->has_field('cash_drawer')) { $payobj->cash_drawer($drawer); }
399         if ($payobj->has_field('check_number')) { $payobj->check_number($check_number); }
400
401         # Store the last 4 digits of the CC number
402         if ($payobj->has_field('cc_number')) {
403             $payobj->cc_number(substr($cc_args->{number}, -4));
404         }
405
406         # Note: It is important not to set approval_code
407         # on the fieldmapper object yet.
408
409         push(@payment_objs, $payobj);
410
411     } # all payment objects have been created and inserted. 
412
413     # return to decimal format, forcing X.YY format for consistency.
414     $total_paid = sprintf("%.2f", $total_paid / 100);
415
416     #### NO WRITES TO THE DB ABOVE THIS LINE -- THEY'LL ONLY BE DISCARDED  ###
417     $e->rollback;
418
419     # After we try to externally process a credit card (if desired), we'll
420     # open a new transaction.  We cannot leave one open while credit card
421     # processing might be happening, as it can easily time out the database
422     # transaction.
423
424     my $cc_payload;
425
426     if($type eq 'credit_card_payment') {
427         $approval_code = $cc_args->{approval_code};
428         # If an approval code was not given, we'll need
429         # to call to the payment processor ourselves.
430         if ($cc_args->{where_process} == 1) {
431             my $response = process_stripe_or_bop_payment(
432                 $e, $user_id, $this_ou, $total_paid, $cc_args
433             );
434
435             if ($U->event_code($response)) { # non-success (success is 0)
436                 $logger->info(
437                     "Credit card payment for user $user_id failed: " .
438                     $response->{textcode} . " " .
439                     ($response->{payload}->{error_message} ||
440                         $response->{payload}{message})
441                 );
442                 return $response;
443             } else {
444                 # We need to save this for later in case there's a failure on
445                 # the EG side to store the processor's result.
446
447                 $cc_payload = $response->{"payload"};   # also used way later
448
449                 {
450                     no warnings 'uninitialized';
451                     $approval_code = $cc_payload->{authorization} ||
452                         $cc_payload->{id};
453                     $cc_processor = $cc_payload->{processor} ||
454                         $cc_args->{processor};
455                     $cc_order_number = $cc_payload->{order_number} ||
456                         $cc_payload->{invoice};
457                 };
458                 $logger->info("Credit card payment for user $user_id succeeded");
459             }
460         } else {
461             return OpenILS::Event->new(
462                 'BAD_PARAMS', note => 'Need approval code'
463             ) if not $cc_args->{approval_code};
464         }
465     }
466
467     ### RE-OPEN TRANSACTION HERE ###
468     $e->xact_begin;
469     my @payment_ids;
470
471     # create payment records
472     my $create_money_method = "create_money_" . $type;
473     for my $payment (@payment_objs) {
474         # update the transaction if it's done
475         my $amount = $payment->amount;
476         my $transid = $payment->xact;
477         my $trans = $xacts{$transid};
478         # making payment with existing patron credit.
479         $credit -= $amount if $type eq 'credit_payment';
480         if( (my $cred = ($trans->balance_owed - $amount)) <= 0 ) {
481             # Any overpay on this transaction goes directly into patron
482             # credit
483             $cred = -$cred;
484             $credit += $cred;
485
486             # Attempt to close the transaction.
487             my $close_xact_fail = $CC->maybe_close_xact($e, $transid);
488             if ($close_xact_fail) {
489                 return _recording_failure(
490                     $e, $close_xact_fail->{message},
491                     $payment, $cc_payload
492                 );
493             }
494         }
495
496         # Urgh, clean up this mega-function one day.
497         if ($cc_processor eq 'Stripe' and $approval_code and $cc_payload) {
498             $payment->cc_number($cc_payload->{card}); # not actually available :)
499         }
500
501         $payment->approval_code($approval_code) if $approval_code;
502         $payment->cc_order_number($cc_order_number) if $cc_order_number;
503         $payment->cc_processor($cc_processor) if $cc_processor;
504         if (!$e->$create_money_method($payment)) {
505             return _recording_failure(
506                 $e, "$create_money_method failed", $payment, $cc_payload
507             );
508         }
509
510         push(@payment_ids, $payment->id);
511     }
512
513     my $evt = _update_patron_credit($e, $patron, $credit);
514     if ($evt) {
515         return _recording_failure(
516             $e, "_update_patron_credit() failed", undef, $cc_payload
517         );
518     }
519
520     for my $org_id (keys %orgs) {
521         # calculate penalties for each of the affected orgs
522         $evt = OpenILS::Utils::Penalty->calculate_penalties(
523             $e, $user_id, $org_id
524         );
525         if ($evt) {
526             return _recording_failure(
527                 $e, "calculate_penalties() failed", undef, $cc_payload
528             );
529         }
530     }
531
532     # update the user to create a new last_xact_id
533     $e->update_actor_user($patron) or return $e->die_event;
534     $patron = $e->retrieve_actor_user($patron) or return $e->die_event;
535     $e->commit;
536
537     # update the cached user object if a user is making a payment toward 
538     # his/her own account
539     $U->simplereq('open-ils.auth', 'open-ils.auth.session.reset_timeout', $auth, 1)
540         if $user_id == $e->requestor->id;
541
542     return {last_xact_id => $patron->last_xact_id, payments => \@payment_ids};
543 }
544
545 sub _recording_failure {
546     my ($e, $msg, $payment, $payload) = @_;
547
548     if ($payload) { # If the payment processor already accepted a payment:
549         $logger->error($msg);
550         $logger->error("Payment processor payload: " . Dumper($payload));
551         # payment shouldn't contain CC number
552         $logger->error("Payment: " . Dumper($payment)) if $payment;
553
554         $e->rollback;
555
556         return new OpenILS::Event(
557             "CREDIT_PROCESSOR_SUCCESS_WO_RECORD",
558             "payload" => $payload
559         );
560     } else { # Otherwise, the problem is somewhat less severe:
561         $logger->warn($msg);
562         $logger->warn("Payment: " . Dumper($payment)) if $payment;
563         return $e->die_event;
564     }
565 }
566
567 sub _update_patron_credit {
568     my($e, $patron, $credit) = @_;
569     return undef if $credit == 0;
570     $patron->credit_forward_balance($patron->credit_forward_balance + $credit);
571     return OpenILS::Event->new('NEGATIVE_PATRON_BALANCE') if $patron->credit_forward_balance < 0;
572     $e->update_actor_user($patron) or return $e->die_event;
573     return undef;
574 }
575
576
577 __PACKAGE__->register_method(
578     method    => "retrieve_payments",
579     api_name    => "open-ils.circ.money.payment.retrieve.all_",
580     notes        => "Returns a list of payments attached to a given transaction"
581     );
582 sub retrieve_payments {
583     my( $self, $client, $login, $transid ) = @_;
584
585     my( $staff, $evt ) =  
586         $apputils->checksesperm($login, 'VIEW_TRANSACTION');
587     return $evt if $evt;
588
589     # XXX the logic here is wrong.. we need to check the owner of the transaction
590     # to make sure the requestor has access
591
592     # XXX grab the view, for each object in the view, grab the real object
593
594     return $apputils->simplereq(
595         'open-ils.cstore',
596         'open-ils.cstore.direct.money.payment.search.atomic', { xact => $transid } );
597 }
598
599
600 __PACKAGE__->register_method(
601     method    => "retrieve_payments2",
602     authoritative => 1,
603     api_name    => "open-ils.circ.money.payment.retrieve.all",
604     notes        => "Returns a list of payments attached to a given transaction"
605     );
606     
607 sub retrieve_payments2 {
608     my( $self, $client, $login, $transid ) = @_;
609
610     my $e = new_editor(authtoken=>$login);
611     return $e->event unless $e->checkauth;
612     return $e->event unless $e->allowed('VIEW_TRANSACTION');
613
614     my @payments;
615     my $pmnts = $e->search_money_payment({ xact => $transid });
616     for( @$pmnts ) {
617         my $type = $_->payment_type;
618         my $meth = "retrieve_money_$type";
619         my $p = $e->$meth($_->id) or return $e->event;
620         $p->payment_type($type);
621         $p->cash_drawer($e->retrieve_actor_workstation($p->cash_drawer))
622             if $p->has_field('cash_drawer');
623         push( @payments, $p );
624     }
625
626     return \@payments;
627 }
628
629 __PACKAGE__->register_method(
630     method    => "format_payment_receipt",
631     api_name  => "open-ils.circ.money.payment_receipt.print",
632     signature => {
633         desc   => 'Returns a printable receipt for the specified payments',
634         params => [
635             { desc => 'Authentication token',  type => 'string'},
636             { desc => 'Payment ID or array of payment IDs', type => 'number' },
637         ],
638         return => {
639             desc => q/An action_trigger.event object or error event./,
640             type => 'object',
641         }
642     }
643 );
644 __PACKAGE__->register_method(
645     method    => "format_payment_receipt",
646     api_name  => "open-ils.circ.money.payment_receipt.email",
647     signature => {
648         desc   => 'Emails a receipt for the specified payments to the user associated with the first payment',
649         params => [
650             { desc => 'Authentication token',  type => 'string'},
651             { desc => 'Payment ID or array of payment IDs', type => 'number' },
652         ],
653         return => {
654             desc => q/Undefined on success, otherwise an error event./,
655             type => 'object',
656         }
657     }
658 );
659
660 sub format_payment_receipt {
661     my($self, $conn, $auth, $mp_id) = @_;
662
663     my $mp_ids;
664     if (ref $mp_id ne 'ARRAY') {
665         $mp_ids = [ $mp_id ];
666     } else {
667         $mp_ids = $mp_id;
668     }
669
670     my $for_print = ($self->api_name =~ /print/);
671     my $for_email = ($self->api_name =~ /email/);
672
673     # manually use xact (i.e. authoritative) so we can kill the cstore
674     # connection before sending the action/trigger request.  This prevents our cstore
675     # backend from sitting idle while A/T (which uses its own transactions) runs.
676     my $e = new_editor(xact => 1, authtoken => $auth);
677     return $e->die_event unless $e->checkauth;
678
679     my $payments = [];
680     for my $id (@$mp_ids) {
681
682         my $payment = $e->retrieve_money_payment([
683             $id,
684             {   flesh => 2,
685                 flesh_fields => {
686                     mp => ['xact'],
687                     mbt => ['usr']
688                 }
689             }
690         ]) or return $e->die_event;
691
692         return $e->die_event unless 
693             $e->requestor->id == $payment->xact->usr->id or
694             $e->allowed('VIEW_TRANSACTION', $payment->xact->usr->home_ou); 
695
696         push @$payments, $payment;
697     }
698
699     $e->rollback;
700
701     if ($for_print) {
702
703         return $U->fire_object_event(undef, 'money.format.payment_receipt.print', $payments, $$payments[0]->xact->usr->home_ou);
704
705     } elsif ($for_email) {
706
707         for my $p (@$payments) {
708             $U->create_events_for_hook('money.format.payment_receipt.email', $p, $p->xact->usr->home_ou, undef, undef, 1);
709         }
710     }
711
712     return undef;
713 }
714
715 __PACKAGE__->register_method(
716     method    => "create_grocery_bill",
717     api_name    => "open-ils.circ.money.grocery.create",
718     notes        => <<"    NOTE");
719     Creates a new grocery transaction using the transaction object provided
720     PARAMS: (login_session, money.grocery (mg) object)
721     NOTE
722
723 sub create_grocery_bill {
724     my( $self, $client, $login, $transaction ) = @_;
725
726     my( $staff, $evt ) = $apputils->checkses($login);
727     return $evt if $evt;
728     $evt = $apputils->check_perms($staff->id, 
729         $transaction->billing_location, 'CREATE_TRANSACTION' );
730     return $evt if $evt;
731
732
733     $logger->activity("Creating grocery bill " . Dumper($transaction) );
734
735     $transaction->clear_id;
736     my $session = $apputils->start_db_session;
737     $apputils->set_audit_info($session, $login, $staff->id, $staff->wsid);
738     my $transid = $session->request(
739         'open-ils.storage.direct.money.grocery.create', $transaction)->gather(1);
740
741     throw OpenSRF::EX ("Error creating new money.grocery") unless defined $transid;
742
743     $logger->debug("Created new grocery transaction $transid");
744     
745     $apputils->commit_db_session($session);
746
747     my $e = new_editor(xact=>1);
748     $evt = $U->check_open_xact($e, $transid);
749     return $evt if $evt;
750     $e->commit;
751
752     return $transid;
753 }
754
755
756 __PACKAGE__->register_method(
757     method => 'fetch_reservation',
758     api_name => 'open-ils.circ.booking.reservation.retrieve'
759 );
760 sub fetch_reservation {
761     my( $self, $conn, $auth, $id ) = @_;
762     my $e = new_editor(authtoken=>$auth);
763     return $e->event unless $e->checkauth;
764     return $e->event unless $e->allowed('VIEW_TRANSACTION'); # eh.. basically the same permission
765     my $g = $e->retrieve_booking_reservation($id)
766         or return $e->event;
767     return $g;
768 }
769
770 __PACKAGE__->register_method(
771     method   => 'fetch_grocery',
772     api_name => 'open-ils.circ.money.grocery.retrieve'
773 );
774 sub fetch_grocery {
775     my( $self, $conn, $auth, $id ) = @_;
776     my $e = new_editor(authtoken=>$auth);
777     return $e->event unless $e->checkauth;
778     return $e->event unless $e->allowed('VIEW_TRANSACTION'); # eh.. basically the same permission
779     my $g = $e->retrieve_money_grocery($id)
780         or return $e->event;
781     return $g;
782 }
783
784
785 __PACKAGE__->register_method(
786     method        => "billing_items",
787     api_name      => "open-ils.circ.money.billing.retrieve.all",
788     authoritative => 1,
789     signature     => {
790         desc   => 'Returns a list of billing items for the given transaction ID.  ' .
791                   'If the operator is not the owner of the transaction, the VIEW_TRANSACTION permission is required.',
792         params => [
793             { desc => 'Authentication token', type => 'string'},
794             { desc => 'Transaction ID',       type => 'number'}
795         ],
796         return => {
797             desc => 'Transaction object, event on error'
798         },
799     }
800 );
801
802 sub billing_items {
803     my( $self, $client, $login, $transid ) = @_;
804
805     my( $trans, $evt ) = $U->fetch_billable_xact($transid);
806     return $evt if $evt;
807
808     my $staff;
809     ($staff, $evt ) = $apputils->checkses($login);
810     return $evt if $evt;
811
812     if($staff->id ne $trans->usr) {
813         $evt = $U->check_perms($staff->id, $staff->home_ou, 'VIEW_TRANSACTION');
814         return $evt if $evt;
815     }
816     
817     return $apputils->simplereq( 'open-ils.cstore',
818         'open-ils.cstore.direct.money.billing.search.atomic', { xact => $transid } )
819 }
820
821
822 __PACKAGE__->register_method(
823     method   => "billing_items_create",
824     api_name => "open-ils.circ.money.billing.create",
825     notes    => <<"    NOTE");
826     Creates a new billing line item
827     PARAMS( login, bill_object (mb) )
828     NOTE
829
830 sub billing_items_create {
831     my( $self, $client, $login, $billing ) = @_;
832
833     my $e = new_editor(authtoken => $login, xact => 1);
834     return $e->die_event unless $e->checkauth;
835     return $e->die_event unless $e->allowed('CREATE_BILL');
836
837     my $xact = $e->retrieve_money_billable_transaction($billing->xact)
838         or return $e->die_event;
839
840     # if the transaction was closed, re-open it
841     if($xact->xact_finish) {
842         $xact->clear_xact_finish;
843         $e->update_money_billable_transaction($xact)
844             or return $e->die_event;
845     }
846
847     my $amt = $billing->amount;
848     $amt =~ s/\$//og;
849     $billing->amount($amt);
850
851     $e->create_money_billing($billing) or return $e->die_event;
852     my $evt = OpenILS::Utils::Penalty->calculate_penalties($e, $xact->usr, $U->xact_org($xact->id,$e));
853     return $evt if $evt;
854
855     $evt = $U->check_open_xact($e, $xact->id, $xact);
856     return $evt if $evt;
857
858     $e->commit;
859
860     return $billing->id;
861 }
862
863
864 __PACKAGE__->register_method(
865     method        =>    'void_bill',
866     api_name        => 'open-ils.circ.money.billing.void',
867     signature    => q/
868         Voids a bill
869         @param authtoken Login session key
870         @param billid Id for the bill to void.  This parameter may be repeated to reference other bills.
871         @return 1 on success, Event on error
872     /
873 );
874 sub void_bill {
875     my( $s, $c, $authtoken, @billids ) = @_;
876     my $editor = new_editor(authtoken=>$authtoken, xact=>1);
877     return $editor->die_event unless $editor->checkauth;
878     return $editor->die_event unless $editor->allowed('VOID_BILLING');
879     my $rv = $CC->void_bills($editor, \@billids);
880     if (ref($rv) eq 'HASH') {
881         # We got an event.
882         $editor->rollback();
883     } else {
884         # We should have gotten 1.
885         $editor->commit();
886     }
887     return $rv;
888 }
889
890
891 __PACKAGE__->register_method(
892     method => 'adjust_bills_to_zero_manual',
893     api_name => 'open-ils.circ.money.billable_xact.adjust_to_zero',
894     signature => {
895         desc => q/
896             Given a list of billable transactions, manipulate the
897             transaction using account adjustments to result in a
898             balance of $0.
899             /,
900         params => [
901             {desc => 'Authtoken', type => 'string'},
902             {desc => 'Array of transaction IDs', type => 'array'}
903         ],
904         return => {
905             desc => q/Array of IDs for each transaction updated,
906             Event on error./
907         }
908     }
909 );
910
911 sub _rebill_xact {
912     my ($e, $xact) = @_;
913
914     my $xact_id = $xact->id;
915     # the plan: rebill voided billings until we get a positive balance
916     #
917     # step 1: get the voided/adjusted billings
918     my $billings = $e->search_money_billing([
919         {
920             xact => $xact_id,
921         },
922         {
923             order_by => {mb => 'amount desc'},
924             flesh => 1,
925             flesh_fields => {mb => ['adjustments']},
926         }
927     ]);
928     my @billings = grep { $U->is_true($_->voided) or @{$_->adjustments} } @$billings;
929
930     my $xact_balance = $xact->balance_owed;
931     $logger->debug("rebilling for xact $xact_id with balance $xact_balance");
932
933     my $rebill_amount = 0;
934     my @rebill_ids;
935     # step 2: generate new bills just like the old ones
936     for my $billing (@billings) {
937         my $amount = 0;
938         if ($U->is_true($billing->voided)) {
939             $amount = $billing->amount;
940         } else { # adjusted billing
941             map { $amount = $U->fpsum($amount, $_->amount) } @{$billing->adjustments};
942         }
943         my $evt = $CC->create_bill(
944             $e,
945             $amount,
946             $billing->btype,
947             $billing->billing_type,
948             $xact_id,
949             "System: MANUAL ADJUSTMENT, BILLING #".$billing->id." REINSTATED\n(PREV: ".$billing->note.")",
950             $billing->period_start(),
951             $billing->period_end()
952         );
953         return $evt if $evt;
954         $rebill_amount += $billing->amount;
955
956         # if we have a postive (or zero) balance now, stop
957         last if ($xact_balance + $rebill_amount >= 0);
958     }
959 }
960
961 sub _is_fully_adjusted {
962     my ($billing) = @_;
963
964     my $amount_adj = 0;
965     map { $amount_adj = $U->fpsum($amount_adj, $_->amount) } @{$billing->adjustments};
966
967     return $billing->amount == $amount_adj;
968 }
969
970 sub adjust_bills_to_zero_manual {
971     my ($self, $client, $auth, $xact_ids) = @_;
972
973     my $e = new_editor(xact => 1, authtoken => $auth);
974     return $e->die_event unless $e->checkauth;
975
976     # in case a bare ID is passed
977     $xact_ids = [$xact_ids] unless ref $xact_ids;
978
979     my @modified;
980     for my $xact_id (@$xact_ids) {
981
982         my $xact =
983             $e->retrieve_money_billable_transaction_summary([
984                 $xact_id,
985                 {flesh => 1, flesh_fields => {mbts => ['usr']}}
986             ]) or return $e->die_event;
987
988         if ($xact->balance_owed == 0) {
989             # zero already, all done
990             next;
991         }
992
993         return $e->die_event unless
994             $e->allowed('ADJUST_BILLS', $xact->usr->home_ou);
995
996         if ($xact->balance_owed < 0) {
997             my $evt = _rebill_xact($e, $xact);
998             return $evt if $evt;
999             # refetch xact to get new balance
1000             $xact =
1001                 $e->retrieve_money_billable_transaction_summary([
1002                     $xact_id,
1003                     {flesh => 1, flesh_fields => {mbts => ['usr']}}
1004                 ]) or return $e->die_event;
1005         }
1006
1007         if ($xact->balance_owed > 0) {
1008             # it's positive and needs to be adjusted
1009             # (it either started positive, or we rebilled it positive)
1010             my $billings = $e->search_money_billing([
1011                 {
1012                     xact => $xact_id,
1013                 },
1014                 {
1015                     order_by => {mb => 'amount desc'},
1016                     flesh => 1,
1017                     flesh_fields => {mb => ['adjustments']},
1018                 }
1019             ]);
1020
1021             my @billings_to_zero = grep { !$U->is_true($_->voided) or !_is_fully_adjusted($_) } @$billings;
1022             $CC->adjust_bills_to_zero($e, \@billings_to_zero, "System: MANUAL ADJUSTMENT");
1023         }
1024
1025         push(@modified, $xact->id);
1026
1027         # now we see if we can close the transaction
1028         # same logic as make_payments();
1029         my $close_xact_fail = $CC->maybe_close_xact($e, $xact_id);
1030         if ($close_xact_fail) {
1031             return $close_xact_fail->{evt};
1032         }
1033     }
1034
1035     $e->commit;
1036     return \@modified;
1037 }
1038
1039
1040 __PACKAGE__->register_method(
1041     method        =>    'edit_bill_note',
1042     api_name        => 'open-ils.circ.money.billing.note.edit',
1043     signature    => q/
1044         Edits the note for a bill
1045         @param authtoken Login session key
1046         @param note The replacement note for the bills we're editing
1047         @param billid Id for the bill to edit the note of.  This parameter may be repeated to reference other bills.
1048         @return 1 on success, Event on error
1049     /
1050 );
1051 sub edit_bill_note {
1052     my( $s, $c, $authtoken, $note, @billids ) = @_;
1053
1054     my $e = new_editor( authtoken => $authtoken, xact => 1 );
1055     return $e->die_event unless $e->checkauth;
1056     return $e->die_event unless $e->allowed('UPDATE_BILL_NOTE');
1057
1058     for my $billid (@billids) {
1059
1060         my $bill = $e->retrieve_money_billing($billid)
1061             or return $e->die_event;
1062
1063         $bill->note($note);
1064         # 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.
1065     
1066         $e->update_money_billing($bill) or return $e->die_event;
1067     }
1068     $e->commit;
1069     return 1;
1070 }
1071
1072
1073 __PACKAGE__->register_method(
1074     method        =>    'edit_payment_note',
1075     api_name        => 'open-ils.circ.money.payment.note.edit',
1076     signature    => q/
1077         Edits the note for a payment
1078         @param authtoken Login session key
1079         @param note The replacement note for the payments we're editing
1080         @param paymentid Id for the payment to edit the note of.  This parameter may be repeated to reference other payments.
1081         @return 1 on success, Event on error
1082     /
1083 );
1084 sub edit_payment_note {
1085     my( $s, $c, $authtoken, $note, @paymentids ) = @_;
1086
1087     my $e = new_editor( authtoken => $authtoken, xact => 1 );
1088     return $e->die_event unless $e->checkauth;
1089     return $e->die_event unless $e->allowed('UPDATE_PAYMENT_NOTE');
1090
1091     for my $paymentid (@paymentids) {
1092
1093         my $payment = $e->retrieve_money_payment($paymentid)
1094             or return $e->die_event;
1095
1096         $payment->note($note);
1097         # 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.
1098     
1099         $e->update_money_payment($payment) or return $e->die_event;
1100     }
1101
1102     $e->commit;
1103     return 1;
1104 }
1105
1106
1107 __PACKAGE__->register_method (
1108     method => 'fetch_mbts',
1109     authoritative => 1,
1110     api_name => 'open-ils.circ.money.billable_xact_summary.retrieve'
1111 );
1112 sub fetch_mbts {
1113     my( $self, $conn, $auth, $id) = @_;
1114
1115     my $e = new_editor(xact => 1, authtoken=>$auth);
1116     return $e->event unless $e->checkauth;
1117     my ($mbts) = $U->fetch_mbts($id, $e);
1118
1119     my $user = $e->retrieve_actor_user($mbts->usr)
1120         or return $e->die_event;
1121
1122     return $e->die_event unless $e->allowed('VIEW_TRANSACTION', $user->home_ou);
1123     $e->rollback;
1124     return $mbts
1125 }
1126
1127
1128 __PACKAGE__->register_method(
1129     method => 'desk_payments',
1130     api_name => 'open-ils.circ.money.org_unit.desk_payments'
1131 );
1132 sub desk_payments {
1133     my( $self, $conn, $auth, $org, $start_date, $end_date ) = @_;
1134     my $e = new_editor(authtoken=>$auth);
1135     return $e->event unless $e->checkauth;
1136     return $e->event unless $e->allowed('VIEW_TRANSACTION', $org);
1137     my $data = $U->storagereq(
1138         'open-ils.storage.money.org_unit.desk_payments.atomic',
1139         $org, $start_date, $end_date );
1140
1141     $_->workstation( $_->workstation->name ) for(@$data);
1142     return $data;
1143 }
1144
1145
1146 __PACKAGE__->register_method(
1147     method => 'user_payments',
1148     api_name => 'open-ils.circ.money.org_unit.user_payments'
1149 );
1150
1151 sub user_payments {
1152     my( $self, $conn, $auth, $org, $start_date, $end_date ) = @_;
1153     my $e = new_editor(authtoken=>$auth);
1154     return $e->event unless $e->checkauth;
1155     return $e->event unless $e->allowed('VIEW_TRANSACTION', $org);
1156     my $data = $U->storagereq(
1157         'open-ils.storage.money.org_unit.user_payments.atomic',
1158         $org, $start_date, $end_date );
1159     for(@$data) {
1160         $_->usr->card(
1161             $e->retrieve_actor_card($_->usr->card)->barcode);
1162         $_->usr->home_ou(
1163             $e->retrieve_actor_org_unit($_->usr->home_ou)->shortname);
1164     }
1165     return $data;
1166 }
1167
1168
1169 __PACKAGE__->register_method(
1170     method    => 'retrieve_credit_payable_balance',
1171     api_name  => 'open-ils.circ.credit.payable_balance.retrieve',
1172     authoritative => 1,
1173     signature => {
1174         desc   => q/Returns the total amount the patron can pay via credit card/,
1175         params => [
1176             { desc => 'Authentication token', type => 'string' },
1177             { desc => 'User id', type => 'number' }
1178         ],
1179         return => { desc => 'The ID of the new provider' }
1180     }
1181 );
1182
1183 sub retrieve_credit_payable_balance {
1184     my ( $self, $conn, $auth, $user_id ) = @_;
1185     my $e = new_editor(authtoken => $auth);
1186     return $e->event unless $e->checkauth;
1187
1188     my $user = $e->retrieve_actor_user($user_id) 
1189         or return $e->event;
1190
1191     if($e->requestor->id != $user_id) {
1192         return $e->event unless $e->allowed('VIEW_USER_TRANSACTIONS', $user->home_ou)
1193     }
1194
1195     my $circ_orgs = $e->json_query({
1196         "select" => {circ => ["circ_lib"]},
1197         from     => "circ",
1198         "where"  => {usr => $user_id, xact_finish => undef},
1199         distinct => 1
1200     });
1201
1202     my $groc_orgs = $e->json_query({
1203         "select" => {mg => ["billing_location"]},
1204         from     => "mg",
1205         "where"  => {usr => $user_id, xact_finish => undef},
1206         distinct => 1
1207     });
1208
1209     my %hash;
1210     for my $org ( @$circ_orgs, @$groc_orgs ) {
1211         my $o = $org->{billing_location};
1212         $o = $org->{circ_lib} unless $o;
1213         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.
1214         $hash{$o} = $U->ou_ancestor_setting_value($o, 'credit.payments.allow', $e);
1215     }
1216
1217     my @credit_orgs = map { $hash{$_} ? ($_) : () } keys %hash;
1218     $logger->debug("credit: relevant orgs that allow credit payments => @credit_orgs");
1219
1220     my $xact_summaries =
1221       OpenILS::Application::AppUtils->simplereq('open-ils.actor',
1222         'open-ils.actor.user.transactions.have_charge', $auth, $user_id);
1223
1224     my $sum = 0.0;
1225
1226     for my $xact (@$xact_summaries) {
1227
1228         # make two lists and grab them in batch XXX
1229         if ( $xact->xact_type eq 'circulation' ) {
1230             my $circ = $e->retrieve_action_circulation($xact->id) or return $e->event;
1231             next unless grep { $_ == $circ->circ_lib } @credit_orgs;
1232
1233         } elsif ($xact->xact_type eq 'grocery') {
1234             my $bill = $e->retrieve_money_grocery($xact->id) or return $e->event;
1235             next unless grep { $_ == $bill->billing_location } @credit_orgs;
1236         } elsif ($xact->xact_type eq 'reservation') {
1237             my $bill = $e->retrieve_booking_reservation($xact->id) or return $e->event;
1238             next unless grep { $_ == $bill->pickup_lib } @credit_orgs;
1239         }
1240         $sum += $xact->balance_owed();
1241     }
1242
1243     return $sum;
1244 }
1245
1246
1247 __PACKAGE__->register_method(
1248     method    => "retrieve_statement",
1249     authoritative => 1,
1250     api_name    => "open-ils.circ.money.statement.retrieve",
1251     notes        => "Returns an organized summary of a billable transaction, including all bills, payments, adjustments, and voids."
1252     );
1253
1254 sub _to_epoch {
1255     my $ts = shift @_;
1256
1257     return $parser->parse_datetime(clean_ISO8601($ts))->epoch;
1258 }
1259
1260 my %_statement_sort = (
1261     'billing' => 0,
1262     'account_adjustment' => 1,
1263     'void' => 2,
1264     'payment' => 3
1265 );
1266
1267 sub retrieve_statement {
1268     my ( $self, $client, $auth, $xact_id ) = @_;
1269
1270     my $e = new_editor(authtoken=>$auth);
1271     return $e->event unless $e->checkauth;
1272     return $e->event unless $e->allowed('VIEW_TRANSACTION');
1273
1274     # XXX: move this lookup login into a DB query?
1275     my @line_prep;
1276
1277     # collect all payments/adjustments
1278     my $payments = $e->search_money_payment({ xact => $xact_id });
1279     foreach my $payment (@$payments) {
1280         my $type = $payment->payment_type;
1281         $type = 'payment' if $type ne 'account_adjustment';
1282         push(@line_prep, [$type, _to_epoch($payment->payment_ts), $payment->payment_ts, $payment->id, $payment]);
1283     }
1284
1285     # collect all billings
1286     my $billings = $e->search_money_billing({ xact => $xact_id });
1287     foreach my $billing (@$billings) {
1288         if ($U->is_true($billing->voided)){
1289             push(@line_prep, ['void', _to_epoch($billing->void_time), $billing->void_time, $billing->id, $billing]); # voids get two entries, one to represent the bill event, one for the void event
1290         }
1291         push(@line_prep, ['billing', _to_epoch($billing->billing_ts), $billing->billing_ts, $billing->id, $billing]);
1292     }
1293
1294     # order every event by timestamp, then bills/adjustments/voids/payments order, then id
1295     my @ordered_line_prep = sort {
1296         $a->[1] <=> $b->[1]
1297             ||
1298         $_statement_sort{$a->[0]} <=> $_statement_sort{$b->[0]}
1299             ||
1300         $a->[3] <=> $b->[3]
1301     } @line_prep;
1302
1303     # let's start building the statement structure
1304     my (@lines, %current_line, $running_balance);
1305     foreach my $event (@ordered_line_prep) {
1306         my $obj = $event->[4];
1307         my $type = $event->[0];
1308         my $ts = $event->[2];
1309         my $billing_type = $type =~ /billing|void/ ? $obj->billing_type : ''; # TODO: get non-legacy billing type
1310         my $note = $obj->note || '';
1311         # last line should be void information, try to isolate it
1312         if ($type eq 'billing' and $obj->voided) {
1313             $note =~ s/\n.*$//;
1314         } elsif ($type eq 'void') {
1315             $note = (split(/\n/, $note))[-1];
1316         }
1317
1318         # if we have new details, start a new line
1319         if ($current_line{amount} and (
1320                 $type ne $current_line{type}
1321                 or ($note ne $current_line{note})
1322                 or ($billing_type ne $current_line{billing_type})
1323             )
1324         ) {
1325             push(@lines, {%current_line}); # push a copy of the hash, not the real thing
1326             %current_line = ();
1327         }
1328         if (!$current_line{type}) {
1329             $current_line{type} = $type;
1330             $current_line{billing_type} = $billing_type;
1331             $current_line{note} = $note;
1332         }
1333         if (!$current_line{start_date}) {
1334             $current_line{start_date} = $ts;
1335         } elsif ($ts ne $current_line{start_date}) {
1336             $current_line{end_date} = $ts;
1337         }
1338         $current_line{amount} += $obj->amount;
1339         if ($current_line{details}) {
1340             push(@{$current_line{details}}, $obj);
1341         } else {
1342             $current_line{details} = [$obj];
1343         }
1344     }
1345     push(@lines, {%current_line}); # push last one on
1346
1347     # get/update totals, format notes
1348     my %totals = (
1349         billing => 0,
1350         payment => 0,
1351         account_adjustment => 0,
1352         void => 0
1353     );
1354     foreach my $line (@lines) {
1355         $totals{$line->{type}} += $line->{amount};
1356         if ($line->{type} eq 'billing') {
1357             $running_balance += $line->{amount};
1358         } else { # not a billing; balance goes down for everything else
1359             $running_balance -= $line->{amount};
1360         }
1361         $line->{running_balance} = $running_balance;
1362         $line->{note} = $line->{note} ? [split(/\n/, $line->{note})] : [];
1363     }
1364
1365     my $xact = $e->retrieve_money_billable_transaction([
1366         $xact_id, {
1367             flesh => 5,
1368             flesh_fields => {
1369                 mbt =>  [qw/circulation grocery/],
1370                 circ => [qw/target_copy/],
1371                 acp =>  [qw/call_number location status age_protect total_circ_count/],
1372                 acn =>  [qw/record prefix suffix/],
1373                 bre =>  [qw/wide_display_entry/]
1374             },
1375             select => {bre => ['id']} 
1376         }
1377     ]);
1378
1379     my $title;
1380     my $billing_location;
1381     my $title_id;
1382     if ($xact->circulation) {
1383         $billing_location = $xact->circulation->circ_lib;
1384         my $copy = $xact->circulation->target_copy;
1385         if ($copy->call_number->id == -1) {
1386             $title = $copy->dummy_title;
1387         } else {
1388             $title_id = $copy->call_number->record->id;
1389             $title = OpenSRF::Utils::JSON->JSON2perl(
1390                 $copy->call_number->record->wide_display_entry->title);
1391         }
1392     } else {
1393         $billing_location = $xact->grocery->billing_location;
1394         $title = $xact->grocery->note;
1395     }
1396
1397     return {
1398         xact_id => $xact_id,
1399         xact => $xact,
1400         title => $title,
1401         title_id => $title_id,
1402         billing_location => $billing_location,
1403         summary => {
1404             balance_due => $totals{billing} - ($totals{payment} + $totals{account_adjustment} + $totals{void}),
1405             billing_total => $totals{billing},
1406             credit_total => $totals{payment} + $totals{account_adjustment},
1407             payment_total => $totals{payment},
1408             account_adjustment_total => $totals{account_adjustment},
1409             void_total => $totals{void}
1410         },
1411         lines => \@lines
1412     }
1413 }
1414
1415
1416 1;