File Coverage

blib/lib/Business/OnlinePayment/PayflowPro.pm
Criterion Covered Total %
statement 62 152 40.7
branch 16 68 23.5
condition 4 38 10.5
subroutine 13 17 76.4
pod 6 6 100.0
total 101 281 35.9


line stmt bran cond sub pod time code
1             package Business::OnlinePayment::PayflowPro;
2              
3 2     2   27379 use strict;
  2         3  
  2         77  
4 2     2   11 use vars qw($VERSION $DEBUG);
  2         4  
  2         113  
5 2     2   12 use Carp qw(carp croak);
  2         7  
  2         131  
6 2     2   11 use Digest::MD5;
  2         3  
  2         73  
7 2     2   1802 use Business::OnlinePayment::HTTPS 0.06;
  2         66697  
  2         88  
8              
9 2     2   27 use base qw(Business::OnlinePayment::HTTPS);
  2         6  
  2         391  
10              
11             $VERSION = '1.01';
12             $VERSION = eval $VERSION;
13             $DEBUG = 0;
14              
15             # CGI::Util was included starting with Perl 5.6. For previous
16             # Perls, let them use the old simple CGI method of unescaping
17             my $no_cgi_util;
18             BEGIN {
19 2     2   12 eval { require CGI::Util; };
  2         2324  
20 2 50       17318 $no_cgi_util = 1 if $@;
21             }
22              
23             # return current request_id or generate a new one if not yet set
24             sub request_id {
25 5     5 1 953 my $self = shift;
26 5 50       14 if ( ref($self) ) {
27 5 100       17 $self->{"__request_id"} = shift if (@_); # allow value change/reset
28 5 100       31 $self->{"__request_id"} = $self->_new_request_id()
29             unless ( $self->{"__request_id"} );
30 5         25 return $self->{"__request_id"};
31             }
32             else {
33 0         0 return $self->_new_request_id();
34             }
35             }
36              
37             sub _new_request_id {
38 2     2   4 my $self = shift;
39 2         18 my $md5 = Digest::MD5->new();
40 2         98 $md5->add( $$, time(), rand(time) );
41 2         26 return $md5->hexdigest();
42             }
43              
44             sub debug {
45 0     0 1 0 my $self = shift;
46              
47 0 0       0 if (@_) {
48 0   0     0 my $level = shift || 0;
49 0 0       0 if ( ref($self) ) {
50 0         0 $self->{"__DEBUG"} = $level;
51             }
52             else {
53 0         0 $DEBUG = $level;
54             }
55 0         0 $Business::OnlinePayment::HTTPS::DEBUG = $level;
56             }
57 0 0 0     0 return ref($self) ? ( $self->{"__DEBUG"} || $DEBUG ) : $DEBUG;
58             }
59              
60             # cvv2_code: support legacy code and but deprecate method
61 1     1 1 880 sub cvv2_code { shift->cvv2_response(@_); }
62              
63             sub set_defaults {
64 8     8 1 16970 my $self = shift;
65 8         56 my %opts = @_;
66              
67             # standard B::OP methods/data
68 8         245 $self->server("payflowpro.paypal.com");
69 8         555 $self->port("443");
70 8         568 $self->path("/transaction");
71              
72 8         290 $self->build_subs(
73             qw(
74             partner vendor
75             client_certification_id client_timeout
76             headers test_server
77             cert_path
78             order_number avs_code cvv2_response
79             response_page response_code response_headers
80             )
81             );
82              
83             # module specific data
84 8 50       1602 if ( $opts{debug} ) {
85 0         0 $self->debug( $opts{debug} );
86 0         0 delete $opts{debug};
87             }
88              
89             # HTTPS Interface Dev Guide: must be set but will be removed in future
90 8         317 $self->client_certification_id("ClientCertificationIdNotSet");
91              
92             # required: 45 secs recommended by HTTPS Interface Dev Guide
93 8         890 $self->client_timeout(45);
94              
95 8         421 $self->test_server("pilot-payflowpro.paypal.com");
96             }
97              
98             sub _map_fields {
99 0     0   0 my ($self) = @_;
100              
101 0         0 my %content = $self->content();
102              
103             #ACTION MAP
104 0         0 my %actions = (
105             'normal authorization' => 'S', # Sale transaction
106             'credit' => 'C', # Credit (refund)
107             'authorization only' => 'A', # Authorization
108             'post authorization' => 'D', # Delayed Capture
109             'void' => 'V', # Void
110             );
111              
112 0   0     0 $content{'action'} = $actions{ lc( $content{'action'} ) }
113             || $content{'action'};
114              
115             # TYPE MAP
116 0         0 my %types = (
117             'visa' => 'C',
118             'mastercard' => 'C',
119             'american express' => 'C',
120             'discover' => 'C',
121             'cc' => 'C',
122              
123             #'check' => 'ECHECK',
124             );
125              
126 0   0     0 $content{'type'} = $types{ lc( $content{'type'} ) } || $content{'type'};
127              
128 0         0 $self->transaction_type( $content{'type'} );
129              
130             # stuff it back into %content
131 0         0 $self->content(%content);
132             }
133              
134             sub _revmap_fields {
135 0     0   0 my ( $self, %map ) = @_;
136 0         0 my %content = $self->content();
137 0         0 foreach ( keys %map ) {
138 0         0 $content{$_} =
139             ref( $map{$_} )
140 0 0       0 ? ${ $map{$_} }
141             : $content{ $map{$_} };
142             }
143 0         0 $self->content(%content);
144             }
145              
146             sub expdate_mmyy {
147 4     4 1 2565 my $self = shift;
148 4         8 my $expiration = shift;
149 4         7 my $expdate_mmyy;
150 4 50 33     207 if ( defined($expiration) and $expiration =~ /^(\d+)\D+\d*(\d{2})$/ ) {
151 4         15 my ( $month, $year ) = ( $1, $2 );
152 4         22 $expdate_mmyy = sprintf( "%02d", $month ) . $year;
153             }
154 4 50       29 return defined($expdate_mmyy) ? $expdate_mmyy : $expiration;
155             }
156              
157             sub submit {
158 0     0 1 0 my ($self) = @_;
159              
160 0         0 $self->_map_fields();
161              
162 0         0 my %content = $self->content;
163              
164 0 0       0 if ( $self->transaction_type() ne 'C' ) {
165 0         0 croak( "PayflowPro can't (yet?) handle transaction type: "
166             . $self->transaction_type() );
167             }
168              
169 0         0 my $expdate_mmyy = $self->expdate_mmyy( $content{"expiration"} );
170 0         0 my $zip = $content{'zip'};
171 0         0 $zip =~ s/[^[:alnum:]]//g;
172              
173 0 0       0 $self->server( $self->test_server ) if $self->test_transaction;
174              
175 0         0 my $vendor = $self->vendor;
176 0         0 my $partner = $self->partner;
177              
178 0 0       0 $self->_revmap_fields(
179              
180             # BUG?: VENDOR B::OP:PayflowPro < 0.05 backward compatibility. If
181             # vendor not set use login although test indicate undef vendor is ok
182             VENDOR => $vendor ? \$vendor : 'login',
183             PARTNER => \$partner,
184             USER => 'login',
185             PWD => 'password',
186             TRXTYPE => 'action',
187             TENDER => 'type',
188             ORIGID => 'order_number',
189             COMMENT1 => 'description',
190             COMMENT2 => 'invoice_number',
191              
192             ACCT => 'card_number',
193             CVV2 => 'cvv2',
194             EXPDATE => \$expdate_mmyy, # MM/YY from 'expiration'
195             AMT => 'amount',
196              
197             FIRSTNAME => 'first_name',
198             LASTNAME => 'last_name',
199             NAME => 'name',
200             EMAIL => 'email',
201             COMPANYNAME => 'company',
202             STREET => 'address',
203             CITY => 'city',
204             STATE => 'state',
205             ZIP => \$zip, # 'zip' with non-alnums removed
206             COUNTRY => 'country',
207              
208             # As of 8/18/2009: CUSTCODE appears to be cut off at 18
209             # characters and isn't currently reportable. Consider storing
210             # local customer ids in the COMMENT1/2 fields as a workaround.
211             CUSTCODE => 'customer_id',
212             SHIPTOFIRSTNAME => 'ship_first_name',
213             SHIPTOLASTNAME => 'ship_last_name',
214             SHIPTOSTREET => 'ship_address',
215             SHIPTOCITY => 'ship_city',
216             SHIPTOSTATE => 'ship_state',
217             SHIPTOZIP => 'ship_zip',
218             SHIPTOCOUNTRY => 'ship_country',
219             );
220              
221             # Reload %content as _revmap_fields makes our copy old/invalid!
222 0         0 %content = $self->content;
223              
224 0         0 my @required = qw( TRXTYPE TENDER PARTNER VENDOR USER PWD );
225              
226             # NOTE: we croak above if transaction_type ne 'C'
227 0 0       0 if ( $self->transaction_type() eq 'C' ) { # credit card
228 0 0 0     0 if ( defined( $content{'ORIGID'} ) && length( $content{'ORIGID'} ) ) {
229 0         0 push @required, qw(ORIGID);
230             }
231             else {
232 0         0 push @required, qw(AMT ACCT EXPDATE);
233             }
234             }
235              
236 0         0 $self->required_fields(@required);
237              
238 0         0 my %params = $self->get_fields(
239             qw(
240             VENDOR PARTNER USER PWD TRXTYPE TENDER ORIGID COMMENT1 COMMENT2
241             ACCT CVV2 EXPDATE AMT
242             FIRSTNAME LASTNAME NAME EMAIL COMPANYNAME
243             STREET CITY STATE ZIP COUNTRY
244             SHIPTOFIRSTNAME SHIPTOLASTNAME
245             SHIPTOSTREET SHIPTOCITY SHIPTOSTATE SHIPTOZIP SHIPTOCOUNTRY
246             CUSTCODE
247             )
248             );
249              
250             # get header data
251 0 0       0 my %req_headers = %{ $self->headers || {} };
  0         0  
252              
253             # get request_id from %content if defined for ease of use
254 0 0       0 if ( defined $content{"request_id"} ) {
255 0         0 $self->request_id( $content{"request_id"} );
256             }
257              
258 0 0       0 unless ( defined( $req_headers{"X-VPS-Request-ID"} ) ) {
259 0         0 $req_headers{"X-VPS-Request-ID"} = $self->request_id();
260             }
261              
262 0 0       0 unless ( defined( $req_headers{"X-VPS-VIT-Client-Certification-Id"} ) ) {
263 0         0 $req_headers{"X-VPS-VIT-Client-Certification-Id"} =
264             $self->client_certification_id;
265             }
266              
267 0 0       0 unless ( defined( $req_headers{"X-VPS-Client-Timeout"} ) ) {
268 0         0 $req_headers{"X-VPS-Client-Timeout"} = $self->client_timeout();
269             }
270              
271 0         0 my %options = (
272             "Content-Type" => "text/namevalue",
273             "headers" => \%req_headers,
274             );
275              
276             # Payflow Pro does not use URL encoding for the request. The
277             # following implements their custom encoding scheme. Per the
278             # developer docs, the PARMLIST Syntax Guidelines are:
279             # - Spaces are allowed in values
280             # - Enclose the PARMLIST in quotation marks ("")
281             # - Do not place quotation marks ("") within the body of the PARMLIST
282             # - Separate all PARMLIST name-value pairs using an ampersand (&)
283             #
284             # Because '&' and '=' have special meanings/uses values containing
285             # these special characters must be encoded using a special "length
286             # tag". The "length tag" is simply the length of the "value"
287             # enclosed in square brackets ([]) and appended to the "name"
288             # portion of the name-value pair.
289             #
290             # For more details see the sections 'Using Special Characters in
291             # Values' and 'PARMLIST Syntax Guidelines' in the PayPal Payflow
292             # Pro Developer's Guide
293             #
294             # NOTE: we pass a string to https_post so it does not do encoding
295 0         0 my $params_string = join(
296             '&',
297             map {
298 0         0 my $key = $_;
299 0 0       0 my $value = defined( $params{$key} ) ? $params{$key} : '';
300 0 0 0     0 if ( index( $value, '&' ) != -1 || index( $value, '=' ) != -1 ) {
301 0         0 $key = $key . "[" . length($value) . "]";
302             }
303 0         0 "$key=$value";
304             } keys %params
305             );
306              
307 0         0 my ( $page, $resp, %resp_headers ) =
308             $self->https_post( \%options, $params_string );
309              
310 0         0 $self->response_code($resp);
311 0         0 $self->response_page($page);
312 0         0 $self->response_headers( \%resp_headers );
313              
314             # $page should contain name=value[[&name=value]...] pairs
315 0         0 my $response = $self->_get_response( \$page );
316              
317             # AVS and CVS values may be set on success or failure
318 0         0 my $avs_code;
319 0 0 0     0 if ( defined $response->{"AVSADDR"} or defined $response->{"AVSZIP"} ) {
320 0 0 0     0 if ( $response->{"AVSADDR"} eq "Y" && $response->{"AVSZIP"} eq "Y" ) {
    0 0        
    0          
    0          
321 0         0 $avs_code = "Y";
322             }
323             elsif ( $response->{"AVSADDR"} eq "Y" ) {
324 0         0 $avs_code = "A";
325             }
326             elsif ( $response->{"AVSZIP"} eq "Y" ) {
327 0         0 $avs_code = "Z";
328             }
329             elsif ( $response->{"AVSADDR"} eq "N" or $response->{"AVSZIP"} eq "N" )
330             {
331 0         0 $avs_code = "N";
332             }
333             else {
334 0         0 $avs_code = "";
335             }
336             }
337              
338 0         0 $self->avs_code($avs_code);
339 0         0 $self->cvv2_response( $response->{"CVV2MATCH"} );
340 0         0 $self->result_code( $response->{"RESULT"} );
341 0         0 $self->order_number( $response->{"PNREF"} );
342 0         0 $self->error_message( $response->{"RESPMSG"} );
343 0         0 $self->authorization( $response->{"AUTHCODE"} );
344              
345             # RESULT must be an explicit zero, not just numerically equal
346 0 0 0     0 if ( defined( $response->{"RESULT"} ) && $response->{"RESULT"} eq "0" ) {
347 0         0 $self->is_success(1);
348             }
349             else {
350 0         0 $self->is_success(0);
351             }
352             }
353              
354             # Process the response page for params. Based on parse_params in CGI
355             # by Lincoln D. Stein.
356             sub _get_response {
357 6     6   63 my ( $self, $page ) = @_;
358              
359 6         9 my %response;
360              
361 6 100 33     30 if ( !defined($page) || ( ref($page) && !defined($$page) ) ) {
      66        
362 1         15 return \%response;
363             }
364              
365 5         9 my ( $param, $value );
366 5 50       34 foreach ( split( /[&;]/, ref($page) ? $$page : $page ) ) {
367 19         49 ( $param, $value ) = split( '=', $_, 2 );
368 19 100       40 next unless defined $param;
369 14 50       24 $value = '' unless defined $value;
370              
371 14 50       28 if ($no_cgi_util) { # use old pre-CGI::Util method of unescaping
372 0         0 $param =~ tr/+/ /; # pluses become spaces
373 0         0 $param =~ s/%([0-9a-fA-F]{2})/pack("c",hex($1))/ge;
  0         0  
374 0         0 $value =~ tr/+/ /; # pluses become spaces
375 0         0 $value =~ s/%([0-9a-fA-F]{2})/pack("c",hex($1))/ge;
  0         0  
376             }
377             else {
378 14         31 $param = CGI::Util::unescape($param);
379 14         162 $value = CGI::Util::unescape($value);
380             }
381 14         321 $response{$param} = $value;
382             }
383 5         48 return \%response;
384             }
385              
386             1;
387              
388             __END__