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