File Coverage

blib/lib/WebService/MinFraud/Client.pm
Criterion Covered Total %
statement 146 147 99.3
branch 20 24 83.3
condition 9 16 56.2
subroutine 41 41 100.0
pod 4 5 80.0
total 220 233 94.4


line stmt bran cond sub pod time code
1             package WebService::MinFraud::Client;
2              
3 1     1   160546 use 5.010;
  1         9  
4 1     1   444 use Moo 1.004005;
  1         5562  
  1         4  
5 1     1   1501 use namespace::autoclean;
  1         1249  
  1         3  
6              
7             our $VERSION = '1.010000';
8              
9 1     1   60 use HTTP::Headers ();
  1         2  
  1         12  
10 1     1   3 use HTTP::Request ();
  1         2  
  1         12  
11 1     1   4 use JSON::MaybeXS;
  1         2  
  1         38  
12 1     1   4 use LWP::UserAgent;
  1         2  
  1         21  
13 1     1   15 use Scalar::Util qw( blessed );
  1         2  
  1         40  
14 1     1   409 use Sub::Quote qw( quote_sub );
  1         3862  
  1         45  
15 1     1   7 use Try::Tiny qw( catch try );
  1         2  
  1         39  
16 1     1   459 use Types::Standard qw( Defined InstanceOf );
  1         66619  
  1         10  
17 1     1   827 use URI ();
  1         2  
  1         16  
18 1     1   390 use WebService::MinFraud::Error::Generic;
  1         4  
  1         34  
19 1     1   410 use WebService::MinFraud::Error::HTTP;
  1         5  
  1         32  
20 1     1   381 use WebService::MinFraud::Error::WebService;
  1         2  
  1         26  
21 1     1   385 use WebService::MinFraud::Model::Factors;
  1         4  
  1         40  
22 1     1   544 use WebService::MinFraud::Model::Insights;
  1         4  
  1         39  
23 1     1   556 use WebService::MinFraud::Model::Score;
  1         4  
  1         33  
24 1     1   561 use WebService::MinFraud::Model::Chargeback;
  1         3  
  1         31  
25 1         64 use WebService::MinFraud::Types qw(
26             JSONObject
27             MaxMindID
28             Str
29             URIObject
30             UserAgentObject
31 1     1   6 );
  1         1  
32 1     1   355 use WebService::MinFraud::Validator;
  1         4  
  1         1416  
33              
34             with 'WebService::MinFraud::Role::HasLocales';
35              
36             has account_id => (
37             is => 'ro',
38             isa => MaxMindID,
39             required => 1,
40             );
41             *user_id = \&account_id; # for backwards-compatibility
42              
43             has _base_uri => (
44             is => 'lazy',
45             isa => URIObject,
46             builder => sub {
47 55     55   564 my $self = shift;
48 55         657 URI->new( $self->uri_scheme . '://' . $self->host . '/minfraud' );
49             },
50             );
51             has host => (
52             is => 'ro',
53             isa => Str,
54             default => q{minfraud.maxmind.com},
55             );
56             has _json => (
57             is => 'ro',
58             isa => JSONObject,
59             init_arg => undef,
60             default => quote_sub(q{ JSON::MaybeXS->new->utf8 }),
61             );
62             has license_key => (
63             is => 'ro',
64             isa => Defined,
65             required => 1,
66             );
67              
68             has timeout => (
69             is => 'ro',
70             isa => Str,
71             default => q{},
72             );
73              
74             has ua => (
75             is => 'lazy',
76             isa => UserAgentObject,
77 4     4   52 builder => sub { LWP::UserAgent->new },
78             );
79              
80             has uri_scheme => (
81             is => 'ro',
82             isa => Str,
83             default => q{https},
84             );
85              
86             has _validator => (
87             is => 'lazy',
88             isa => InstanceOf ['WebService::MinFraud::Validator'],
89 55     55   2305 builder => sub { WebService::MinFraud::Validator->new },
90             handles => { _remove_trivial_hash_values => '_delete' },
91             );
92              
93             around BUILDARGS => sub {
94             my $orig = shift;
95              
96             my $args = $orig->(@_);
97              
98             $args->{account_id} = delete $args->{user_id} if exists $args->{user_id};
99              
100             return $args;
101             };
102              
103             sub BUILD {
104 59     59 0 16518 my $self = shift;
105              
106             ## no critic (RequireBlockTermination)
107 59   50 59   412 my $self_version = try { 'v' . $self->VERSION() } || 'v?';
  59         2064  
108              
109 59         1779 my $ua = $self->ua();
110 59   50 59   1796 my $ua_version = try { 'v' . $ua->VERSION() } || 'v?';
  59         1444  
111             ## use critic
112              
113 59         1222 my $agent
114             = blessed($self)
115             . " $self_version" . ' ('
116             . blessed($ua) . q{ }
117             . $ua_version . q{ / }
118             . "Perl $^V)";
119              
120 59         256 $ua->agent($agent);
121             }
122              
123             sub factors {
124 13     13 1 782 my $self = shift;
125              
126 13         44 return $self->_response_for(
127             'v2.0',
128             'factors',
129             'WebService::MinFraud::Model::Factors', @_,
130             );
131             }
132              
133             sub insights {
134 13     13 1 777 my $self = shift;
135              
136 13         47 return $self->_response_for(
137             'v2.0',
138             'insights',
139             'WebService::MinFraud::Model::Insights', @_,
140             );
141             }
142              
143             sub score {
144 15     15 1 937 my $self = shift;
145              
146 15         64 return $self->_response_for(
147             'v2.0',
148             'score',
149             'WebService::MinFraud::Model::Score', @_,
150             );
151             }
152              
153             sub chargeback {
154 14     14 1 844 my $self = shift;
155              
156 14         53 return $self->_response_for(
157             undef,
158             'chargeback',
159             'WebService::MinFraud::Model::Chargeback', @_,
160             );
161             }
162              
163             sub _response_for {
164 55     55   144 my ( $self, $version, $path, $model_class, $content ) = @_;
165              
166 55         1206 $content = $self->_remove_trivial_hash_values($content);
167              
168 55         11155 $self->_fix_booleans($content);
169              
170 55         917 my $uri = $self->_base_uri->clone;
171 55         14833 my @path_segments = ( $uri->path_segments, );
172 55 100       2145 push @path_segments, $version if $version;
173 55         141 push @path_segments, $path;
174 55         182 $uri->path_segments(@path_segments);
175              
176 55         4560 $self->_validator->validate_request( $content, $path );
177 55         3719 my $request = HTTP::Request->new(
178             'POST', $uri,
179             HTTP::Headers->new(
180             Accept => 'application/json',
181             'Content-Type' => 'application/json'
182             ),
183             $self->_json->encode($content)
184             );
185              
186 55         14842 $request->authorization_basic( $self->account_id, $self->license_key );
187              
188 55         5637 my $response = $self->ua->request($request);
189              
190 55 100       61719 if ( $response->code == 200 ) {
    100          
191 11         144 my $body = $self->_handle_success( $response, $uri );
192 8         20 return $model_class->new( %{$body}, locales => $self->locales, );
  8         211  
193             }
194             elsif ( $response->code == 204 ) {
195 1         29 return $model_class->new;
196             }
197             else {
198             # all other error codes throw an exception
199 43         1485 $self->_handle_error_status( $response, $uri );
200             }
201             }
202              
203             {
204             my @Booleans = (
205             [ 'order', 'is_gift' ],
206             [ 'order', 'has_gift_message' ],
207             [ 'payment', 'was_authorized' ],
208             );
209              
210             sub _fix_booleans {
211 55     55   107 my $self = shift;
212 55         86 my $content = shift;
213              
214 55 50       174 return unless $content;
215              
216 55         166 for my $boolean (@Booleans) {
217 165         210 my ( $object, $key ) = @{$boolean};
  165         348  
218 165 50 33     381 if ( !exists $content->{$object}
219             || !exists $content->{$object}{$key} ) {
220 165         269 next;
221             }
222              
223             $content->{$object}{$key}
224 0 0       0 = $content->{$object}{$key}
225             ? JSON()->true
226             : JSON()->false;
227             }
228             }
229             }
230              
231             sub _handle_success {
232 11     11   23 my $self = shift;
233 11         19 my $response = shift;
234 11         20 my $uri = shift;
235              
236 11         18 my $body;
237             try {
238 11     11   435 $body = $self->_json->decode( $response->decoded_content );
239             }
240             catch {
241 3     3   443 WebService::MinFraud::Error::Generic->throw(
242             message =>
243             "Received a 200 response for $uri but could not decode the response as JSON: $_",
244             );
245 11         96 };
246              
247 8         1602 return $body;
248             }
249              
250             sub _handle_error_status {
251 43     43   87 my $self = shift;
252 43         104 my $response = shift;
253 43         86 my $uri = shift;
254              
255 43         107 my $status = $response->code;
256              
257 43 100       544 if ( $status =~ /^4/ ) {
    100          
258 36         131 $self->_handle_4xx_status( $response, $status, $uri );
259             }
260             elsif ( $status =~ /^5/ ) {
261 4         21 $self->_handle_5xx_status( $status, $uri );
262             }
263             else {
264 3         13 $self->_handle_non_200_status( $status, $uri );
265             }
266             }
267              
268             sub _handle_4xx_status {
269 36     36   85 my $self = shift;
270 36         74 my $response = shift;
271 36         73 my $status = shift;
272 36         67 my $uri = shift;
273              
274 36         121 my $content = $response->decoded_content;
275              
276 36   66     4451 my $has_body = defined $content && length $content;
277             my $body = try {
278 36 100 66 36   940 $has_body
279             && $response->content_type =~ /json/
280             && $self->_json->decode($content)
281 36         259 };
282              
283 36 100       1693 if ($body) {
284 32 100 66     154 if ( $body->{code} || $body->{error} ) {
285             WebService::MinFraud::Error::WebService->throw(
286             message => delete $body->{error},
287 29         74 %{$body},
  29         368  
288             http_status => $status,
289             uri => $uri,
290             );
291             }
292             else {
293 3         44 WebService::MinFraud::Error::Generic->throw( message =>
294             'Response contains JSON but it does not specify code or error keys'
295             );
296             }
297             }
298             else {
299 4 100       37 WebService::MinFraud::Error::HTTP->throw(
300             message => $has_body
301             ? "Received a $status error for $uri with the following body: $content"
302             : "Received a $status error for $uri with no body",
303             http_status => $status,
304             uri => $uri,
305             );
306             }
307             }
308              
309             sub _handle_5xx_status {
310 4     4   10 my $self = shift;
311 4         11 my $status = shift;
312 4         8 my $uri = shift;
313              
314 4         26 WebService::MinFraud::Error::HTTP->throw(
315             message => "Received a server error ($status) for $uri",
316             http_status => $status,
317             uri => $uri,
318             );
319             }
320              
321             sub _handle_non_200_status {
322 3     3   8 my $self = shift;
323 3         7 my $status = shift;
324 3         8 my $uri = shift;
325              
326 3         21 WebService::MinFraud::Error::HTTP->throw(
327             message =>
328             "Received an unexpected HTTP status ($status) for $uri that is neither 2xx, 4xx nor 5xx",
329             http_status => $status,
330             uri => $uri,
331             );
332             }
333              
334             1;
335              
336             # ABSTRACT: Perl API for MaxMind's minFraud Score and Insights web services
337              
338             __END__
339              
340             =pod
341              
342             =encoding UTF-8
343              
344             =head1 NAME
345              
346             WebService::MinFraud::Client - Perl API for MaxMind's minFraud Score and Insights web services
347              
348             =head1 VERSION
349              
350             version 1.010000
351              
352             =head1 SYNOPSIS
353              
354             use 5.010;
355              
356             use WebService::MinFraud::Client;
357              
358             # The Client object can be re-used across several requests.
359             # Your MaxMind account_id and license_key are available at
360             # https://www.maxmind.com/en/my_license_key
361             my $client = WebService::MinFraud::Client->new(
362             account_id => 42,
363             license_key => 'abcdef123456',
364             );
365              
366             # Request HashRef must contain a 'device' key, with a value that is a
367             # HashRef containing an 'ip_address' key with a valid IPv4 or IPv6 address.
368             # All other keys/values are optional; see other modules in minFraud Perl API
369             # distribution for details.
370              
371             my $request = { device => { ip_address => '24.24.24.24' } };
372              
373             # Use the 'score', 'insights', or 'factors' client methods, depending on
374             # the minFraud web service you are using.
375              
376             my $score = $client->score( $request );
377             say $score->risk_score;
378              
379             my $insights = $client->insights( $request );
380             say $insights->shipping_address->is_high_risk;
381              
382             my $factors = $client->factors( $request );
383             say $factors->subscores->ip_tenure;
384              
385              
386             # Request HashRef must contain an 'ip_address' key containing a valid
387             # IPv4 or IPv6 address. All other keys/values are optional; see other modules
388             # in minFraud Perl API distribution for details.
389              
390             $request = { ip_address => '24.24.24.24' };
391              
392             # Use the chargeback client method to submit an IP address back to Maxmind.
393             # The chargeback api does not return any content from the server.
394              
395             my $chargeback = $client->chargeback( $request );
396             if ($chargeback->isa('WebService::MinFraud::Model::Chargeback')) {
397             say 'Successfully submitted chargeback';
398             }
399              
400             =head1 DESCRIPTION
401              
402             This class provides a client API for the MaxMind minFraud Score, Insights
403             Factors web services, and the Chargeback web service. The B<Insights>
404             service returns more data about a transaction than the B<Score> service.
405             See the L<API documentation|https://dev.maxmind.com/minfraud/>
406             for more details.
407              
408             Each web service is represented by a different model class, and
409             these model classes in turn contain multiple Record classes. The Record
410             classes have attributes which contain data about the transaction or IP address.
411              
412             If the web service does not return a particular piece of data for a
413             transaction or IP address, the associated attribute is not populated.
414              
415             The web service may not return any information for an entire record, in which
416             case all of the attributes for that record class will be empty.
417              
418             =head1 TRANSPORT SECURITY
419              
420             Requests to the minFraud web service are made over an HTTPS connection.
421              
422             =head1 USAGE
423              
424             The basic API for this class is the same for all of the web services. First you
425             create a web service object with your MaxMind C<account_id> and C<license_key>,
426             then you call the method corresponding to the specific web service, passing it
427             the transaction you want analyzed.
428              
429             If the request succeeds, the method call will return a model class for the web
430             service you called. This model in turn contains multiple record classes, each of
431             which represents part of the data returned by the web service.
432              
433             If the request fails, the client class throws an exception.
434              
435             =head1 CONSTRUCTOR
436              
437             This class has a single constructor method:
438              
439             =head2 WebService::MinFraud::Client->new
440              
441             This method creates a new client object. It accepts the following arguments:
442              
443             =over 4
444              
445             =item * account_id
446              
447             Your MaxMind User ID. Go to L<https://www.maxmind.com/en/my_license_key> to see
448             your MaxMind User ID and license key.
449              
450             This argument is required.
451              
452             =item * license_key
453              
454             Your MaxMind license key.
455              
456             This argument is required.
457              
458             =item * locales
459              
460             This is an array reference where each value is a string indicating a locale.
461             This argument will be passed on to record classes to use when their C<name>
462             methods are called.
463              
464             The order of the locales is significant. When a record class has multiple
465             names (country, city, etc.), its C<name> method will look at each element of
466             this array reference and return the first locale for which it has a name.
467              
468             Note that the only locale which is always present in the minFraud data is
469             C<en>. If you do not include this locale, the C<name> method may return
470             C<undef> even when the record in question has an English name.
471              
472             Currently, the valid list of locale codes is:
473              
474             =over 8
475              
476             =item * de - German
477              
478             =item * en - English
479              
480             English names may still include accented characters if that is the accepted
481             spelling in English. In other words, English does not mean ASCII.
482              
483             =item * es - Spanish
484              
485             =item * fr - French
486              
487             =item * ja - Japanese
488              
489             =item * pt-BR - Brazilian Portuguese
490              
491             =item * ru - Russian
492              
493             =item * zh-CN - Simplified Chinese
494              
495             =back
496              
497             Passing any other locale code will result in an error.
498              
499             The default value for this argument is C<['en']>.
500              
501             =item * host
502              
503             The hostname of the minFraud web service used when making requests. This
504             defaults to C<minfraud.maxmind.com>. In most cases, you do not need to set this
505             explicitly.
506              
507             =item * ua
508              
509             This argument allows you to set your own L<LWP::UserAgent> object. This is
510             useful if you have to override the default object (C<< LWP::UserAgent->new() >>)
511             to set http proxy parameters, for example.
512              
513             This attribute can be any object which supports C<agent> and C<request>
514             methods:
515              
516             =over 8
517              
518             =item * request
519              
520             The C<request> method will be called with an L<HTTP::Request> object as its only
521             argument. This method must return an L<HTTP::Response> object.
522              
523             =item * agent
524              
525             The C<agent> method will be called with a User-Agent string, constructed as
526             described below.
527              
528             =back
529              
530             =back
531              
532             =head1 REQUEST
533              
534             The request methods are passed a HashRef as the only argument. See the L</SYNOPSIS> and L<WebService::MinFraud::Example> for detailed usage examples. Some important notes regarding values passed to the minFraud web service via the Perl API are described below.
535              
536             =head2 device => ip_address or ip_address
537              
538             This must be a valid IPv4 or IPv6 address in presentation format, i.e.,
539             dotted-quad notation or the IPv6 hexadecimal-colon notation.
540              
541             =head1 REQUEST METHODS
542              
543             All of the fraud service request methods require a device ip_address. See the
544             L<API documentation for fraud services|https://dev.maxmind.com/minfraud/>
545             for details on all the values that can be part of the request. Portions of the
546             request hash with undefined and empty string values are automatically removed
547             from the request.
548              
549             The chargeback request method requires an ip_address. See the
550             L<API documentation for chargeback|https:://dev.maxmind.com/minfraud/chargeback/>
551             for details on all the values that can be part of the request.
552              
553             =head2 score
554              
555             This method calls the minFraud Score web service. It returns a
556             L<WebService::MinFraud::Model::Score> object.
557              
558             =head2 insights
559              
560             This method calls the minFraud Insights web service. It returns a
561             L<WebService::MinFraud::Model::Insights> object.
562              
563             =head2 factors
564              
565             This method calls the minFraud Factors web service. It returns a
566             L<WebService::MinFraud::Model::Factors> object.
567              
568             =head2 chargeback
569              
570             This method calls the minFraud Chargeback web service. It returns a
571             L<WebService::MinFraud::Model::Chargeback> object.
572              
573             =head1 User-Agent HEADER
574              
575             Requests by the minFraud Perl API will have a User-Agent header containing the
576             package name and version of this module (or a subclass if you use one), the
577             package name and version of the user agent object, and the version of Perl.
578              
579             This header is set in order to help us support individual users, as well to determine
580             support policies for dependencies and Perl itself.
581              
582             =head1 EXCEPTIONS
583              
584             For details on the possible errors returned by the web service itself, please
585             refer to the
586             L<API documentation|https://dev.maxmind.com/minfraud/>.
587              
588             Prior to making the request to the web service, the request HashRef is passed
589             to L<WebService::MinFraud::Validator> for checks. If the request fails
590             validation an exception is thrown, containing a string describing all of the
591             validation errors.
592              
593             If the web service returns an explicit error document, this is thrown as a
594             L<WebService::MinFraud::Error::WebService> exception object. If some other
595             sort of error occurs, this is thrown as a L<WebService::MinFraud::Error::HTTP>
596             object. The difference is that the web service error includes an error message
597             and error code delivered by the web service. The latter is thrown when an
598             unanticipated error occurs, such as the web service returning a 500 status or an
599             invalid error document.
600              
601             If the web service returns any status code besides 200, 4xx, or 5xx, this also
602             becomes a L<WebService::MinFraud::Error::HTTP> object.
603              
604             Finally, if the web service returns a 200 but the body is invalid, the client
605             throws a L<WebService::MinFraud::Error::Generic> object.
606              
607             All of these error classes have a C<< message >> method and
608             overload stringification to show that message. This means that if you don't
609             explicitly catch errors they will ultimately be sent to C<STDERR> with some
610             sort of (hopefully) useful error message.
611              
612             =head1 WHAT DATA IS RETURNED?
613              
614             Please see the
615             L<API documentation|https://dev.maxmind.com/minfraud/>
616             for details on what data each web service may return.
617              
618             Every record class attribute has a corresponding predicate method so that you
619             can check to see if the attribute is set.
620              
621             my $insights = $client->insights( $request );
622             my $issuer = $insights->issuer;
623             # phone_number attribute, with has_phone_number predicate method
624             if ( $issuer->has_phone_number ) {
625             say "issuer phone number: " . $issuer->phone_number;
626             }
627             else {
628             say "no phone number found for issuer";
629             }
630              
631             =head1 SUPPORT
632              
633             Bugs may be submitted through L<https://github.com/maxmind/minfraud-api-perl/issues>.
634              
635             =head1 AUTHOR
636              
637             Mateu Hunter <mhunter@maxmind.com>
638              
639             =head1 COPYRIGHT AND LICENSE
640              
641             This software is copyright (c) 2015 - 2020 by MaxMind, Inc.
642              
643             This is free software; you can redistribute it and/or modify it under
644             the same terms as the Perl 5 programming language system itself.
645              
646             =cut