92984da3162f4f114cc1f9fa8411c140d904ed54
[Business-OnlinePayment-FirstDataGlobalGateway.git] / lib / Business / OnlinePayment / FirstDataGlobalGateway.pm
1 package Business::OnlinePayment::FirstDataGlobalGateway;
2 use base qw( Business::OnlinePayment );
3
4 use warnings;
5 use strict;
6 use Data::Dumper;
7 use Business::CreditCard;
8 use SOAP::Lite; #+trace => 'all';
9 #SOAP::Lite->import(+trace=>'debug');
10
11 our $VERSION = '0.01';
12 $VERSION = eval $VERSION; # modperlstyle: convert the string into a number
13
14 our @alpha = ( 'a'..'z', 'A'..'Z', '0'..'9' );
15
16 our %failure_status = (
17   302 => 'nsf',
18   501 => 'pickup',
19   502 => 'stolen',
20   503 => 'stolen', # "Fraud/Security violation"
21   504 => 'blacklisted',
22   509 => 'nsf',
23   510 => 'nsf',
24   519 => 'blacklisted',
25   521 => 'nsf',
26   522 => 'expired',
27   530 => 'blacklisted',
28   534 => 'blacklisted',
29   # others are all "declined"
30 );
31   
32
33 sub _info {
34   {
35     'info_compat'       => '0.01',
36     'gateway_name'      => 'First Data Global Gateway e4',
37     'gateway_url'       => 'https://www.firstdata.com/en_us/products/merchants/ecommerce/online-payment-processing.html',
38     'module_version'    => $VERSION,
39     'supported_types'   => [ 'CC' ], #, 'ECHECK' ],
40     #'token_support'     => 1, # "Transarmor" is this, but not implemented yet
41     'test_transaction'  => 1,
42
43     'supported_actions' => [ 'Normal Authorization',
44                              'Authorization Only',
45                              'Post Authorization',
46                              'Credit',
47                              'Void',
48                            ],
49   };
50 }
51
52 sub set_defaults {
53     my $self = shift;
54     #my %opts = @_;
55
56     $self->build_subs(qw( order_number avs_code cvv2_response
57                           authorization failure_status result_code
58                           ));
59 }
60
61 sub map_fields {
62     my($self) = @_;
63
64     my %content = $self->content();
65
66     # TYPE MAP
67     my %types = ( 'visa'               => 'CC',
68                   'mastercard'         => 'CC',
69                   'american express'   => 'CC',
70                   'discover'           => 'CC',
71                   'check'              => 'ECHECK',
72                 );
73     $content{'type'} = $types{lc($content{'type'})} || $content{'type'};
74     $self->transaction_type($content{'type'});
75     
76     # ACTION MAP 
77     my $action = lc($content{'action'});
78     my %actions =
79       ( 'normal authorization' => '00', # Purchase
80         'authorization_only'   => '01', # 
81         'post authorization'   => '02', # Pre-Authorization Completion
82         # '' => '03', # Forced Post
83         'credit'               => '04', # Refund
84         # '' => '05', # Pre-Authorization Only
85         'void'                 => '13', # Void
86         #'reverse authorization' => '',
87
88         # '' => '07', # PayPal Order
89         # '' => '32', # Tagged Pre-Authorization Completion
90         # '' => '33', # Tagged Void
91         # '' => '34', # Tagged Refund
92         # '' => '83', # CashOut (ValueLink, v9 or higher end point only)
93         # '' => '85', # Activation (ValueLink, v9 or higher end point only)
94         # '' => '86', # Balance Inquiry (ValueLink, v9 or higher end point only)
95         # '' => '88', # Reload (ValueLink, v9 or higher end point only)
96         # '' => '89', # Deactivation (ValueLink, v9 or higher end point only)
97       );
98
99     $content{'action'} = $actions{$action} || $action;
100
101     # make sure there's a combined name
102     $content{name} ||= $content{first_name} . ' ' . $content{last_name};
103
104     # stuff it back into %content
105     $self->content(%content);
106
107 }
108
109 sub remap_fields {
110     my($self,%map) = @_;
111
112     my %content = $self->content();
113     foreach(keys %map) {
114         $content{$map{$_}} = $content{$_};
115     }
116     $self->content(%content);
117 }
118
119 sub submit {
120   my($self) = @_;
121
122   $self->map_fields;
123
124   $self->remap_fields(
125         'login'             => 'ExactID',
126         'password'          => 'Password',
127
128         'action'            => 'Transaction_Type',
129
130         'amount'            => 'DollarAmount',
131         'currency'          => 'Currency',
132         'card_number'       => 'Card_Number',
133         'track1'            => 'Track1',
134         'track2'            => 'Track2',
135         'expiration'        => 'Expiry_Date',
136         'name'              => 'CardHoldersName',
137         'cvv2'              => 'VerificationStr2',
138
139         'authorization'     => 'Authorization_Num',
140         'order_number'      => 'Reference_No',
141
142         'zip'               => 'ZipCode',
143         'tax'               => 'Tax1Amount',
144         'customer_id'       => 'Customer_Ref',
145         'customer_ip'       => 'Client_IP',
146         'email'             => 'Client_Email',
147
148         #account_type      => 'accountType',
149
150   );
151
152   my %content = $self->content();
153
154   $content{Expiry_Date} =~ s/\///;
155
156   $content{country} ||= 'US';
157
158   $content{VerificationStr1} =
159     join('|', map $content{$_}, qw( address zip city state country ));
160   $content{VerificationStr1} .= '|'. $content{'phone'}
161     if $content{'type'} eq 'ECHECK';
162
163   $content{CVD_Presence_Ind} = '1' if length($content{VerificationStr2});
164
165   $content{'Reference_No'} ||= join('', map $alpha[int(rand(62))], (1..20) );
166
167   #XXX this should be exposed as a standard B:OP field, not just recurring/no
168   if ( defined($content{'recurring_billing'})
169        && $content{'recurring_billing'} =~ /^[y1]/ ) {
170     $content{'Ecommerce_Flag'} = '2';
171   } else {
172     #$content{'Ecommerce_Flag'} = '1'; 7?  if there's an IP?
173   }
174
175   my $base_uri;
176   if ( $self->test_transaction ) {
177     $base_uri =
178       'https://api.demo.globalgatewaye4.firstdata.com/transaction';
179   } else {
180     $base_uri =
181       'https://api.globalgatewaye4.firstdata.com/vplug-in/transaction';
182   }
183
184   my $proxy = "$base_uri/v11";
185
186   my @transaction = map { SOAP::Data->name($_)->value( $content{$_} ) }
187   grep { defined($content{$_}) }
188   (qw(
189     ExactID Password Transaction_Type DollarAmount Card_Number Transaction_Tag
190     Track1 Track2 Authorization_Num Expiry_Date CardHoldersName
191     VerificationStr1 VerificationStr2 CVD_Presence_Ind Reference_No ZipCode
192     Tax1Amount Tax1Number Tax2Amount Tax2Number Customer_Ref Reference_3
193     Language Client_IP Client_Email User_Name Currency PartialRedemption
194     CAVV XID Ecommerce_Flag
195  ));
196    #TransarmorToken CardType EAN VirtualCard CardCost FraudSuspected
197    #CheckNumber CheckType BankAccountNumber BankRoutingNumber CustomerName
198    #CustomerIDType CustomerID
199
200   my $wsdl = "$proxy/wsdl";
201   my $client = SOAP::Lite->service($wsdl)->proxy($proxy)->readable(1);
202   my $action_prefix = 'http://secure2.e-xact.com/vplug-in/transaction/rpc-enc';
203   my $type_prefix = $action_prefix . '/encodedTypes';
204   $client->on_action( sub { $action_prefix . '/' . $_[1] } );
205   my $source = SOAP::Data->name('SendAndCommitSource')
206                          ->value(\@transaction)
207                          ->type("$type_prefix:Transaction");
208   local $@;
209   my $som = eval { $client->call('SendAndCommit', $source) };
210   die $@ if $@;
211   if ($som->fault) { # indicates a protocol error
212     die $som->faultstring;
213   }
214
215   $DB::single = 1;
216   $som->match('/Envelope/Body/SendAndCommitResponse/SendAndCommitResult');
217   my $result = $som->valueof; # hashref of the result properties
218   $self->is_success( $result->{Transaction_Approved} );
219   $self->authorization( $result->{Authorization_Num} );
220   $self->order_number( $result->{SequenceNo} );
221   $self->avs_code( $result->{AVS} );
222   $self->cvv2_response( $result->{CVV2} );
223
224   if (!$self->is_success) {
225     # note spelling of "EXact_Resp_Code"
226     if ($result->{EXact_Resp_Code} ne '00') {
227       # then there's something wrong with the transaction inputs
228       # (invalid card number, malformed amount, attempt to refund a 
229       # transaction that didn't happen, etc.)
230       $self->error_message($result->{EXact_Message});
231       $self->result_code($result->{EXact_Resp_Code});
232       $self->failure_status('');
233       # not a decline, as the transaction was never really detected
234     } else {
235       $self->error_message($result->{Bank_Message});
236       $self->result_code($result->{Bank_Resp_Code});
237       $self->failure_status(
238         $failure_status{$result->{Bank_Resp_Code}} || 'declined'
239       );
240     }
241   }
242 }
243
244 1;
245
246 __END__
247
248 =head1 NAME
249
250 Business::OnlinePayment::FirstDataGlobalGateway - First Data Global Gateway e4 backend for Business::OnlinePayment
251
252 =head1 SYNOPSIS
253
254   use Business::OnlinePayment;
255
256   my $tx =
257     new Business::OnlinePayment( 'FirstDataGlobalGateway' );
258
259   $tx->content(
260       login          => 'TEST88', # ExactID
261       password       => 'TEST88', #password 
262
263       type           => 'CC',
264       action         => 'Normal Authorization',
265       amount         => '1.00',
266
267       first_name     => 'Tofu',
268       last_name      => 'Beast',
269       address        => '123 Anystreet',
270       city           => 'Anywhere',
271       state          => 'UT',
272       zip            => '84058',
273
274       card_number    => '4111111111111111',
275       expiration     => '09/20',
276       cvv2           => '124',
277
278       #optional
279       customer_ip    => '1.2.3.4',
280   );
281   $tx->submit();
282
283   if($tx->is_success()) {
284       print "Card processed successfully: ".$tx->authorization."\n";
285   } else {
286       print "Card was rejected: ".$tx->error_message."\n";
287   }
288
289 =head1 SUPPORTED TRANSACTION TYPES
290
291 =head2 CC, Visa, MasterCard, American Express, Discover
292
293 Content required: type, login, action, amount, card_number, expiration.
294
295 =head2 (NOT YET) Check
296
297 Content required: type, login, action, amount, name, account_number, routing_code.
298
299 =head1 DESCRIPTION
300
301 For detailed information see L<Business::OnlinePayment>.
302
303 =head1 METHODS AND FUNCTIONS
304
305 See L<Business::OnlinePayment> for the complete list. The following methods either override the methods in L<Business::OnlinePayment> or provide additional functions.  
306
307 =head2 result_code
308
309 Returns the response error code.
310
311 =head2 error_message
312
313 Returns the response error number.
314
315 =head2 action
316
317 The following actions are valid
318
319   Normal Authorization
320   Authorization Only
321   Post Authorization
322   Credit
323   Void
324
325 =head1 COMPATIBILITY
326
327 Business::OnlinePayment::FirstDataGlobalGateway uses the v11 version of the API
328 at this time.
329
330 =head1 AUTHORS
331
332 Ivan Kohler <ivan-firstdataglobalgateway@freeside.biz>
333
334 =head1 SEE ALSO
335
336 perl(1). L<Business::OnlinePayment>.
337
338 =cut
339