File Coverage

blib/lib/WWW/Picnic.pm
Criterion Covered Total %
statement 92 134 68.6
branch 9 28 32.1
condition 5 17 29.4
subroutine 24 29 82.7
pod 16 17 94.1
total 146 225 64.8


line stmt bran cond sub pod time code
1             package WWW::Picnic;
2             our $VERSION = '0.100';
3             our $AUTHORITY = 'cpan:GETTY';
4             # ABSTRACT: Library to access Picnic Supermarket API
5              
6 2     2   448778 use Moo;
  2         17706  
  2         10  
7              
8 2     2   3265 use Carp qw( croak );
  2         4  
  2         104  
9 2     2   944 use JSON::MaybeXS;
  2         22087  
  2         142  
10 2     2   2764 use HTTP::Request;
  2         44181  
  2         79  
11 2     2   1643 use LWP::UserAgent;
  2         67962  
  2         121  
12 2     2   17 use Digest::MD5 qw( md5_hex );
  2         3  
  2         137  
13              
14 2     2   1453 use WWW::Picnic::Result::Login;
  2         9  
  2         77  
15 2     2   1155 use WWW::Picnic::Result::User;
  2         7  
  2         81  
16 2     2   1079 use WWW::Picnic::Result::Cart;
  2         6  
  2         71  
17 2     2   1142 use WWW::Picnic::Result::DeliverySlots;
  2         8  
  2         70  
18 2     2   1030 use WWW::Picnic::Result::Search;
  2         7  
  2         76  
19 2     2   1046 use WWW::Picnic::Result::Article;
  2         12  
  2         5102  
20              
21              
22             has user => (
23             is => 'ro',
24             required => 1,
25             );
26              
27              
28             has pass => (
29             is => 'ro',
30             required => 1,
31             );
32              
33              
34             has client_id => (
35             is => 'ro',
36             default => sub { 30100 },
37             );
38              
39              
40             has api_version => (
41             isa => sub { $_[0] >= 15 },
42             is => 'ro',
43             default => sub { 15 },
44             );
45              
46              
47             has country => (
48             is => 'ro',
49             default => sub { 'de' },
50             );
51              
52              
53             sub api_endpoint {
54 11     11 0 7204 my ( $self ) = @_;
55 11         225 return sprintf('https://storefront-prod.%s.picnicinternational.com/api/%s', $self->country, "".$self->api_version."");
56             }
57              
58             has http_agent => (
59             is => 'ro',
60             lazy => 1,
61             default => sub {
62             my $self = shift;
63             my $ua = LWP::UserAgent->new;
64             $ua->agent($self->http_agent_name);
65             return $ua;
66             },
67             );
68              
69              
70             has http_agent_name => (
71             is => 'ro',
72             lazy => 1,
73             default => sub { 'okhttp/3.12.2' },
74             );
75              
76              
77             has picnic_agent => (
78             is => 'ro',
79             lazy => 1,
80             default => sub { '30100;1.15.232-15154' },
81             );
82              
83              
84             has picnic_did => (
85             is => 'ro',
86             lazy => 1,
87             default => sub {
88             # Generate a random device ID (16 hex chars)
89             my @chars = ('0'..'9', 'A'..'F');
90             return join '', map { $chars[rand @chars] } 1..16;
91             },
92             );
93              
94              
95             has json => (
96             is => 'ro',
97             lazy => 1,
98             default => sub { return JSON::MaybeXS->new->utf8 },
99             );
100              
101             has _auth_cache => (
102             is => 'ro',
103             default => sub {{}},
104             );
105              
106             sub login {
107 1     1 1 5691 my ( $self ) = @_;
108 1         7 my $url = URI->new(join('/',$self->api_endpoint,'user','login'));
109 1         14665 my $request = HTTP::Request->new( POST => $url );
110 1         291 $request->header('Accept' => 'application/json');
111 1         170 $request->header('Content-Type' => 'application/json; charset=UTF-8');
112 1         134 $request->content($self->json->encode({
113             key => $self->user,
114             secret => md5_hex($self->pass),
115             client_id => $self->client_id,
116             }));
117 1         157 my $response = $self->http_agent->request($request);
118 1 50       519 if ($response->is_success) {
119 1         22 my $auth = $response->header('X-Picnic-Auth');
120 1         112 my $data = $self->json->decode($response->content);
121 1         57 $data->{auth_key} = $auth;
122 1 50 33     15 if ($auth && $data->{user_id} && !$data->{second_factor_authentication_required}) {
      33        
123 1         7 $self->_auth_cache->{auth} = $auth;
124 1         6 $self->_auth_cache->{time} = time;
125 1         5 $self->_auth_cache->{user_id} = $data->{user_id};
126             }
127 1         15 return WWW::Picnic::Result::Login->new($data);
128             } else {
129 0         0 croak __PACKAGE__.": login failed! ".$response->status_line;
130             }
131             }
132              
133              
134             sub generate_2fa_code {
135 0     0 1 0 my ( $self, $channel ) = @_;
136 0   0     0 $channel //= 'SMS';
137 0         0 my $url = URI->new(join('/',$self->api_endpoint,'user','2fa','generate'));
138 0         0 my $request = HTTP::Request->new( POST => $url );
139 0         0 $request->header('Accept' => 'application/json');
140 0         0 $request->header('Content-Type' => 'application/json; charset=UTF-8');
141 0         0 $request->content($self->json->encode({ channel => $channel }));
142 0         0 my $response = $self->http_agent->request($request);
143 0 0       0 unless ($response->is_success) {
144 0         0 croak __PACKAGE__.": 2FA code generation failed! ".$response->status_line;
145             }
146 0         0 return 1;
147             }
148              
149              
150             sub verify_2fa_code {
151 0     0 1 0 my ( $self, $code ) = @_;
152 0 0       0 croak __PACKAGE__.": 2FA code required" unless defined $code;
153 0         0 my $url = URI->new(join('/',$self->api_endpoint,'user','2fa','verify'));
154 0         0 my $request = HTTP::Request->new( POST => $url );
155 0         0 $request->header('Accept' => 'application/json');
156 0         0 $request->header('Content-Type' => 'application/json; charset=UTF-8');
157 0         0 $request->content($self->json->encode({ otp => $code }));
158 0         0 my $response = $self->http_agent->request($request);
159 0 0       0 if ($response->is_success) {
160 0         0 my $auth = $response->header('X-Picnic-Auth');
161 0 0       0 croak __PACKAGE__.": 2FA verify success, but no auth token!" unless $auth;
162 0         0 my $data = $self->json->decode($response->content);
163 0         0 $self->_auth_cache->{auth} = $auth;
164 0         0 $self->_auth_cache->{time} = time;
165 0 0       0 $self->_auth_cache->{user_id} = $data->{user_id} if $data->{user_id};
166 0         0 return 1;
167             } else {
168 0         0 croak __PACKAGE__.": 2FA verification failed! ".$response->status_line;
169             }
170             }
171              
172              
173             sub picnic_auth {
174 9     9 1 22 my ( $self ) = @_;
175 9 50       85 unless (defined $self->_auth_cache->{auth}) {
176 0         0 my $login = $self->login;
177 0 0       0 if ($login->requires_2fa) {
178 0         0 croak __PACKAGE__.": 2FA required! Call login() and handle 2FA flow manually.";
179             }
180 0 0       0 unless ($self->_auth_cache->{auth}) {
181 0         0 croak __PACKAGE__.": login failed to obtain auth token!";
182             }
183             }
184 9         50 return $self->_auth_cache->{auth};
185             }
186              
187              
188             sub request {
189 9     9 1 43 my ( $self, @original_args ) = @_;
190 9         36 my ( $method, $path, $data, %params ) = @original_args;
191 9 50 33     49 $data = [] if $method eq 'PUT' and !$data;
192 9         74 my $url = URI->new(join('/',$self->api_endpoint,$path));
193 9 100       1290 if (%params) {
194 1         19 $url->query_form(%params);
195             }
196 9         273 my $request = HTTP::Request->new( $method => $url );
197 9         909 $request->header('Accept' => 'application/json');
198 9         797 $request->header('X-Picnic-Auth' => $self->picnic_auth );
199 9         1124 $request->header('X-Picnic-Agent' => $self->picnic_agent );
200 9         1048 $request->header('X-Picnic-Did' => $self->picnic_did );
201 9 100       845 if (defined $data) {
202 2         10 $request->header('Content-Type' => 'application/json');
203 2         166 $request->content($self->json->encode($data));
204             }
205 9         411 my $response = $self->http_agent->request($request);
206 9 50       3363 unless ($response->is_success) {
207 0         0 croak __PACKAGE__.": request to ".$url->as_string." failed! ".$response->status_line;
208             }
209 9         422 return $self->json->decode($response->content);
210             }
211              
212              
213             sub get_user {
214 2     2 1 8959 my ( $self ) = @_;
215 2         7 return WWW::Picnic::Result::User->new( $self->request( GET => 'user' ) );
216             }
217              
218              
219             sub get_cart {
220 1     1 1 5518 my ( $self ) = @_;
221 1         6 return WWW::Picnic::Result::Cart->new( $self->request( GET => 'cart' ) );
222             }
223              
224              
225             sub clear_cart {
226 1     1 1 5769 my ( $self ) = @_;
227 1         8 return WWW::Picnic::Result::Cart->new( $self->request( POST => 'cart/clear' ) );
228             }
229              
230              
231             sub get_delivery_slots {
232 1     1 1 4240 my ( $self ) = @_;
233 1         5 return WWW::Picnic::Result::DeliverySlots->new( $self->request( GET => 'cart/delivery_slots' ) );
234             }
235              
236              
237             sub search {
238 1     1 1 6005 my ( $self, $term ) = @_;
239 1         11 return WWW::Picnic::Result::Search->new( $self->request( GET => 'pages/search-page-results', undef, search_term => $term ) );
240             }
241              
242              
243             sub get_article {
244 1     1 1 5850 my ( $self, $product_id ) = @_;
245 1         9 return WWW::Picnic::Result::Article->new( $self->request( GET => "articles/$product_id" ) );
246             }
247              
248              
249             sub add_to_cart {
250 1     1 1 6697 my ( $self, $product_id, $count ) = @_;
251 1   50     6 $count //= 1;
252 1         8 return WWW::Picnic::Result::Cart->new(
253             $self->request( POST => 'cart/add_product', { product_id => $product_id, count => $count } )
254             );
255             }
256              
257              
258             sub remove_from_cart {
259 1     1 1 4203 my ( $self, $product_id, $count ) = @_;
260 1   50     6 $count //= 1;
261 1         7 return WWW::Picnic::Result::Cart->new(
262             $self->request( POST => 'cart/remove_product', { product_id => $product_id, count => $count } )
263             );
264             }
265              
266              
267             sub set_delivery_slot {
268 0     0 1   my ( $self, $slot_id ) = @_;
269 0           return WWW::Picnic::Result::Cart->new(
270             $self->request( POST => 'cart/set_delivery_slot', { slot_id => $slot_id } )
271             );
272             }
273              
274              
275             sub get_categories {
276 0     0 1   my ( $self, $depth ) = @_;
277 0   0       $depth //= 0;
278 0           return $self->request( GET => 'my_store', undef, depth => $depth );
279             }
280              
281              
282             sub get_suggestions {
283 0     0 1   my ( $self, $term ) = @_;
284 0           return $self->request( GET => 'suggest', undef, search_term => $term );
285             }
286              
287              
288             1;
289              
290             __END__