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