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