]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Money.pm
Add the session to Circ set_audit_info calls
[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                     "cvv2" => $cc_args->{cvv2},
288                 });
289
290             if ($U->event_code($response)) { # non-success
291                 $logger->info(
292                     "Credit card payment for user $user_id failed: " .
293                     $response->{"textcode"} . " " .
294                     $response->{"payload"}->{"error_message"}
295                 );
296
297                 return $response;
298             } else {
299                 # We need to save this for later in case there's a failure on
300                 # the EG side to store the processor's result.
301                 $cc_payload = $response->{"payload"};
302
303                 $approval_code = $cc_payload->{"authorization"};
304                 $cc_type = $cc_payload->{"card_type"};
305                 $cc_processor = $cc_payload->{"processor"};
306                 $cc_order_number = $cc_payload->{"order_number"};
307                 $logger->info("Credit card payment for user $user_id succeeded");
308             }
309         } else {
310             return OpenILS::Event->new(
311                 'BAD_PARAMS', note => 'Need approval code'
312             ) if not $cc_args->{approval_code};
313         }
314     }
315
316     ### RE-OPEN TRANSACTION HERE ###
317     $e->xact_begin;
318     my @payment_ids;
319
320     # create payment records
321     my $create_money_method = "create_money_" . $type;
322     for my $payment (@payment_objs) {
323         # update the transaction if it's done
324         my $amount = $payment->amount;
325         my $transid = $payment->xact;
326         my $trans = $xacts{$transid};
327         # making payment with existing patron credit.
328         $credit -= $amount if $type eq 'credit_payment';
329         if( (my $cred = ($trans->balance_owed - $amount)) <= 0 ) {
330             # Any overpay on this transaction goes directly into patron
331             # credit
332             $cred = -$cred;
333             $credit += $cred;
334             my $circ = $e->retrieve_action_circulation($transid);
335
336             if(!$circ || $circ->stop_fines) {
337                 # If this is a circulation, we can't close the transaction
338                 # unless stop_fines is set.
339                 $trans = $e->retrieve_money_billable_transaction($transid);
340                 $trans->xact_finish("now");
341                 if (!$e->update_money_billable_transaction($trans)) {
342                     return _recording_failure(
343                         $e, "update_money_billable_transaction() failed",
344                         $payment, $cc_payload
345                     )
346                 }
347             }
348         }
349
350         $payment->approval_code($approval_code) if $approval_code;
351         $payment->cc_order_number($cc_order_number) if $cc_order_number;
352         $payment->cc_type($cc_type) if $cc_type;
353         $payment->cc_processor($cc_processor) if $cc_processor;
354         $payment->cc_first_name($cc_args->{'billing_first'}) if $cc_args->{'billing_first'};
355         $payment->cc_last_name($cc_args->{'billing_last'}) if $cc_args->{'billing_last'};
356         if (!$e->$create_money_method($payment)) {
357             return _recording_failure(
358                 $e, "$create_money_method failed", $payment, $cc_payload
359             );
360         }
361
362         push(@payment_ids, $payment->id);
363     }
364
365     my $evt = _update_patron_credit($e, $patron, $credit);
366     if ($evt) {
367         return _recording_failure(
368             $e, "_update_patron_credit() failed", undef, $cc_payload
369         );
370     }
371
372     for my $org_id (keys %orgs) {
373         # calculate penalties for each of the affected orgs
374         $evt = OpenILS::Utils::Penalty->calculate_penalties(
375             $e, $user_id, $org_id
376         );
377         if ($evt) {
378             return _recording_failure(
379                 $e, "calculate_penalties() failed", undef, $cc_payload
380             );
381         }
382     }
383
384     # update the user to create a new last_xact_id
385     $e->update_actor_user($patron) or return $e->die_event;
386     $patron = $e->retrieve_actor_user($patron) or return $e->die_event;
387     $e->commit;
388
389     # update the cached user object if a user is making a payment toward 
390     # his/her own account
391     $U->simplereq('open-ils.auth', 'open-ils.auth.session.reset_timeout', $auth, 1)
392         if $user_id == $e->requestor->id;
393
394     return {last_xact_id => $patron->last_xact_id, payments => \@payment_ids};
395 }
396
397 sub _recording_failure {
398     my ($e, $msg, $payment, $payload) = @_;
399
400     if ($payload) { # If the payment processor already accepted a payment:
401         $logger->error($msg);
402         $logger->error("Payment processor payload: " . Dumper($payload));
403         # payment shouldn't contain CC number
404         $logger->error("Payment: " . Dumper($payment)) if $payment;
405
406         $e->rollback;
407
408         return new OpenILS::Event(
409             "CREDIT_PROCESSOR_SUCCESS_WO_RECORD",
410             "payload" => $payload
411         );
412     } else { # Otherwise, the problem is somewhat less severe:
413         $logger->warn($msg);
414         $logger->warn("Payment: " . Dumper($payment)) if $payment;
415         return $e->die_event;
416     }
417 }
418
419 sub _update_patron_credit {
420     my($e, $patron, $credit) = @_;
421     return undef if $credit == 0;
422     $patron->credit_forward_balance($patron->credit_forward_balance + $credit);
423     return OpenILS::Event->new('NEGATIVE_PATRON_BALANCE') if $patron->credit_forward_balance < 0;
424     $e->update_actor_user($patron) or return $e->die_event;
425     return undef;
426 }
427
428
429 __PACKAGE__->register_method(
430     method    => "retrieve_payments",
431     api_name    => "open-ils.circ.money.payment.retrieve.all_",
432     notes        => "Returns a list of payments attached to a given transaction"
433     );
434 sub retrieve_payments {
435     my( $self, $client, $login, $transid ) = @_;
436
437     my( $staff, $evt ) =  
438         $apputils->checksesperm($login, 'VIEW_TRANSACTION');
439     return $evt if $evt;
440
441     # XXX the logic here is wrong.. we need to check the owner of the transaction
442     # to make sure the requestor has access
443
444     # XXX grab the view, for each object in the view, grab the real object
445
446     return $apputils->simplereq(
447         'open-ils.cstore',
448         'open-ils.cstore.direct.money.payment.search.atomic', { xact => $transid } );
449 }
450
451
452 __PACKAGE__->register_method(
453     method    => "retrieve_payments2",
454     authoritative => 1,
455     api_name    => "open-ils.circ.money.payment.retrieve.all",
456     notes        => "Returns a list of payments attached to a given transaction"
457     );
458     
459 sub retrieve_payments2 {
460     my( $self, $client, $login, $transid ) = @_;
461
462     my $e = new_editor(authtoken=>$login);
463     return $e->event unless $e->checkauth;
464     return $e->event unless $e->allowed('VIEW_TRANSACTION');
465
466     my @payments;
467     my $pmnts = $e->search_money_payment({ xact => $transid });
468     for( @$pmnts ) {
469         my $type = $_->payment_type;
470         my $meth = "retrieve_money_$type";
471         my $p = $e->$meth($_->id) or return $e->event;
472         $p->payment_type($type);
473         $p->cash_drawer($e->retrieve_actor_workstation($p->cash_drawer))
474             if $p->has_field('cash_drawer');
475         push( @payments, $p );
476     }
477
478     return \@payments;
479 }
480
481 __PACKAGE__->register_method(
482     method    => "format_payment_receipt",
483     api_name  => "open-ils.circ.money.payment_receipt.print",
484     signature => {
485         desc   => 'Returns a printable receipt for the specified payments',
486         params => [
487             { desc => 'Authentication token',  type => 'string'},
488             { desc => 'Payment ID or array of payment IDs', type => 'number' },
489         ],
490         return => {
491             desc => q/An action_trigger.event object or error event./,
492             type => 'object',
493         }
494     }
495 );
496 __PACKAGE__->register_method(
497     method    => "format_payment_receipt",
498     api_name  => "open-ils.circ.money.payment_receipt.email",
499     signature => {
500         desc   => 'Emails a receipt for the specified payments to the user associated with the first payment',
501         params => [
502             { desc => 'Authentication token',  type => 'string'},
503             { desc => 'Payment ID or array of payment IDs', type => 'number' },
504         ],
505         return => {
506             desc => q/Undefined on success, otherwise an error event./,
507             type => 'object',
508         }
509     }
510 );
511
512 sub format_payment_receipt {
513     my($self, $conn, $auth, $mp_id) = @_;
514
515     my $mp_ids;
516     if (ref $mp_id ne 'ARRAY') {
517         $mp_ids = [ $mp_id ];
518     } else {
519         $mp_ids = $mp_id;
520     }
521
522     my $for_print = ($self->api_name =~ /print/);
523     my $for_email = ($self->api_name =~ /email/);
524
525     # manually use xact (i.e. authoritative) so we can kill the cstore
526     # connection before sending the action/trigger request.  This prevents our cstore
527     # backend from sitting idle while A/T (which uses its own transactions) runs.
528     my $e = new_editor(xact => 1, authtoken => $auth);
529     return $e->die_event unless $e->checkauth;
530
531     my $payments = [];
532     for my $id (@$mp_ids) {
533
534         my $payment = $e->retrieve_money_payment([
535             $id,
536             {   flesh => 2,
537                 flesh_fields => {
538                     mp => ['xact'],
539                     mbt => ['usr']
540                 }
541             }
542         ]) or return $e->die_event;
543
544         return $e->die_event unless 
545             $e->requestor->id == $payment->xact->usr->id or
546             $e->allowed('VIEW_TRANSACTION', $payment->xact->usr->home_ou); 
547
548         push @$payments, $payment;
549     }
550
551     $e->rollback;
552
553     if ($for_print) {
554
555         return $U->fire_object_event(undef, 'money.format.payment_receipt.print', $payments, $$payments[0]->xact->usr->home_ou);
556
557     } elsif ($for_email) {
558
559         for my $p (@$payments) {
560             $U->create_events_for_hook('money.format.payment_receipt.email', $p, $p->xact->usr->home_ou, undef, undef, 1);
561         }
562     }
563
564     return undef;
565 }
566
567 __PACKAGE__->register_method(
568     method    => "create_grocery_bill",
569     api_name    => "open-ils.circ.money.grocery.create",
570     notes        => <<"    NOTE");
571     Creates a new grocery transaction using the transaction object provided
572     PARAMS: (login_session, money.grocery (mg) object)
573     NOTE
574
575 sub create_grocery_bill {
576     my( $self, $client, $login, $transaction ) = @_;
577
578     my( $staff, $evt ) = $apputils->checkses($login);
579     return $evt if $evt;
580     $evt = $apputils->check_perms($staff->id, 
581         $transaction->billing_location, 'CREATE_TRANSACTION' );
582     return $evt if $evt;
583
584
585     $logger->activity("Creating grocery bill " . Dumper($transaction) );
586
587     $transaction->clear_id;
588     my $session = $apputils->start_db_session;
589     $apputils->set_audit_info($session, $login, $staff->id, $staff->wsid);
590     my $transid = $session->request(
591         'open-ils.storage.direct.money.grocery.create', $transaction)->gather(1);
592
593     throw OpenSRF::EX ("Error creating new money.grocery") unless defined $transid;
594
595     $logger->debug("Created new grocery transaction $transid");
596     
597     $apputils->commit_db_session($session);
598
599     my $e = new_editor(xact=>1);
600     $evt = _check_open_xact($e, $transid);
601     return $evt if $evt;
602     $e->commit;
603
604     return $transid;
605 }
606
607
608 __PACKAGE__->register_method(
609     method => 'fetch_reservation',
610     api_name => 'open-ils.circ.booking.reservation.retrieve'
611 );
612 sub fetch_reservation {
613     my( $self, $conn, $auth, $id ) = @_;
614     my $e = new_editor(authtoken=>$auth);
615     return $e->event unless $e->checkauth;
616     return $e->event unless $e->allowed('VIEW_TRANSACTION'); # eh.. basically the same permission
617     my $g = $e->retrieve_booking_reservation($id)
618         or return $e->event;
619     return $g;
620 }
621
622 __PACKAGE__->register_method(
623     method   => 'fetch_grocery',
624     api_name => 'open-ils.circ.money.grocery.retrieve'
625 );
626 sub fetch_grocery {
627     my( $self, $conn, $auth, $id ) = @_;
628     my $e = new_editor(authtoken=>$auth);
629     return $e->event unless $e->checkauth;
630     return $e->event unless $e->allowed('VIEW_TRANSACTION'); # eh.. basically the same permission
631     my $g = $e->retrieve_money_grocery($id)
632         or return $e->event;
633     return $g;
634 }
635
636
637 __PACKAGE__->register_method(
638     method        => "billing_items",
639     api_name      => "open-ils.circ.money.billing.retrieve.all",
640     authoritative => 1,
641     signature     => {
642         desc   => 'Returns a list of billing items for the given transaction ID.  ' .
643                   'If the operator is not the owner of the transaction, the VIEW_TRANSACTION permission is required.',
644         params => [
645             { desc => 'Authentication token', type => 'string'},
646             { desc => 'Transaction ID',       type => 'number'}
647         ],
648         return => {
649             desc => 'Transaction object, event on error'
650         },
651     }
652 );
653
654 sub billing_items {
655     my( $self, $client, $login, $transid ) = @_;
656
657     my( $trans, $evt ) = $U->fetch_billable_xact($transid);
658     return $evt if $evt;
659
660     my $staff;
661     ($staff, $evt ) = $apputils->checkses($login);
662     return $evt if $evt;
663
664     if($staff->id ne $trans->usr) {
665         $evt = $U->check_perms($staff->id, $staff->home_ou, 'VIEW_TRANSACTION');
666         return $evt if $evt;
667     }
668     
669     return $apputils->simplereq( 'open-ils.cstore',
670         'open-ils.cstore.direct.money.billing.search.atomic', { xact => $transid } )
671 }
672
673
674 __PACKAGE__->register_method(
675     method   => "billing_items_create",
676     api_name => "open-ils.circ.money.billing.create",
677     notes    => <<"    NOTE");
678     Creates a new billing line item
679     PARAMS( login, bill_object (mb) )
680     NOTE
681
682 sub billing_items_create {
683     my( $self, $client, $login, $billing ) = @_;
684
685     my $e = new_editor(authtoken => $login, xact => 1);
686     return $e->die_event unless $e->checkauth;
687     return $e->die_event unless $e->allowed('CREATE_BILL');
688
689     my $xact = $e->retrieve_money_billable_transaction($billing->xact)
690         or return $e->die_event;
691
692     # if the transaction was closed, re-open it
693     if($xact->xact_finish) {
694         $xact->clear_xact_finish;
695         $e->update_money_billable_transaction($xact)
696             or return $e->die_event;
697     }
698
699     my $amt = $billing->amount;
700     $amt =~ s/\$//og;
701     $billing->amount($amt);
702
703     $e->create_money_billing($billing) or return $e->die_event;
704     my $evt = OpenILS::Utils::Penalty->calculate_penalties($e, $xact->usr, $U->xact_org($xact->id,$e));
705     return $evt if $evt;
706     $e->commit;
707
708     return $billing->id;
709 }
710
711
712 __PACKAGE__->register_method(
713     method        =>    'void_bill',
714     api_name        => 'open-ils.circ.money.billing.void',
715     signature    => q/
716         Voids a bill
717         @param authtoken Login session key
718         @param billid Id for the bill to void.  This parameter may be repeated to reference other bills.
719         @return 1 on success, Event on error
720     /
721 );
722 sub void_bill {
723     my( $s, $c, $authtoken, @billids ) = @_;
724
725     my $e = new_editor( authtoken => $authtoken, xact => 1 );
726     return $e->die_event unless $e->checkauth;
727     return $e->die_event unless $e->allowed('VOID_BILLING');
728
729     my %users;
730     for my $billid (@billids) {
731
732         my $bill = $e->retrieve_money_billing($billid)
733             or return $e->die_event;
734
735         my $xact = $e->retrieve_money_billable_transaction($bill->xact)
736             or return $e->die_event;
737
738         if($U->is_true($bill->voided)) {
739             $e->rollback;
740             return OpenILS::Event->new('BILL_ALREADY_VOIDED', payload => $bill);
741         }
742
743         my $org = $U->xact_org($bill->xact, $e);
744         $users{$xact->usr} = {} unless $users{$xact->usr};
745         $users{$xact->usr}->{$org} = 1;
746
747         $bill->voided('t');
748         $bill->voider($e->requestor->id);
749         $bill->void_time('now');
750     
751         $e->update_money_billing($bill) or return $e->die_event;
752         my $evt = _check_open_xact($e, $bill->xact, $xact);
753         return $evt if $evt;
754     }
755
756     # calculate penalties for all user/org combinations
757     for my $user_id (keys %users) {
758         for my $org_id (keys %{$users{$user_id}}) {
759             OpenILS::Utils::Penalty->calculate_penalties($e, $user_id, $org_id);
760         }
761     }
762     $e->commit;
763     return 1;
764 }
765
766
767 __PACKAGE__->register_method(
768     method        =>    'edit_bill_note',
769     api_name        => 'open-ils.circ.money.billing.note.edit',
770     signature    => q/
771         Edits the note for a bill
772         @param authtoken Login session key
773         @param note The replacement note for the bills we're editing
774         @param billid Id for the bill to edit the note of.  This parameter may be repeated to reference other bills.
775         @return 1 on success, Event on error
776     /
777 );
778 sub edit_bill_note {
779     my( $s, $c, $authtoken, $note, @billids ) = @_;
780
781     my $e = new_editor( authtoken => $authtoken, xact => 1 );
782     return $e->die_event unless $e->checkauth;
783     return $e->die_event unless $e->allowed('UPDATE_BILL_NOTE');
784
785     for my $billid (@billids) {
786
787         my $bill = $e->retrieve_money_billing($billid)
788             or return $e->die_event;
789
790         $bill->note($note);
791         # 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.
792     
793         $e->update_money_billing($bill) or return $e->die_event;
794     }
795     $e->commit;
796     return 1;
797 }
798
799
800 __PACKAGE__->register_method(
801     method        =>    'edit_payment_note',
802     api_name        => 'open-ils.circ.money.payment.note.edit',
803     signature    => q/
804         Edits the note for a payment
805         @param authtoken Login session key
806         @param note The replacement note for the payments we're editing
807         @param paymentid Id for the payment to edit the note of.  This parameter may be repeated to reference other payments.
808         @return 1 on success, Event on error
809     /
810 );
811 sub edit_payment_note {
812     my( $s, $c, $authtoken, $note, @paymentids ) = @_;
813
814     my $e = new_editor( authtoken => $authtoken, xact => 1 );
815     return $e->die_event unless $e->checkauth;
816     return $e->die_event unless $e->allowed('UPDATE_PAYMENT_NOTE');
817
818     for my $paymentid (@paymentids) {
819
820         my $payment = $e->retrieve_money_payment($paymentid)
821             or return $e->die_event;
822
823         $payment->note($note);
824         # 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.
825     
826         $e->update_money_payment($payment) or return $e->die_event;
827     }
828
829     $e->commit;
830     return 1;
831 }
832
833 sub _check_open_xact {
834     my( $editor, $xactid, $xact ) = @_;
835
836     # Grab the transaction
837     $xact ||= $editor->retrieve_money_billable_transaction($xactid);
838     return $editor->event unless $xact;
839     $xactid ||= $xact->id;
840
841     # grab the summary and see how much is owed on this transaction
842     my ($summary) = $U->fetch_mbts($xactid, $editor);
843
844     # grab the circulation if it is a circ;
845     my $circ = $editor->retrieve_action_circulation($xactid);
846
847     # If nothing is owed on the transaction but it is still open
848     # and this transaction is not an open circulation, close it
849     if( 
850         ( $summary->balance_owed == 0 and ! $xact->xact_finish ) and
851         ( !$circ or $circ->stop_fines )) {
852
853         $logger->info("closing transaction ".$xact->id. ' becauase balance_owed == 0');
854         $xact->xact_finish('now');
855         $editor->update_money_billable_transaction($xact)
856             or return $editor->event;
857         return undef;
858     }
859
860     # If money is owed or a refund is due on the xact and xact_finish
861     # is set, clear it (to reopen the xact) and update
862     if( $summary->balance_owed != 0 and $xact->xact_finish ) {
863         $logger->info("re-opening transaction ".$xact->id. ' becauase balance_owed != 0');
864         $xact->clear_xact_finish;
865         $editor->update_money_billable_transaction($xact)
866             or return $editor->event;
867         return undef;
868     }
869     return undef;
870 }
871
872
873 __PACKAGE__->register_method (
874     method => 'fetch_mbts',
875     authoritative => 1,
876     api_name => 'open-ils.circ.money.billable_xact_summary.retrieve'
877 );
878 sub fetch_mbts {
879     my( $self, $conn, $auth, $id) = @_;
880
881     my $e = new_editor(xact => 1, authtoken=>$auth);
882     return $e->event unless $e->checkauth;
883     my ($mbts) = $U->fetch_mbts($id, $e);
884
885     my $user = $e->retrieve_actor_user($mbts->usr)
886         or return $e->die_event;
887
888     return $e->die_event unless $e->allowed('VIEW_TRANSACTION', $user->home_ou);
889     $e->rollback;
890     return $mbts
891 }
892
893
894 __PACKAGE__->register_method(
895     method => 'desk_payments',
896     api_name => 'open-ils.circ.money.org_unit.desk_payments'
897 );
898 sub desk_payments {
899     my( $self, $conn, $auth, $org, $start_date, $end_date ) = @_;
900     my $e = new_editor(authtoken=>$auth);
901     return $e->event unless $e->checkauth;
902     return $e->event unless $e->allowed('VIEW_TRANSACTION', $org);
903     my $data = $U->storagereq(
904         'open-ils.storage.money.org_unit.desk_payments.atomic',
905         $org, $start_date, $end_date );
906
907     $_->workstation( $_->workstation->name ) for(@$data);
908     return $data;
909 }
910
911
912 __PACKAGE__->register_method(
913     method => 'user_payments',
914     api_name => 'open-ils.circ.money.org_unit.user_payments'
915 );
916
917 sub user_payments {
918     my( $self, $conn, $auth, $org, $start_date, $end_date ) = @_;
919     my $e = new_editor(authtoken=>$auth);
920     return $e->event unless $e->checkauth;
921     return $e->event unless $e->allowed('VIEW_TRANSACTION', $org);
922     my $data = $U->storagereq(
923         'open-ils.storage.money.org_unit.user_payments.atomic',
924         $org, $start_date, $end_date );
925     for(@$data) {
926         $_->usr->card(
927             $e->retrieve_actor_card($_->usr->card)->barcode);
928         $_->usr->home_ou(
929             $e->retrieve_actor_org_unit($_->usr->home_ou)->shortname);
930     }
931     return $data;
932 }
933
934
935 __PACKAGE__->register_method(
936     method    => 'retrieve_credit_payable_balance',
937     api_name  => 'open-ils.circ.credit.payable_balance.retrieve',
938     authoritative => 1,
939     signature => {
940         desc   => q/Returns the total amount the patron can pay via credit card/,
941         params => [
942             { desc => 'Authentication token', type => 'string' },
943             { desc => 'User id', type => 'number' }
944         ],
945         return => { desc => 'The ID of the new provider' }
946     }
947 );
948
949 sub retrieve_credit_payable_balance {
950     my ( $self, $conn, $auth, $user_id ) = @_;
951     my $e = new_editor(authtoken => $auth);
952     return $e->event unless $e->checkauth;
953
954     my $user = $e->retrieve_actor_user($user_id) 
955         or return $e->event;
956
957     if($e->requestor->id != $user_id) {
958         return $e->event unless $e->allowed('VIEW_USER_TRANSACTIONS', $user->home_ou)
959     }
960
961     my $circ_orgs = $e->json_query({
962         "select" => {circ => ["circ_lib"]},
963         from     => "circ",
964         "where"  => {usr => $user_id, xact_finish => undef},
965         distinct => 1
966     });
967
968     my $groc_orgs = $e->json_query({
969         "select" => {mg => ["billing_location"]},
970         from     => "mg",
971         "where"  => {usr => $user_id, xact_finish => undef},
972         distinct => 1
973     });
974
975     my %hash;
976     for my $org ( @$circ_orgs, @$groc_orgs ) {
977         my $o = $org->{billing_location};
978         $o = $org->{circ_lib} unless $o;
979         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.
980         $hash{$o} = $U->ou_ancestor_setting_value($o, 'credit.payments.allow', $e);
981     }
982
983     my @credit_orgs = map { $hash{$_} ? ($_) : () } keys %hash;
984     $logger->debug("credit: relevant orgs that allow credit payments => @credit_orgs");
985
986     my $xact_summaries =
987       OpenILS::Application::AppUtils->simplereq('open-ils.actor',
988         'open-ils.actor.user.transactions.have_charge', $auth, $user_id);
989
990     my $sum = 0.0;
991
992     for my $xact (@$xact_summaries) {
993
994         # make two lists and grab them in batch XXX
995         if ( $xact->xact_type eq 'circulation' ) {
996             my $circ = $e->retrieve_action_circulation($xact->id) or return $e->event;
997             next unless grep { $_ == $circ->circ_lib } @credit_orgs;
998
999         } elsif ($xact->xact_type eq 'grocery') {
1000             my $bill = $e->retrieve_money_grocery($xact->id) or return $e->event;
1001             next unless grep { $_ == $bill->billing_location } @credit_orgs;
1002         } elsif ($xact->xact_type eq 'reservation') {
1003             my $bill = $e->retrieve_booking_reservation($xact->id) or return $e->event;
1004             next unless grep { $_ == $bill->pickup_lib } @credit_orgs;
1005         }
1006         $sum += $xact->balance_owed();
1007     }
1008
1009     return $sum;
1010 }
1011
1012
1013 1;