]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/src/perlmods/OpenILS/Application/Circ/Money.pm
push the CC processing to the end so that it runs just before the final commit
[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         # TODO send to credit card processor
204         # amount == $total_paid
205         # user == $user_id
206         # other args == $cc_args (hash, see api docs)
207         # $e->rollback if processing fails.  This will undo everything.
208     }
209
210     $e->commit;
211     return 1;
212 }
213
214
215 sub _update_patron_credit {
216     my($e, $patron, $credit) = @_;
217     return undef if $credit == 0;
218     $patron->credit_forward_balance($patron->credit_forward_balance + $credit);
219     return OpenILS::Event->new('NEGATIVE_PATRON_BALANCE') if $patron->credit_forward_balance < 0;
220     $e->update_actor_user($patron) or return $e->die_event;
221     return undef;
222 }
223
224
225 __PACKAGE__->register_method(
226     method  => "retrieve_payments",
227     api_name    => "open-ils.circ.money.payment.retrieve.all_",
228     notes       => "Returns a list of payments attached to a given transaction"
229     );
230     
231 sub retrieve_payments {
232     my( $self, $client, $login, $transid ) = @_;
233
234     my( $staff, $evt ) =  
235         $apputils->checksesperm($login, 'VIEW_TRANSACTION');
236     return $evt if $evt;
237
238     # XXX the logic here is wrong.. we need to check the owner of the transaction
239     # to make sure the requestor has access
240
241     # XXX grab the view, for each object in the view, grab the real object
242
243     return $apputils->simplereq(
244         'open-ils.cstore',
245         'open-ils.cstore.direct.money.payment.search.atomic', { xact => $transid } );
246 }
247
248
249
250 __PACKAGE__->register_method(
251     method  => "retrieve_payments2",
252     authoritative => 1,
253     api_name    => "open-ils.circ.money.payment.retrieve.all",
254     notes       => "Returns a list of payments attached to a given transaction"
255     );
256     
257 sub retrieve_payments2 {
258     my( $self, $client, $login, $transid ) = @_;
259
260     my $e = new_editor(authtoken=>$login);
261     return $e->event unless $e->checkauth;
262     return $e->event unless $e->allowed('VIEW_TRANSACTION');
263
264     my @payments;
265     my $pmnts = $e->search_money_payment({ xact => $transid });
266     for( @$pmnts ) {
267         my $type = $_->payment_type;
268         my $meth = "retrieve_money_$type";
269         my $p = $e->$meth($_->id) or return $e->event;
270         $p->payment_type($type);
271         $p->cash_drawer($e->retrieve_actor_workstation($p->cash_drawer))
272             if $p->has_field('cash_drawer');
273         push( @payments, $p );
274     }
275
276     return \@payments;
277 }
278
279
280
281 __PACKAGE__->register_method(
282     method  => "create_grocery_bill",
283     api_name    => "open-ils.circ.money.grocery.create",
284     notes       => <<"  NOTE");
285     Creates a new grocery transaction using the transaction object provided
286     PARAMS: (login_session, money.grocery (mg) object)
287     NOTE
288
289 sub create_grocery_bill {
290     my( $self, $client, $login, $transaction ) = @_;
291
292     my( $staff, $evt ) = $apputils->checkses($login);
293     return $evt if $evt;
294     $evt = $apputils->check_perms($staff->id, 
295         $transaction->billing_location, 'CREATE_TRANSACTION' );
296     return $evt if $evt;
297
298
299     $logger->activity("Creating grocery bill " . Dumper($transaction) );
300
301     $transaction->clear_id;
302     my $session = $apputils->start_db_session;
303     my $transid = $session->request(
304         'open-ils.storage.direct.money.grocery.create', $transaction)->gather(1);
305
306     throw OpenSRF::EX ("Error creating new money.grocery") unless defined $transid;
307
308     $logger->debug("Created new grocery transaction $transid");
309     
310     $apputils->commit_db_session($session);
311
312     my $e = new_editor(xact=>1);
313     $evt = _check_open_xact($e, $transid);
314     return $evt if $evt;
315     $e->commit;
316
317     return $transid;
318 }
319
320
321 __PACKAGE__->register_method(
322     method => 'fetch_grocery',
323     api_name => 'open-ils.circ.money.grocery.retrieve'
324 );
325
326 sub fetch_grocery {
327     my( $self, $conn, $auth, $id ) = @_;
328     my $e = new_editor(authtoken=>$auth);
329     return $e->event unless $e->checkauth;
330     return $e->event unless $e->allowed('VIEW_TRANSACTION'); # eh.. basically the same permission
331     my $g = $e->retrieve_money_grocery($id)
332         or return $e->event;
333     return $g;
334 }
335
336
337 __PACKAGE__->register_method(
338     method  => "billing_items",
339     authoritative => 1,
340     api_name    => "open-ils.circ.money.billing.retrieve.all",
341     notes       =><<"   NOTE");
342     Returns a list of billing items for the given transaction.
343     PARAMS( login, transaction_id )
344     NOTE
345
346 sub billing_items {
347     my( $self, $client, $login, $transid ) = @_;
348
349     my( $trans, $evt ) = $U->fetch_billable_xact($transid);
350     return $evt if $evt;
351
352     my $staff;
353     ($staff, $evt ) = $apputils->checkses($login);
354     return $evt if $evt;
355
356     if($staff->id ne $trans->usr) {
357         $evt = $U->check_perms($staff->id, $staff->home_ou, 'VIEW_TRANSACTION');
358         return $evt if $evt;
359     }
360     
361     return $apputils->simplereq( 'open-ils.cstore',
362         'open-ils.cstore.direct.money.billing.search.atomic', { xact => $transid } )
363 }
364
365
366 __PACKAGE__->register_method(
367     method  => "billing_items_create",
368     api_name    => "open-ils.circ.money.billing.create",
369     notes       =><<"   NOTE");
370     Creates a new billing line item
371     PARAMS( login, bill_object (mb) )
372     NOTE
373
374 sub billing_items_create {
375     my( $self, $client, $login, $billing ) = @_;
376
377     my $e = new_editor(authtoken => $login, xact => 1);
378     return $e->die_event unless $e->checkauth;
379     return $e->die_event unless $e->allowed('CREATE_BILL');
380
381     my $xact = $e->retrieve_money_billable_transaction($billing->xact)
382         or return $e->die_event;
383
384     # if the transaction was closed, re-open it
385     if($xact->xact_finish) {
386         $xact->clear_xact_finish;
387         $e->update_money_billable_transaction($xact)
388             or return $e->die_event;
389     }
390
391     my $amt = $billing->amount;
392     $amt =~ s/\$//og;
393     $billing->amount($amt);
394
395     $e->create_money_billing($billing) or return $e->die_event;
396     my $evt = OpenILS::Utils::Penalty->calculate_penalties($e, $xact->usr, $U->xact_org($xact->id));
397     return $evt if $evt;
398     $e->commit;
399
400     return $billing->id;
401 }
402
403 __PACKAGE__->register_method(
404     method      =>  'void_bill',
405     api_name        => 'open-ils.circ.money.billing.void',
406     signature   => q/
407         Voids a bill
408         @param authtoken Login session key
409         @param billid Id for the bill to void.  This parameter may be repeated to reference other bills.
410         @return 1 on success, Event on error
411     /
412 );
413
414
415 sub void_bill {
416     my( $s, $c, $authtoken, @billids ) = @_;
417
418     my $e = new_editor( authtoken => $authtoken, xact => 1 );
419     return $e->die_event unless $e->checkauth;
420     return $e->die_event unless $e->allowed('VOID_BILLING');
421
422     my %users;
423     for my $billid (@billids) {
424
425         my $bill = $e->retrieve_money_billing($billid)
426             or return $e->die_event;
427
428         my $xact = $e->retrieve_money_billable_transaction($bill->xact)
429             or return $e->die_event;
430
431         if($U->is_true($bill->voided)) {
432             $e->rollback;
433             return OpenILS::Event->new('BILL_ALREADY_VOIDED', payload => $bill);
434         }
435
436         my $org = $U->xact_org($bill->xact, $e);
437         $users{$xact->usr} = {} unless $users{$xact->usr};
438         $users{$xact->usr}->{$org} = 1;
439
440         $bill->voided('t');
441         $bill->voider($e->requestor->id);
442         $bill->void_time('now');
443     
444         $e->update_money_billing($bill) or return $e->die_event;
445         my $evt = _check_open_xact($e, $bill->xact, $xact);
446         return $evt if $evt;
447     }
448
449     # calculate penalties for all user/org combinations
450     for my $user_id (keys %users) {
451         for my $org_id (keys %{$users{$user_id}}) {
452             OpenILS::Utils::Penalty->calculate_penalties($e, $user_id, $org_id);
453         }
454     }
455
456     $e->commit;
457     return 1;
458 }
459
460 __PACKAGE__->register_method(
461     method      =>  'edit_bill_note',
462     api_name        => 'open-ils.circ.money.billing.note.edit',
463     signature   => q/
464         Edits the note for a bill
465         @param authtoken Login session key
466         @param note The replacement note for the bills we're editing
467         @param billid Id for the bill to edit the note of.  This parameter may be repeated to reference other bills.
468         @return 1 on success, Event on error
469     /
470 );
471
472
473 sub edit_bill_note {
474     my( $s, $c, $authtoken, $note, @billids ) = @_;
475
476     my $e = new_editor( authtoken => $authtoken, xact => 1 );
477     return $e->die_event unless $e->checkauth;
478     return $e->die_event unless $e->allowed('UPDATE_BILL_NOTE');
479
480     for my $billid (@billids) {
481
482         my $bill = $e->retrieve_money_billing($billid)
483             or return $e->die_event;
484
485         $bill->note($note);
486         # 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.
487     
488         $e->update_money_billing($bill) or return $e->die_event;
489     }
490
491     $e->commit;
492     return 1;
493 }
494
495 __PACKAGE__->register_method(
496     method      =>  'edit_payment_note',
497     api_name        => 'open-ils.circ.money.payment.note.edit',
498     signature   => q/
499         Edits the note for a payment
500         @param authtoken Login session key
501         @param note The replacement note for the payments we're editing
502         @param paymentid Id for the payment to edit the note of.  This parameter may be repeated to reference other payments.
503         @return 1 on success, Event on error
504     /
505 );
506
507
508 sub edit_payment_note {
509     my( $s, $c, $authtoken, $note, @paymentids ) = @_;
510
511     my $e = new_editor( authtoken => $authtoken, xact => 1 );
512     return $e->die_event unless $e->checkauth;
513     return $e->die_event unless $e->allowed('UPDATE_PAYMENT_NOTE');
514
515     for my $paymentid (@paymentids) {
516
517         my $payment = $e->retrieve_money_payment($paymentid)
518             or return $e->die_event;
519
520         $payment->note($note);
521         # 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.
522     
523         $e->update_money_payment($payment) or return $e->die_event;
524     }
525
526     $e->commit;
527     return 1;
528 }
529
530 sub _check_open_xact {
531     my( $editor, $xactid, $xact ) = @_;
532
533     # Grab the transaction
534     $xact ||= $editor->retrieve_money_billable_transaction($xactid);
535     return $editor->event unless $xact;
536     $xactid ||= $xact->id;
537
538     # grab the summary and see how much is owed on this transaction
539     my ($summary) = $U->fetch_mbts($xactid, $editor);
540
541     # grab the circulation if it is a circ;
542     my $circ = $editor->retrieve_action_circulation($xactid);
543
544     # If nothing is owed on the transaction but it is still open
545     # and this transaction is not an open circulation, close it
546     if( 
547         ( $summary->balance_owed == 0 and ! $xact->xact_finish ) and
548         ( !$circ or $circ->stop_fines )) {
549
550         $logger->info("closing transaction ".$xact->id. ' becauase balance_owed == 0');
551         $xact->xact_finish('now');
552         $editor->update_money_billable_transaction($xact)
553             or return $editor->event;
554         return undef;
555     }
556
557     # If money is owed or a refund is due on the xact and xact_finish
558     # is set, clear it (to reopen the xact) and update
559     if( $summary->balance_owed != 0 and $xact->xact_finish ) {
560         $logger->info("re-opening transaction ".$xact->id. ' becauase balance_owed != 0');
561         $xact->clear_xact_finish;
562         $editor->update_money_billable_transaction($xact)
563             or return $editor->event;
564         return undef;
565     }
566
567     return undef;
568 }
569
570
571
572 __PACKAGE__->register_method (
573     method => 'fetch_mbts',
574     authoritative => 1,
575     api_name => 'open-ils.circ.money.billable_xact_summary.retrieve'
576 );
577 sub fetch_mbts {
578     my( $self, $conn, $auth, $id) = @_;
579
580     my $e = new_editor(xact => 1, authtoken=>$auth);
581     return $e->event unless $e->checkauth;
582     my ($mbts) = $U->fetch_mbts($id, $e);
583
584     my $user = $e->retrieve_actor_user($mbts->usr)
585         or return $e->die_event;
586
587     return $e->die_event unless $e->allowed('VIEW_TRANSACTION', $user->home_ou);
588     $e->rollback;
589     return $mbts
590 }
591
592
593
594 __PACKAGE__->register_method(
595     method => 'desk_payments',
596     api_name => 'open-ils.circ.money.org_unit.desk_payments'
597 );
598
599 sub desk_payments {
600     my( $self, $conn, $auth, $org, $start_date, $end_date ) = @_;
601     my $e = new_editor(authtoken=>$auth);
602     return $e->event unless $e->checkauth;
603     return $e->event unless $e->allowed('VIEW_TRANSACTION', $org);
604     my $data = $U->storagereq(
605         'open-ils.storage.money.org_unit.desk_payments.atomic',
606         $org, $start_date, $end_date );
607
608     $_->workstation( $_->workstation->name ) for(@$data);
609     return $data;
610 }
611
612
613 __PACKAGE__->register_method(
614     method => 'user_payments',
615     api_name => 'open-ils.circ.money.org_unit.user_payments'
616 );
617
618 sub user_payments {
619     my( $self, $conn, $auth, $org, $start_date, $end_date ) = @_;
620     my $e = new_editor(authtoken=>$auth);
621     return $e->event unless $e->checkauth;
622     return $e->event unless $e->allowed('VIEW_TRANSACTION', $org);
623     my $data = $U->storagereq(
624         'open-ils.storage.money.org_unit.user_payments.atomic',
625         $org, $start_date, $end_date );
626     for(@$data) {
627         $_->usr->card(
628             $e->retrieve_actor_card($_->usr->card)->barcode);
629         $_->usr->home_ou(
630             $e->retrieve_actor_org_unit($_->usr->home_ou)->shortname);
631     }
632     return $data;
633 }
634
635
636
637
638 1;
639
640
641