+package Business::OnlinePayment::PaymenTech;
+
+use strict;
+use Carp;
+use Business::OnlinePayment::HTTPS;
+use XML::Simple;
+use Tie::IxHash;
+use vars qw($VERSION $DEBUG @ISA $me);
+
+@ISA = qw(Business::OnlinePayment::HTTPS);
+$VERSION = '2.00';
+$DEBUG = 0;
+$me='Business::OnlinePayment::PaymenTech';
+
+my %request_header = (
+ 'MIME-VERSION' => '1.0',
+ 'Content-Transfer-Encoding' => 'text',
+ 'Request-Number' => 1,
+ 'Document-Type' => 'Request',
+ #'Trace-Number' => 1,
+ 'Interface-Version' => "$me $VERSION",
+); # Content-Type has to be passed separately
+
+tie my %new_order, 'Tie::IxHash', (
+ OrbitalConnectionUsername => ':login',
+ OrbitalConnectionPassword => ':password',
+ IndustryType => 'EC', # Assume industry = Ecommerce
+ MessageType => ':message_type',
+ BIN => ':bin',
+ MerchantID => ':merchant_id',
+ TerminalID => ':terminal_id',
+ CardBrand => '',
+ AccountNum => ':card_number',
+ Exp => ':expiration',
+ CurrencyCode => ':currency_code',
+ CurrencyExponent => ':currency_exp',
+ CardSecValInd => ':cvvind',
+ CardSecVal => ':cvv2',
+# AVSname => ':name', not needed
+ AVSzip => ':zip',
+ AVSaddress1 => ':address',
+ AVScity => ':city',
+ AVSstate => ':state',
+ OrderID => ':invoice_number',
+ Amount => ':amount',
+ Comments => ':email', # as per B:OP:WesternACH
+);
+
+my %defaults = (
+ terminal_id => '001',
+ currency => 'USD',
+ cvvind => '',
+);
+
+my @required = ( qw(
+ login
+ password
+ action
+ bin
+ merchant_id
+ card_number
+ expiration
+ currency
+ address
+ city
+ zip
+ invoice_number
+ amount
+ )
+);
+
+my %currency_code = (
+# Per ISO 4217. Add to this as needed.
+ USD => [840, 2],
+ CAD => [124, 2],
+ MXN => [484, 2],
+);
+
+sub set_defaults {
+ my $self = shift;
+
+ $self->server('orbitalvar1.paymentech.net') unless $self->server; # this is the test server.
+ $self->port('443') unless $self->port;
+ $self->path('/authorize') unless $self->path;
+
+ $self->build_subs(qw( TxRefNum ProcStatus ApprovalStatus StatusMsg Response ));
+
+}
+
+sub build {
+ my $self = shift;
+ my %content = $self->content();
+ my $skel = shift;
+ tie my %data, 'Tie::IxHash';
+ ref($skel) eq 'HASH' or die 'Tried to build non-hash';
+ foreach my $k (keys(%$skel)) {
+ my $v = $skel->{$k};
+ # Not recursive like B:OP:WesternACH; Paymentech requests are only one layer deep.
+ if($v =~ /^:(.*)/) {
+ # Get the content field with that name.
+ $data{$k} = $content{$1};
+ }
+ else {
+ $data{$k} = $v;
+ }
+ }
+ return \%data;
+}
+
+sub map_fields {
+ my($self) = @_;
+
+ my %content = $self->content();
+ foreach(qw(merchant_id terminal_id currency)) {
+ $content{$_} = $self->{$_} if exists($self->{$_});
+ }
+
+ $self->required_fields('action');
+ my %message_type =
+ ('normal authorization' => 'AC',
+ 'authorization only' => 'A',
+ 'credit' => 'R',
+ 'post authorization' => 'MFC', # for our use, doesn't go in the request
+ );
+ $content{'message_type'} = $message_type{lc($content{'action'})}
+ or die "unsupported action: '".$content{'action'}."'";
+ if($content{'message_type'} eq 'MFC') {
+ die 'MarkForCapture not implemented';
+ # for later implementation
+ }
+
+ foreach (keys(%defaults) ) {
+ $content{$_} = $defaults{$_} if !defined($content{$_});
+ }
+ $DB::single=1;
+ if(length($content{merchant_id}) == 12) {
+ $content{bin} = '000002' # PNS
+ }
+ elsif(length($content{merchant_id}) == 6) {
+ $content{bin} = '000001' # Salem
+ }
+ else {
+ die "invalid merchant ID: '".$content{merchant_id}."'";
+ }
+
+ @content{qw(currency_code currency_exp)} = @{$currency_code{$content{currency}}}
+ if $content{currency};
+
+ if($content{card_number} =~ /^(4|6011)/) { # Matches Visa and Discover transactions
+ if(defined($content{cvv2})) {
+ $content{cvvind} = 1; # "Value is present"
+ }
+ else {
+ $content{cvvind} = 9; # "Value is not available"
+ }
+ }
+ $content{amount} = int($content{amount}*100);
+ $content{name} = $content{first_name} . ' ' . $content{last_name};
+# According to the spec, the first 8 characters of this have to be unique.
+# The test server doesn't enforce this, but we comply anyway to the extent possible.
+ if($content{invoice_number}) {
+ # Mark it so that it's obvious that this is an invoice number
+ $content{invoice_number} = 'INV '.$content{invoice_number};
+ }
+ else {
+ # Otherwise, make something up!
+ $content{invoice_number} ||= sprintf("%04x%04x",time % 2**16,int(rand() * 2**16));
+ }
+
+ $content{expiration} =~ s/\D//g; # Because Freeside sends it as mm/yy, not mmyy.
+
+ $self->content(%content);
+ return;
+}
+
+sub submit {
+ my($self) = @_;
+ $DB::single = $DEBUG;
+
+ $self->map_fields();
+
+ # This will change when we add e-check support
+ my @required_fields = @required;
+
+ $self->required_fields(@required_fields);
+
+ # This will change when we add mark-for-capture support
+ my $request = { NewOrder => $self->build(\%new_order) };
+
+ my $post_data = XMLout({ Request => $request }, KeepRoot => 1, NoAttr => 1, NoSort => 1);
+
+ if (!$self->test_transaction()) {
+ $self->server('orbital1.paymentech.net');
+ }
+
+ warn $post_data if $DEBUG;
+ $DB::single = $DEBUG;
+ my($page,$server_response,%headers) =
+ $self->https_post( { 'Content-Type' => 'application/PTI47',
+ 'headers' => \%request_header } ,
+ $post_data);
+
+ warn $page if $DEBUG;
+
+ my $response;
+ my $error = '';
+ if ($server_response =~ /200/){
+ $response = XMLin($page, KeepRoot => 0);
+ $self->Response($response);
+ my ($r) = values(%$response);
+ if(!exists($r->{'ProcStatus'})) {
+ $error = "Malformed response: '$page'";
+ }
+ elsif($r->{'ProcStatus'} != 0 || $r->{'ApprovalStatus'} != 1) {
+ $error = "Transaction error: '". ($r->{'ProcStatusMsg'} || $r->{'StatusMsg'}) . "'";
+ }
+ else {
+ # success!
+ $self->is_success(1);
+ $self->authorization($r->{'TxRefNum'});
+ }
+ }else{
+ $error = "Server error: '$server_response'";
+ }
+ $self->error_message($error);
+ $self->is_success(0) if $error;
+
+}
+
+1;
+__END__
+
+=head1 NAME
+
+Business::OnlinePayment::PaymenTech - Chase Paymentech backend for Business::OnlinePayment
+
+=head1 SYNOPSIS
+
+$trans = new Business::OnlinePayment('PaymenTech');
+$trans->content(
+ login => "login",
+ password => "password",
+ merchant_id => "000111222333",
+ terminal_id => "001",
+ type => "CC",
+ card_number => "5500000000000004",
+ expiration => "0211",
+ address => "123 Anystreet",
+ city => "Sacramento",
+ zip => "95824",
+ action => "Normal Authorization",
+ amount => "24.99",
+
+);
+
+$trans->submit;
+if($trans->is_approved) {
+ print "Approved: ".$trans->authorization;
+
+} else {
+ print "Failed: ".$trans->error_message;
+
+}
+
+=head1 NOTES
+
+The only supported transaction types are Normal Authorization and Credit. Paymentech
+supports separate Authorize and Capture actions as well as recurring billing, but
+those are not yet implemented.
+
+Electronic check processing is not yet supported.
+
+=head1 AUTHOR
+
+Mark Wells, mark@freeside.biz
+
+=head1 SEE ALSO
+
+perl(1). L<Business::OnlinePayment>.
+
+=cut
+