File Coverage

blib/lib/Business/GoCardless/Client.pm
Criterion Covered Total %
statement 124 126 98.4
branch 26 32 81.2
condition 2 6 33.3
subroutine 26 26 100.0
pod 0 3 0.0
total 178 193 92.2


line stmt bran cond sub pod time code
1             package Business::GoCardless::Client;
2              
3             =head1 NAME
4              
5             Business::GoCardless::Client
6              
7             =head1 DESCRIPTION
8              
9             This is a class for the lower level requests to the gocardless API. generally
10             there is nothing you should be doing with this.
11              
12             =cut
13              
14 19     19   1749034 use strict;
  19         153  
  19         608  
15 19     19   95 use warnings;
  19         35  
  19         552  
16 19     19   104 use feature qw/ say /;
  19         36  
  19         2096  
17              
18 19     19   7246 use Moo;
  19         151965  
  19         89  
19             with 'Business::GoCardless::Utils';
20              
21 19     19   27840 use Business::GoCardless::Exception;
  19         66  
  19         635  
22 19     19   6991 use Business::GoCardless::Bill;
  19         60  
  19         645  
23 19     19   7666 use Business::GoCardless::Merchant;
  19         61  
  19         631  
24 19     19   123 use Business::GoCardless::Payout;
  19         38  
  19         417  
25 19     19   6653 use Business::GoCardless::RedirectFlow;
  19         49  
  19         560  
26 19     19   6429 use Business::GoCardless::Subscription;
  19         64  
  19         670  
27              
28 19     19   141 use Carp qw/ confess /;
  19         46  
  19         1193  
29 19     19   9047 use POSIX qw/ strftime /;
  19         112063  
  19         110  
30 19     19   32287 use MIME::Base64 qw/ encode_base64 /;
  19         10705  
  19         1026  
31 19     19   11009 use LWP::UserAgent;
  19         806606  
  19         663  
32 19     19   159 use JSON ();
  19         45  
  19         34070  
33              
34             =head1 ATTRIBUTES
35              
36             =head2 token
37              
38             Your gocardless API token, this attribute is required.
39              
40             =head2 base_url
41              
42             The gocardless API URL, defaults to $ENV{GOCARDLESS_URL} or
43             https://gocardless.com.
44              
45             =head2 api_path
46              
47             The gocardless API path, defaults to /api/v1.
48              
49             =head2 app_id
50              
51             Your gocardless app identifier, defaults to $ENV{GOCARDLESS_APP_ID} or will
52             exit if not set.
53              
54             =head2 app_secret
55              
56             Your gocardless app secret, defaults to $ENV{GOCARDLESS_APP_SECRET} or will
57             exit if not set.
58              
59             =head2 webhook_secret
60              
61             Your gocardless webhook secret, defaults to $ENV{GOCARDLESS_WEBHOOK_SECRET} or will
62             exit if not set.
63              
64             =head2 merchant_id
65              
66             Your gocardless merchant identifier, defaults to $ENV{GOCARDLESS_MERCHANT_ID}
67             or will exit if not set.
68              
69             =head2 user_agent
70              
71             The user agent string used in requests to the gocardless API, defaults to
72             business-gocardless/perl/v . $version_of_this_library . - . $api_version
73              
74             =cut
75              
76             has api_version => (
77             is => 'ro',
78             required => 0,
79             lazy => 1,
80             default => sub { $ENV{GOCARDLESS_API_VERSION} // 1 },
81             );
82              
83             has token => (
84             is => 'ro',
85             required => 1,
86             );
87              
88             has base_url => (
89             is => 'ro',
90             required => 0,
91             default => sub {
92             my ( $self ) = @_;
93              
94             if ( my $url = $ENV{GOCARDLESS_URL} ) {
95             return $url;
96             } else {
97             return $self->api_version == 1
98             ? 'https://gocardless.com'
99             : 'https://api.gocardless.com';
100             }
101             },
102             );
103              
104             has api_path => (
105             is => 'ro',
106             required => 0,
107             lazy => 1,
108             default => sub {
109             my ( $self ) = @_;
110              
111             if ( $self->api_version == 1 ) {
112             return "/api/v" . $self->api_version;
113             } else {
114             return '';
115             }
116             },
117             );
118              
119             has app_id => (
120             is => 'ro',
121             required => 0,
122             lazy => 1,
123             default => sub {
124             return undef if shift->api_version > 1;
125             $ENV{'GOCARDLESS_APP_ID'}
126             or confess( "Missing required argument: app_id" );
127             }
128             );
129              
130             has app_secret => (
131             is => 'ro',
132             required => 0,
133             lazy => 1,
134             default => sub {
135             return undef if shift->api_version > 1;
136             $ENV{'GOCARDLESS_APP_SECRET'}
137             or confess( "Missing required argument: app_secret" );
138             }
139             );
140              
141             has webhook_secret => (
142             is => 'ro',
143             required => 0,
144             lazy => 1,
145             default => sub {
146             $ENV{'GOCARDLESS_WEBHOOK_SECRET'}
147             or confess( "Missing required argument: webhook_secret" );
148             }
149             );
150              
151             has merchant_id => (
152             is => 'ro',
153             required => 0,
154             lazy => 1,
155             default => sub {
156             my ( $self ) = @_;
157             return undef if $self->api_version > 1;
158             $ENV{'GOCARDLESS_MERCHANT_ID'}
159             or confess( "Missing required argument: merchant_id" );
160             }
161             );
162              
163             has user_agent => (
164             is => 'ro',
165             default => sub {
166             my ( $self ) = @_;
167             # maybe want more info in here, version of perl, platform, and such
168             require Business::GoCardless;
169             return "business-gocardless/perl/v"
170             . $Business::GoCardless::VERSION
171             . "-" . $self->api_version
172             ;
173             }
174             );
175              
176             # making these methods "private" to prevent confusion with the
177             # public methods of the same name in Business::GoCardless
178              
179             sub _new_bill_url {
180 1     1   10 my ( $self,$params ) = @_;
181 1         4 return $self->_new_limit_url( 'bill',$params );
182             }
183              
184             sub _new_pre_authorization_url {
185 1     1   10 my ( $self,$params ) = @_;
186 1         5 return $self->_new_limit_url( 'pre_authorization',$params );
187             }
188              
189             sub _new_subscription_url {
190 1     1   11 my ( $self,$params ) = @_;
191 1         4 return $self->_new_limit_url( 'subscription',$params );
192             }
193              
194             sub _new_limit_url {
195 3     3   9 my ( $self,$type,$limit_params ) = @_;
196              
197 3         45 $limit_params->{merchant_id} = $self->merchant_id;
198              
199             my $params = {
200             nonce => $self->generate_nonce,
201             timestamp => strftime( "%Y-%m-%dT%H:%M:%SZ",gmtime ),
202             client_id => $self->app_id,
203 3         26 ( map { ( $limit_params->{$_}
204 12 100       71 ? ( $_ => delete( $limit_params->{$_} ) ) : ()
205             ) } qw/ redirect_uri cancel_uri cancel_uri state / ),
206             $type => $limit_params,
207             };
208              
209 3         53 $params->{signature} = $self->sign_params( $params,$self->app_secret );
210              
211 3         18 return sprintf(
212             "%s/connect/%ss/new?%s",
213             $self->base_url,
214             $type,
215             $self->normalize_params( $params )
216             );
217             }
218              
219             sub _new_redirect_flow_url {
220 3     3   11 my ( $self,$params ) = @_;
221              
222             my $data = $self->api_post(
223             '/redirect_flows',
224 3         10 { redirect_flows => { %{ $params } } },
  3         24  
225             );
226              
227             my $RedirectFlow = Business::GoCardless::RedirectFlow->new(
228             client => $self,
229 3         13 %{ $data->{redirect_flows} }
  3         75  
230             );
231              
232 3         114 return $RedirectFlow->redirect_url;
233             }
234              
235             sub _confirm_redirect_flow {
236 3     3   35 my ( $self,$redirect_flow_id ) = @_;
237              
238             # first find the original session token
239 3         53 my $RedirectFlow = Business::GoCardless::RedirectFlow->new(
240             client => $self,
241             id => $redirect_flow_id,
242             );
243              
244 3         47 $RedirectFlow->find_with_client( 'redirect_flows' );
245              
246             # now confirm the redirect flow
247 3         24 my $data = $self->api_post(
248             "/redirect_flows/$redirect_flow_id/actions/complete",
249             { data => { session_token => $RedirectFlow->session_token } },
250             );
251              
252             $RedirectFlow = Business::GoCardless::RedirectFlow->new(
253             client => $self,
254 3         11 %{ $data->{redirect_flows} }
  3         79  
255             );
256              
257 3         68 return $RedirectFlow;
258             }
259              
260             sub _confirm_resource {
261 4     4   37 my ( $self,$params ) = @_;
262              
263 4 100       57 if ( ! $self->signature_valid( $params,$self->app_secret ) ) {
264 1         11 Business::GoCardless::Exception->throw({
265             message => "Invalid signature for confirm_resource"
266             });
267             }
268              
269             my $data = {
270             resource_id => $params->{resource_id},
271             resource_type => $params->{resource_type},
272 3         12 };
273              
274 3         43 my $credentials = encode_base64( $self->app_id . ':' . $self->app_secret );
275 3         91 $credentials =~ s/\s//g;
276              
277 3         42 my $ua = LWP::UserAgent->new;
278 3         2781 $ua->agent( $self->user_agent );
279              
280 3         210 my $req = HTTP::Request->new(
281             POST => join( '/',$self->base_url . $self->api_path,'confirm' )
282             );
283              
284 3         6700 $req->header( 'Authorization' => "Basic $credentials" );
285 3         204 $req->header( 'Accept' => 'application/json' );
286              
287 3         123 $req->content_type( 'application/x-www-form-urlencoded' );
288 3         82 $req->content( $self->normalize_params( $data ) );
289              
290 3         71 my $res = $ua->request( $req );
291              
292 3 50       33 if ( $res->is_success ) {
293            
294 3         202 my $class_suffix = ucfirst( $params->{resource_type} );
295 3         8 $class_suffix =~ s/_([A-z])/uc($1)/ge;
  1         5  
296 3         7 my $class = "Business::GoCardless::$class_suffix";
297             my $obj = $class->new(
298             client => $self,
299             id => $params->{resource_id}
300 3         34 );
301 3         89 return $obj->find_with_client;
302             }
303             else {
304 0         0 Business::GoCardless::Exception->throw({
305             message => $res->content,
306             code => $res->code,
307             response => $res->status_line,
308             });
309             }
310             }
311              
312             =head1 METHODS
313              
314             api_get
315             api_post
316             api_put
317              
318             Make a request to the gocardless API:
319              
320             my $data = $Client->api_get( '/merchants/123ABCD/bills',\%params );
321              
322             In list context returns the links and pagination headers:
323              
324             my ( $data,$links,$info ) = $Client->api_get( ... );
325              
326             =cut
327              
328             sub api_get {
329 41     41 0 157 my ( $self,$path,$params ) = @_;
330 41         163 return $self->_api_request( 'GET',$path,$params );
331             }
332              
333             sub api_post {
334 16     16 0 74 my ( $self,$path,$params ) = @_;
335 16         51 return $self->_api_request( 'POST',$path,$params );
336             }
337              
338             sub api_put {
339 3     3 0 9 my ( $self,$path,$params ) = @_;
340 3         9 return $self->_api_request( 'PUT',$path,$params );
341             }
342              
343             sub _api_request {
344 60     60   142 my ( $self,$method,$path,$params ) = @_;
345              
346 60         302 my $ua = LWP::UserAgent->new;
347 60         23042 $ua->agent( $self->user_agent );
348              
349 60 100       4606 my $req = HTTP::Request->new(
350             # passing through the absolute URL means we don't build it
351             $method => my $uri = $path =~ /^http/
352             ? $path : join( '/',$self->base_url . $self->api_path . $path ),
353             );
354              
355 60 50       29354 say STDERR "GOCARDLESS -> $uri" if $ENV{GOCARDLESS_DEBUG};
356              
357 60         376 $req->header( 'Authorization' => "Bearer " . $self->token );
358 60         3345 $req->header( 'Accept' => 'application/json' );
359              
360 60 100       3687 if ( $self->api_version > 1 ) {
361             # pegged to a specific version for this library and not user controlled
362             # https://developer.gocardless.com/api-reference/#making-requests-versions
363 27         284 $req->header( 'GoCardless-Version' => '2015-07-06' );
364             }
365              
366 60 100       2121 if ( $method =~ /POST|PUT/ ) {
367 19 100       333 if ( $self->api_version > 1 ) {
368 13         138 $req->content_type( 'application/json' );
369 13         282 my $json;
370 13 100       193 $json = JSON->new->utf8->canonical->encode( $params ) if $params;
371 13 100       93 $req->content( $json ) if $json;
372 13 100       256 $req->header( 'Content-Length' => 0 ) if ! $json; # always have a content length
373 13 50 33     186 say STDERR "GOCARDLESS -> $json" if $ENV{GOCARDLESS_DEBUG} && $json;
374             } else {
375 6         52 $req->content_type( 'application/x-www-form-urlencoded' );
376 6         139 $req->content( my $normalize_params = $self->normalize_params( $params ) );
377 6 50 33     125 say STDERR "GOCARDLESS -> $normalize_params" if $ENV{GOCARDLESS_DEBUG} && $normalize_params;
378             }
379             }
380              
381 60         238 my $res = $ua->request( $req );
382              
383 60 50       452 if ( $res->is_success ) {
384 60 50       4161 say STDERR "GOCARDLESS <- " . $res->content if $ENV{GOCARDLESS_DEBUG};
385 60         606 my $data = JSON->new->canonical->decode( $res->content );
386 60         5059 my $links = $res->header( 'link' );
387 60         3581 my $info = $res->header( 'x-pagination' );
388 60 100       3967 return wantarray ? ( $data,$links,$info ) : $data;
389             }
390             else {
391 0           Business::GoCardless::Exception->throw({
392             message => $res->content,
393             code => $res->code,
394             response => $res->status_line,
395             });
396             }
397             }
398              
399             =head1 AUTHOR
400              
401             Lee Johnson - C
402              
403             This library is free software; you can redistribute it and/or modify it under
404             the same terms as Perl itself. If you would like to contribute documentation,
405             features, bug fixes, or anything else then please raise an issue / pull request:
406              
407             https://github.com/Humanstate/business-gocardless
408              
409             =cut
410              
411             1;
412              
413             # vim: ts=4:sw=4:et