Fix in-transit hold retarget
[Evergreen.git] / Open-ILS / src / perlmods / lib / OpenILS / Application / Circ / CreditCard.pm
1 # --------------------------------------------------------------------
2 # Copyright (C) 2008 Niles Ingalls 
3 # Niles Ingalls <nilesi@zionsville.lib.in.us>
4 # Bill Erickson <erickson@esilibrary.com>
5 # Joe Atzberger <jatzberger@esilibrary.com>
6 # Lebbeous Fogle-Weekley <lebbeous@esilibrary.com>
7 #
8 # This program is free software; you can redistribute it and/or
9 # modify it under the terms of the GNU General Public License
10 # as published by the Free Software Foundation; either version 2
11 # of the License, or (at your option) any later version.
12 #
13 # This program is distributed in the hope that it will be useful,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16 # GNU General Public License for more details.
17 # --------------------------------------------------------------------
18 package OpenILS::Application::Circ::CreditCard;
19 use base qw/OpenSRF::Application/;
20 use strict; use warnings;
21
22 use Business::CreditCard;
23 use Business::OnlinePayment;
24 use UUID::Tiny qw/:std/;
25 use Locale::Country;
26
27 use OpenILS::Event;
28 use OpenSRF::Utils::Logger qw/:logger/;
29 use OpenILS::Utils::CStoreEditor qw/:funcs/;
30 use OpenILS::Application::AppUtils;
31 my $U = "OpenILS::Application::AppUtils";
32
33 use constant CREDIT_NS => "credit";
34
35 # Given the argshash from process_payment(), this helper function just finds
36 # a function in the current namespace named "bop_args_{processor}" and calls
37 # it with $argshash as an argument, returning the result, or returning an
38 # empty hash if it can't find such a function.
39 sub get_bop_args_filler {
40     no strict 'refs';
41
42     my $argshash = shift;
43     my $funcname = "bop_args_" . $argshash->{processor};
44     return &{$funcname}($argshash) if defined &{$funcname};
45     return ();
46 }
47
48 # Provide default arguments for calls using the AuthorizeNet processor
49 sub bop_args_AuthorizeNet {
50     my $argshash = shift;
51     if ($argshash->{server}) {
52         return (
53             # One might provide "test.authorize.net" here.
54             Server => $argshash->{server},
55         );
56     }
57     else {
58         return ();
59     }
60 }
61
62 # Provide default arguments for calls using the PayPal processor
63 sub bop_args_PayPal {
64     my $argshash = shift;
65     return (
66         Username => $argshash->{login},
67         Password => $argshash->{password},
68         Signature => $argshash->{signature}
69     );
70 }
71
72 # Provide default arguments for calls using the PayflowPro processor
73 sub bop_args_PayflowPro {
74     my $argshash = shift;
75     return (
76         "vendor" => $argshash->{vendor},
77         "partner" => $argshash->{partner} || "PayPal" # reasonable default?
78     );
79 }
80
81 sub get_processor_settings {
82     my $org_unit = shift;
83     my $processor = lc shift;
84
85     # XXX TODO: make this one single cstore request instead of many
86     +{ map { ($_ =>
87         $U->ou_ancestor_setting_value(
88             $org_unit, CREDIT_NS . ".processor.${processor}.${_}"
89         )) } qw/enabled login password signature server testmode vendor partner/
90     };
91 }
92
93 #        argshash (Hash of arguments with these keys):
94 #                patron_id: Not a barcode, but a patron's internal ID
95 #                       ou: Org unit where transaction happens
96 #                processor: Payment processor to use
97 #                           (AuthorizeNet/PayPal/PayflowPro)
98 #                       cc: credit card number
99 #                     cvv2: 3 or 4 digits from back of card
100 #                   amount: transaction value
101 #                   action: optional (default: Normal Authorization)
102 #               first_name: optional (default: patron's first_given_name field)
103 #                last_name: optional (default: patron's family_name field)
104 #                  address: optional (default: patron's street1 field + street2)
105 #                     city: optional (default: patron's city field)
106 #                    state: optional (default: patron's state field)
107 #                      zip: optional (default: patron's zip field)
108 #                  country: optional (some processor APIs: 2 letter code.)
109 #              description: optional
110
111 sub process_payment {
112     my ($argshash) = @_;
113
114     # Confirm some required arguments.
115     return OpenILS::Event->new('BAD_PARAMS')
116         unless $argshash
117             and $argshash->{cc}
118             and $argshash->{amount}
119             and $argshash->{expiration}
120             and $argshash->{ou};
121
122     if (!$argshash->{processor}) {
123         if (!($argshash->{processor} =
124                 $U->ou_ancestor_setting_value(
125                     $argshash->{ou}, CREDIT_NS . '.processor.default'))) {
126             return OpenILS::Event->new('CREDIT_PROCESSOR_NOT_SPECIFIED');
127         }
128     }
129     # Basic sanity check on processor name.
130     if ($argshash->{processor} !~ /^[a-z0-9_\-]+$/i) {
131         return OpenILS::Event->new('CREDIT_PROCESSOR_NOT_ALLOWED');
132     }
133
134     # Get org unit settings related to our processor
135     my $psettings = get_processor_settings(
136         $argshash->{ou}, $argshash->{processor}
137     );
138
139     if (!$psettings->{enabled}) {
140         return OpenILS::Event->new('CREDIT_PROCESSOR_NOT_ENABLED');
141     }
142
143     # Add the org unit settings for the chosen processor to our argshash.
144     $argshash = +{ %{$argshash}, %{$psettings} };
145
146     # At least the following (derived from org unit settings) are required.
147     return OpenILS::Event->new('CREDIT_PROCESSOR_BAD_PARAMS')
148         unless $argshash->{login}
149             and $argshash->{password};
150
151     # A valid patron_id is also required.
152     my $e = new_editor();
153     my $patron = $e->retrieve_actor_user(
154         [
155             $argshash->{patron_id},
156             {
157                 flesh        => 1,
158                 flesh_fields => { au => ["mailing_address", "card"] }
159             }
160         ]
161     ) or return $e->event;
162
163     return dispatch($argshash, $patron);
164 }
165
166 sub prepare_bop_content {
167     my ($argshash, $patron, $cardtype) = @_;
168
169     my %content;
170     foreach (qw/
171         login
172         password
173         description
174         first_name
175         last_name
176         amount
177         expiration
178         cvv2
179         address
180         city
181         state
182         zip
183         country/) {
184         if (exists $argshash->{$_}) {
185             $content{$_} = $argshash->{$_};
186         }
187     }
188     
189     $content{action}       = $argshash->{action} || "Normal Authorization";
190     $content{type}         = $cardtype;      #'American Express', 'VISA', 'MasterCard'
191     $content{card_number}  = $argshash->{cc};
192     $content{customer_id}  = $patron->id;
193     
194     $content{first_name} ||= $patron->first_given_name;
195     $content{last_name}  ||= $patron->family_name;
196
197     $content{FirstName}    = $content{first_name};   # kludge mcugly for PP
198     $content{LastName}     = $content{last_name};
199
200     # makes patron barcode accessible in CC payment records
201     my $bc = ($patron->card) ? $patron->card->barcode : '';
202     $content{description}  = "$bc " . ($content{description} || '');
203
204     # Especially for the following fields, do we need to support different
205     # mapping of fields for different payment processors, particularly ones
206     # in other countries?
207     if(!$content{address}) {
208         $content{address}  = $patron->mailing_address->street1;
209         $content{address} .= ", " . $patron->mailing_address->street2
210             if $patron->mailing_address->street2;
211     }
212
213     $content{city}       ||= $patron->mailing_address->city;
214     $content{state}      ||= $patron->mailing_address->state;
215     $content{zip}        ||= $patron->mailing_address->post_code;
216     $content{country}    ||= $patron->mailing_address->country;
217
218     # Yet another fantastic kludge. country2code() comes from Locale::Country.
219     # PayPal must have 2 letter country field (ISO 3166) that's uppercase.
220     if (length($content{country}) > 2 && $argshash->{processor} eq 'PayPal') {
221         $content{country} = uc country2code($content{country});
222     } elsif($argshash->{processor} eq "PayflowPro") {
223         ($content{request_id} = create_uuid_as_string(UUID_V4)) =~ s/-//;
224     }
225
226     %content;
227 }
228
229 sub dispatch {
230     my ($argshash, $patron) = @_;
231     
232     # The validate() sub is exported by Business::CreditCard.
233     if (!validate($argshash->{cc})) {
234         # Although it might help a troubleshooter, it's probably not a good
235         # idea to put the credit card number in the log file.
236         $logger->info("Credit card number invalid");
237
238         return new OpenILS::Event("CREDIT_PROCESSOR_INVALID_CC_NUMBER");
239     }
240
241     # cardtype() also comes from Business::CreditCard.  It is not certain that
242     # a) the card type returned by this method will be suitable input for
243     #   a payment processor, nor that
244     # b) it is even necessary to supply this argument to processors in all
245     #   cases.  Testing this with several processors would be a good idea.
246     (my $cardtype = cardtype($argshash->{cc})) =~ s/ card//i;
247
248     if (lc($cardtype) eq "unknown") {
249         $logger->info("Credit card number passed validate(), " .
250             "yet cardtype() returned $cardtype");
251         return new OpenILS::Event(
252             "CREDIT_PROCESSOR_INVALID_CC_NUMBER", "note" => "cardtype $cardtype"
253         );
254     }
255
256     $logger->debug(
257         "applying payment via processor '" . $argshash->{processor} . "'"
258     );
259
260     # Find B:OP constructor arguments specific to our payment processor.
261     my %bop_args = get_bop_args_filler($argshash);
262
263     # We're assuming that all B:OP processors accept this argument to the
264     # constructor.
265     $bop_args{test_transaction} = $argshash->{testmode};
266
267     my $transaction = new Business::OnlinePayment(
268         $argshash->{processor}, %bop_args
269     );
270
271     my %content = prepare_bop_content($argshash, $patron, $cardtype);
272     $transaction->content(%content);
273
274     # submit() does not return a value, although crashing is possible here
275     # with some bad input depending on the payment processor.
276     $transaction->submit;
277
278     my $payload = {
279         "processor" => $argshash->{"processor"}, "card_type" => $cardtype
280     };
281
282     # Put the values of any of these fields into the event payload, if present.
283     foreach (qw/authorization correlationid avs_code request_id
284         server_response cvv2_response cvv2_code error_message order_number/) {
285         $payload->{$_} = $transaction->$_ if $transaction->can($_);
286     }
287
288     my $event_name;
289
290     if ($transaction->is_success) {
291         $logger->info($argshash->{processor} . " payment succeeded");
292         $event_name = "SUCCESS";
293     } else {
294         $logger->info($argshash->{processor} . " payment failed");
295         $event_name = "CREDIT_PROCESSOR_DECLINED_TRANSACTION";
296     }
297
298     return new OpenILS::Event($event_name, "payload" => $payload);
299 }
300
301
302 1;