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_compat' => '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 UserIPAddress => '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 UserIPAddress => '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 Need a complete, open-source back-office and customer self-service solution?
631 The Freeside software includes support for credit card and electronic check
632 processing with IPPay and over 50 other gateways, invoicing, integrated
633 trouble ticketing, and customer signup and self-service web interfaces.
635 http://freeside.biz/freeside/
639 perl(1). L<Business::OnlinePayment>.