]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/src/perlmods/OpenILS/Application/Circ/Money.pm
Patch from Lebbeous Fogle-Weekley which integrates credit card payments into the...
[working/Evergreen.git] / Open-ILS / src / perlmods / OpenILS / Application / Circ / Money.pm
1 # ---------------------------------------------------------------
2 # Copyright (C) 2005  Georgia Public Library Service 
3 # Bill Erickson <billserickson@gmail.com>
4
5 # This program is free software; you can redistribute it and/or
6 # modify it under the terms of the GNU General Public License
7 # as published by the Free Software Foundation; either version 2
8 # of the License, or (at your option) any later version.
9
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 # GNU General Public License for more details.
14 # ---------------------------------------------------------------
15
16
17 package OpenILS::Application::Circ::Money;
18 use base qw/OpenILS::Application/;
19 use strict; use warnings;
20 use OpenILS::Application::AppUtils;
21 my $apputils = "OpenILS::Application::AppUtils";
22 my $U = "OpenILS::Application::AppUtils";
23
24 use OpenSRF::EX qw(:try);
25 use OpenILS::Perm;
26 use Data::Dumper;
27 use OpenILS::Event;
28 use OpenSRF::Utils::Logger qw/:logger/;
29 use OpenILS::Utils::CStoreEditor qw/:funcs/;
30 use OpenILS::Utils::Penalty;
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 priveleges.
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                         type
49                         number
50                         expire_month
51                         expire_year
52                         approval_code
53                     }
54                     check_number
55                     payments: [ 
56                         [trans_id, amt], 
57                         [...]
58                     ], 
59                 }/, type => 'hash'
60             },
61         ]
62     }
63 );
64
65 sub make_payments {
66         my($self, $client, $auth, $payments) = @_;
67
68         my $e = new_editor(authtoken => $auth, xact => 1);
69     return $e->die_event unless $e->checkauth;
70
71         my $type = $payments->{payment_type};
72         my $user_id = $payments->{userid};
73         my $credit = $payments->{patron_credit} || 0;
74         my $drawer = $e->requestor->wsid;
75         my $note = $payments->{note};
76         my $check_number = $payments->{check_number};
77     my $cc_args = $payments->{cc_args};
78         my $total_paid = 0;
79     my %orgs;
80
81     my $patron = $e->retrieve_actor_user($user_id) or return $e->die_event;
82
83     # A user is allowed to make credit card payments on his/her own behalf
84     # All other scenarious require permission
85     unless($type eq 'credit_card_payment' and $user_id == $e->requestor->id) {
86             return $e->die_event unless $e->allowed('CREATE_PAYMENT', $patron->home_ou);
87     }
88
89     # first collect the transactions and make sure the transaction
90     # user matches the requested user
91     my %xacts;
92     for my $pay (@{$payments->{payments}}) {
93
94         my $xact_id = $pay->[0];
95         my $xact = $e->retrieve_money_billable_transaction_summary($xact_id)
96             or return $e->die_event;
97         
98         if($xact->usr != $user_id) {
99             $e->rollback;
100             return OpenILS::Event->new('BAD_PARAMS', note => q/user does not match transaction/);
101         }
102
103         $xacts{$xact_id} = $xact;
104     }
105
106         for my $pay (@{$payments->{payments}}) {
107
108         my $transid = $pay->[0];
109                 my $amount = $pay->[1];
110                 $amount =~ s/\$//og; # just to be safe
111         my $trans = $xacts{$transid};
112
113                 $total_paid += $amount;
114
115         $orgs{$U->xact_org($transid, $e)} = 1;
116
117         # making payment with existing patron credit
118                 $credit -= $amount if $type eq 'credit_payment';
119
120                 # A negative payment is a refund.  
121                 if( $amount < 0 ) {
122
123             # Negative credit card payments are not allowed
124             if($type eq 'credit_card_payment') {
125                 $e->rollback;
126                                 return OpenILS::Event->new(
127                     'BAD_PARAMS', 
128                     note => q/Negative credit card payments not allowed/
129                 );
130             }
131
132                         # If the refund causes the transaction balance to exceed 0 dollars, 
133                         # we are in effect loaning the patron money.  This is not allowed.
134                         if( ($trans->balance_owed - $amount) > 0 ) {
135                 $e->rollback;
136                                 return OpenILS::Event->new('REFUND_EXCEEDS_BALANCE');
137                         }
138
139                         # Otherwise, make sure the refund does not exceed desk payments
140                         # This is also not allowed
141                         my $desk_total = 0;
142                         my $desk_payments = $e->search_money_desk_payment({xact => $transid, voided => 'f'});
143                         $desk_total += $_->amount for @$desk_payments;
144
145                         if( (-$amount) > $desk_total ) {
146                 $e->rollback;
147                                 return OpenILS::Event->new(
148                                         'REFUND_EXCEEDS_DESK_PAYMENTS', 
149                                         payload => { allowed_refund => $desk_total, submitted_refund => -$amount } );
150                         }
151                 }
152
153                 my $payobj = "Fieldmapper::money::$type";
154                 $payobj = $payobj->new;
155
156                 $payobj->amount($amount);
157                 $payobj->amount_collected($amount);
158                 $payobj->xact($transid);
159                 $payobj->note($note);
160
161                 if ($payobj->has_field('accepting_usr')) { $payobj->accepting_usr($e->requestor->id); }
162                 if ($payobj->has_field('cash_drawer')) { $payobj->cash_drawer($drawer); }
163                 if ($payobj->has_field('cc_type')) { $payobj->cc_type($cc_args->{type}); }
164                 if ($payobj->has_field('check_number')) { $payobj->check_number($check_number); }
165
166         # Store the last 4 digits?
167                 #if ($payobj->has_field('cc_number')) { $payobj->cc_number($cc_args->{number}); }
168                 #if ($payobj->has_field('approval_code')) { $payobj->approval_code($cc_args->{approval_code}); }
169                 if ($payobj->has_field('expire_month')) { $payobj->expire_month($cc_args->{expire_month}); }
170                 if ($payobj->has_field('expire_year')) { $payobj->expire_year($cc_args->{expire_year}); }
171                 
172                 # update the transaction if it's done 
173                 if( (my $cred = ($trans->balance_owed - $amount)) <= 0 ) {
174
175                         # Any overpay on this transaction goes directly into patron credit 
176                         $cred = -$cred;
177                         $credit += $cred;
178             my $circ = $e->retrieve_action_circulation($transid);
179
180                         if(!$circ || $circ->stop_fines) {
181                             # If this is a circulation, we can't close the transaction unless stop_fines is set
182                 $trans = $e->retrieve_money_billable_transaction($transid);
183                                 $trans->xact_finish("now");
184                 $e->update_money_billable_transaction($trans) or return $e->die_event;
185                         }
186                 }
187
188         my $method = "create_money_$type";
189         $e->$method($payobj) or return $e->die_event;
190
191         } # all payment objects have been created and inserted. 
192
193         my $evt = _update_patron_credit($e, $patron, $credit);
194         return $evt if $evt;
195
196     for my $org_id (keys %orgs) {
197         # calculate penalties for each of the affected orgs
198         $evt = OpenILS::Utils::Penalty->calculate_penalties($e, $user_id, $org_id);
199         return $evt if $evt;
200     }
201
202     if($type eq 'credit_card_payment') {
203         my $this_ou = $e->requestor->ws_ou;
204         my $response = $apputils->simplereq(
205             'open-ils.credit',
206             'open-ils.credit.process',
207             {
208                 "desc" => $payments->{note},
209                 "amount" => $total_paid,
210                 "patron_id" => $user_id,
211                 "cc" => $payments->{cc_number},
212                 "expiration" => sprintf(
213                     "%02d-%04d",
214                     $payments->{expire_month},
215                     $payments->{expire_year}
216                 ),
217                 "ou" => $this_ou
218             }
219         );
220         # senator: Should failures and/or declines be logged somewhere?  Is
221         # some other cog taking care of this?  Actually, what about the
222         # successes, too?  There's an approval code from the payment processor
223         # that could go somewhere...
224         if (exists $response->{ilsevent}) {
225             $e->rollback;
226             return $response;
227         }
228         if ($response->{statusCode} != 200) {
229             $e->rollback;
230             return OpenILS::Event->new(
231                 'CREDIT_PROCESSOR_DECLINED_TRANSACTION',
232                 note => $response->{statusText}
233             );
234         }
235     }
236
237     $e->commit;
238     return 1;
239 }
240
241
242 sub _update_patron_credit {
243         my($e, $patron, $credit) = @_;
244     return undef if $credit == 0;
245         $patron->credit_forward_balance($patron->credit_forward_balance + $credit);
246     return OpenILS::Event->new('NEGATIVE_PATRON_BALANCE') if $patron->credit_forward_balance < 0;
247     $e->update_actor_user($patron) or return $e->die_event;
248         return undef;
249 }
250
251
252 __PACKAGE__->register_method(
253         method  => "retrieve_payments",
254         api_name        => "open-ils.circ.money.payment.retrieve.all_",
255         notes           => "Returns a list of payments attached to a given transaction"
256         );
257         
258 sub retrieve_payments {
259         my( $self, $client, $login, $transid ) = @_;
260
261         my( $staff, $evt ) =  
262                 $apputils->checksesperm($login, 'VIEW_TRANSACTION');
263         return $evt if $evt;
264
265         # XXX the logic here is wrong.. we need to check the owner of the transaction
266         # to make sure the requestor has access
267
268         # XXX grab the view, for each object in the view, grab the real object
269
270         return $apputils->simplereq(
271                 'open-ils.cstore',
272                 'open-ils.cstore.direct.money.payment.search.atomic', { xact => $transid } );
273 }
274
275
276
277 __PACKAGE__->register_method(
278         method  => "retrieve_payments2",
279     authoritative => 1,
280         api_name        => "open-ils.circ.money.payment.retrieve.all",
281         notes           => "Returns a list of payments attached to a given transaction"
282         );
283         
284 sub retrieve_payments2 {
285         my( $self, $client, $login, $transid ) = @_;
286
287         my $e = new_editor(authtoken=>$login);
288         return $e->event unless $e->checkauth;
289         return $e->event unless $e->allowed('VIEW_TRANSACTION');
290
291         my @payments;
292         my $pmnts = $e->search_money_payment({ xact => $transid });
293         for( @$pmnts ) {
294                 my $type = $_->payment_type;
295                 my $meth = "retrieve_money_$type";
296                 my $p = $e->$meth($_->id) or return $e->event;
297                 $p->payment_type($type);
298                 $p->cash_drawer($e->retrieve_actor_workstation($p->cash_drawer))
299                         if $p->has_field('cash_drawer');
300                 push( @payments, $p );
301         }
302
303         return \@payments;
304 }
305
306
307
308 __PACKAGE__->register_method(
309         method  => "create_grocery_bill",
310         api_name        => "open-ils.circ.money.grocery.create",
311         notes           => <<"  NOTE");
312         Creates a new grocery transaction using the transaction object provided
313         PARAMS: (login_session, money.grocery (mg) object)
314         NOTE
315
316 sub create_grocery_bill {
317         my( $self, $client, $login, $transaction ) = @_;
318
319         my( $staff, $evt ) = $apputils->checkses($login);
320         return $evt if $evt;
321         $evt = $apputils->check_perms($staff->id, 
322                 $transaction->billing_location, 'CREATE_TRANSACTION' );
323         return $evt if $evt;
324
325
326         $logger->activity("Creating grocery bill " . Dumper($transaction) );
327
328         $transaction->clear_id;
329         my $session = $apputils->start_db_session;
330         my $transid = $session->request(
331                 'open-ils.storage.direct.money.grocery.create', $transaction)->gather(1);
332
333         throw OpenSRF::EX ("Error creating new money.grocery") unless defined $transid;
334
335         $logger->debug("Created new grocery transaction $transid");
336         
337         $apputils->commit_db_session($session);
338
339     my $e = new_editor(xact=>1);
340     $evt = _check_open_xact($e, $transid);
341     return $evt if $evt;
342     $e->commit;
343
344         return $transid;
345 }
346
347
348 __PACKAGE__->register_method(
349         method => 'fetch_grocery',
350         api_name => 'open-ils.circ.money.grocery.retrieve'
351 );
352
353 sub fetch_grocery {
354         my( $self, $conn, $auth, $id ) = @_;
355         my $e = new_editor(authtoken=>$auth);
356         return $e->event unless $e->checkauth;
357         return $e->event unless $e->allowed('VIEW_TRANSACTION'); # eh.. basically the same permission
358         my $g = $e->retrieve_money_grocery($id)
359                 or return $e->event;
360         return $g;
361 }
362
363
364 __PACKAGE__->register_method(
365         method  => "billing_items",
366     authoritative => 1,
367         api_name        => "open-ils.circ.money.billing.retrieve.all",
368         notes           =><<"   NOTE");
369         Returns a list of billing items for the given transaction.
370         PARAMS( login, transaction_id )
371         NOTE
372
373 sub billing_items {
374         my( $self, $client, $login, $transid ) = @_;
375
376         my( $trans, $evt ) = $U->fetch_billable_xact($transid);
377         return $evt if $evt;
378
379         my $staff;
380         ($staff, $evt ) = $apputils->checkses($login);
381         return $evt if $evt;
382
383         if($staff->id ne $trans->usr) {
384                 $evt = $U->check_perms($staff->id, $staff->home_ou, 'VIEW_TRANSACTION');
385                 return $evt if $evt;
386         }
387         
388         return $apputils->simplereq( 'open-ils.cstore',
389                 'open-ils.cstore.direct.money.billing.search.atomic', { xact => $transid } )
390 }
391
392
393 __PACKAGE__->register_method(
394         method  => "billing_items_create",
395         api_name        => "open-ils.circ.money.billing.create",
396         notes           =><<"   NOTE");
397         Creates a new billing line item
398         PARAMS( login, bill_object (mb) )
399         NOTE
400
401 sub billing_items_create {
402         my( $self, $client, $login, $billing ) = @_;
403
404         my $e = new_editor(authtoken => $login, xact => 1);
405         return $e->die_event unless $e->checkauth;
406         return $e->die_event unless $e->allowed('CREATE_BILL');
407
408         my $xact = $e->retrieve_money_billable_transaction($billing->xact)
409                 or return $e->die_event;
410
411         # if the transaction was closed, re-open it
412         if($xact->xact_finish) {
413                 $xact->clear_xact_finish;
414                 $e->update_money_billable_transaction($xact)
415                         or return $e->die_event;
416         }
417
418         my $amt = $billing->amount;
419         $amt =~ s/\$//og;
420         $billing->amount($amt);
421
422         $e->create_money_billing($billing) or return $e->die_event;
423     my $evt = OpenILS::Utils::Penalty->calculate_penalties($e, $xact->usr, $U->xact_org($xact->id));
424     return $evt if $evt;
425         $e->commit;
426
427         return $billing->id;
428 }
429
430 __PACKAGE__->register_method(
431         method          =>      'void_bill',
432         api_name                => 'open-ils.circ.money.billing.void',
433         signature       => q/
434                 Voids a bill
435                 @param authtoken Login session key
436                 @param billid Id for the bill to void.  This parameter may be repeated to reference other bills.
437                 @return 1 on success, Event on error
438         /
439 );
440
441
442 sub void_bill {
443         my( $s, $c, $authtoken, @billids ) = @_;
444
445         my $e = new_editor( authtoken => $authtoken, xact => 1 );
446         return $e->die_event unless $e->checkauth;
447         return $e->die_event unless $e->allowed('VOID_BILLING');
448
449     my %users;
450     for my $billid (@billids) {
451
452             my $bill = $e->retrieve_money_billing($billid)
453                     or return $e->die_event;
454
455         my $xact = $e->retrieve_money_billable_transaction($bill->xact)
456             or return $e->die_event;
457
458         if($U->is_true($bill->voided)) {
459             $e->rollback;
460                 return OpenILS::Event->new('BILL_ALREADY_VOIDED', payload => $bill);
461         }
462
463         my $org = $U->xact_org($bill->xact, $e);
464         $users{$xact->usr} = {} unless $users{$xact->usr};
465         $users{$xact->usr}->{$org} = 1;
466
467             $bill->voided('t');
468             $bill->voider($e->requestor->id);
469             $bill->void_time('now');
470     
471             $e->update_money_billing($bill) or return $e->die_event;
472             my $evt = _check_open_xact($e, $bill->xact, $xact);
473             return $evt if $evt;
474     }
475
476     # calculate penalties for all user/org combinations
477     for my $user_id (keys %users) {
478         for my $org_id (keys %{$users{$user_id}}) {
479             OpenILS::Utils::Penalty->calculate_penalties($e, $user_id, $org_id);
480         }
481     }
482
483         $e->commit;
484         return 1;
485 }
486
487 __PACKAGE__->register_method(
488         method          =>      'edit_bill_note',
489         api_name                => 'open-ils.circ.money.billing.note.edit',
490         signature       => q/
491                 Edits the note for a bill
492                 @param authtoken Login session key
493         @param note The replacement note for the bills we're editing
494                 @param billid Id for the bill to edit the note of.  This parameter may be repeated to reference other bills.
495                 @return 1 on success, Event on error
496         /
497 );
498
499
500 sub edit_bill_note {
501         my( $s, $c, $authtoken, $note, @billids ) = @_;
502
503         my $e = new_editor( authtoken => $authtoken, xact => 1 );
504         return $e->die_event unless $e->checkauth;
505         return $e->die_event unless $e->allowed('UPDATE_BILL_NOTE');
506
507     for my $billid (@billids) {
508
509             my $bill = $e->retrieve_money_billing($billid)
510                     or return $e->die_event;
511
512             $bill->note($note);
513         # 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.
514     
515             $e->update_money_billing($bill) or return $e->die_event;
516     }
517
518         $e->commit;
519         return 1;
520 }
521
522 __PACKAGE__->register_method(
523         method          =>      'edit_payment_note',
524         api_name                => 'open-ils.circ.money.payment.note.edit',
525         signature       => q/
526                 Edits the note for a payment
527                 @param authtoken Login session key
528         @param note The replacement note for the payments we're editing
529                 @param paymentid Id for the payment to edit the note of.  This parameter may be repeated to reference other payments.
530                 @return 1 on success, Event on error
531         /
532 );
533
534
535 sub edit_payment_note {
536         my( $s, $c, $authtoken, $note, @paymentids ) = @_;
537
538         my $e = new_editor( authtoken => $authtoken, xact => 1 );
539         return $e->die_event unless $e->checkauth;
540         return $e->die_event unless $e->allowed('UPDATE_PAYMENT_NOTE');
541
542     for my $paymentid (@paymentids) {
543
544             my $payment = $e->retrieve_money_payment($paymentid)
545                     or return $e->die_event;
546
547             $payment->note($note);
548         # 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.
549     
550             $e->update_money_payment($payment) or return $e->die_event;
551     }
552
553         $e->commit;
554         return 1;
555 }
556
557 sub _check_open_xact {
558         my( $editor, $xactid, $xact ) = @_;
559
560         # Grab the transaction
561         $xact ||= $editor->retrieve_money_billable_transaction($xactid);
562     return $editor->event unless $xact;
563     $xactid ||= $xact->id;
564
565         # grab the summary and see how much is owed on this transaction
566         my ($summary) = $U->fetch_mbts($xactid, $editor);
567
568         # grab the circulation if it is a circ;
569         my $circ = $editor->retrieve_action_circulation($xactid);
570
571         # If nothing is owed on the transaction but it is still open
572         # and this transaction is not an open circulation, close it
573         if( 
574                 ( $summary->balance_owed == 0 and ! $xact->xact_finish ) and
575                 ( !$circ or $circ->stop_fines )) {
576
577                 $logger->info("closing transaction ".$xact->id. ' becauase balance_owed == 0');
578                 $xact->xact_finish('now');
579                 $editor->update_money_billable_transaction($xact)
580                         or return $editor->event;
581                 return undef;
582         }
583
584         # If money is owed or a refund is due on the xact and xact_finish
585         # is set, clear it (to reopen the xact) and update
586         if( $summary->balance_owed != 0 and $xact->xact_finish ) {
587                 $logger->info("re-opening transaction ".$xact->id. ' becauase balance_owed != 0');
588                 $xact->clear_xact_finish;
589                 $editor->update_money_billable_transaction($xact)
590                         or return $editor->event;
591                 return undef;
592         }
593
594         return undef;
595 }
596
597
598
599 __PACKAGE__->register_method (
600         method => 'fetch_mbts',
601     authoritative => 1,
602         api_name => 'open-ils.circ.money.billable_xact_summary.retrieve'
603 );
604 sub fetch_mbts {
605         my( $self, $conn, $auth, $id) = @_;
606
607         my $e = new_editor(xact => 1, authtoken=>$auth);
608         return $e->event unless $e->checkauth;
609         my ($mbts) = $U->fetch_mbts($id, $e);
610
611         my $user = $e->retrieve_actor_user($mbts->usr)
612                 or return $e->die_event;
613
614         return $e->die_event unless $e->allowed('VIEW_TRANSACTION', $user->home_ou);
615         $e->rollback;
616         return $mbts
617 }
618
619
620
621 __PACKAGE__->register_method(
622         method => 'desk_payments',
623         api_name => 'open-ils.circ.money.org_unit.desk_payments'
624 );
625
626 sub desk_payments {
627         my( $self, $conn, $auth, $org, $start_date, $end_date ) = @_;
628         my $e = new_editor(authtoken=>$auth);
629         return $e->event unless $e->checkauth;
630         return $e->event unless $e->allowed('VIEW_TRANSACTION', $org);
631         my $data = $U->storagereq(
632                 'open-ils.storage.money.org_unit.desk_payments.atomic',
633                 $org, $start_date, $end_date );
634
635         $_->workstation( $_->workstation->name ) for(@$data);
636         return $data;
637 }
638
639
640 __PACKAGE__->register_method(
641         method => 'user_payments',
642         api_name => 'open-ils.circ.money.org_unit.user_payments'
643 );
644
645 sub user_payments {
646         my( $self, $conn, $auth, $org, $start_date, $end_date ) = @_;
647         my $e = new_editor(authtoken=>$auth);
648         return $e->event unless $e->checkauth;
649         return $e->event unless $e->allowed('VIEW_TRANSACTION', $org);
650         my $data = $U->storagereq(
651                 'open-ils.storage.money.org_unit.user_payments.atomic',
652                 $org, $start_date, $end_date );
653         for(@$data) {
654                 $_->usr->card(
655                         $e->retrieve_actor_card($_->usr->card)->barcode);
656                 $_->usr->home_ou(
657                         $e->retrieve_actor_org_unit($_->usr->home_ou)->shortname);
658         }
659         return $data;
660 }
661
662
663
664
665 1;
666
667
668