File Coverage

blib/lib/Business/OnlinePayment/Ogone.pm
Criterion Covered Total %
statement 13 15 86.6
branch n/a
condition n/a
subroutine 5 5 100.0
pod n/a
total 18 20 90.0


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