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