File Coverage

blib/lib/Stancer/Core/Request.pm
Criterion Covered Total %
statement 113 113 100.0
branch 27 28 96.4
condition 9 12 75.0
subroutine 18 18 100.0
pod 4 4 100.0
total 171 175 97.7


line stmt bran cond sub pod time code
1             package Stancer::Core::Request;
2              
3 39     39   207588 use 5.020;
  39         163  
4 39     39   237 use strict;
  39         105  
  39         1168  
5 39     39   240 use warnings;
  39         106  
  39         3133  
6              
7             # ABSTRACT: Handle API request
8             our $VERSION = '1.0.3'; # VERSION
9              
10 39     39   7176 use Stancer::Config;
  39         112  
  39         1490  
11 39     39   22110 use Stancer::Core::Request::Call;
  39         164  
  39         1861  
12 39     39   22159 use Stancer::Exceptions::Http;
  39         192  
  39         2099  
13 39     39   26536 use JSON qw(decode_json);
  39         397221  
  39         344  
14 39     39   23736 use Log::Any qw($log);
  39         324048  
  39         325  
15 39     39   86034 use Try::Tiny;
  39         107  
  39         3030  
16              
17 39     39   318 use Moo;
  39         97  
  39         409  
18 39     39   20995 use namespace::clean;
  39         120  
  39         399  
19              
20              
21             sub del {
22 3     3 1 1109 my $this = shift;
23 3         9 my $object = shift;
24              
25 3         20 my $request = HTTP::Request->new(DELETE => $object->uri);
26              
27 3         39 return $this->_request($request);
28             }
29              
30              
31             sub get {
32 86     86 1 45998 my ($this, $object, @args) = @_;
33 86         238 my $query;
34              
35 86 100       570 if (scalar @args == 1) {
36 22         62 $query = $args[0];
37             } else {
38 64         184 $query = {@args};
39             }
40              
41 86         629 my $uri = $object->uri;
42 86         216 my @params;
43              
44 86         199 for my $key (keys %{$query}) {
  86         490  
45 50         229 push @params, $key . q(=) . $query->{$key};
46             }
47              
48 86 100       356 if (scalar @params) {
49 23         132 $uri .= q(?) . join q(&), @params;
50             }
51              
52 86         824 my $request = HTTP::Request->new(GET => $uri);
53              
54 86         1519 return $this->_request($request);
55             }
56              
57              
58             sub patch {
59 6     6 1 238 my $this = shift;
60 6         19 my $object = shift;
61              
62 6         38 my $request = HTTP::Request->new(PATCH => $object->uri);
63              
64 6         84 $request->content($object->toJSON());
65              
66 6         701 return $this->_request($request, $object);
67             }
68              
69              
70             sub post {
71 21     21 1 392 my $this = shift;
72 21         54 my $object = shift;
73              
74 21         165 my $request = HTTP::Request->new(POST => $object->uri);
75              
76 21         555 $request->content($object->toJSON());
77              
78 21         3360 return $this->_request($request, $object);
79             }
80              
81             sub _clean_request {
82 114     114   306 my $this = shift;
83 114         247 my $request = shift;
84 114         226 my $object = shift;
85              
86 114 100 100     837 if ($object && $object->isa('Stancer::Payment')) {
87 9 100 66     242 if ($object->card && $object->card->number) {
88 4         294 my $content = $request->content;
89 4         469 my $number = $object->card->number;
90 4         176 my $last4 = ('x' x (length($number) - 4)) . $object->card->last4;
91              
92 4         290 $content =~ s/$number/$last4/sm;
93              
94 4         20 $request->content($content);
95             }
96              
97 9 100 66     755 if ($object->sepa && $object->sepa->iban) {
98 2         191 my $content = $request->content;
99 2         228 my $number = $object->sepa->iban;
100 2         118 my $last4 = ('x' x (length($number) - 4)) . $object->sepa->last4;
101              
102 2         119 $content =~ s/$number/$last4/sm;
103              
104 2         46 $request->content($content);
105             }
106             }
107              
108 114         4726 return $request;
109             }
110              
111             sub _request {
112 116     116   336 my $this = shift;
113 116         248 my $request = shift;
114 116         244 my $object = shift;
115              
116 116         694 my $config = Stancer::Config->init();
117 116         3855 my $ua = $config->lwp;
118              
119 116 50       5598 $ua->timeout($config->timeout) if defined $config->timeout;
120 116         1500 $ua->agent($config->user_agent);
121              
122 116         16663 $request->header('Content-Type' => 'application/json');
123 116         15857 $request->authorization_basic($config->secret_key, q//);
124              
125 116         12178 $log->debug(sprintf 'API call: %s %s', $request->method, $request->url);
126              
127 116         29504 my $response = $ua->request($request);
128              
129 116 100       10949 if ($response->is_error) {
130 19         2700 $this->_clean_request($request, $object);
131              
132 19         81 my %params = (
133             request => $request,
134             response => $response,
135             );
136              
137             try {
138 19     19   701 my $content = decode_json $response->decoded_content;
139              
140 5 100 66     405 if (ref $content eq 'HASH' && exists $content->{error}) {
141 4         11 $params{message} = $content->{error}->{message};
142              
143 4 100       19 if (ref $content->{error}->{message} eq 'HASH') {
144 3 100       9 if (exists $content->{error}->{message}->{id}) {
145 2         7 $params{message} = $content->{error}->{message}->{id};
146             }
147              
148 3 100       8 if (exists $content->{error}->{message}->{error}) {
149 2         6 $params{message} = $content->{error}->{message}->{error};
150              
151 2 100       6 if (exists $content->{error}->{message}->{id}) {
152 1         9 $params{message} .= q/ (/ . $content->{error}->{message}->{id} . q/)/;
153             }
154             }
155             }
156             }
157 19         262 };
158              
159 19         1709 my $error = Stancer::Exceptions::Http->factory($response->code, %params);
160 19         128 my $level = $error->log_level;
161              
162 19         112 $log->$level(sprintf 'HTTP %d - %s', $response->code, $error->message);
163              
164 19 100       2377 if ($config->debug) {
165 5         59 push @{ $config->calls }, Stancer::Core::Request::Call->new(
  5         163  
166             exception => $error,
167             request => $request,
168             response => $response,
169             );
170             }
171              
172 19         410 $error->throw();
173             }
174              
175 97 100       21603 if ($config->debug) {
176 95         1375 push @{ $config->calls }, Stancer::Core::Request::Call->new(
  95         897  
177             request => $this->_clean_request($request, $object),
178             response => $response,
179             );
180             }
181              
182 97         4248 return $response->decoded_content;
183             }
184              
185             1;
186              
187             __END__
188              
189             =pod
190              
191             =encoding UTF-8
192              
193             =head1 NAME
194              
195             Stancer::Core::Request - Handle API request
196              
197             =head1 VERSION
198              
199             version 1.0.3
200              
201             =head1 SYNOPSIS
202              
203             Handle request to the API.
204              
205             It uses L<LWP::UserAgent module|LWP::UserAgent> as API consumer.
206              
207             You should not have to use this class directly, everything is done internally.
208              
209             =head1 METHODS
210              
211             =head2 C<< $request->del($object) : I<undef> >>
212              
213             Delete an object on the API.
214              
215             =head2 C<< $request->get($object) : I<string> >>
216              
217             Get data available on the API.
218              
219             =head2 C<< $request->patch($object) : I<string> >>
220              
221             Update data on the API.
222              
223             =head2 C<< $request->post($object) : I<string> >>
224              
225             Send data to the API.
226              
227             =head1 USAGE
228              
229             =head2 Logging
230              
231              
232              
233             We use the L<Log::Any> framework for logging events.
234             You may tell where it should log using any available L<Log::Any::Adapter> module.
235              
236             For example, to log everything to a file you just have to add a line to your script, like this:
237             #! /usr/bin/env perl
238             use Log::Any::Adapter (File => '/var/log/payment.log');
239             use Stancer::Core::Request;
240              
241             You must import C<Log::Any::Adapter> before our libraries, to initialize the logger instance before use.
242              
243             You can choose your log level on import directly:
244             use Log::Any::Adapter (File => '/var/log/payment.log', log_level => 'info');
245              
246             Read the L<Log::Any> documentation to know what other options you have.
247              
248             =cut
249              
250             =head1 SECURITY
251              
252             =over
253              
254             =item *
255              
256             Never, never, NEVER register a card or a bank account number in your database.
257              
258             =item *
259              
260             Always uses HTTPS in card/SEPA in communication.
261              
262             =item *
263              
264             Our API will never give you a complete card/SEPA number, only the last four digits.
265             If you need to keep track, use these last four digit.
266              
267             =back
268              
269             =cut
270              
271             =head1 BUGS
272              
273             Please report any bugs or feature requests on the bugtracker website
274             L<https://gitlab.com/wearestancer/library/lib-perl/-/issues> or by email to
275             L<bug-stancer@rt.cpan.org|mailto:bug-stancer@rt.cpan.org>.
276              
277             When submitting a bug or request, please include a test-file or a
278             patch to an existing test-file that illustrates the bug or desired
279             feature.
280              
281             =head1 AUTHOR
282              
283             Joel Da Silva <jdasilva@cpan.org>
284              
285             =head1 COPYRIGHT AND LICENSE
286              
287             This software is Copyright (c) 2018-2024 by Stancer / Iliad78.
288              
289             This is free software, licensed under:
290              
291             The Artistic License 2.0 (GPL Compatible)
292              
293             =cut