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?
451 $att{'SEC'} = $self->{_content}->{'nacha_sec_code'}
452 || ( $att{'Type'} =~ /business/i ? 'CCD' : 'PPD' );
455 $writer->startTag($item, %att);
457 if ( ref( $value ) eq 'HASH' ) {
458 foreach ( keys ( %$value ) ) {
459 $self->_xmlwrite($writer, $_, $value->{$_});
462 $writer->characters($value);
465 $writer->endTag($item);
474 Business::OnlinePayment::IPPay - IPPay backend for Business::OnlinePayment
478 use Business::OnlinePayment;
481 new Business::OnlinePayment( "IPPay",
482 'default_Origin' => 'PHONE ORDER',
486 login => 'testdrive',
487 password => '', #password
488 action => 'Normal Authorization',
489 description => 'Business::OnlinePayment test',
491 customer_id => 'tfb',
492 name => 'Tofu Beast',
493 address => '123 Anystreet',
497 card_number => '4007000000027',
498 expiration => '09/02',
499 cvv2 => '1234', #optional
503 if($tx->is_success()) {
504 print "Card processed successfully: ".$tx->authorization."\n";
506 print "Card was rejected: ".$tx->error_message."\n";
509 =head1 SUPPORTED TRANSACTION TYPES
511 =head2 CC, Visa, MasterCard, American Express, Discover
513 Content required: type, login, action, amount, card_number, expiration.
517 Content required: type, login, action, amount, name, account_number, routing_code.
521 For detailed information see L<Business::OnlinePayment>.
523 =head1 METHODS AND FUNCTIONS
525 See L<Business::OnlinePayment> for the complete list. The following methods either override the methods in L<Business::OnlinePayment> or provide additional functions.
529 Returns the response error code.
533 Returns the response error description text.
535 =head2 server_response
537 Returns the complete response from the server.
539 =head1 Handling of content(%content) data:
543 The following actions are valid
547 reverse authorization
552 =head1 Setting IPPay parameters from content(%content)
554 The following rules are applied to map data to IPPay parameters
555 from content(%content):
557 # param => $content{<key>}
558 TransactionType => 'TransactionType',
559 TerminalID => 'login',
560 TransactionID => 'order_number',
561 RoutingCode => 'RoutingCode',
562 Approval => 'authorization',
563 BatchID => 'BatchID',
565 Password => 'password',
566 OrderNumber => 'invoice_number',
567 CardNum => 'card_number',
569 Issue => 'issue_number',
570 CardExpMonth => \( $month ), # MM from MM(-)YY(YY) of 'expiration'
571 CardExpYear => \( $year ), # YY from MM(-)YY(YY) of 'expiration'
572 CardStartMonth => \( $month ), # MM from MM(-)YY(YY) of 'card_start'
573 CardStartYear => \( $year ), # YY from MM(-)YY(YY) of 'card_start'
577 AccountNumber => 'account_number',
578 ABA => 'routing_code',
579 CheckNumber => 'check_number',
581 DispositionType => 'DispositionType',
582 TotalAmount => 'amount' reformatted into cents
583 FeeAmount => 'FeeAmount',
584 TaxAmount => 'TaxAmount',
585 BillingAddress => 'address',
586 BillingCity => 'city',
587 BillingStateProv => 'state',
588 BillingPostalCode => 'zip',
589 BillingCountry => 'country', # forced to ISO-3166-alpha-3
590 BillingPhone => 'phone',
592 UserIPAddress => 'customer_ip',
593 UserHost => 'UserHost',
594 UDField1 => 'UDField1',
595 UDField2 => 'UDField2',
596 ActionCode => 'ActionCode',
598 Type => 'IndustryInfo',
600 CustomerPO => 'CustomerPO',
601 ShippingMethod => 'ShippingMethod',
602 ShippingName => 'ship_name',
604 Address => 'ship_address',
606 StateProv => 'ship_state',
607 Country => 'ship_country', # forced to ISO-3166-alpha-3
608 Phone => 'ship_phone',
614 Version 0.07 changes the server name and path for IPPay's late 2012 update.
616 Business::OnlinePayment::IPPay uses IPPay XML Product Specifications version
619 See http://www.ippay.com/ for more information.
623 Original author: Jeff Finucane
625 Current maintainer: Ivan Kohler <ivan-ippay@freeside.biz>
627 Reverse Authorization patch from dougforpres
631 Need a complete, open-source back-office and customer self-service solution?
632 The Freeside software includes support for credit card and electronic check
633 processing with IPPay and over 50 other gateways, invoicing, integrated
634 trouble ticketing, and customer signup and self-service web interfaces.
636 http://freeside.biz/freeside/
640 perl(1). L<Business::OnlinePayment>.