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