File Coverage

blib/lib/PayflowPro.pm
Criterion Covered Total %
statement 27 61 44.2
branch 2 14 14.2
condition 0 5 0.0
subroutine 9 10 90.0
pod 3 3 100.0
total 41 93 44.0


line stmt bran cond sub pod time code
1             # $Id: PayflowPro.pm 3101 2011-04-04 19:12:45Z khera $
2             #
3             # Copyright 2007 MailerMailer, LLC
4             #
5             # Based on documentation found at:
6             # http://www.pdncommunity.com/pdn/board/message?message.uid=28775
7             # http://www.pdncommunity.com/pdn/board/message?board.id=payflow&thread.id=1123
8              
9             package PayflowPro;
10 1     1   1019 use strict;
  1         2  
  1         80  
11              
12             =pod
13              
14             =head1 NAME
15              
16             PayflowPro - Library for accessing PayPal's Payflow Pro HTTP interface
17              
18             =head1 SYNOPSIS
19              
20             use PayflowPro qw(pfpro);
21             my $data = {
22             USER=>'MyUserId',
23             VENDOR=>'MyVendorId',
24             PARTNER=>'MyPartnerId',
25             PWD=>'MyPassword',
26              
27             AMT=> '42.24',
28             TAXAMT=>'0.00', # no tax charged, but specifying it lowers cost
29             INVNUM=>$$,
30             DESC=>"Test invoice $$",
31             COMMENT1=>"Comment 1 $$",
32             COMMENT2=>"Comment 2 $$",
33             CUSTCODE=>$$ . 'a' . $$,
34              
35             TRXTYPE=>'S', # sale
36             TENDER=>'C', # credit card
37              
38             # Commercial Card additional info
39             PONUM=>$$.'-'.$$,
40             SHIPTOZIP=>'20850', # for AmEx Level 2
41             DESC4=>'FRT0.00', # for AmEx Level 2
42              
43             # verisign tracking info
44             STREET => '123 AnyStreet',
45             CITY => 'Anytown',
46             COUNTRY => 'us',
47             FIRSTNAME => 'Firsty',
48             LASTNAME => 'Lasty',
49             STATE => 'md',
50             ZIP => '20850',
51              
52             ACCT => '5555555555554444',
53             EXPDATE => '1009',
54             CVV2 => '123',
55             };
56              
57             my $res = pfpro($data);
58              
59             if ($res->{RESULT} == 0) {
60             print "Woohooo! We charged the card!\n";
61             }
62              
63             =head1 DESCRIPTION
64              
65             Interface to HTTP gateway for PayPal's Payflow Pro service. Implements
66             the pfpro() function to simplify replacing the old PFProAPI perl module.
67              
68             Methods implemented are:
69              
70             =cut
71              
72 1     1   8 use base qw(Exporter);
  1         2  
  1         189  
73             @PayflowPro::EXPORT_OK = qw(pfpro pftestmode pfdebug);
74              
75 1     1   1116 use LWP::UserAgent;
  1         48595  
  1         47  
76 1     1   11 use HTTP::Request;
  1         2  
  1         25  
77 1     1   31 use Config;
  1         3  
  1         46  
78              
79 1     1   6 use constant NUMRETRIES => 3; # number of times to retry HTTP timeout/err
  1         2  
  1         69  
80 1     1   6 use vars qw($VERSION);
  1         3  
  1         1134  
81              
82             $VERSION = sprintf "%d", q$Revision: 3101 $ =~ /(\d+)/;
83             my $agent = "MailerMailer PFPro";
84              
85             my ($pfprohost,$debug);
86             pftestmode(0); # set "live" mode as default.
87              
88             our $timeout = 30;
89              
90             my $ua = new LWP::UserAgent;
91             $ua->agent("$agent/$VERSION");
92              
93             =pod
94              
95             =head2 pftestmode($testmode)
96              
97             Set test mode on or off. Test mode means it uses the testing server
98             rather than the live one. Default mode is live (C<$testmode> == 0).
99              
100             Returns true.
101              
102             =cut
103              
104             sub pftestmode {
105 2     2 1 9 my $testmode = shift;
106              
107 2 100       11 $pfprohost = $testmode ?
108             'pilot-payflowpro.paypal.com' :
109             'payflowpro.paypal.com';
110              
111 2         4 return 1;
112             }
113              
114             =pod
115              
116             =head2 pfdebug($mode)
117              
118             Set debug mode on or off. Turns on some warn statements to track progress
119             of the request. Default mode is off (C<$mode> == 0).
120              
121             Returns current setting.
122              
123             =cut
124              
125             sub pfdebug {
126 1     1 1 5 my $mode = shift;
127              
128 1         10 $ENV{'HTTPS_DEBUG'} = $mode; # assumes Crypt::SSLeay as the SSL engine
129 1         2 return $debug = $mode;
130             }
131              
132             =pod
133              
134             =head2 pfpro($data)
135              
136             Process request as per hash ref C<$data>. See PFPro API docs on
137             name/value pairs to pass in. All we do here is convert them into an
138             HTTP request, then convert the response back into a hash and return
139             the reference to it. This emulates the pfpro() function in the
140             original API.
141              
142             Additionally, we honor a C value which specifies the number
143             of seconds to wait for a response from the server. The default is 30
144             seconds. Normally for production you should not need to alter this
145             value. The test servers are slower so may need larger timeout. The
146             minimum value that PayPal will accept is 5 seconds.
147              
148             It uses the time and the C (Invoice Number) field of input to
149             generate the unique request ID, so don't try to process the same
150             INVNUM more than once per second. C is a required datum to be
151             passed into this function. Bad things happen if you don't.
152              
153             Upon communications failure, it fakes up a response message with
154             C = -1. Internally, the library tries several times to process
155             the transaction if there are network problems before returning this
156             failure mode.
157              
158             To validate the SSL certificate, you need a ca-bundle file with a list
159             of valid certificate signers. Then set the environment variable
160             HTTPS_CA_FILE to point to that file. This assumes you are using the
161             C SSL driver for LWP (should be the default). In your code,
162             add some lines like this:
163              
164             # CA cert peer verification
165             $ENV{HTTPS_CA_FILE} = '/path/to/ca-bundle.crt';
166              
167             It is likely to be in F or F or
168             F depending on your OS version. The script F
169             included with this module can be used to create the bundle file based on the
170             current Mozilla certificate data if you don't already have such a file. One
171             is also included in the source for this module, but it may be out of date
172             so it is recommended that you run the F script to ensure you have
173             the latest information.
174              
175             If you do not set HTTPS_CA_FILE it will still work, but you don't get
176             the certificate validation to ensure you're speaking to the authentic
177             site. You will also get in the HTTPS response headers
178              
179             Client-SSL-Warning: Peer certificate not verified
180              
181             but you'll only see that if you turn on debugging.
182              
183             =cut
184              
185             sub pfpro {
186 0     0 1   my $data = shift;
187              
188             # for the case of a referenced credit, the INVNUM is not required to be set
189             # so use the ORIGID instead. If that's not set, just use a fixed string
190             # to avoid undef warnings.
191 0   0       my $request_id=substr(time . $data->{TRXTYPE} . ($data->{INVNUM} || $data->{ORIGID} || 'NOID'),0,32);
192              
193 0 0         if (defined $data->{TIMEOUT}) {
194 0           $timeout = $data->{TIMEOUT};
195             }
196              
197 0           $ua->timeout($timeout + 1); # one more than timeout in VPS header below
198              
199 0           my $r = HTTP::Request->new(POST => "https://$pfprohost/");
200 0           $r->content_type('text/namevalue');
201 0           $r->header('X-VPS-REQUEST-ID' => $request_id,
202             'X-VPS-CLIENT-TIMEOUT' => $timeout, # timeout in seconds
203             'X-VPS-VIT-INTEGRATION-PRODUCT' => $agent,
204             'X-VPS-VIT-INTEGRATION-VERSION' => $VERSION,
205             'X-VPS-VIT-OS-NAME' => $Config::Config{osname},
206             'X-VPS-VIT-OS-VERSION' => $Config::Config{osvers},
207             'X-VPS-VIT-RUNTIME-VERSION' => $],
208             'Connection' => 'close',
209             'Host' => $pfprohost,
210             );
211              
212             # build the body of the request
213 0           while (my ($k,$v) = each %{$data}) {
  0            
214 0           my $len = length($v);
215 0           $r->add_content($k."[$len]=".$v.'&');
216             }
217 0           $r->add_content('VERBOSITY=MEDIUM'); # from example code. unsure what it does
218              
219 0           $r->content_length(length(${$r->content_ref}));
  0            
220              
221 0 0         warn "HTTP Request:\n\n",$r->as_string() if $debug;
222              
223 0           my $retval = {}; # hash of values to return
224              
225 0           my $maxtries = NUMRETRIES;
226 0           my $response;
227              
228             # Keep trying the request until we succeed, or fail NUMRETRIES times.
229             # Since the REQUEST_ID is the same, we don't ever process
230             # the request more than once, but we deal with timout cases:
231             # If the request worked and we failed to get the response, we just
232             # get the original response back; if it failed to reach PayPal, we
233             # just retry it. NOTE: This does not retry on payflow errors, just
234             # when the HTTP protocol has failures/errors such as timeout.
235 0   0       do {
236 0 0         warn "Running request, $maxtries left\n" if $debug;
237 0           sleep ((NUMRETRIES - $maxtries) * 30); # delay for a bit between failures
238 0           $response = $ua->request($r);
239             } while (--$maxtries and not $response->is_success);
240              
241             # Check the outcome of the response
242 0 0         if ($response->is_success) {
243             # parse the return value into the hash and send it back.
244 0 0         warn "\nHTTP response:\n\n",$response->as_string if $debug;
245 0           my $c = $response->content;
246 0           foreach my $part (split '&',$c) {
247 0           my ($k,$v) = split '=',$part;
248 0           $retval->{$k} = $v;
249             }
250             } else {
251             # some error. fake up the old API's error code so existing code continues
252             # to work. this should just cause a retry on the application.
253 0 0         warn "HTTP communication error: ".$response->status_line()."\n" if $debug;
254 0           $retval->{RESULT} = -1;
255 0           $retval->{RESPMSG} = 'Failed to connect to host';
256             }
257              
258 0           $retval->{'X-VPS-REQUEST-ID'} = $request_id; # useful for debugging
259              
260 0           return $retval;
261             }
262              
263             1;
264              
265              
266             =pod
267              
268             =head1 AUTHOR
269              
270             Vivek Khera >
271              
272             =head1 LICENSE
273              
274             This module is Copyright 2007-2009 Khera Communications, Inc. It is
275             licensed under the same terms as Perl itself.
276              
277             =cut