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