]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Money.pm
c127d7514c3bdf9df074a173824ccad65640720e
[working/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
146     # unless/until determined by payment processor API
147     my ($approval_code, $cc_processor, $cc_type, $cc_order_number) = (undef,undef,undef, undef);
148
149     my $patron = $e->retrieve_actor_user($user_id) or return $e->die_event;
150
151     if($patron->last_xact_id ne $last_xact_id) {
152         $e->rollback;
153         return OpenILS::Event->new('INVALID_USER_XACT_ID');
154     }
155
156     # A user is allowed to make credit card payments on his/her own behalf
157     # All other scenarious require permission
158     unless($type eq 'credit_card_payment' and $user_id == $e->requestor->id) {
159         return $e->die_event unless $e->allowed('CREATE_PAYMENT', $patron->home_ou);
160     }
161
162     # first collect the transactions and make sure the transaction
163     # user matches the requested user
164     my %xacts;
165
166     # We rewrite the payments array for sanity's sake, to avoid more
167     # than one payment per transaction per call, which is not legitimate
168     # but has been seen in the wild coming from the staff client.  This
169     # is presumably a staff client (xulrunner) bug.
170     my @unique_xact_payments;
171     for my $pay (@{$payments->{payments}}) {
172         my $xact_id = $pay->[0];
173         next if (exists($xacts{$xact_id}));
174
175         my $xact = $e->retrieve_money_billable_transaction_summary($xact_id)
176             or return $e->die_event;
177         
178         if($xact->usr != $user_id) {
179             $e->rollback;
180             return OpenILS::Event->new('BAD_PARAMS', note => q/user does not match transaction/);
181         }
182
183         $xacts{$xact_id} = $xact;
184         push @unique_xact_payments, $pay;
185     }
186     $payments->{payments} = \@unique_xact_payments;
187
188     my @payment_objs;
189
190     for my $pay (@{$payments->{payments}}) {
191         my $transid = $pay->[0];
192         my $amount = $pay->[1];
193         $amount =~ s/\$//og; # just to be safe
194         my $trans = $xacts{$transid};
195
196         $total_paid += $amount;
197
198         my $org_id = $U->xact_org($transid, $e);
199
200         if (!$orgs{$org_id}) {
201             $orgs{$org_id} = 1;
202
203             # patron credit has to be allowed at all orgs receiving payment
204             if ($type eq 'credit_payment' and $U->ou_ancestor_setting_value(
205                     $org_id, 'circ.disable_patron_credit', $e)) {
206                 $e->rollback;
207                 return OpenILS::Event->new('PATRON_CREDIT_DISABLED');
208             }
209         }
210
211         # A negative payment is a refund.  
212         if( $amount < 0 ) {
213
214             # Negative credit card payments are not allowed
215             if($type eq 'credit_card_payment') {
216                 $e->rollback;
217                 return OpenILS::Event->new(
218                     'BAD_PARAMS', 
219                     note => q/Negative credit card payments not allowed/
220                 );
221             }
222
223             # If the refund causes the transaction balance to exceed 0 dollars, 
224             # we are in effect loaning the patron money.  This is not allowed.
225             if( ($trans->balance_owed - $amount) > 0 ) {
226                 $e->rollback;
227                 return OpenILS::Event->new('REFUND_EXCEEDS_BALANCE');
228             }
229
230             # Otherwise, make sure the refund does not exceed desk payments
231             # This is also not allowed
232             my $desk_total = 0;
233             my $desk_payments = $e->search_money_desk_payment({xact => $transid, voided => 'f'});
234             $desk_total += $_->amount for @$desk_payments;
235
236             if( (-$amount) > $desk_total ) {
237                 $e->rollback;
238                 return OpenILS::Event->new(
239                     'REFUND_EXCEEDS_DESK_PAYMENTS', 
240                     payload => { allowed_refund => $desk_total, submitted_refund => -$amount } );
241             }
242         }
243
244         my $payobj = "Fieldmapper::money::$type";
245         $payobj = $payobj->new;
246
247         $payobj->amount($amount);
248         $payobj->amount_collected($amount);
249         $payobj->xact($transid);
250         $payobj->note($note);
251         if ((not $payobj->note) and ($type eq 'credit_card_payment')) {
252             $payobj->note($cc_args->{note});
253         }
254
255         if ($payobj->has_field('accepting_usr')) { $payobj->accepting_usr($e->requestor->id); }
256         if ($payobj->has_field('cash_drawer')) { $payobj->cash_drawer($drawer); }
257         if ($payobj->has_field('cc_type')) { $payobj->cc_type($cc_args->{type}); }
258         if ($payobj->has_field('check_number')) { $payobj->check_number($check_number); }
259
260         # Store the last 4 digits of the CC number
261         if ($payobj->has_field('cc_number')) {
262             $payobj->cc_number(substr($cc_args->{number}, -4));
263         }
264         if ($payobj->has_field('expire_month')) { $payobj->expire_month($cc_args->{expire_month}); }
265         if ($payobj->has_field('expire_year')) { $payobj->expire_year($cc_args->{expire_year}); }
266         
267         # Note: It is important not to set approval_code
268         # on the fieldmapper object yet.
269
270         push(@payment_objs, $payobj);
271
272     } # all payment objects have been created and inserted. 
273
274     #### NO WRITES TO THE DB ABOVE THIS LINE -- THEY'LL ONLY BE DISCARDED  ###
275     $e->rollback;
276
277     # After we try to externally process a credit card (if desired), we'll
278     # open a new transaction.  We cannot leave one open while credit card
279     # processing might be happening, as it can easily time out the database
280     # transaction.
281
282     my $cc_payload;
283
284     if($type eq 'credit_card_payment') {
285         $approval_code = $cc_args->{approval_code};
286         # If an approval code was not given, we'll need
287         # to call to the payment processor ourselves.
288         if ($cc_args->{where_process} == 1) {
289             return OpenILS::Event->new('BAD_PARAMS', note => 'Need CC number')
290                 if not $cc_args->{number};
291             my $response =
292                 OpenILS::Application::Circ::CreditCard::process_payment({
293                     "desc" => $cc_args->{note},
294                     "amount" => $total_paid,
295                     "patron_id" => $user_id,
296                     "cc" => $cc_args->{number},
297                     "expiration" => sprintf(
298                         "%02d-%04d",
299                         $cc_args->{expire_month},
300                         $cc_args->{expire_year}
301                     ),
302                     "ou" => $this_ou,
303                     "first_name" => $cc_args->{billing_first},
304                     "last_name" => $cc_args->{billing_last},
305                     "address" => $cc_args->{billing_address},
306                     "city" => $cc_args->{billing_city},
307                     "state" => $cc_args->{billing_state},
308                     "zip" => $cc_args->{billing_zip},
309                     "cvv2" => $cc_args->{cvv2},
310                 });
311
312             if ($U->event_code($response)) { # non-success
313                 $logger->info(
314                     "Credit card payment for user $user_id failed: " .
315                     $response->{"textcode"} . " " .
316                     $response->{"payload"}->{"error_message"}
317                 );
318
319                 return $response;
320             } else {
321                 # We need to save this for later in case there's a failure on
322                 # the EG side to store the processor's result.
323                 $cc_payload = $response->{"payload"};
324
325                 $approval_code = $cc_payload->{"authorization"};
326                 $cc_type = $cc_payload->{"card_type"};
327                 $cc_processor = $cc_payload->{"processor"};
328                 $cc_order_number = $cc_payload->{"order_number"};
329                 $logger->info("Credit card payment for user $user_id succeeded");
330             }
331         } else {
332             return OpenILS::Event->new(
333                 'BAD_PARAMS', note => 'Need approval code'
334             ) if not $cc_args->{approval_code};
335         }
336     }
337
338     ### RE-OPEN TRANSACTION HERE ###
339     $e->xact_begin;
340     my @payment_ids;
341
342     # create payment records
343     my $create_money_method = "create_money_" . $type;
344     for my $payment (@payment_objs) {
345         # update the transaction if it's done
346         my $amount = $payment->amount;
347         my $transid = $payment->xact;
348         my $trans = $xacts{$transid};
349         # making payment with existing patron credit.
350         $credit -= $amount if $type eq 'credit_payment';
351         if( (my $cred = ($trans->balance_owed - $amount)) <= 0 ) {
352             # Any overpay on this transaction goes directly into patron
353             # credit
354             $cred = -$cred;
355             $credit += $cred;
356             my $circ = $e->retrieve_action_circulation($transid);
357
358             # Whether or not we close the transaction. We definitely
359             # close is no circulation transaction is present,
360             # otherwise we check if the circulation is in a state that
361             # allows itself to be closed.
362             if (!$circ || OpenILS::Application::Circ::CircCommon->can_close_circ($e, $circ)) {
363                 $trans = $e->retrieve_money_billable_transaction($transid);
364                 $trans->xact_finish("now");
365                 if (!$e->update_money_billable_transaction($trans)) {
366                     return _recording_failure(
367                         $e, "update_money_billable_transaction() failed",
368                         $payment, $cc_payload
369                     )
370                 }
371             }
372         }
373
374         $payment->approval_code($approval_code) if $approval_code;
375         $payment->cc_order_number($cc_order_number) if $cc_order_number;
376         $payment->cc_type($cc_type) if $cc_type;
377         $payment->cc_processor($cc_processor) if $cc_processor;
378         $payment->cc_first_name($cc_args->{'billing_first'}) if $cc_args->{'billing_first'};
379         $payment->cc_last_name($cc_args->{'billing_last'}) if $cc_args->{'billing_last'};
380         if (!$e->$create_money_method($payment)) {
381             return _recording_failure(
382                 $e, "$create_money_method failed", $payment, $cc_payload
383             );
384         }
385
386         push(@payment_ids, $payment->id);
387     }
388
389     my $evt = _update_patron_credit($e, $patron, $credit);
390     if ($evt) {
391         return _recording_failure(
392             $e, "_update_patron_credit() failed", undef, $cc_payload
393         );
394     }
395
396     for my $org_id (keys %orgs) {
397         # calculate penalties for each of the affected orgs
398         $evt = OpenILS::Utils::Penalty->calculate_penalties(
399             $e, $user_id, $org_id
400         );
401         if ($evt) {
402             return _recording_failure(
403                 $e, "calculate_penalties() failed", undef, $cc_payload
404             );
405         }
406     }
407
408     # update the user to create a new last_xact_id
409     $e->update_actor_user($patron) or return $e->die_event;
410     $patron = $e->retrieve_actor_user($patron) or return $e->die_event;
411     $e->commit;
412
413     # update the cached user object if a user is making a payment toward 
414     # his/her own account
415     $U->simplereq('open-ils.auth', 'open-ils.auth.session.reset_timeout', $auth, 1)
416         if $user_id == $e->requestor->id;
417
418     return {last_xact_id => $patron->last_xact_id, payments => \@payment_ids};
419 }
420
421 sub _recording_failure {
422     my ($e, $msg, $payment, $payload) = @_;
423
424     if ($payload) { # If the payment processor already accepted a payment:
425         $logger->error($msg);
426         $logger->error("Payment processor payload: " . Dumper($payload));
427         # payment shouldn't contain CC number
428         $logger->error("Payment: " . Dumper($payment)) if $payment;
429
430         $e->rollback;
431
432         return new OpenILS::Event(
433             "CREDIT_PROCESSOR_SUCCESS_WO_RECORD",
434             "payload" => $payload
435         );
436     } else { # Otherwise, the problem is somewhat less severe:
437         $logger->warn($msg);
438         $logger->warn("Payment: " . Dumper($payment)) if $payment;
439         return $e->die_event;
440     }
441 }
442
443 sub _update_patron_credit {
444     my($e, $patron, $credit) = @_;
445     return undef if $credit == 0;
446     $patron->credit_forward_balance($patron->credit_forward_balance + $credit);
447     return OpenILS::Event->new('NEGATIVE_PATRON_BALANCE') if $patron->credit_forward_balance < 0;
448     $e->update_actor_user($patron) or return $e->die_event;
449     return undef;
450 }
451
452
453 __PACKAGE__->register_method(
454     method    => "retrieve_payments",
455     api_name    => "open-ils.circ.money.payment.retrieve.all_",
456     notes        => "Returns a list of payments attached to a given transaction"
457     );
458 sub retrieve_payments {
459     my( $self, $client, $login, $transid ) = @_;
460
461     my( $staff, $evt ) =  
462         $apputils->checksesperm($login, 'VIEW_TRANSACTION');
463     return $evt if $evt;
464
465     # XXX the logic here is wrong.. we need to check the owner of the transaction
466     # to make sure the requestor has access
467
468     # XXX grab the view, for each object in the view, grab the real object
469
470     return $apputils->simplereq(
471         'open-ils.cstore',
472         'open-ils.cstore.direct.money.payment.search.atomic', { xact => $transid } );
473 }
474
475
476 __PACKAGE__->register_method(
477     method    => "retrieve_payments2",
478     authoritative => 1,
479     api_name    => "open-ils.circ.money.payment.retrieve.all",
480     notes        => "Returns a list of payments attached to a given transaction"
481     );
482     
483 sub retrieve_payments2 {
484     my( $self, $client, $login, $transid ) = @_;
485
486     my $e = new_editor(authtoken=>$login);
487     return $e->event unless $e->checkauth;
488     return $e->event unless $e->allowed('VIEW_TRANSACTION');
489
490     my @payments;
491     my $pmnts = $e->search_money_payment({ xact => $transid });
492     for( @$pmnts ) {
493         my $type = $_->payment_type;
494         my $meth = "retrieve_money_$type";
495         my $p = $e->$meth($_->id) or return $e->event;
496         $p->payment_type($type);
497         $p->cash_drawer($e->retrieve_actor_workstation($p->cash_drawer))
498             if $p->has_field('cash_drawer');
499         push( @payments, $p );
500     }
501
502     return \@payments;
503 }
504
505 __PACKAGE__->register_method(
506     method    => "format_payment_receipt",
507     api_name  => "open-ils.circ.money.payment_receipt.print",
508     signature => {
509         desc   => 'Returns a printable receipt for the specified payments',
510         params => [
511             { desc => 'Authentication token',  type => 'string'},
512             { desc => 'Payment ID or array of payment IDs', type => 'number' },
513         ],
514         return => {
515             desc => q/An action_trigger.event object or error event./,
516             type => 'object',
517         }
518     }
519 );
520 __PACKAGE__->register_method(
521     method    => "format_payment_receipt",
522     api_name  => "open-ils.circ.money.payment_receipt.email",
523     signature => {
524         desc   => 'Emails a receipt for the specified payments to the user associated with the first payment',
525         params => [
526             { desc => 'Authentication token',  type => 'string'},
527             { desc => 'Payment ID or array of payment IDs', type => 'number' },
528         ],
529         return => {
530             desc => q/Undefined on success, otherwise an error event./,
531             type => 'object',
532         }
533     }
534 );
535
536 sub format_payment_receipt {
537     my($self, $conn, $auth, $mp_id) = @_;
538
539     my $mp_ids;
540     if (ref $mp_id ne 'ARRAY') {
541         $mp_ids = [ $mp_id ];
542     } else {
543         $mp_ids = $mp_id;
544     }
545
546     my $for_print = ($self->api_name =~ /print/);
547     my $for_email = ($self->api_name =~ /email/);
548
549     # manually use xact (i.e. authoritative) so we can kill the cstore
550     # connection before sending the action/trigger request.  This prevents our cstore
551     # backend from sitting idle while A/T (which uses its own transactions) runs.
552     my $e = new_editor(xact => 1, authtoken => $auth);
553     return $e->die_event unless $e->checkauth;
554
555     my $payments = [];
556     for my $id (@$mp_ids) {
557
558         my $payment = $e->retrieve_money_payment([
559             $id,
560             {   flesh => 2,
561                 flesh_fields => {
562                     mp => ['xact'],
563                     mbt => ['usr']
564                 }
565             }
566         ]) or return $e->die_event;
567
568         return $e->die_event unless 
569             $e->requestor->id == $payment->xact->usr->id or
570             $e->allowed('VIEW_TRANSACTION', $payment->xact->usr->home_ou); 
571
572         push @$payments, $payment;
573     }
574
575     $e->rollback;
576
577     if ($for_print) {
578
579         return $U->fire_object_event(undef, 'money.format.payment_receipt.print', $payments, $$payments[0]->xact->usr->home_ou);
580
581     } elsif ($for_email) {
582
583         for my $p (@$payments) {
584             $U->create_events_for_hook('money.format.payment_receipt.email', $p, $p->xact->usr->home_ou, undef, undef, 1);
585         }
586     }
587
588     return undef;
589 }
590
591 __PACKAGE__->register_method(
592     method    => "create_grocery_bill",
593     api_name    => "open-ils.circ.money.grocery.create",
594     notes        => <<"    NOTE");
595     Creates a new grocery transaction using the transaction object provided
596     PARAMS: (login_session, money.grocery (mg) object)
597     NOTE
598
599 sub create_grocery_bill {
600     my( $self, $client, $login, $transaction ) = @_;
601
602     my( $staff, $evt ) = $apputils->checkses($login);
603     return $evt if $evt;
604     $evt = $apputils->check_perms($staff->id, 
605         $transaction->billing_location, 'CREATE_TRANSACTION' );
606     return $evt if $evt;
607
608
609     $logger->activity("Creating grocery bill " . Dumper($transaction) );
610
611     $transaction->clear_id;
612     my $session = $apputils->start_db_session;
613     $apputils->set_audit_info($session, $login, $staff->id, $staff->wsid);
614     my $transid = $session->request(
615         'open-ils.storage.direct.money.grocery.create', $transaction)->gather(1);
616
617     throw OpenSRF::EX ("Error creating new money.grocery") unless defined $transid;
618
619     $logger->debug("Created new grocery transaction $transid");
620     
621     $apputils->commit_db_session($session);
622
623     my $e = new_editor(xact=>1);
624     $evt = $U->check_open_xact($e, $transid);
625     return $evt if $evt;
626     $e->commit;
627
628     return $transid;
629 }
630
631
632 __PACKAGE__->register_method(
633     method => 'fetch_reservation',
634     api_name => 'open-ils.circ.booking.reservation.retrieve'
635 );
636 sub fetch_reservation {
637     my( $self, $conn, $auth, $id ) = @_;
638     my $e = new_editor(authtoken=>$auth);
639     return $e->event unless $e->checkauth;
640     return $e->event unless $e->allowed('VIEW_TRANSACTION'); # eh.. basically the same permission
641     my $g = $e->retrieve_booking_reservation($id)
642         or return $e->event;
643     return $g;
644 }
645
646 __PACKAGE__->register_method(
647     method   => 'fetch_grocery',
648     api_name => 'open-ils.circ.money.grocery.retrieve'
649 );
650 sub fetch_grocery {
651     my( $self, $conn, $auth, $id ) = @_;
652     my $e = new_editor(authtoken=>$auth);
653     return $e->event unless $e->checkauth;
654     return $e->event unless $e->allowed('VIEW_TRANSACTION'); # eh.. basically the same permission
655     my $g = $e->retrieve_money_grocery($id)
656         or return $e->event;
657     return $g;
658 }
659
660
661 __PACKAGE__->register_method(
662     method        => "billing_items",
663     api_name      => "open-ils.circ.money.billing.retrieve.all",
664     authoritative => 1,
665     signature     => {
666         desc   => 'Returns a list of billing items for the given transaction ID.  ' .
667                   'If the operator is not the owner of the transaction, the VIEW_TRANSACTION permission is required.',
668         params => [
669             { desc => 'Authentication token', type => 'string'},
670             { desc => 'Transaction ID',       type => 'number'}
671         ],
672         return => {
673             desc => 'Transaction object, event on error'
674         },
675     }
676 );
677
678 sub billing_items {
679     my( $self, $client, $login, $transid ) = @_;
680
681     my( $trans, $evt ) = $U->fetch_billable_xact($transid);
682     return $evt if $evt;
683
684     my $staff;
685     ($staff, $evt ) = $apputils->checkses($login);
686     return $evt if $evt;
687
688     if($staff->id ne $trans->usr) {
689         $evt = $U->check_perms($staff->id, $staff->home_ou, 'VIEW_TRANSACTION');
690         return $evt if $evt;
691     }
692     
693     return $apputils->simplereq( 'open-ils.cstore',
694         'open-ils.cstore.direct.money.billing.search.atomic', { xact => $transid } )
695 }
696
697
698 __PACKAGE__->register_method(
699     method   => "billing_items_create",
700     api_name => "open-ils.circ.money.billing.create",
701     notes    => <<"    NOTE");
702     Creates a new billing line item
703     PARAMS( login, bill_object (mb) )
704     NOTE
705
706 sub billing_items_create {
707     my( $self, $client, $login, $billing ) = @_;
708
709     my $e = new_editor(authtoken => $login, xact => 1);
710     return $e->die_event unless $e->checkauth;
711     return $e->die_event unless $e->allowed('CREATE_BILL');
712
713     my $xact = $e->retrieve_money_billable_transaction($billing->xact)
714         or return $e->die_event;
715
716     # if the transaction was closed, re-open it
717     if($xact->xact_finish) {
718         $xact->clear_xact_finish;
719         $e->update_money_billable_transaction($xact)
720             or return $e->die_event;
721     }
722
723     my $amt = $billing->amount;
724     $amt =~ s/\$//og;
725     $billing->amount($amt);
726
727     $e->create_money_billing($billing) or return $e->die_event;
728     my $evt = OpenILS::Utils::Penalty->calculate_penalties($e, $xact->usr, $U->xact_org($xact->id,$e));
729     return $evt if $evt;
730
731     $evt = $U->check_open_xact($e, $xact->id, $xact);
732     return $evt if $evt;
733
734     $e->commit;
735
736     return $billing->id;
737 }
738
739
740 __PACKAGE__->register_method(
741     method        =>    'void_bill',
742     api_name        => 'open-ils.circ.money.billing.void',
743     signature    => q/
744         Voids a bill
745         @param authtoken Login session key
746         @param billid Id for the bill to void.  This parameter may be repeated to reference other bills.
747         @return 1 on success, Event on error
748     /
749 );
750 sub void_bill {
751     my( $s, $c, $authtoken, @billids ) = @_;
752
753     my $e = new_editor( authtoken => $authtoken, xact => 1 );
754     return $e->die_event unless $e->checkauth;
755     return $e->die_event unless $e->allowed('VOID_BILLING');
756
757     my %users;
758     for my $billid (@billids) {
759
760         my $bill = $e->retrieve_money_billing($billid)
761             or return $e->die_event;
762
763         my $xact = $e->retrieve_money_billable_transaction($bill->xact)
764             or return $e->die_event;
765
766         if($U->is_true($bill->voided)) {
767             $e->rollback;
768             return OpenILS::Event->new('BILL_ALREADY_VOIDED', payload => $bill);
769         }
770
771         my $org = $U->xact_org($bill->xact, $e);
772         $users{$xact->usr} = {} unless $users{$xact->usr};
773         $users{$xact->usr}->{$org} = 1;
774
775         $bill->voided('t');
776         $bill->voider($e->requestor->id);
777         $bill->void_time('now');
778     
779         $e->update_money_billing($bill) or return $e->die_event;
780         my $evt = $U->check_open_xact($e, $bill->xact, $xact);
781         return $evt if $evt;
782     }
783
784     # calculate penalties for all user/org combinations
785     for my $user_id (keys %users) {
786         for my $org_id (keys %{$users{$user_id}}) {
787             OpenILS::Utils::Penalty->calculate_penalties($e, $user_id, $org_id);
788         }
789     }
790     $e->commit;
791     return 1;
792 }
793
794
795 __PACKAGE__->register_method(
796     method        =>    'edit_bill_note',
797     api_name        => 'open-ils.circ.money.billing.note.edit',
798     signature    => q/
799         Edits the note for a bill
800         @param authtoken Login session key
801         @param note The replacement note for the bills we're editing
802         @param billid Id for the bill to edit the note of.  This parameter may be repeated to reference other bills.
803         @return 1 on success, Event on error
804     /
805 );
806 sub edit_bill_note {
807     my( $s, $c, $authtoken, $note, @billids ) = @_;
808
809     my $e = new_editor( authtoken => $authtoken, xact => 1 );
810     return $e->die_event unless $e->checkauth;
811     return $e->die_event unless $e->allowed('UPDATE_BILL_NOTE');
812
813     for my $billid (@billids) {
814
815         my $bill = $e->retrieve_money_billing($billid)
816             or return $e->die_event;
817
818         $bill->note($note);
819         # 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.
820     
821         $e->update_money_billing($bill) or return $e->die_event;
822     }
823     $e->commit;
824     return 1;
825 }
826
827
828 __PACKAGE__->register_method(
829     method        =>    'edit_payment_note',
830     api_name        => 'open-ils.circ.money.payment.note.edit',
831     signature    => q/
832         Edits the note for a payment
833         @param authtoken Login session key
834         @param note The replacement note for the payments we're editing
835         @param paymentid Id for the payment to edit the note of.  This parameter may be repeated to reference other payments.
836         @return 1 on success, Event on error
837     /
838 );
839 sub edit_payment_note {
840     my( $s, $c, $authtoken, $note, @paymentids ) = @_;
841
842     my $e = new_editor( authtoken => $authtoken, xact => 1 );
843     return $e->die_event unless $e->checkauth;
844     return $e->die_event unless $e->allowed('UPDATE_PAYMENT_NOTE');
845
846     for my $paymentid (@paymentids) {
847
848         my $payment = $e->retrieve_money_payment($paymentid)
849             or return $e->die_event;
850
851         $payment->note($note);
852         # 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.
853     
854         $e->update_money_payment($payment) or return $e->die_event;
855     }
856
857     $e->commit;
858     return 1;
859 }
860
861
862 __PACKAGE__->register_method (
863     method => 'fetch_mbts',
864     authoritative => 1,
865     api_name => 'open-ils.circ.money.billable_xact_summary.retrieve'
866 );
867 sub fetch_mbts {
868     my( $self, $conn, $auth, $id) = @_;
869
870     my $e = new_editor(xact => 1, authtoken=>$auth);
871     return $e->event unless $e->checkauth;
872     my ($mbts) = $U->fetch_mbts($id, $e);
873
874     my $user = $e->retrieve_actor_user($mbts->usr)
875         or return $e->die_event;
876
877     return $e->die_event unless $e->allowed('VIEW_TRANSACTION', $user->home_ou);
878     $e->rollback;
879     return $mbts
880 }
881
882
883 __PACKAGE__->register_method(
884     method => 'desk_payments',
885     api_name => 'open-ils.circ.money.org_unit.desk_payments'
886 );
887 sub desk_payments {
888     my( $self, $conn, $auth, $org, $start_date, $end_date ) = @_;
889     my $e = new_editor(authtoken=>$auth);
890     return $e->event unless $e->checkauth;
891     return $e->event unless $e->allowed('VIEW_TRANSACTION', $org);
892     my $data = $U->storagereq(
893         'open-ils.storage.money.org_unit.desk_payments.atomic',
894         $org, $start_date, $end_date );
895
896     $_->workstation( $_->workstation->name ) for(@$data);
897     return $data;
898 }
899
900
901 __PACKAGE__->register_method(
902     method => 'user_payments',
903     api_name => 'open-ils.circ.money.org_unit.user_payments'
904 );
905
906 sub user_payments {
907     my( $self, $conn, $auth, $org, $start_date, $end_date ) = @_;
908     my $e = new_editor(authtoken=>$auth);
909     return $e->event unless $e->checkauth;
910     return $e->event unless $e->allowed('VIEW_TRANSACTION', $org);
911     my $data = $U->storagereq(
912         'open-ils.storage.money.org_unit.user_payments.atomic',
913         $org, $start_date, $end_date );
914     for(@$data) {
915         $_->usr->card(
916             $e->retrieve_actor_card($_->usr->card)->barcode);
917         $_->usr->home_ou(
918             $e->retrieve_actor_org_unit($_->usr->home_ou)->shortname);
919     }
920     return $data;
921 }
922
923
924 __PACKAGE__->register_method(
925     method    => 'retrieve_credit_payable_balance',
926     api_name  => 'open-ils.circ.credit.payable_balance.retrieve',
927     authoritative => 1,
928     signature => {
929         desc   => q/Returns the total amount the patron can pay via credit card/,
930         params => [
931             { desc => 'Authentication token', type => 'string' },
932             { desc => 'User id', type => 'number' }
933         ],
934         return => { desc => 'The ID of the new provider' }
935     }
936 );
937
938 sub retrieve_credit_payable_balance {
939     my ( $self, $conn, $auth, $user_id ) = @_;
940     my $e = new_editor(authtoken => $auth);
941     return $e->event unless $e->checkauth;
942
943     my $user = $e->retrieve_actor_user($user_id) 
944         or return $e->event;
945
946     if($e->requestor->id != $user_id) {
947         return $e->event unless $e->allowed('VIEW_USER_TRANSACTIONS', $user->home_ou)
948     }
949
950     my $circ_orgs = $e->json_query({
951         "select" => {circ => ["circ_lib"]},
952         from     => "circ",
953         "where"  => {usr => $user_id, xact_finish => undef},
954         distinct => 1
955     });
956
957     my $groc_orgs = $e->json_query({
958         "select" => {mg => ["billing_location"]},
959         from     => "mg",
960         "where"  => {usr => $user_id, xact_finish => undef},
961         distinct => 1
962     });
963
964     my %hash;
965     for my $org ( @$circ_orgs, @$groc_orgs ) {
966         my $o = $org->{billing_location};
967         $o = $org->{circ_lib} unless $o;
968         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.
969         $hash{$o} = $U->ou_ancestor_setting_value($o, 'credit.payments.allow', $e);
970     }
971
972     my @credit_orgs = map { $hash{$_} ? ($_) : () } keys %hash;
973     $logger->debug("credit: relevant orgs that allow credit payments => @credit_orgs");
974
975     my $xact_summaries =
976       OpenILS::Application::AppUtils->simplereq('open-ils.actor',
977         'open-ils.actor.user.transactions.have_charge', $auth, $user_id);
978
979     my $sum = 0.0;
980
981     for my $xact (@$xact_summaries) {
982
983         # make two lists and grab them in batch XXX
984         if ( $xact->xact_type eq 'circulation' ) {
985             my $circ = $e->retrieve_action_circulation($xact->id) or return $e->event;
986             next unless grep { $_ == $circ->circ_lib } @credit_orgs;
987
988         } elsif ($xact->xact_type eq 'grocery') {
989             my $bill = $e->retrieve_money_grocery($xact->id) or return $e->event;
990             next unless grep { $_ == $bill->billing_location } @credit_orgs;
991         } elsif ($xact->xact_type eq 'reservation') {
992             my $bill = $e->retrieve_booking_reservation($xact->id) or return $e->event;
993             next unless grep { $_ == $bill->pickup_lib } @credit_orgs;
994         }
995         $sum += $xact->balance_owed();
996     }
997
998     return $sum;
999 }
1000
1001
1002 1;