Initial release
[Business-OnlinePayment-GlobalPayments.git] / GlobalPayments.pm
1 package Business::OnlinePayment::GlobalPayments;
2
3 use warnings;
4 use strict;
5 use Carp qw(croak);
6 use vars qw($VERSION $DEBUG @ISA $me);
7 use base 'Business::OnlinePayment::HTTPS';
8 use XML::Simple 'XMLin'; # for parsing reply
9
10 $VERSION = 0.01;
11 $DEBUG = 0;
12 $me = __PACKAGE__;
13
14 my %trans_type = (
15   'normal authorization' => 'Sale',
16   'authorization only'   => 'Auth',
17   'post authorization'   => 'Force',
18   'void'                 => 'Void',
19   'credit'               => 'Return',
20 );
21
22 my %cc_fields = (
23   'GlobalUserName'  => 'login',
24   'GlobalPassword'  => 'password',
25   'TransType'       => sub { my %c = @_; $trans_type{ lc($c{action}) } },
26   'CardNum'         => 'card_number',
27   'ExpDate'         => sub { my %c = @_; join('', split /\D/,$c{'expiration'}) },
28   'MagData'         => 'track2',
29   'NameOnCard'      => sub { my %c = @_; $c{'first_name'} . ' ' . $c{'last_name'} },
30   'Amount'          => 'amount',
31   'InvNum'          => 'invoice_number',
32   'Zip'             => 'zip',
33   'Street'          => 'address',
34   'CVNum'           => 'cvv2',
35   'PNRef'           => 'order_number',
36   'ExtData'         => \&ext_data,
37 );
38
39 sub ext_data {
40   my %c = @_; # = $self->{_content}
41   my $ext_data = '';
42   if($c{'authorization'}) {
43     $ext_data .= '<AuthCode>'.$c{'authorization'}.'</AuthCode>';
44   }
45   if($c{'force_duplicate'}) { # set to any true value
46     $ext_data .= '<Force>T</Force>';
47   }
48   return $ext_data;
49 }
50
51 my %required_fields = (
52   'All'    => [ qw(GlobalUserName GlobalPassword TransType) ],
53   'Sale'   => [ qw(CardNum ExpDate Amount) ],
54   'Auth'   => [ qw(CardNum ExpDate Amount) ],
55   'Force'  => [ ],
56   'Void'   => [ 'PNRef' ],
57   'Return' => [ ],
58   'Return.blind' => [ qw(CardNum ExpDate Amount) ],
59 );
60
61 sub set_defaults {
62   my $self = shift;
63   $self->port(443);
64   $self->path('/GlobalPay/transact.asmx/ProcessCreditCard');
65   $self->build_subs('domain', 'avs_code', 'cvv2_response' );
66
67   return;
68 }
69
70 sub remap_fields {
71   my ($self, %map) = @_;
72   my %content = $self->content();
73
74   foreach (keys(%map)) {
75     if(ref($map{$_}) eq 'CODE') {
76       $content{$_} = $map{$_}->(%content);
77     }
78     else {
79       $content{$_} = $content{$map{$_}} if defined( $content{$map{$_}} );
80     }
81   }
82
83   if(lc($content{'action'}) eq 'post authorization') {
84     # GlobalPayments uses this transaction type to complete an authorized 
85     # transaction, given either its PNRef (if it was authorized by an Auth 
86     # transaction to the gateway) or its AuthCode (if it was authorized by 
87     # telephone).
88     if(!exists($content{'PNRef'}) and !exists($content{'authorization'})) {
89       croak("missing required field(s): PNRef or AuthCode\n");
90     }
91   }
92
93   $self->content(%content);
94   return;
95 }
96
97 sub submit {
98   my $self = shift;
99   my $content = $self->{_content};
100   $DB::single = 1 if $DEBUG;
101   
102   $self->setup_test if $self->test_transaction();
103
104   die "missing required option: domain\n" if !$self->domain();
105   $self->server($self->domain() . '.globalpay.com');
106
107   $self->remap_fields(%cc_fields);
108
109   my $action = $content->{'TransType'} or
110     croak "unknown action: '".$content->{'action'}."'\n";
111   $self->required_fields(@{ $required_fields{'All'} });
112   $self->required_fields(@{ $required_fields{$action} });
113
114   if($action eq 'Return' and !exists($content->{'PNRef'})) {
115     # This handles the case where a credit is ordered "blind", without
116     # an order_number.  Card information must be supplied.  Allowing 
117     # these is somewhat risky, and can be disabled at the account level 
118     # by the "Require Original PNRef" flag.
119     $self->required_fields(@{ $required_fields{'Return.blind'} });
120   }
121   
122   tie my %request, 'Tie::IxHash', 
123     map { $_ => $self->{_content}->{$_} } keys(%cc_fields);
124
125   $Business::OnlinePayment::HTTPS::DEBUG = $DEBUG;
126   $DB::single = 1 if $DEBUG;
127   my ($page, $response, %headers) = $self->https_post(\%request);
128
129   $self->server_response($page);
130   $self->is_success(0);
131   if(not $response =~ /^200/) {
132     $self->error_message("Connection failed: '$response'\n");
133     return;
134   }
135   my $data = XMLin($page);
136   if(!$data or !exists($data->{'Result'})) {
137     $self->error_message("Malformed server response: '$page'\n");
138     return;
139   }
140   $self->result_code($data->{'Result'});
141   $self->avs_code($data->{'GetAVSResult'});
142   $self->cvv2_response($data->{'GetCVResult'});
143   if($data->{'Result'} != 0) {
144     $self->error_message($data->{'Message'});
145     return;
146   }
147   else {
148     $self->is_success(1);
149     $self->authorization($data->{'AuthCode'});
150     $self->order_number($data->{'PNRef'});
151     return;
152   }
153 }
154
155 sub setup_test {
156   my $self = shift;
157   $self->domain('certapia');
158 # For test card information, see Global Transport API documentation.
159 }
160
161
162 =head1 NAME
163
164 Business::OnlinePayment::GlobalPayments - Global Transport backend for Business::OnlinePayment
165
166 =head1 SYNOPSIS
167
168 =head2 Initialization
169
170   my $trans = new Business::OnlinePayment('GlobalPayments',
171     domain => 'mymerchant' # Your account rep will supply this
172   );
173
174 =head2 Sale transaction
175
176   $trans->content(
177     login           => 'login',
178     password        => 'password',
179     type            => 'CC',
180     card_number     => '5500000000000004',
181     expiration      => '0211',
182     cvv2            => '255',
183     invoice_number  => '123321',
184     first_name      => 'Joe',
185     last_name       => 'Schmoe',
186     address         => '123 Anystreet',
187     city            => 'Sacramento',
188     state           => 'CA',
189     zip             => '95824',
190     action          => 'normal authorization',
191     amount          => '24.99'
192   );
193
194 =head2 Processing
195
196   $trans->submit;
197   if($trans->is_approved) {
198     print "Approved\n",
199           "Authorization: ", $trans->authorization, "\n",
200           "Order ID: ", $trans->order_number, "\n"
201   }
202   else {
203     print "Failed: ".$trans->error_message;
204   }
205
206 =head2 Void transaction
207   (or Return (credit) for full amount of original sale)
208
209   $trans->content(
210     login           => 'login',
211     password        => 'password',
212     action          => 'void', # or 'credit' for a Return
213     order_number    => '1001245',
214   );
215   $trans->submit;
216
217 =head1 NOTES
218
219 The following transaction types are supported:
220   Normal Authorization
221   Authorization Only
222   Post Authorization
223   Credit
224   Void
225
226 For Post Authorization, Credit, and Void, 'order_number' should be set to 
227 the order_number of the previous transaction.
228
229 Alternately, Post Authorization can be sent with 'authorization' set to an 
230 auth code obtained by telephone.  Similarly, Credit can be sent with credit 
231 account information instead of an order_number.
232
233 By default, Global Transport will reject duplicate transactions (identical 
234 card number, expiration date, and amount) sent on the same day.  This can be 
235 overridden by setting 'force_duplicate' => 1.
236
237 =head1 AUTHOR
238
239 Mark Wells <mark@freeside.biz>
240
241 =head1 SUPPORT
242
243 Support for commercial users is available from Freeside Internet Services, 
244 Inc. <http://www.freeside.biz>
245
246 =head1 COPYRIGHT & LICENSE
247
248 Copyright 2009 Mark Wells, all rights reserved.
249
250 This program is free software; you can redistribute it and/or modify it
251 under the same terms as Perl itself.
252
253
254 =cut
255
256 1; # End of Business::OnlinePayment::GlobalPayments