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