1 package Business::OnlinePayment::IPPay;
9 use Business::OnlinePayment;
10 use Business::OnlinePayment::HTTPS;
11 use vars qw($VERSION $DEBUG @ISA $me);
13 @ISA = qw(Business::OnlinePayment::HTTPS);
15 $VERSION = eval $VERSION; # modperlstyle: convert the string into a number
18 $me = 'Business::OnlinePayment::IPPay';
22 'info_version' => '0.01',
23 'module_version' => $VERSION,
24 'supported_types' => [ qw( CC ECHECK ) ],
25 'supported_actions' => { 'CC' => [
26 'Normal Authorization',
31 'Reverse Authorization',
34 'Normal Authorization',
39 'CC_void_requires_card' => 1,
40 'ECHECK_void_requires_account' => 1,
48 # standard B::OP methods/data
49 $self->server('gtwy.ippay.com') unless $self->server;
50 $self->port('443') unless $self->port;
51 $self->path('/ippay') unless $self->path;
53 $self->build_subs(qw( order_number avs_code cvv2_response
54 response_page response_code response_headers
57 $DEBUG = exists($opts{debug}) ? $opts{debug} : 0;
59 # module specific data
61 foreach my $key (keys %opts) {
62 $key =~ /^default_(\w*)$/ or next;
63 $_defaults{$1} = $opts{$key};
66 $self->{_defaults} = \%_defaults;
72 my %content = $self->content();
75 my %types = ( 'visa' => 'CC',
77 'american express' => 'CC',
81 $content{'type'} = $types{lc($content{'type'})} || $content{'type'};
82 $self->transaction_type($content{'type'});
85 my $action = lc($content{'action'});
87 ( 'normal authorization' => 'SALE',
88 'authorization only' => 'AUTHONLY',
89 'post authorization' => 'CAPT',
90 'reverse authorization' => 'REVERSEAUTH',
95 ( 'normal authorization' => 'CHECK',
97 'credit' => 'REVERSAL',
100 if ($self->transaction_type eq 'CC') {
101 $content{'TransactionType'} = $actions{$action} || $action;
102 } elsif ($self->transaction_type eq 'ECHECK') {
104 $content{'TransactionType'} = $check_actions{$action} || $action;
107 my %account_types = ('personal checking' => 'CHECKING',
108 'personal savings' => 'SAVINGS',
109 'business checking' => 'CHECKING',
110 'business savings' => 'SAVINGS',
111 #not technically B:OP valid i guess?
112 'checking' => 'CHECKING',
113 'savings' => 'SAVINGS',
115 $content{'account_type'} = $account_types{lc($content{'account_type'})}
116 || $content{'account_type'};
119 $content{Origin} = 'RECURRING'
120 if ($content{recurring_billing} &&$content{recurring_billing} eq 'YES' );
122 # stuff it back into %content
123 $self->content(%content);
128 my ($self, $exp) = (shift, shift);
130 if ( defined($exp) and $exp =~ /^(\d+)\D+\d*\d{2}$/ ) {
131 $month = sprintf( "%02d", $1 );
132 }elsif ( defined($exp) and $exp =~ /^(\d{2})\d{2}$/ ) {
133 $month = sprintf( "%02d", $1 );
139 my ($self, $exp) = (shift, shift);
141 if ( defined($exp) and $exp =~ /^\d+\D+\d*(\d{2})$/ ) {
142 $year = sprintf( "%02d", $1 );
143 }elsif ( defined($exp) and $exp =~ /^\d{2}(\d{2})$/ ) {
144 $year = sprintf( "%02d", $1 );
151 tie my(%map), 'Tie::IxHash', @_;
152 my %content = $self->content();
155 if ( ref( $map{$_} ) eq 'HASH' ) {
156 $value = $map{$_} if ( keys %{ $map{$_} } );
157 }elsif( ref( $map{$_} ) ) {
158 $value = ${ $map{$_} };
159 }elsif( exists( $content{ $map{$_} } ) ) {
160 $value = $content{ $map{$_} };
163 if (defined($value)) {
174 $self->is_success(0);
177 my @required_fields = qw(action login password type);
179 my $action = lc($self->{_content}->{action});
180 my $type = $self->transaction_type();
181 if ( $action eq 'normal authorization'
182 || $action eq 'credit'
183 || $action eq 'authorization only' && $type eq 'CC')
185 push @required_fields, qw( amount );
187 push @required_fields, qw( card_number expiration )
190 push @required_fields,
191 qw( routing_code account_number name ) # account_type
192 if ($type eq "ECHECK");
194 }elsif ( $action eq 'post authorization' && $type eq 'CC') {
195 push @required_fields, qw( order_number );
196 }elsif ( $action eq 'reverse authorization' && $type eq 'CC') {
197 push @required_fields, qw( order_number card_number expiration amount );
198 }elsif ( $action eq 'void') {
199 push @required_fields, qw( order_number amount );
201 push @required_fields, qw( authorization card_number )
204 push @required_fields,
205 qw( routing_code account_number name ) # account_type
206 if ($type eq "ECHECK");
209 croak "$me can't handle transaction type: ".
210 $self->{_content}->{action}. " for ".
211 $self->transaction_type();
214 my %content = $self->content();
215 foreach ( keys ( %{($self->{_defaults})} ) ) {
216 $content{$_} = $self->{_defaults}->{$_} unless exists($content{$_});
218 if ($self->test_transaction()) {
219 $content{'login'} = 'TESTTERMINAL';
221 $self->content(%content);
223 $self->required_fields(@required_fields);
225 #quick validation because ippay dumps an error indecipherable to the end user
226 if (grep { /^routing_code$/ } @required_fields) {
227 unless( $content{routing_code} =~ /^\d{9}$/ ) {
228 $self->_error_response('Invalid routing code');
233 my $transaction_id = $content{'order_number'};
234 unless ($transaction_id) {
235 my ($page, $server_response, %headers) = $self->https_get('dummy' => 1);
236 warn "fetched transaction id: (HTTPS response: $server_response) ".
238 join(", ", map { "$_ => ". $headers{$_} } keys %headers ). ") ".
239 "(Raw HTTPS content: $page)"
241 return unless $server_response=~ /^200/;
242 $transaction_id = $page;
245 my $cardexpmonth = $self->expdate_month($content{expiration});
246 my $cardexpyear = $self->expdate_year($content{expiration});
247 my $cardstartmonth = $self->expdate_month($content{card_start});
248 my $cardstartyear = $self->expdate_year($content{card_start});
251 if (defined($content{amount})) {
252 $amount = sprintf("%.2f", $content{amount});
256 my $check_number = $content{check_number} || "100" # make one up
257 if($content{account_number});
259 my $terminalid = $content{login} if $type eq 'CC';
260 my $merchantid = $content{login} if $type eq 'ECHECK';
262 my $country = country2code( $content{country}, LOCALE_CODE_ALPHA_3 );
263 $country = country_code2code( $content{country},
268 $country = $content{country}
270 $country = uc($country) if $country;
273 country2code( $content{ship_country}, LOCALE_CODE_ALPHA_3 );
274 $ship_country = country_code2code( $content{ship_country},
278 unless $ship_country;
279 $ship_country = $content{ship_country}
280 unless $ship_country;
281 $ship_country = uc($ship_country) if $ship_country;
283 tie my %ach, 'Tie::IxHash',
284 $self->revmap_fields(
285 #wtf, this is a "Type"" attribute of the ACH element,
286 # not a child element like the others
287 #AccountType => 'account_type',
288 AccountNumber => 'account_number',
289 ABA => 'routing_code',
290 CheckNumber => \$check_number,
293 tie my %industryinfo, 'Tie::IxHash',
294 $self->revmap_fields(
295 Type => 'IndustryInfo',
298 tie my %shippingaddr, 'Tie::IxHash',
299 $self->revmap_fields(
300 Address => 'ship_address',
302 StateProv => 'ship_state',
303 Country => \$ship_country,
304 Phone => 'ship_phone',
307 unless ( $type ne 'CC' || keys %shippingaddr ) {
308 tie %shippingaddr, 'Tie::IxHash',
309 $self->revmap_fields(
310 Address => 'address',
312 StateProv => 'state',
313 Country => \$country,
317 delete $shippingaddr{Country} unless $shippingaddr{Country};
319 tie my %shippinginfo, 'Tie::IxHash',
320 $self->revmap_fields(
321 CustomerPO => 'CustomerPO',
322 ShippingMethod => 'ShippingMethod',
323 ShippingName => 'ship_name',
324 ShippingAddr => \%shippingaddr,
327 tie my %req, 'Tie::IxHash',
328 $self->revmap_fields(
329 TransactionType => 'TransactionType',
330 TerminalID => 'login',
331 # TerminalID => \$terminalid,
332 # MerchantID => \$merchantid,
333 TransactionID => \$transaction_id,
334 RoutingCode => 'RoutingCode',
335 Approval => 'authorization',
336 BatchID => 'BatchID',
338 Password => 'password',
339 OrderNumber => 'invoice_number',
340 CardNum => 'card_number',
342 Issue => 'issue_number',
343 CardExpMonth => \$cardexpmonth,
344 CardExpYear => \$cardexpyear,
345 CardStartMonth => \$cardstartmonth,
346 CardStartYear => \$cardstartyear,
351 DispositionType => 'DispositionType',
352 TotalAmount => \$amount,
353 FeeAmount => 'FeeAmount',
354 TaxAmount => 'TaxAmount',
355 BillingAddress => 'address',
356 BillingCity => 'city',
357 BillingStateProv => 'state',
358 BillingPostalCode => 'zip',
359 BillingCountry => \$country,
360 BillingPhone => 'phone',
362 UserIPAddr => 'customer_ip',
363 UserHost => 'UserHost',
364 UDField1 => 'UDField1',
365 UDField2 => 'UDField2',
366 UDField3 => \"$me $VERSION", #'UDField3',
367 ActionCode => 'ActionCode',
368 IndustryInfo => \%industryinfo,
369 ShippingInfo => \%shippinginfo,
371 delete $req{BillingCountry} unless $req{BillingCountry};
374 my $writer = new XML::Writer( OUTPUT => \$post_data,
377 ENCODING => 'us-ascii',
380 $writer->startTag('JetPay');
381 foreach ( keys ( %req ) ) {
382 $self->_xmlwrite($writer, $_, $req{$_});
384 $writer->endTag('JetPay');
387 warn "$post_data\n" if $DEBUG > 1;
389 my ($page,$server_response,%headers) = $self->https_post($post_data);
391 warn "$page\n" if $DEBUG > 1;
394 if ($server_response =~ /^200/){
395 $response = XMLin($page);
396 if ( exists($response->{ActionCode}) && !exists($response->{ErrMsg})) {
397 $self->error_message($response->{ResponseText});
399 $self->error_message($response->{ErrMsg});
402 # $self->error_message("Server Failed");
405 $self->result_code($response->{ActionCode} || '');
406 $self->order_number($response->{TransactionID} || '');
407 $self->authorization($response->{Approval} || '');
408 $self->cvv2_response($response->{CVV2} || '');
409 $self->avs_code($response->{AVS} || '');
411 $self->is_success($self->result_code() eq '000' ? 1 : 0);
413 unless ($self->is_success()) {
414 unless ( $self->error_message() ) {
416 #additional logging information, possibly too sensitive for an error msg
417 # (IPPay seems to have a failure mode where they return the full
418 # original request including card number)
419 $self->error_message(
420 "(HTTPS response: $server_response) ".
422 join(", ", map { "$_ => ". $headers{$_} } keys %headers ). ") ".
423 "(Raw HTTPS content: $page)"
426 $self->error_message('No ResponseText or ErrMsg was returned by IPPay (enable debugging for raw HTTPS response)');
433 sub _error_response {
434 my ($self, $error_message) = (shift, shift);
435 $self->result_code('');
436 $self->order_number('');
437 $self->authorization('');
438 $self->cvv2_response('');
440 $self->is_success( 0);
441 $self->error_message($error_message);
445 my ($self, $writer, $item, $value) = @_;
448 if ( $item eq 'ACH' ) {
449 $att{'Type'} = $self->{_content}->{'account_type'}
450 if $self->{_content}->{'account_type'}; #necessary so we don't pass empty?
454 $writer->startTag($item, %att);
456 if ( ref( $value ) eq 'HASH' ) {
457 foreach ( keys ( %$value ) ) {
458 $self->_xmlwrite($writer, $_, $value->{$_});
461 $writer->characters($value);
464 $writer->endTag($item);
473 Business::OnlinePayment::IPPay - IPPay backend for Business::OnlinePayment
477 use Business::OnlinePayment;
480 new Business::OnlinePayment( "IPPay",
481 'default_Origin' => 'PHONE ORDER',
485 login => 'testdrive',
486 password => '', #password
487 action => 'Normal Authorization',
488 description => 'Business::OnlinePayment test',
490 customer_id => 'tfb',
491 name => 'Tofu Beast',
492 address => '123 Anystreet',
496 card_number => '4007000000027',
497 expiration => '09/02',
498 cvv2 => '1234', #optional
502 if($tx->is_success()) {
503 print "Card processed successfully: ".$tx->authorization."\n";
505 print "Card was rejected: ".$tx->error_message."\n";
508 =head1 SUPPORTED TRANSACTION TYPES
510 =head2 CC, Visa, MasterCard, American Express, Discover
512 Content required: type, login, action, amount, card_number, expiration.
516 Content required: type, login, action, amount, name, account_number, routing_code.
520 For detailed information see L<Business::OnlinePayment>.
522 =head1 METHODS AND FUNCTIONS
524 See L<Business::OnlinePayment> for the complete list. The following methods either override the methods in L<Business::OnlinePayment> or provide additional functions.
528 Returns the response error code.
532 Returns the response error description text.
534 =head2 server_response
536 Returns the complete response from the server.
538 =head1 Handling of content(%content) data:
542 The following actions are valid
546 reverse authorization
551 =head1 Setting IPPay parameters from content(%content)
553 The following rules are applied to map data to IPPay parameters
554 from content(%content):
556 # param => $content{<key>}
557 TransactionType => 'TransactionType',
558 TerminalID => 'login',
559 TransactionID => 'order_number',
560 RoutingCode => 'RoutingCode',
561 Approval => 'authorization',
562 BatchID => 'BatchID',
564 Password => 'password',
565 OrderNumber => 'invoice_number',
566 CardNum => 'card_number',
568 Issue => 'issue_number',
569 CardExpMonth => \( $month ), # MM from MM(-)YY(YY) of 'expiration'
570 CardExpYear => \( $year ), # YY from MM(-)YY(YY) of 'expiration'
571 CardStartMonth => \( $month ), # MM from MM(-)YY(YY) of 'card_start'
572 CardStartYear => \( $year ), # YY from MM(-)YY(YY) of 'card_start'
576 AccountNumber => 'account_number',
577 ABA => 'routing_code',
578 CheckNumber => 'check_number',
580 DispositionType => 'DispositionType',
581 TotalAmount => 'amount' reformatted into cents
582 FeeAmount => 'FeeAmount',
583 TaxAmount => 'TaxAmount',
584 BillingAddress => 'address',
585 BillingCity => 'city',
586 BillingStateProv => 'state',
587 BillingPostalCode => 'zip',
588 BillingCountry => 'country', # forced to ISO-3166-alpha-3
589 BillingPhone => 'phone',
591 UserIPAddr => 'customer_ip',
592 UserHost => 'UserHost',
593 UDField1 => 'UDField1',
594 UDField2 => 'UDField2',
595 ActionCode => 'ActionCode',
597 Type => 'IndustryInfo',
599 CustomerPO => 'CustomerPO',
600 ShippingMethod => 'ShippingMethod',
601 ShippingName => 'ship_name',
603 Address => 'ship_address',
605 StateProv => 'ship_state',
606 Country => 'ship_country', # forced to ISO-3166-alpha-3
607 Phone => 'ship_phone',
613 Version 0.07 changes the server name and path for IPPay's late 2012 update.
615 Business::OnlinePayment::IPPay uses IPPay XML Product Specifications version
618 See http://www.ippay.com/ for more information.
622 Original author: Jeff Finucane
624 Current maintainer: Ivan Kohler <ivan-ippay@freeside.biz>
626 Reverse Authorization patch from dougforpres
630 perl(1). L<Business::OnlinePayment>.