1 # Vend::Payment::PayflowPro - Interchange support for PayPal Payflow Pro HTTPS POST
3 # $Id: PayflowPro.pm,v 1.2 2009-03-20 15:44:59 markj Exp $
5 # Copyright (C) 2002-2009 Interchange Development Group and others
6 # Copyright (C) 1999-2002 Red Hat, Inc.
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
13 # You should have received a copy of the GNU General Public
14 # License along with this program; if not, write to the Free
15 # Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston,
18 package Vend::Payment::PayflowPro;
22 Vend::Payment::PayflowPro - Interchange support for PayPal Payflow Pro HTTPS POST
30 [charge mode=payflowpro param1=value1 param2=value2]
34 The following Perl modules:
42 PayPal's Payflow Pro HTTPS POST does NOT require the proprietary binary-only
43 shared library that was used for the Verisign Payflow Pro service.
47 The Vend::Payment::PayflowPro module implements the payflowpro() payment routine
48 for use with Interchange. It is compatible on a call level with the other
49 Interchange payment modules -- in theory (and even usually in practice) you
50 could switch from a different payment module to PayflowPro with a few
51 configuration file changes.
53 To enable this module, place this directive in F<interchange.cfg>:
55 Require module Vend::Payment::PayflowPro
57 This I<must> be in interchange.cfg or a file included from it.
59 NOTE: Make sure CreditCardAuto is off (default in Interchange demos).
61 The mode can be named anything, but the C<gateway> parameter must be set
62 to C<payflowpro>. To make it the default payment gateway for all credit
63 card transactions in a specific catalog, you can set in F<catalog.cfg>:
65 Variable MV_PAYMENT_MODE payflowpro
67 It uses several of the standard settings from Interchange payment. Any time
68 we speak of a setting, it is obtained either first from the tag/call options,
69 then from an Interchange order Route named for the mode, then finally a
70 default global payment variable. For example, the C<id> parameter would
73 [charge mode=payflowpro id=YourPayflowProID]
77 Route payflowpro id YourPayflowProID
79 or with only Payflow Pro as a payment provider
81 Variable MV_PAYMENT_ID YourPayflowProID
83 The active settings are:
89 Your account ID, supplied by PayPal when you sign up.
90 Global parameter is MV_PAYMENT_ID.
94 Your account password, selected by you or provided by PayPal when you sign up.
95 Global parameter is MV_PAYMENT_SECRET.
99 Your account partner, selected by you or provided by PayPal when you
100 sign up. Global parameter is MV_PAYMENT_PARTNER.
104 Your account vendor, selected by you or provided by PayPal when you
105 sign up. Global parameter is MV_PAYMENT_VENDOR.
109 The type of transaction to be run. Valid values are:
111 Interchange Payflow Pro
112 ---------------- -----------------
117 settle D (from previous A trans)
123 The following should rarely be used, as the supplied defaults are
130 This remaps the form variable names to the ones needed by PayPal. See
131 the C<Payment Settings> heading in the Interchange documentation for use.
135 The payment gateway host to use, to override the default.
139 Name of a Sub or GlobalSub to be called after the result hash has been
140 received from PayPal. A reference to the modifiable result hash is
141 passed into the subroutine, and it should return true (in the Perl truth
142 sense) if its checks were successful, or false if not. The transaction type
143 is passed in as a second arg, if needed.
145 This can come in handy since, strangely, PayPal has no option to decline
146 a charge when AVS or CSC data come back negative.
148 If you want to fail based on a bad AVS check, make sure you're only
149 doing an auth -- B<not a sale>, or your customers would get charged on
150 orders that fail the AVS check and never get logged in your system!
152 Add the parameters like this:
154 Route payflowpro check_sub avs_check
156 This is a matching sample subroutine you could put in interchange.cfg:
161 my ($addr, $zip) = @{$result}{qw( AVSADDR AVSZIP )};
162 return 1 if $addr eq 'Y' or $zip eq 'Y';
163 return 1 if $addr eq 'X' and $zip eq 'X';
164 return 1 if $addr !~ /\S/ and $zip !~ /\S/;
165 $result->{RESULT} = 112;
166 $result->{RESPMSG} = "The billing address you entered does not match the cardholder's billing address";
171 That would work equally well as a Sub in catalog.cfg. It will succeed if
172 either the address or zip is 'Y', or if both are unknown. If it fails,
173 it sets the result code and error message in the result hash using
174 PayPal's own (otherwise unused) 112 result code, meaning "Failed AVS
177 Of course you can use this sub to do any other post-processing you
182 =head2 Troubleshooting
184 Try the instructions above, then enable test mode. A test order should
187 Then move to live mode and try a sale with the card number C<4111 1111
188 1111 1111> and a valid future expiration date. The sale should be denied,
189 and the reason should be in [data session payment_error].
197 Make sure you "Require"d the module in interchange.cfg:
199 Require module Vend::Payment::PayflowPro
203 Check the error logs, both catalog and global.
207 Make sure you set your account ID and secret properly.
211 Try an order, then put this code in a page:
215 my $string = $Tag->uneval( { ref => $Session->{payment_result} });
217 $string =~ s/,/,\n/g;
222 That should show what happened.
226 If all else fails, consultants are available to help with
227 integration for a fee. You can find consultants by asking on the
228 C<interchange-biz@icdevgroup.org> mailing list.
234 There is actually nothing in the package Vend::Payment::PayflowPro.
235 It changes packages to Vend::Payment and places things there.
239 Tom Tucker <tom@ttucker.com>
240 Mark Johnson <mark@endpoint.com>
242 David Christensen <david@endpoint.com>
243 Cameron Prince <cameronbprince@yahoo.com>
244 Mike Heins <mike@perusion.com>
245 Jon Jensen <jon@endpoint.com>
249 package Vend::Payment;
257 require HTTP::Request;
258 require HTTP::Headers;
259 require Crypt::SSLeay;
262 die "Required modules for PayPal Payflow Pro HTTPS NOT found. $@\n";
267 my ($user, $amount) = @_;
268 # Uncomment all the following lines to use the debug statement. It strips
269 # the arg of any sensitive credit card information and is safe
270 # (and recommended) to enable in production.
272 # my $debug_user = ::uneval($user);
273 # $debug_user =~ s{('mv_credit_card_[^']+' => ')((?:\\'|\\|[^\\']*)*)(')}{$1 . ('X' x length($2)) . $3}ge;
274 #::logDebug("payflowpro called\n" . $debug_user);
279 $user = $opt->{id} || undef;
280 $secret = $opt->{secret} || undef;
286 if ($opt->{actual}) {
287 %actual = %{$opt->{actual}};
290 %actual = map_actual();
294 $user = charge_param('id')
296 MStatus => 'failure-hard',
297 MErrMsg => errmsg('No account id'),
300 #::logDebug("payflowpro user $user");
303 $secret = charge_param('secret')
305 MStatus => 'failure-hard',
306 MErrMsg => errmsg('No account password'),
310 #::logDebug("payflowpro OrderID: |$opt->{order_id}|");
313 if (! $opt->{host} and charge_param('test')) {
314 #::logDebug("payflowpro: setting server to pilot/test mode");
315 $server = 'pilot-payflowpro.paypal.com';
319 #::logDebug("payflowpro: setting server based on options");
320 $server = $opt->{host} || 'payflowpro.paypal.com';
321 $port = $opt->{port} || '443';
324 my $uri = "https://$server:$port/transaction";
325 #::logDebug("payflowpro: using uri: $uri");
327 $actual{mv_credit_card_exp_month} =~ s/\D//g;
328 $actual{mv_credit_card_exp_month} =~ s/^0+//;
329 $actual{mv_credit_card_exp_year} =~ s/\D//g;
330 $actual{mv_credit_card_exp_year} =~ s/\d\d(\d\d)/$1/;
331 $actual{mv_credit_card_number} =~ s/\D//g;
333 my $exp = sprintf '%02d%02d',
334 $actual{mv_credit_card_exp_month},
335 $actual{mv_credit_card_exp_year};
356 my $transtype = $opt->{transaction} || charge_param('transaction') || 'A';
358 $transtype = $type_map{$transtype}
360 MStatus => 'failure-hard',
361 MErrMsg => errmsg('Unrecognized transaction: %s', $transtype),
365 my $orderID = $opt->{order_id};
366 $amount = $opt->{total_cost} if ! $amount;
369 my $precision = $opt->{precision} || charge_param('precision') || 2;
370 my $cost = Vend::Interpolate::total_cost();
371 $amount = Vend::Util::round_to_frac_digits($cost, $precision);
375 ACCT mv_credit_card_number
376 CVV2 mv_credit_card_cvv2
391 TRXTYPE => $transtype,
394 $query{PARTNER} = $opt->{partner} || charge_param('partner');
395 $query{VENDOR} = $opt->{vendor} || charge_param('vendor');
396 $query{ORIGID} = $orderID if $orderID;
398 # We want a unique orderID for each call, to better than second granularity
399 ( $opt->{order_id} = Time::HiRes::clock_gettime() ) =~ s/\D//g;
400 $orderID = gen_order_id($opt);
401 #::logDebug("payflowpro AUTH gen_order_id: " . $orderID);
404 $query{$_} = $actual{$varmap{$_}} if defined $actual{$varmap{$_}};
407 # Uncomment all the following block to use the debug statement. It strips
408 # the arg of any sensitive credit card information and is safe
409 # (and recommended) to enable in production.
412 # my %munged_query = %query;
413 # $munged_query{PWD} = 'X';
414 # $munged_query{ACCT} =~ s/^(\d{4})(.*)/$1 . ('X' x length($2))/e;
415 # $munged_query{CVV2} =~ s/./X/g;
416 # $munged_query{EXPDATE} =~ s/./X/g;
417 #::logDebug("payflowpro query: " . ::uneval(\%munged_query));
420 my $timeout = $opt->{timeout} || 10;
421 die "Bad timeout value, security violation." unless $timeout && $timeout !~ /\D/;
422 die "Bad port value, security violation." unless $port && $port !~ /\D/;
423 die "Bad server value, security violation." unless $server && $server !~ /[^-\w.]/;
427 my (@query, @debug_query);
428 for my $key (keys %query) {
429 my $val = $query{$key};
430 $val =~ s/["\$\n\r]//g;
431 my $len = length($val);
433 push @query, "$key=$val";
435 if $key =~ /^(?:PWD|ACCT|CVV2|EXPDATE)\b/;
436 push @debug_query, "$key=$val";
438 my $string = join '&', @query;
439 my $debug_string = join '&', @debug_query;
442 'Content-Type' => 'text/namevalue',
443 'X-VPS-Request-Id' => $orderID,
444 'X-VPS-Timeout' => $timeout,
445 'X-VPS-VIT-Client-Architecture' => $Config{archname},
446 'X-VPS-VIT-Client-Type' => 'Perl',
447 'X-VPS-VIT-Client-Version' => $VERSION,
448 'X-VPS-VIT-Integration-Product' => 'Interchange',
449 'X-VPS-VIT-Integration-Version' => $::VERSION,
450 'X-VPS-VIT-OS-Name' => $Config{osname},
451 'X-VPS-VIT-OS-Version' => $Config{osvers},
453 # Debug statement is stripped of any sensitive card data and is safe (and
454 # recommended) to enable in production.
456 #::logDebug(qq{--------------------\nPosting to PayflowPro: \n\t$orderID\n\t$uri "$debug_string"});
458 my $headers = HTTP::Headers->new(%headers);
459 my $request = HTTP::Request->new('POST', $uri, $headers, $string);
460 my $ua = LWP::UserAgent->new(timeout => $timeout);
461 $ua->agent('Vend::Payment::PayflowPro');
462 my $response = $ua->request($request);
463 my $resultstr = $response->content;
464 #::logDebug(qq{PayflowPro response:\n\t$resultstr\n--------------------});
466 unless ( $response->is_success ) {
469 RESPMSG => 'System Error',
470 MStatus => 'failure-hard',
471 MErrMsg => 'System Error',
472 lwp_response => $resultstr,
476 %$result = split /[&=]/, $resultstr;
477 my $decline = $result->{RESULT};
480 $result->{RESULT} == 0
482 my $check_sub_name = $opt->{check_sub} || charge_param('check_sub')
484 my $check_sub = $Vend::Cfg->{Sub}{$check_sub_name}
485 || $Global::GlobalSub->{$check_sub_name};
486 if (ref $check_sub eq 'CODE') {
492 #::logDebug(qq{payflowpro called check_sub sub=$check_sub_name decline=$decline});
495 logError("payflowpro: non-existent check_sub routine %s.", $check_sub_name);
499 my %result_map = (qw/
504 pop.auth-code AUTHCODE
511 $result->{ICSTATUS} = 'failed';
512 my $msg = errmsg("Charge error: %s Reason: %s. Please call in your order or try again.",
513 $result->{RESULT} || 'no details available',
514 $result->{RESPMSG} || 'unknown error',
516 $result->{MErrMsg} = $result{'pop.error-message'} = $msg;
519 $result->{ICSTATUS} = 'success';
522 for (keys %result_map) {
523 $result->{$_} = $result->{$result_map{$_}}
524 if defined $result->{$result_map{$_}};
527 #::logDebug('payflowpro result: ' . ::uneval($result));
531 package Vend::Payment::PayflowPro;