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>
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.
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;
22 use Business::CreditCard;
23 use Business::OnlinePayment;
24 use UUID::Tiny qw/:std/;
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";
33 use constant CREDIT_NS => "credit";
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 {
43 my $funcname = "bop_args_" . $argshash->{processor};
44 return &{$funcname}($argshash) if defined &{$funcname};
48 # Provide default arguments for calls using the AuthorizeNet processor
49 sub bop_args_AuthorizeNet {
51 if ($argshash->{server}) {
53 # One might provide "test.authorize.net" here.
54 Server => $argshash->{server},
62 # Provide default arguments for calls using the PayPal processor
66 Username => $argshash->{login},
67 Password => $argshash->{password},
68 Signature => $argshash->{signature}
72 # Provide default arguments for calls using the PayflowPro processor
73 sub bop_args_PayflowPro {
76 "vendor" => $argshash->{vendor},
77 "partner" => $argshash->{partner} || "PayPal" # reasonable default?
81 sub get_processor_settings {
83 my $processor = lc shift;
85 # XXX TODO: make this one single cstore request instead of many
87 $U->ou_ancestor_setting_value(
88 $org_unit, CREDIT_NS . ".processor.${processor}.${_}"
89 )) } qw/enabled login password signature server testmode vendor partner/
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
111 sub process_payment {
114 # Confirm some required arguments.
115 return OpenILS::Event->new('BAD_PARAMS')
118 and $argshash->{amount}
119 and $argshash->{expiration}
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');
129 # Basic sanity check on processor name.
130 if ($argshash->{processor} !~ /^[a-z0-9_\-]+$/i) {
131 return OpenILS::Event->new('CREDIT_PROCESSOR_NOT_ALLOWED');
134 # Get org unit settings related to our processor
135 my $psettings = get_processor_settings(
136 $argshash->{ou}, $argshash->{processor}
139 if (!$psettings->{enabled}) {
140 return OpenILS::Event->new('CREDIT_PROCESSOR_NOT_ENABLED');
143 # Add the org unit settings for the chosen processor to our argshash.
144 $argshash = +{ %{$argshash}, %{$psettings} };
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};
151 # A valid patron_id is also required.
152 my $e = new_editor();
153 my $patron = $e->retrieve_actor_user(
155 $argshash->{patron_id},
158 flesh_fields => { au => ["mailing_address", "card"] }
161 ) or return $e->event;
163 return dispatch($argshash, $patron);
166 sub prepare_bop_content {
167 my ($argshash, $patron, $cardtype) = @_;
184 if (exists $argshash->{$_}) {
185 $content{$_} = $argshash->{$_};
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;
194 $content{first_name} ||= $patron->first_given_name;
195 $content{last_name} ||= $patron->family_name;
197 $content{FirstName} = $content{first_name}; # kludge mcugly for PP
198 $content{LastName} = $content{last_name};
200 # makes patron barcode accessible in CC payment records
201 my $bc = ($patron->card) ? $patron->card->barcode : '';
202 $content{description} = "$bc " . ($content{description} || '');
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 $content{address} ||= $patron->mailing_address->street1;
208 $content{address} .= ", " . $patron->mailing_address->street2
209 if $patron->mailing_address->street2;
211 $content{city} ||= $patron->mailing_address->city;
212 $content{state} ||= $patron->mailing_address->state;
213 $content{zip} ||= $patron->mailing_address->post_code;
214 $content{country} ||= $patron->mailing_address->country;
216 # Yet another fantastic kludge. country2code() comes from Locale::Country.
217 # PayPal must have 2 letter country field (ISO 3166) that's uppercase.
218 if (length($content{country}) > 2 && $argshash->{processor} eq 'PayPal') {
219 $content{country} = uc country2code($content{country});
220 } elsif($argshash->{processor} eq "PayflowPro") {
221 ($content{request_id} = create_uuid_as_string(UUID_V4)) =~ s/-//;
228 my ($argshash, $patron) = @_;
230 # The validate() sub is exported by Business::CreditCard.
231 if (!validate($argshash->{cc})) {
232 # Although it might help a troubleshooter, it's probably not a good
233 # idea to put the credit card number in the log file.
234 $logger->info("Credit card number invalid");
236 return new OpenILS::Event("CREDIT_PROCESSOR_INVALID_CC_NUMBER");
239 # cardtype() also comes from Business::CreditCard. It is not certain that
240 # a) the card type returned by this method will be suitable input for
241 # a payment processor, nor that
242 # b) it is even necessary to supply this argument to processors in all
243 # cases. Testing this with several processors would be a good idea.
244 (my $cardtype = cardtype($argshash->{cc})) =~ s/ card//i;
246 if (lc($cardtype) eq "unknown") {
247 $logger->info("Credit card number passed validate(), " .
248 "yet cardtype() returned $cardtype");
249 return new OpenILS::Event(
250 "CREDIT_PROCESSOR_INVALID_CC_NUMBER", "note" => "cardtype $cardtype"
255 "applying payment via processor '" . $argshash->{processor} . "'"
258 # Find B:OP constructor arguments specific to our payment processor.
259 my %bop_args = get_bop_args_filler($argshash);
261 # We're assuming that all B:OP processors accept this argument to the
263 $bop_args{test_transaction} = $argshash->{testmode};
265 my $transaction = new Business::OnlinePayment(
266 $argshash->{processor}, %bop_args
269 my %content = prepare_bop_content($argshash, $patron, $cardtype);
270 $transaction->content(%content);
272 # submit() does not return a value, although crashing is possible here
273 # with some bad input depending on the payment processor.
274 $transaction->submit;
277 "processor" => $argshash->{"processor"}, "card_type" => $cardtype
280 # Put the values of any of these fields into the event payload, if present.
281 foreach (qw/authorization correlationid avs_code request_id
282 server_response cvv2_response cvv2_code error_message order_number/) {
283 $payload->{$_} = $transaction->$_ if $transaction->can($_);
288 if ($transaction->is_success) {
289 $logger->info($argshash->{processor} . " payment succeeded");
290 $event_name = "SUCCESS";
292 $logger->info($argshash->{processor} . " payment failed");
293 $event_name = "CREDIT_PROCESSOR_DECLINED_TRANSACTION";
296 return new OpenILS::Event($event_name, "payload" => $payload);