File Coverage

blib/lib/Business/3DSecure/Cardinal.pm
Criterion Covered Total %
statement 19 21 90.4
branch n/a
condition n/a
subroutine 7 7 100.0
pod n/a
total 26 28 92.8


line stmt bran cond sub pod time code
1             package Business::3DSecure::Cardinal;
2              
3 2     2   1638 use strict;
  2         3  
  2         11016  
4 2     2   23 use warnings;
  2         5  
  2         75  
5              
6 2     2   1135 use Business::3DSecure;
  2         1181  
  2         91  
7 2     2   10 use Carp;
  2         3  
  2         199  
8 2     2   1809 use Error qw( try );
  2         11381  
  2         31  
9 2     2   2718 use LWP::UserAgent;
  2         133692  
  2         87  
10 2     2   2380 use SOAP::Lite;
  0            
  0            
11              
12             use vars qw( $VERSION @ISA @EXPORT @EXPORT_OK );
13              
14             require Exporter;
15              
16             @ISA = qw( Exporter AutoLoader Business::3DSecure );
17             @EXPORT = qw();
18             @EXPORT_OK = qw();
19             $VERSION = '0.06';
20              
21             # constants
22             use constant TIMEOUT => '10';
23              
24             # Transaction type map
25             use constant ACTIONS => ( 'cmpi_lookup', 'cmpi_authenticate' );
26              
27             use constant RECOVERABLE_ERRORS => (
28             350, 1001, 1002, 1051, 1055, 1060, 1085, 1120, 1130, 1140,
29             1150, 1160, 1355, 1360, 1380, 1390, 1400, 1710, 1752, 1755,
30             1789, 2001, 2003, 2006, 2007, 2009, 2010, 4000, 4020, 4240,
31             4243, 4245, 4268, 4310, 4375, 4400, 4770, 4780, 4790, 4800,
32             4810, 4820, 4930, 4951, 4963, 4965
33             );
34              
35             use constant ERRORS => {
36             6000 => "General Error Communicating with MAPS Server" ,
37             6010 => "Failed to connect() to server via socket connection" ,
38             6020 => "Failed Parse of Response XML Message Returned From the MPI Server - Socket Communication" ,
39             6030 => "Failed Parse of Response XML Message Returned From the MPI Server - HTTP Communication" ,
40             6040 => "Failed Parse of Response XML Message Returned From the MPI Server - HTTPS Communication" ,
41             6050 => "Failed to initialize socket connection" ,
42             6060 => "Error Communicating with MAPS Server, No Response Message Received - Socket Communication" ,
43             6070 => "The URL to the MAPS Server does not use a recognized protocol (https required)" ,
44             6080 => "Error Communicating with MAPS Server, Error Response - HTTP Communication" ,
45             6090 => "Error Communicating with MAPS Server, Error Response - HTTPS Communication" ,
46             6100 => "Unable to Verify Trusted Server" ,
47             6110 => "Unable to Establish a SSL Context" ,
48             6120 => "Unable to Establish a SSL Connection" ,
49             6130 => "Error extract the underlying file descriptor" ,
50             6140 => "Error establishing Network Connection" ,
51             6150 => "Error during SSL Read of Reponse Data" ,
52             6160 => "Unable to Establish a Socket Connection for SSL connectivity" ,
53             6170 => "Unable to capture a Socket for SSL connectivity" ,
54             9999 => "DOLLAR AMOUNT ERROR: TWO DECIMALS NEEDED",
55             };
56              
57             # fields required for different transaction types
58             use constant REQUIRED_FIELDS => {
59             cmpi_lookup => [ qw{ MsgType Version ProcessorId MerchantId TransactionPwd TransactionType RawAmount PurchaseAmount PurchaseCurrency PAN PANExpr OrderNumber } ],
60             cmpi_authenticate => [ qw{ MsgType Version ProcessorId MerchantId TransactionId PAResPayload } ],
61             };
62              
63             # optional fields for different transaction types
64             use constant OPTIONAL_FIELDS => {
65             cmpi_lookup => [ qw{ OrderDescription UserAgent BrowserHeader Recurring RecurringFrequency RecurringEnd Installment AcquirerPassword EMail IPAddress BillingFirstName BillingMiddleName BillingLastName BillingAddress1 BillingAddress2 BillingCity BillingState BillingPostalCode BillingCountryCode BillingPhone BillingAltPhone ShippingFirstName ShippingMiddleName ShippingLastName ShippingAddress1 ShippingAddress2 ShippingCity ShippingState ShippingPostalCode ShippingCountryCode ShippingPhone ShippingAltPhone Item_Name_X Item_Desc_X Item_Price_X Item_Quantity_X Item_SKU_X} ],
66             cmpi_authenticate =>[qw{ NONE }],
67             };
68              
69             use constant REMAP => {
70             version => 'Version',
71             action => 'MsgType',
72             password => 'TransactionPwd',
73             trans_type => 'TransactionType',
74             vendor => 'ProcessorId',
75             brand => 'MerchantId',
76             amount => 'PurchaseAmount',
77             currency => 'PurchaseCurrency',
78             cc_num => 'PAN',
79             ordernum => 'OrderNumber',
80             auth_result => 'PAResPayload',
81             auth_id => 'TransactionId',
82             };
83              
84             sub set_defaults
85             {
86             my $self = shift;
87             $self->build_subs( qw( cavv eci enrolled error_desc error_num authorized verified unparsed_response auth_request auth_id issuer_url ) );
88             }
89              
90             sub submit
91             {
92             my ( $self ) = @_;
93              
94             $self->{ _content }->{ action } = "cmpi_" . lc( $self->{ _content }->{ action } );
95              
96             my $action = $self->{ _content }->{ action };
97              
98             unless ( grep /$action/, ACTIONS )
99             {
100             Carp::croak( $self->{ processor } . " can't handle transaction type: " . $action );
101             }
102              
103             $self->map_fields();
104             $self->remap_fields();
105             $self->required_fields( @{ REQUIRED_FIELDS->{ $action } } );
106              
107             unless ( $self->{ _content }->{ amount_error } )
108             {
109             # get data ready to send
110             my %post_data = $self->get_fields( @{ REQUIRED_FIELDS->{ $action } }, @{ OPTIONAL_FIELDS->{ $action } } );
111              
112             $self->{ _post_data } = \%post_data;
113              
114             my $xmlMsg = "";
115              
116             while( my ( $tagname, $tagvalue ) = each %post_data )
117             {
118             $tagvalue =~ s/&/&/g;
119             $tagvalue =~ s/
120             $xmlMsg .= "<" . $tagname . ">" . $tagvalue . "";
121             }
122              
123             $xmlMsg .= "";
124              
125             my $ua = LWP::UserAgent->new();
126             $ua->timeout( TIMEOUT ) ;
127             $ua->cookie_jar( { } );
128              
129             my $response = $ua->post( $self->{ _content }->{ transaction_url }, [ 'cmpi_msg' => $xmlMsg ] );
130              
131             $self->is_success( 0 );
132              
133             # if post to Cardinal was successful
134             if ( $response->is_success )
135             {
136             $self->unparsed_response( $response->content );
137              
138             my $som = SOAP::Deserializer->deserialize( $response->content );
139              
140             $self->{ _response }->{ $_->name } = $_->value foreach ( $som->dataof( "//CardinalMPI/*" ) );
141              
142             #if defined use the errors above otherwise load straigh in from server
143             if ( defined ERRORS->{ $self->{ _response }->{ ErrorNo } } )
144             {
145             $self->error_num( $self->{ _response }->{ ErrorNo } ) ;
146             $self->error_desc( ERRORS->{ $self->error_num } ) ;
147             }
148             else
149             {
150             $self->error_num( $self->{ _response }->{ ErrorNo } ) ;
151             $self->error_desc( "ERROR NOT RECOGNIZED:" . $self->{ _response }->{ ErrorDesc } ) ;
152             }
153              
154             if ( !$self->{ _response } )
155             {
156             $self->error_num( 6040 );
157             $self->error_desc( ERRORS->{ 6040 } );
158             }
159              
160             $self->eci( 0 );
161             $self->is_success( 1 );
162              
163             if ( $action eq 'cmpi_lookup' )
164             {
165             $self->auth_request( $self->{ _response }->{ Payload } ) ;
166             $self->auth_id( $self->{ _response }->{ TransactionId } ) ;
167             $self->issuer_url( $self->{ _response }->{ ACSUrl } ) ;
168              
169             my $enrolled = uc $self->{ _response }->{ Enrolled } eq 'Y' ? 1 : 0;
170             $self->enrolled( $enrolled );
171              
172             }
173             elsif ( $action eq 'cmpi_authenticate' )
174             {
175             $self->cavv( $self->{ _response }->{ Cavv } );
176            
177             # possible success are Y U or A this SHOULD be made farther up, in client code
178             my $authorized = uc $self->{ _response }->{ PAResStatus } eq 'N' ? 0 : 1;
179             $self->authorized( $authorized );
180              
181             my $verified = uc $self->{ _response }->{ SignatureVerification } eq 'Y' ? 1 : 0;
182             $self->verified( $verified );
183              
184             # EciFlag
185             my $eci = $self->{ _response }->{ EciFlag } ne '02' ? 0 : 1;
186             $self->eci( $eci );
187             }
188             }
189             else
190             {
191             # post was unsuccessful
192             $self->error_num( 6090 );
193             $self->error_desc( ERRORS->{ 6090 } );
194             }
195              
196             }
197             else
198             {
199             # amount error
200             $self->error_num( 9999 );
201             $self->error_desc( ERRORS->{ 9999 } );
202             }
203             }
204              
205             sub map_fields
206             {
207             my ( $self ) = @_;
208             my %content = $self->content();
209              
210             if ( $content{ action } )
211             {
212             if ( defined $content{ amount } )
213             {
214             my @amount = split( '\.' , $content{ amount } );
215              
216             $content{ amount_error } = $content{ amount } if length $amount[ 1 ] != 2;
217             $content{ RawAmount } = $content{ amount };
218             $content{ RawAmount } =~ s/\.//;
219             }
220              
221             if ( defined $content{ cc_expmonth } && defined $content{ cc_expyear } )
222             {
223              
224             # it will only be 2 or 4
225             if ( length( $content{ cc_expyear } ) == 4 )
226             {
227             $content{ cc_expyear } = substr $content{ cc_expyear }, 2, 4;
228             }
229              
230             $content{ PANExpr } = $content{ cc_expyear } . $content{ cc_expmonth };
231             }
232             }
233              
234             # stuff it back into %content
235             $self->content( %content );
236             }
237              
238             sub remap_fields
239             {
240             my ( $self ) = @_;
241             my %content = $self->content();
242              
243             foreach( keys %{ ( REMAP ) } )
244             {
245             $content{ REMAP->{ $_ } } = $content{ $_ } ;
246             }
247              
248             $self->content( %content );
249             }
250              
251             sub get_fields
252             {
253             my ( $self, @fields ) = @_;
254              
255             my %content = $self->content();
256              
257             my %new = ();
258              
259             $new{ $_ } = $content{ $_ } foreach( grep defined $content{ $_ }, @fields );
260              
261             return %new;
262             }
263              
264             sub is_recoverable_error
265             {
266             my $self = shift;
267              
268             my $error_num = $self->error_num();
269             return ( grep /$error_num/, RECOVERABLE_ERRORS ? 1 : 0 );
270             }
271              
272             sub error
273             {
274             my $self = shift;
275             return $self->error_num() != 0;
276             }
277              
278             1;
279             __END__