]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Money.pm
capture credit card order_number
[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 OpenILS::Event->new('MP_NOT_FOUND');
538
539         return $e->event unless $e->allowed('VIEW_TRANSACTION', $payment->xact->usr->home_ou); 
540
541         push @$payments, $payment;
542     }
543
544     if ($for_print) {
545
546         return $U->fire_object_event(undef, 'money.format.payment_receipt.print', $payments, $$payments[0]->xact->usr->home_ou);
547
548     } elsif ($for_email) {
549
550         for my $p (@$payments) {
551             $U->create_events_for_hook('money.format.payment_receipt.email', $p, $p->xact->usr->home_ou, undef, undef, 1);
552         }
553     }
554
555     return undef;
556 }
557
558 __PACKAGE__->register_method(
559     method    => "create_grocery_bill",
560     api_name    => "open-ils.circ.money.grocery.create",
561     notes        => <<"    NOTE");
562     Creates a new grocery transaction using the transaction object provided
563     PARAMS: (login_session, money.grocery (mg) object)
564     NOTE
565
566 sub create_grocery_bill {
567     my( $self, $client, $login, $transaction ) = @_;
568
569     my( $staff, $evt ) = $apputils->checkses($login);
570     return $evt if $evt;
571     $evt = $apputils->check_perms($staff->id, 
572         $transaction->billing_location, 'CREATE_TRANSACTION' );
573     return $evt if $evt;
574
575
576     $logger->activity("Creating grocery bill " . Dumper($transaction) );
577
578     $transaction->clear_id;
579     my $session = $apputils->start_db_session;
580     my $transid = $session->request(
581         'open-ils.storage.direct.money.grocery.create', $transaction)->gather(1);
582
583     throw OpenSRF::EX ("Error creating new money.grocery") unless defined $transid;
584
585     $logger->debug("Created new grocery transaction $transid");
586     
587     $apputils->commit_db_session($session);
588
589     my $e = new_editor(xact=>1);
590     $evt = _check_open_xact($e, $transid);
591     return $evt if $evt;
592     $e->commit;
593
594     return $transid;
595 }
596
597
598 __PACKAGE__->register_method(
599     method => 'fetch_reservation',
600     api_name => 'open-ils.circ.booking.reservation.retrieve'
601 );
602 sub fetch_reservation {
603     my( $self, $conn, $auth, $id ) = @_;
604     my $e = new_editor(authtoken=>$auth);
605     return $e->event unless $e->checkauth;
606     return $e->event unless $e->allowed('VIEW_TRANSACTION'); # eh.. basically the same permission
607     my $g = $e->retrieve_booking_reservation($id)
608         or return $e->event;
609     return $g;
610 }
611
612 __PACKAGE__->register_method(
613     method   => 'fetch_grocery',
614     api_name => 'open-ils.circ.money.grocery.retrieve'
615 );
616 sub fetch_grocery {
617     my( $self, $conn, $auth, $id ) = @_;
618     my $e = new_editor(authtoken=>$auth);
619     return $e->event unless $e->checkauth;
620     return $e->event unless $e->allowed('VIEW_TRANSACTION'); # eh.. basically the same permission
621     my $g = $e->retrieve_money_grocery($id)
622         or return $e->event;
623     return $g;
624 }
625
626
627 __PACKAGE__->register_method(
628     method        => "billing_items",
629     api_name      => "open-ils.circ.money.billing.retrieve.all",
630     authoritative => 1,
631     signature     => {
632         desc   => 'Returns a list of billing items for the given transaction ID.  ' .
633                   'If the operator is not the owner of the transaction, the VIEW_TRANSACTION permission is required.',
634         params => [
635             { desc => 'Authentication token', type => 'string'},
636             { desc => 'Transaction ID',       type => 'number'}
637         ],
638         return => {
639             desc => 'Transaction object, event on error'
640         },
641     }
642 );
643
644 sub billing_items {
645     my( $self, $client, $login, $transid ) = @_;
646
647     my( $trans, $evt ) = $U->fetch_billable_xact($transid);
648     return $evt if $evt;
649
650     my $staff;
651     ($staff, $evt ) = $apputils->checkses($login);
652     return $evt if $evt;
653
654     if($staff->id ne $trans->usr) {
655         $evt = $U->check_perms($staff->id, $staff->home_ou, 'VIEW_TRANSACTION');
656         return $evt if $evt;
657     }
658     
659     return $apputils->simplereq( 'open-ils.cstore',
660         'open-ils.cstore.direct.money.billing.search.atomic', { xact => $transid } )
661 }
662
663
664 __PACKAGE__->register_method(
665     method   => "billing_items_create",
666     api_name => "open-ils.circ.money.billing.create",
667     notes    => <<"    NOTE");
668     Creates a new billing line item
669     PARAMS( login, bill_object (mb) )
670     NOTE
671
672 sub billing_items_create {
673     my( $self, $client, $login, $billing ) = @_;
674
675     my $e = new_editor(authtoken => $login, xact => 1);
676     return $e->die_event unless $e->checkauth;
677     return $e->die_event unless $e->allowed('CREATE_BILL');
678
679     my $xact = $e->retrieve_money_billable_transaction($billing->xact)
680         or return $e->die_event;
681
682     # if the transaction was closed, re-open it
683     if($xact->xact_finish) {
684         $xact->clear_xact_finish;
685         $e->update_money_billable_transaction($xact)
686             or return $e->die_event;
687     }
688
689     my $amt = $billing->amount;
690     $amt =~ s/\$//og;
691     $billing->amount($amt);
692
693     $e->create_money_billing($billing) or return $e->die_event;
694     my $evt = OpenILS::Utils::Penalty->calculate_penalties($e, $xact->usr, $U->xact_org($xact->id));
695     return $evt if $evt;
696     $e->commit;
697
698     return $billing->id;
699 }
700
701
702 __PACKAGE__->register_method(
703     method        =>    'void_bill',
704     api_name        => 'open-ils.circ.money.billing.void',
705     signature    => q/
706         Voids a bill
707         @param authtoken Login session key
708         @param billid Id for the bill to void.  This parameter may be repeated to reference other bills.
709         @return 1 on success, Event on error
710     /
711 );
712 sub void_bill {
713     my( $s, $c, $authtoken, @billids ) = @_;
714
715     my $e = new_editor( authtoken => $authtoken, xact => 1 );
716     return $e->die_event unless $e->checkauth;
717     return $e->die_event unless $e->allowed('VOID_BILLING');
718
719     my %users;
720     for my $billid (@billids) {
721
722         my $bill = $e->retrieve_money_billing($billid)
723             or return $e->die_event;
724
725         my $xact = $e->retrieve_money_billable_transaction($bill->xact)
726             or return $e->die_event;
727
728         if($U->is_true($bill->voided)) {
729             $e->rollback;
730             return OpenILS::Event->new('BILL_ALREADY_VOIDED', payload => $bill);
731         }
732
733         my $org = $U->xact_org($bill->xact, $e);
734         $users{$xact->usr} = {} unless $users{$xact->usr};
735         $users{$xact->usr}->{$org} = 1;
736
737         $bill->voided('t');
738         $bill->voider($e->requestor->id);
739         $bill->void_time('now');
740     
741         $e->update_money_billing($bill) or return $e->die_event;
742         my $evt = _check_open_xact($e, $bill->xact, $xact);
743         return $evt if $evt;
744     }
745
746     # calculate penalties for all user/org combinations
747     for my $user_id (keys %users) {
748         for my $org_id (keys %{$users{$user_id}}) {
749             OpenILS::Utils::Penalty->calculate_penalties($e, $user_id, $org_id);
750         }
751     }
752     $e->commit;
753     return 1;
754 }
755
756
757 __PACKAGE__->register_method(
758     method        =>    'edit_bill_note',
759     api_name        => 'open-ils.circ.money.billing.note.edit',
760     signature    => q/
761         Edits the note for a bill
762         @param authtoken Login session key
763         @param note The replacement note for the bills we're editing
764         @param billid Id for the bill to edit the note of.  This parameter may be repeated to reference other bills.
765         @return 1 on success, Event on error
766     /
767 );
768 sub edit_bill_note {
769     my( $s, $c, $authtoken, $note, @billids ) = @_;
770
771     my $e = new_editor( authtoken => $authtoken, xact => 1 );
772     return $e->die_event unless $e->checkauth;
773     return $e->die_event unless $e->allowed('UPDATE_BILL_NOTE');
774
775     for my $billid (@billids) {
776
777         my $bill = $e->retrieve_money_billing($billid)
778             or return $e->die_event;
779
780         $bill->note($note);
781         # 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.
782     
783         $e->update_money_billing($bill) or return $e->die_event;
784     }
785     $e->commit;
786     return 1;
787 }
788
789
790 __PACKAGE__->register_method(
791     method        =>    'edit_payment_note',
792     api_name        => 'open-ils.circ.money.payment.note.edit',
793     signature    => q/
794         Edits the note for a payment
795         @param authtoken Login session key
796         @param note The replacement note for the payments we're editing
797         @param paymentid Id for the payment to edit the note of.  This parameter may be repeated to reference other payments.
798         @return 1 on success, Event on error
799     /
800 );
801 sub edit_payment_note {
802     my( $s, $c, $authtoken, $note, @paymentids ) = @_;
803
804     my $e = new_editor( authtoken => $authtoken, xact => 1 );
805     return $e->die_event unless $e->checkauth;
806     return $e->die_event unless $e->allowed('UPDATE_PAYMENT_NOTE');
807
808     for my $paymentid (@paymentids) {
809
810         my $payment = $e->retrieve_money_payment($paymentid)
811             or return $e->die_event;
812
813         $payment->note($note);
814         # 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.
815     
816         $e->update_money_payment($payment) or return $e->die_event;
817     }
818
819     $e->commit;
820     return 1;
821 }
822
823 sub _check_open_xact {
824     my( $editor, $xactid, $xact ) = @_;
825
826     # Grab the transaction
827     $xact ||= $editor->retrieve_money_billable_transaction($xactid);
828     return $editor->event unless $xact;
829     $xactid ||= $xact->id;
830
831     # grab the summary and see how much is owed on this transaction
832     my ($summary) = $U->fetch_mbts($xactid, $editor);
833
834     # grab the circulation if it is a circ;
835     my $circ = $editor->retrieve_action_circulation($xactid);
836
837     # If nothing is owed on the transaction but it is still open
838     # and this transaction is not an open circulation, close it
839     if( 
840         ( $summary->balance_owed == 0 and ! $xact->xact_finish ) and
841         ( !$circ or $circ->stop_fines )) {
842
843         $logger->info("closing transaction ".$xact->id. ' becauase balance_owed == 0');
844         $xact->xact_finish('now');
845         $editor->update_money_billable_transaction($xact)
846             or return $editor->event;
847         return undef;
848     }
849
850     # If money is owed or a refund is due on the xact and xact_finish
851     # is set, clear it (to reopen the xact) and update
852     if( $summary->balance_owed != 0 and $xact->xact_finish ) {
853         $logger->info("re-opening transaction ".$xact->id. ' becauase balance_owed != 0');
854         $xact->clear_xact_finish;
855         $editor->update_money_billable_transaction($xact)
856             or return $editor->event;
857         return undef;
858     }
859     return undef;
860 }
861
862
863 __PACKAGE__->register_method (
864     method => 'fetch_mbts',
865     authoritative => 1,
866     api_name => 'open-ils.circ.money.billable_xact_summary.retrieve'
867 );
868 sub fetch_mbts {
869     my( $self, $conn, $auth, $id) = @_;
870
871     my $e = new_editor(xact => 1, authtoken=>$auth);
872     return $e->event unless $e->checkauth;
873     my ($mbts) = $U->fetch_mbts($id, $e);
874
875     my $user = $e->retrieve_actor_user($mbts->usr)
876         or return $e->die_event;
877
878     return $e->die_event unless $e->allowed('VIEW_TRANSACTION', $user->home_ou);
879     $e->rollback;
880     return $mbts
881 }
882
883
884 __PACKAGE__->register_method(
885     method => 'desk_payments',
886     api_name => 'open-ils.circ.money.org_unit.desk_payments'
887 );
888 sub desk_payments {
889     my( $self, $conn, $auth, $org, $start_date, $end_date ) = @_;
890     my $e = new_editor(authtoken=>$auth);
891     return $e->event unless $e->checkauth;
892     return $e->event unless $e->allowed('VIEW_TRANSACTION', $org);
893     my $data = $U->storagereq(
894         'open-ils.storage.money.org_unit.desk_payments.atomic',
895         $org, $start_date, $end_date );
896
897     $_->workstation( $_->workstation->name ) for(@$data);
898     return $data;
899 }
900
901
902 __PACKAGE__->register_method(
903     method => 'user_payments',
904     api_name => 'open-ils.circ.money.org_unit.user_payments'
905 );
906
907 sub user_payments {
908     my( $self, $conn, $auth, $org, $start_date, $end_date ) = @_;
909     my $e = new_editor(authtoken=>$auth);
910     return $e->event unless $e->checkauth;
911     return $e->event unless $e->allowed('VIEW_TRANSACTION', $org);
912     my $data = $U->storagereq(
913         'open-ils.storage.money.org_unit.user_payments.atomic',
914         $org, $start_date, $end_date );
915     for(@$data) {
916         $_->usr->card(
917             $e->retrieve_actor_card($_->usr->card)->barcode);
918         $_->usr->home_ou(
919             $e->retrieve_actor_org_unit($_->usr->home_ou)->shortname);
920     }
921     return $data;
922 }
923
924
925 __PACKAGE__->register_method(
926     method    => 'retrieve_credit_payable_balance',
927     api_name  => 'open-ils.circ.credit.payable_balance.retrieve',
928     authoritative => 1,
929     signature => {
930         desc   => q/Returns the total amount the patron can pay via credit card/,
931         params => [
932             { desc => 'Authentication token', type => 'string' },
933             { desc => 'User id', type => 'number' }
934         ],
935         return => { desc => 'The ID of the new provider' }
936     }
937 );
938
939 sub retrieve_credit_payable_balance {
940     my ( $self, $conn, $auth, $user_id ) = @_;
941     my $e = new_editor(authtoken => $auth);
942     return $e->event unless $e->checkauth;
943
944     my $user = $e->retrieve_actor_user($user_id) 
945         or return $e->event;
946
947     if($e->requestor->id != $user_id) {
948         return $e->event unless $e->allowed('VIEW_USER_TRANSACTIONS', $user->home_ou)
949     }
950
951     my $circ_orgs = $e->json_query({
952         "select" => {circ => ["circ_lib"]},
953         from     => "circ",
954         "where"  => {usr => $user_id, xact_finish => undef},
955         distinct => 1
956     });
957
958     my $groc_orgs = $e->json_query({
959         "select" => {mg => ["billing_location"]},
960         from     => "mg",
961         "where"  => {usr => $user_id, xact_finish => undef},
962         distinct => 1
963     });
964
965     my %hash;
966     for my $org ( @$circ_orgs, @$groc_orgs ) {
967         my $o = $org->{billing_location};
968         $o = $org->{circ_lib} unless $o;
969         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.
970         $hash{$o} = $U->ou_ancestor_setting_value($o, 'credit.payments.allow', $e);
971     }
972
973     my @credit_orgs = map { $hash{$_} ? ($_) : () } keys %hash;
974     $logger->debug("credit: relevant orgs that allow credit payments => @credit_orgs");
975
976     my $xact_summaries =
977       OpenILS::Application::AppUtils->simplereq('open-ils.actor',
978         'open-ils.actor.user.transactions.have_charge', $auth, $user_id);
979
980     my $sum = 0.0;
981
982     for my $xact (@$xact_summaries) {
983
984         # make two lists and grab them in batch XXX
985         if ( $xact->xact_type eq 'circulation' ) {
986             my $circ = $e->retrieve_action_circulation($xact->id) or return $e->event;
987             next unless grep { $_ == $circ->circ_lib } @credit_orgs;
988
989         } elsif ($xact->xact_type eq 'grocery') {
990             my $bill = $e->retrieve_money_grocery($xact->id) or return $e->event;
991             next unless grep { $_ == $bill->billing_location } @credit_orgs;
992         } elsif ($xact->xact_type eq 'reservation') {
993             my $bill = $e->retrieve_booking_reservation($xact->id) or return $e->event;
994             next unless grep { $_ == $bill->pickup_lib } @credit_orgs;
995         }
996         $sum += $xact->balance_owed();
997     }
998
999     return $sum;
1000 }
1001
1002
1003 1;