File Coverage

blib/lib/Business/OnlinePayment/Ogone.pm
Criterion Covered Total %
statement 14 16 87.5
branch n/a
condition n/a
subroutine 6 6 100.0
pod n/a
total 20 22 90.9


line stmt bran cond sub pod time code
1             package Business::OnlinePayment::Ogone;
2             BEGIN {
3 6     6   105738 $Business::OnlinePayment::Ogone::AUTHORITY = 'cpan:ESSELENS';
4             }
5 6     6   14406 use parent 'Business::OnlinePayment::HTTPS';
  6         3390  
  6         32  
6 6     6   427224 use strict; # keep Perl::Critic happy over common::sense;
  6         20  
  6         236  
7 6     6   10613 use common::sense;
  6         60  
  6         48  
8 6     6   357 use Carp;
  6         16  
  6         618  
9 6     6   2817 use XML::Simple qw/:strict/;
  0            
  0            
10             use Digest::SHA qw/sha1_hex sha256_hex sha512_hex/;
11             use MIME::Base64;
12              
13             # ABSTRACT: Online payment processing via Ogone
14             our $VERSION = 0.2;
15             our $API_VERSION = 4.9;
16              
17             # Ogone config defaults and info ######################################################################################
18              
19             our %defaults = (
20             server => 'secure.ogone.com',
21             port => 443,
22             );
23              
24             our %info = (
25             'info_compat' => '0.01', # always 0.01 for now,
26             'gateway_name' => 'Ogone',
27             'gateway_url' => 'http://www.ogone.com/',
28             'module_version' => $VERSION,
29             'supported_types' => [ qw( CC ) ],
30             'token_support' => 0, #card storage/tokenization support
31             'test_transaction' => 1, #set true if ->test_transaction(1) works
32             'supported_actions' => [
33             'Authorization Only',
34             'Post Authorization',
35             'Query',
36             'Credit',
37             ]
38             );
39              
40             # Methods #############################################################################################################
41              
42             sub _info {
43             return \%info;
44             }
45              
46             sub set_defaults {
47             my $self = shift;
48             my %data = @_;
49             $self->{$_} = $defaults{$_} for keys %defaults;
50              
51             $self->build_subs(qw/http_args result_xml/);
52             }
53              
54              
55             sub submit {
56             my $self = shift;
57             my %data = $self->content();
58              
59             # do not allow submitting same object twice
60             croak 'submitting same object twice is not allowed' if $self->{_dirty}; $self->{_dirty} = 1;
61              
62             # Turn the data into a format usable by the online processor
63             croak 'no action parameter defined in content' unless exists $self->{_content}->{action};
64              
65             # Default currency to Euro
66             $self->{_content}->{currency} ||= 'EUR';
67              
68             # Table to translate from Business::OnlinePayment::Ogone args to Ogone API args
69             # The values of this hash are also used as a list of allowed args for the HTTP POST request, thus preventing information leakage
70             my %ogone_api_args = (
71             # credentials
72             login => 'USERID',
73             password => 'PSWD',
74             PSPID => 'PSPID',
75            
76             # primary identifier
77             invoice_number => 'orderID',
78            
79             # transaction identifiers (action = query)
80             payid => 'PAYID',
81             payidsub => 'PAYIDSUB',
82            
83             # credit card data
84             card_number => 'CARDNO',
85             cvc => 'CVC',
86             expiration => 'ED',
87             alias => 'ALIAS',
88            
89             # financial data
90             currency => 'Currency',
91             amount => 'amount',
92            
93             # Ogone specific arguments
94             operation => 'Operation', # REN, DEL, DES, SAL, SAS, RFD, RFS
95             eci => 'ECI', # defaults 7: e-commerce with ssl (9: recurring e-commerce)
96             accepturl => 'accepturl',
97             declineurl => 'declineurl',
98             exceptionurl => 'exceptionurl',
99             paramplus => 'paramplus',
100             complus => 'complus',
101             language => 'LANGUAGE',
102              
103             # Business::OnlinePayment common
104             description => 'COM',
105             name => 'CN',
106             email => 'EMAIL',
107             address => 'Owneraddress',
108             zip => 'OwnerZip',
109             city => 'ownertown',
110             country => 'ownercty',
111             phone => 'ownertelno',
112              
113             # client authentication (not used directly, only here as valid HTTP POST arg)
114             SHASign => 'SHASign', # see sha_key, sha_type
115            
116             # 3d secure arguments
117             flag3d => 'FLAG3D',
118             win3ds => 'win3ds',
119             http_accept => 'HTTP_ACCEPT',
120             http_user_agent => 'HTTP_USER_AGENT',
121              
122             # recurrent fields
123             subscription_id => 'SUBSCRIPTION_ID',
124             subscription_orderid => 'SUB_ORDERID',
125             subscription_status => 'SUBSCRIPTION_STATUS',
126             startdate => 'SUB_STARTDATE',
127             enddate => 'SUB_ENDDATE',
128             status => 'SUB_STATUS',
129             period_unit => 'SUB_PERIOD_UNIT', # 'd', 'ww', 'm' (yes two 'w's) for resp daily weekly monthly
130             period_moment => 'SUB_PERIOD_MOMENT', # Integer, the moment in time on which the payment is (0-7 when period_unit is ww, 1-31 for d, 1-12 for m?)
131             period_number => 'SUB_PERIOD_NUMBER',
132             );
133              
134             # Only allow max of 2 digits after comma as we need to int ( $amount * 100 ) for Ogone
135             croak 'max 2 digits after comma (or dot) allowed' if $self->{_content}->{amount} =~ m/[\,\.]\d{3}/;
136              
137             # Ogone has multiple users per login, defaults to login
138             $self->{_content}->{PSPID} ||= $self->{pspid} || $self->{PSPID} || $self->{login} || $self->{_content}->{login};
139              
140             # Login information, default to constructor values
141             $self->{_content}->{login} ||= $self->{login};
142             $self->{_content}->{password} ||= $self->{password};
143              
144             # Default Operation request for authorization (RES) for authorization only, (capture full and close) SAS for post authorization
145             $self->{_content}->{operation} ||= 'RES' if $self->{_content}->{action} =~ m/authorization only/;
146             $self->{_content}->{operation} ||= 'SAL' if $self->{_content}->{action} =~ m/normal authorization/;
147             $self->{_content}->{operation} ||= 'SAS' if $self->{_content}->{action} =~ m/post authorization/;
148              
149             # Default ECI is SSL e-commerce (7) or Recurring with e-commerce (9) if subscription_id exists
150             $self->{_content}->{eci} ||= $self->{_content}->{subscription_id} ? 9 : 7;
151              
152             # Remap the fields to their Ogone-API counterparts ie: cvc => CVC
153             $self->remap_fields(%ogone_api_args);
154              
155             croak "no sha_key provided" if $self->{_content}->{sha_type} && ! $self->{_content}->{sha_key};
156            
157             # These fields are required by Businiess::OnlinePayment::Ogone
158             my @args_basic = (qw/login password PSPID action/);
159             my @args_ccard = (qw/card_number expiration cvc/);
160             my @args_alias = (qw/alias cvc/);
161             my @args_recur = (@args_basic, qw/name subscription_id subscription_orderid invoice_number amount currency startdate enddate period_unit period_moment period_number/, $self->{_content}->{card_numer} ? @args_ccard : @args_alias ),
162             my @args_new = (@args_basic, qw/invoice_number amount currency/, $self->{_content}->{card_number} ? @args_ccard : @args_alias);
163             my @args_post = (@args_basic, qw/invoice_number/);
164             my @query = (@args_basic, qw/invoice_number/);
165              
166             # Poor man's given/when
167             my %action_arguments = (
168             qr/normal authorization/i => \@args_new,
169             qr/authorization only/i => \@args_new,
170             qr/post authorization/i => \@args_post,
171             qr/query/i => \@query,
172             qr/recurrent authorization/i => \@args_recur
173             );
174              
175             # Compile a list of required arguments
176             my @args = map { @{$action_arguments{$_}} } # lookup value using regex, return dereffed arrayref
177             grep { $self->{_content}->{action} =~ $_ } # compare action input against regex key
178             keys %action_arguments; # extract regular expressions
179              
180             croak 'unable to determine HTTP POST @args, is the action parameter one of ( authorization only | normal authorization | post authorization | query | recurrent authorization )' unless @args;
181              
182             # Enforce the field requirements by calling parent
183             my @undefs = grep { ! defined $self->{_content}->{$_} } @args;
184            
185             croak "missing required args: ". join(',',@undefs) if scalar @undefs;
186              
187             # Your module should check to see if the require_avs() function returns true, and turn on AVS checking if it does.
188             if ( $self->require_avs() ) {
189             $self->{_content}->{CHECK_AAV} = 1;
190             $self->{_content}->{CAVV_3D} = 1;
191             }
192              
193             # Define all possible arguments for http request
194             my @all_http_args = (values %ogone_api_args);
195              
196             # Construct the HTTP POST parameters by selecting the ones which are defined from all_http_args
197             my %http_req_args = map { $_ => $self->{_content}{$_} }
198             grep { defined $self->{_content}{$_} }
199             map { $ogone_api_args{$_} || $_ } @all_http_args;
200              
201             # Ogone accepts the amount as 100 fold in integer form.
202             # # Adding 0.5 to amount to prevent "rounding" errors, see http://stackoverflow.com/a/1274692 or perldoc -q round
203             $http_req_args{amount} = int(100 * $http_req_args{amount} + 0.5) if exists $http_req_args{amount};
204              
205             # Map normal fields to their SUB_ counterparts when recurrent authorization is used
206             if($self->{_content}->{action} =~ m/recurrent authorization/) {
207             $http_req_args{SUB_COMMENT} = $http_req_args{COM} if exists $http_req_args{COM};
208             $http_req_args{SUB_AMOUNT} = $http_req_args{amount};
209             $http_req_args{SUB_ORDERID} = $http_req_args{orderID};
210             }
211              
212             # PSPID might be entered in lowercase (as per old documentation)
213             $http_req_args{PSPID} = $self->{_content}{pspid} if defined $self->{_content}{pspid};
214              
215             # Calculate sha1 by default, but has to be enabled in the Ogone backend to have any effect
216             my ($sha_type) = ($self->{_content}->{sha_type} =~ m/^(1|256|512)$/);
217              
218             # Create a reference to the correct sha${sha_type}_hex function, default to SHA-1
219             my $sha_hex = sub { my $type = shift; no strict; &{"sha".($type || 1)."_hex"}(@_); use strict; };
220              
221             # Algo: make a list of "KEY=value$passphrase" sort alphabetically
222             my $signature = join('',
223             sort map { uc($_) . "=" . $http_req_args{$_} . ($self->{_content}{sha_key} || '') }
224             keys %http_req_args);
225              
226             $http_req_args{SHASign} = $sha_hex->($sha_type,$signature);
227              
228             # Construct the URL to query, taking into account the action and test_transaction values
229             my %action_file = (
230             qr/normal authorization/i => 'orderdirect.asp',
231             qr/authorization only/i => 'orderdirect.asp',
232             qr/recurrent authorization/i => 'orderdirect.asp',
233             qr/post authorization/i => 'maintenancedirect.asp',
234             qr/query/i => 'querydirect.asp',
235             );
236              
237             my $uri_dir = $self->test_transaction() ? 'test' : 'prod';
238             my ($uri_file) = map { $action_file{$_} }
239             grep { $self->{_content}->{action} =~ $_ }
240             keys %action_file;
241              
242             croak 'unable to determine URI path, is the action parameter one of ( authorization only | normal authorization | post authorization | query | recueent authorization)' unless $uri_file;
243            
244             # Construct the path to be used in https_post
245             $self->{path} = '/ncol/'.$uri_dir.'/'.$uri_file;
246              
247             # Save the http args for later inspection
248             $self->http_args(\%http_req_args);
249              
250             # Submit the transaction to the processor and collect a response.
251             my ($page, $response_code, %reply_headers) = $self->https_post(%http_req_args);
252              
253             # Call server_response() with a copy of the entire unprocessed response
254             $self->server_response([$response_code, \%reply_headers, $page]);
255              
256             my $xml = XMLin($page, ForceArray => [], KeyAttr => [] );
257              
258             # Store the result xml for later inspection
259             $self->result_xml($xml);
260              
261             croak 'Ogone refused SHA digest' if $xml->{NCERRORPLUS} =~ m#^unknown order/1/s#;
262              
263             # Call is_success() with either a true or false value, indicating if the transaction was successful or not.
264             if ( $response_code =~ m/^200/ ) {
265             $self->is_success(0); # defaults to fail
266              
267              
268             # croak 'incorrect credentials. WARNING: continuing with bad credentials will block your account s: '.$xml->{STATUS}.'{}'.$xml->{NCERROR} if $xml->{NCERROR} eq '50001119';
269              
270             if ( $xml->{STATUS} == 46 ) { $self->is_success(1) } # identification required
271             if ( $xml->{STATUS} == 5 ) { $self->is_success(1) } # authorization accepted
272             if ( $xml->{STATUS} == 9 ) { $self->is_success(1) } # payment accepted
273             if ( $xml->{STATUS} == 91 ) { $self->is_success(1) } # partial payment accepted
274             if ( $xml->{STATUS} == 2 ) { $self->failure_status('refused') } # authorization refused
275             if ( $xml->{STATUS} == 0 && $xml->{NCERROR} eq '50001134' ) { $self->failure_status('declined') } # 3d secure wrong identification
276             if ( $xml->{STATUS} == 0 && $xml->{NCERRORPLUS} =~ m/status \(91\)/ ) {
277             $self->failure_status('declined');
278             $self->error_message('Operation only allowed on fully completed transactions (status may not be 91)'); }
279              
280             } else {
281             warn "remote server did not respond with HTTP 200 status code";
282             $self->is_success(0)
283             }
284              
285             # Extract the base64 encoded HTML part
286             if ( $xml->{STATUS} == 46 ) {
287             my $html = decode_base64($xml->{HTML_ANSWER});
288             # remove sillyness
289             $html =~ s/
290             //g;
291             $html =~ s///g;
292              
293             # TODO: parse
294             #open my $fh, '>', '/tmp/ogone_'.$self->{_content}->{win3ds}.'.html';
295             #print $fh $html;
296             }
297            
298             # Call result_code() with the servers result code
299             $self->result_code($xml->{NCERROR});
300              
301             # If the transaction was successful, call authorization() with the authorization code the processor provided.
302             if ( $self->is_success() ) {
303             $self->authorization($xml->{PAYID});
304             }
305              
306             # If the transaction was not successful, call error_message() with either the processor provided error message, or some error message to indicate why it failed.
307             if ( not $self->is_success() and $xml->{NCERRORPLUS} ne '!' ) { # '!' == no errorplus
308             $self->error_message($xml->{NCERRORPLUS});
309             }
310             }
311              
312             42;
313             __END__