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