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 # module specific data
59 $self->debug( $opts{debug} );
64 foreach my $key (keys %opts) {
65 $key =~ /^default_(\w*)$/ or next;
66 $_defaults{$1} = $opts{$key};
69 $self->{_defaults} = \%_defaults;
75 my %content = $self->content();
78 my %types = ( 'visa' => 'CC',
80 'american express' => 'CC',
84 $content{'type'} = $types{lc($content{'type'})} || $content{'type'};
85 $self->transaction_type($content{'type'});
88 my $action = lc($content{'action'});
90 ( 'normal authorization' => 'SALE',
91 'authorization only' => 'AUTHONLY',
92 'post authorization' => 'CAPT',
93 'reverse authorization' => 'REVERSEAUTH',
98 ( 'normal authorization' => 'CHECK',
100 'credit' => 'REVERSAL',
103 if ($self->transaction_type eq 'CC') {
104 $content{'TransactionType'} = $actions{$action} || $action;
105 } elsif ($self->transaction_type eq 'ECHECK') {
107 $content{'TransactionType'} = $check_actions{$action} || $action;
110 my %account_types = ('personal checking' => 'Checking',
111 'personal savings' => 'Savings',
112 'business checking' => 'BusinessCk',
114 $content{'account_type'} = $account_types{lc($content{'account_type'})}
115 || $content{'account_type'};
118 $content{Origin} = 'RECURRING'
119 if ($content{recurring_billing} &&$content{recurring_billing} eq 'YES' );
121 # stuff it back into %content
122 $self->content(%content);
127 my ($self, $exp) = (shift, shift);
129 if ( defined($exp) and $exp =~ /^(\d+)\D+\d*\d{2}$/ ) {
130 $month = sprintf( "%02d", $1 );
131 }elsif ( defined($exp) and $exp =~ /^(\d{2})\d{2}$/ ) {
132 $month = sprintf( "%02d", $1 );
138 my ($self, $exp) = (shift, shift);
140 if ( defined($exp) and $exp =~ /^\d+\D+\d*(\d{2})$/ ) {
141 $year = sprintf( "%02d", $1 );
142 }elsif ( defined($exp) and $exp =~ /^\d{2}(\d{2})$/ ) {
143 $year = sprintf( "%02d", $1 );
150 tie my(%map), 'Tie::IxHash', @_;
151 my %content = $self->content();
154 if ( ref( $map{$_} ) eq 'HASH' ) {
155 $value = $map{$_} if ( keys %{ $map{$_} } );
156 }elsif( ref( $map{$_} ) ) {
157 $value = ${ $map{$_} };
158 }elsif( exists( $content{ $map{$_} } ) ) {
159 $value = $content{ $map{$_} };
162 if (defined($value)) {
173 $self->is_success(0);
176 my @required_fields = qw(action login type);
178 my $action = lc($self->{_content}->{action});
179 my $type = $self->transaction_type();
180 if ( $action eq 'normal authorization'
181 || $action eq 'credit'
182 || $action eq 'authorization only' && $type eq 'CC')
184 push @required_fields, qw( amount );
186 push @required_fields, qw( card_number expiration )
189 push @required_fields,
190 qw( routing_code account_number name ) # account_type
191 if ($type eq "ECHECK");
193 }elsif ( $action eq 'post authorization' && $type eq 'CC') {
194 push @required_fields, qw( order_number );
195 }elsif ( $action eq 'reverse authorization' && $type eq 'CC') {
196 push @required_fields, qw( order_number card_number expiration amount );
197 }elsif ( $action eq 'void') {
198 push @required_fields, qw( order_number amount );
200 push @required_fields, qw( authorization card_number )
203 push @required_fields,
204 qw( routing_code account_number name ) # account_type
205 if ($type eq "ECHECK");
208 croak "$me can't handle transaction type: ".
209 $self->{_content}->{action}. " for ".
210 $self->transaction_type();
213 my %content = $self->content();
214 foreach ( keys ( %{($self->{_defaults})} ) ) {
215 $content{$_} = $self->{_defaults}->{$_} unless exists($content{$_});
217 if ($self->test_transaction()) {
218 $content{'login'} = 'TESTTERMINAL';
220 $self->content(%content);
222 $self->required_fields(@required_fields);
224 #quick validation because ippay dumps an error indecipherable to the end user
225 if (grep { /^routing_code$/ } @required_fields) {
226 unless( $content{routing_code} =~ /^\d{9}$/ ) {
227 $self->_error_response('Invalid routing code');
232 my $transaction_id = $content{'order_number'};
233 unless ($transaction_id) {
234 my ($page, $server_response, %headers) = $self->https_get('dummy' => 1);
235 warn "fetched transaction id: (HTTPS response: $server_response) ".
237 join(", ", map { "$_ => ". $headers{$_} } keys %headers ). ") ".
238 "(Raw HTTPS content: $page)"
240 return unless $server_response=~ /^200/;
241 $transaction_id = $page;
244 my $cardexpmonth = $self->expdate_month($content{expiration});
245 my $cardexpyear = $self->expdate_year($content{expiration});
246 my $cardstartmonth = $self->expdate_month($content{card_start});
247 my $cardstartyear = $self->expdate_year($content{card_start});
250 if (defined($content{amount})) {
251 $amount = sprintf("%.2f", $content{amount});
255 my $check_number = $content{check_number} || "100" # make one up
256 if($content{account_number});
258 my $terminalid = $content{login} if $type eq 'CC';
259 my $merchantid = $content{login} if $type eq 'ECHECK';
261 my $country = country2code( $content{country}, LOCALE_CODE_ALPHA_3 );
262 $country = country_code2code( $content{country},
267 $country = $content{country}
269 $country = uc($country) if $country;
272 country2code( $content{ship_country}, LOCALE_CODE_ALPHA_3 );
273 $ship_country = country_code2code( $content{ship_country},
277 unless $ship_country;
278 $ship_country = $content{ship_country}
279 unless $ship_country;
280 $ship_country = uc($ship_country) if $ship_country;
282 tie my %ach, 'Tie::IxHash',
283 $self->revmap_fields(
284 #AccountType => 'account_type',
285 AccountNumber => 'account_number',
286 ABA => 'routing_code',
287 CheckNumber => \$check_number,
290 tie my %industryinfo, 'Tie::IxHash',
291 $self->revmap_fields(
292 Type => 'IndustryInfo',
295 tie my %shippingaddr, 'Tie::IxHash',
296 $self->revmap_fields(
297 Address => 'ship_address',
299 StateProv => 'ship_state',
300 Country => \$ship_country,
301 Phone => 'ship_phone',
304 unless ( $type ne 'CC' || keys %shippingaddr ) {
305 tie %shippingaddr, 'Tie::IxHash',
306 $self->revmap_fields(
307 Address => 'address',
309 StateProv => 'state',
310 Country => \$country,
314 delete $shippingaddr{Country} unless $shippingaddr{Country};
316 tie my %shippinginfo, 'Tie::IxHash',
317 $self->revmap_fields(
318 CustomerPO => 'CustomerPO',
319 ShippingMethod => 'ShippingMethod',
320 ShippingName => 'ship_name',
321 ShippingAddr => \%shippingaddr,
324 tie my %req, 'Tie::IxHash',
325 $self->revmap_fields(
326 TransactionType => 'TransactionType',
327 TerminalID => 'login',
328 # TerminalID => \$terminalid,
329 # MerchantID => \$merchantid,
330 TransactionID => \$transaction_id,
331 RoutingCode => 'RoutingCode',
332 Approval => 'authorization',
333 BatchID => 'BatchID',
335 Password => 'password',
336 OrderNumber => 'invoice_number',
337 CardNum => 'card_number',
339 Issue => 'issue_number',
340 CardExpMonth => \$cardexpmonth,
341 CardExpYear => \$cardexpyear,
342 CardStartMonth => \$cardstartmonth,
343 CardStartYear => \$cardstartyear,
348 DispositionType => 'DispositionType',
349 TotalAmount => \$amount,
350 FeeAmount => 'FeeAmount',
351 TaxAmount => 'TaxAmount',
352 BillingAddress => 'address',
353 BillingCity => 'city',
354 BillingStateProv => 'state',
355 BillingPostalCode => 'zip',
356 BillingCountry => \$country,
357 BillingPhone => 'phone',
359 UserIPAddr => 'customer_ip',
360 UserHost => 'UserHost',
361 UDField1 => 'UDField1',
362 UDField2 => 'UDField2',
363 UDField3 => 'UDField3',
364 ActionCode => 'ActionCode',
365 IndustryInfo => \%industryinfo,
366 ShippingInfo => \%shippinginfo,
368 delete $req{BillingCountry} unless $req{BillingCountry};
371 my $writer = new XML::Writer( OUTPUT => \$post_data,
374 ENCODING => 'us-ascii',
377 $writer->startTag('JetPay');
378 foreach ( keys ( %req ) ) {
379 $self->_xmlwrite($writer, $_, $req{$_});
381 $writer->endTag('JetPay');
384 warn "$post_data\n" if $DEBUG;
386 my ($page,$server_response,%headers) = $self->https_post($post_data);
388 warn "$page\n" if $DEBUG;
391 if ($server_response =~ /^200/){
392 $response = XMLin($page);
393 if ( exists($response->{ActionCode}) && !exists($response->{ErrMsg})) {
394 $self->error_message($response->{ResponseText});
396 $self->error_message($response->{ErrMsg});
399 # $self->error_message("Server Failed");
402 $self->result_code($response->{ActionCode} || '');
403 $self->order_number($response->{TransactionID} || '');
404 $self->authorization($response->{Approval} || '');
405 $self->cvv2_response($response->{CVV2} || '');
406 $self->avs_code($response->{AVS} || '');
408 $self->is_success($self->result_code() eq '000' ? 1 : 0);
410 unless ($self->is_success()) {
411 unless ( $self->error_message() ) { #additional logging information
412 $self->error_message(
413 "(HTTPS response: $server_response) ".
415 join(", ", map { "$_ => ". $headers{$_} } keys %headers ). ") ".
416 "(Raw HTTPS content: $page)"
423 sub _error_response {
424 my ($self, $error_message) = (shift, shift);
425 $self->result_code('');
426 $self->order_number('');
427 $self->authorization('');
428 $self->cvv2_response('');
430 $self->is_success( 0);
431 $self->error_message($error_message);
435 my ($self, $writer, $item, $value) = @_;
436 $writer->startTag($item);
437 if ( ref( $value ) eq 'HASH' ) {
438 foreach ( keys ( %$value ) ) {
439 $self->_xmlwrite($writer, $_, $value->{$_});
442 $writer->characters($value);
444 $writer->endTag($item);
453 Business::OnlinePayment::IPPay - IPPay backend for Business::OnlinePayment
457 use Business::OnlinePayment;
460 new Business::OnlinePayment( "IPPay",
461 'default_Origin' => 'PHONE ORDER',
465 login => 'testdrive',
466 password => '', #password
467 action => 'Normal Authorization',
468 description => 'Business::OnlinePayment test',
470 customer_id => 'tfb',
471 name => 'Tofu Beast',
472 address => '123 Anystreet',
476 card_number => '4007000000027',
477 expiration => '09/02',
478 cvv2 => '1234', #optional
482 if($tx->is_success()) {
483 print "Card processed successfully: ".$tx->authorization."\n";
485 print "Card was rejected: ".$tx->error_message."\n";
488 =head1 SUPPORTED TRANSACTION TYPES
490 =head2 CC, Visa, MasterCard, American Express, Discover
492 Content required: type, login, action, amount, card_number, expiration.
496 Content required: type, login, action, amount, name, account_number, routing_code.
500 For detailed information see L<Business::OnlinePayment>.
502 =head1 METHODS AND FUNCTIONS
504 See L<Business::OnlinePayment> for the complete list. The following methods either override the methods in L<Business::OnlinePayment> or provide additional functions.
508 Returns the response error code.
512 Returns the response error description text.
514 =head2 server_response
516 Returns the complete response from the server.
518 =head1 Handling of content(%content) data:
522 The following actions are valid
526 reverse authorization
531 =head1 Setting IPPay parameters from content(%content)
533 The following rules are applied to map data to IPPay parameters
534 from content(%content):
536 # param => $content{<key>}
537 TransactionType => 'TransactionType',
538 TerminalID => 'login',
539 TransactionID => 'order_number',
540 RoutingCode => 'RoutingCode',
541 Approval => 'authorization',
542 BatchID => 'BatchID',
544 Password => 'password',
545 OrderNumber => 'invoice_number',
546 CardNum => 'card_number',
548 Issue => 'issue_number',
549 CardExpMonth => \( $month ), # MM from MM(-)YY(YY) of 'expiration'
550 CardExpYear => \( $year ), # YY from MM(-)YY(YY) of 'expiration'
551 CardStartMonth => \( $month ), # MM from MM(-)YY(YY) of 'card_start'
552 CardStartYear => \( $year ), # YY from MM(-)YY(YY) of 'card_start'
556 AccountNumber => 'account_number',
557 ABA => 'routing_code',
558 CheckNumber => 'check_number',
560 DispositionType => 'DispositionType',
561 TotalAmount => 'amount' reformatted into cents
562 FeeAmount => 'FeeAmount',
563 TaxAmount => 'TaxAmount',
564 BillingAddress => 'address',
565 BillingCity => 'city',
566 BillingStateProv => 'state',
567 BillingPostalCode => 'zip',
568 BillingCountry => 'country', # forced to ISO-3166-alpha-3
569 BillingPhone => 'phone',
571 UserIPAddr => 'customer_ip',
572 UserHost => 'UserHost',
573 UDField1 => 'UDField1',
574 UDField2 => 'UDField2',
575 UDField3 => 'UDField3',
576 ActionCode => 'ActionCode',
578 Type => 'IndustryInfo',
580 CustomerPO => 'CustomerPO',
581 ShippingMethod => 'ShippingMethod',
582 ShippingName => 'ship_name',
584 Address => 'ship_address',
586 StateProv => 'ship_state',
587 Country => 'ship_country', # forced to ISO-3166-alpha-3
588 Phone => 'ship_phone',
594 Version 0.07 changes the server name and path for IPPay's late 2012 update.
596 Business::OnlinePayment::IPPay uses IPPay XML Product Specifications version
599 See http://www.ippay.com/ for more information.
603 Original author: Jeff Finucane
605 Current maintainer: Ivan Kohler <ivan-ippay@freeside.biz>
607 Reverse Authorization patch from dougforpres
611 perl(1). L<Business::OnlinePayment>.