File Coverage

blib/lib/Interchange6/Schema/ResultSet/Product.pm
Criterion Covered Total %
statement 44 47 93.6
branch 9 12 75.0
condition n/a
subroutine 13 14 92.8
pod 9 9 100.0
total 75 82 91.4


line stmt bran cond sub pod time code
1 2     2   2356 use utf8;
  2         6  
  2         26  
2              
3             package Interchange6::Schema::ResultSet::Product;
4              
5             =head1 NAME
6              
7             Interchange6::Schema::ResultSet::Product
8              
9             =cut
10              
11             =head1 SYNOPSIS
12              
13             Provides extra accessor methods for L<Interchange6::Schema::Result::Product>
14              
15             =cut
16              
17 2     2   87 use strict;
  2         5  
  2         46  
18 2     2   12 use warnings;
  2         8  
  2         59  
19 2     2   10 use mro 'c3';
  2         5  
  2         11  
20              
21 2     2   49 use parent 'Interchange6::Schema::ResultSet';
  2         7  
  2         37  
22              
23             =head1 METHODS
24              
25             See also L<DBIx::Class::Helper::ResultSet::Shortcut> which is loaded by this
26             result set.
27              
28             =head2 active
29              
30             Returns all rows where L<Interchange6::Schema::Result::Product/active> is true.
31              
32             =cut
33              
34             sub active {
35 1     1 1 5636 return $_[0]->search({ $_[0]->me('active') => 1 });
36             }
37              
38             =head2 canonical_only
39              
40             Returns all rows where L<Interchange6::Schema::Result::Product/canonical_sku>
41             is null, i.e. only canonical products.
42              
43             =cut
44              
45             sub canonical_only {
46 1     1 1 9030 return $_[0]->search({ $_[0]->me('canonical_sku') => undef });
47             }
48              
49             =head2 listing
50              
51             This is just a shortcut for:
52              
53             $self->columns( [ 'sku', 'name', 'uri', 'price', 'short_description' ] )
54             ->with_average_rating
55             ->with_lowest_selling_price
56             ->with_highest_price
57             ->with_quantity_in_stock
58             ->with_variant_count
59              
60             Though in addition if you pass in arguments these are passed through to the
61             appropriate with_* method so you can do:
62              
63             $self->listing({ quantity => 10 })
64              
65             And the result will be:
66              
67             $self->columns( [ 'sku', 'name', 'uri', 'price', 'short_description' ] )
68             ->with_average_rating
69             ->with_lowest_selling_price({ quantity => 10 })
70             ->with_highest_price
71             ->with_quantity_in_stock
72             ->with_variant_count
73              
74             =cut
75              
76             sub listing {
77 5     5 1 89104 my ( $self, $args ) = @_;
78             return $self->columns(
79             [ 'sku', 'name', 'uri', 'price', 'short_description' ] )
80             ->with_average_rating->with_lowest_selling_price(
81             {
82             quantity => $args->{quantity},
83             }
84 5         72 )->with_highest_price->with_quantity_in_stock->with_variant_count;
85             }
86              
87             =head2 with_average_rating
88              
89             Adds C<average_rating> column which is available to order_by clauses and
90             whose value can be retrieved via
91             L<Interchange6::Schema::Result::Product/average_rating>.
92              
93             This is the average rating across all public and approved product reviews or
94             undef if there are no reviews. Product reviews are only related to canonical
95             products so for variants the value returned is that of the canonical product.
96              
97             =cut
98              
99             sub with_average_rating {
100 10     10 1 11816 my $self = shift;
101              
102 10         86 return $self->search(
103             undef,
104             {
105             '+select' => [
106             {
107             coalesce => [
108              
109             $self->correlate('canonical')
110             ->related_resultset('product_messages')
111             ->search_related(
112             'message',
113             { 'message.approved' => 1, 'message.public' => 1,
114             'message_type.name' => 'product_review' },
115             { join => 'message_type' },
116             )->get_column('rating')->func_rs('avg')->as_query,
117              
118             $self->correlate('product_messages')
119             ->search_related(
120             'message',
121             { 'message.approved' => 1, 'message.public' => 1,
122             'message_type.name' => 'product_review' },
123             { join => 'message_type' },
124             )->get_column('rating')->func_rs('avg')->as_query,
125              
126             ],
127             -as => 'average_rating'
128             }
129             ],
130             '+as' => ['average_rating'],
131             }
132             );
133             }
134              
135             =head2 with_media $type?
136              
137             Prefetch related active L<Interchange6::Schema::Result::Media> where
138             L<Interchange6::Schema::Result::MediaType/type> is C<$type>.
139              
140             C<$type> defaults to C<image> if not provided.
141              
142             =cut
143              
144             sub with_media {
145 0     0 1 0 my $self = shift;
146 0 0       0 my $type = defined $_[0] ? $_[0] : 'image';
147              
148 0         0 return $self->search(
149             {
150             'media.active' => 1,
151             'media_type.type' => $type,
152             },
153             {
154             prefetch => { media_products => 'media' },
155             join => { media_products => { media => 'media_type' } },
156             }
157             );
158             }
159              
160             =head2 with_quantity_in_stock
161              
162             Adds C<quantity_in_stock> column which is available to order_by clauses and
163             whose value can be retrieved via
164             L<Interchange6::Schema::Result::Product/quantity_in_stock>.
165              
166             The value is retrieved is L<Interchange6::Schema::Result::Inventory/quantity>.
167              
168             For a product variant and for a canonical product with no variants the
169             quantity returned is for the product itself.
170              
171             For a canonical (parent) product the quantity returned is the total for all its
172             variants.
173              
174             =cut
175              
176             sub with_quantity_in_stock {
177 6     6 1 24160 my $self = shift;
178              
179 6         47 return $self->search(
180             undef,
181             {
182             '+select' => [
183             {
184             coalesce => [
185              
186             $self->correlate('variants')
187             ->related_resultset('inventory')
188             ->get_column('quantity')->sum_rs->as_query,
189              
190             $self->correlate('inventory')->get_column('quantity')
191             ->as_query,
192              
193             ],
194             -as => 'quantity_in_stock',
195             }
196             ],
197             '+as' => ['quantity_in_stock'],
198             }
199             );
200             }
201              
202             =head2 with_lowest_selling_price
203              
204             Arguments should be given as a hash reference with the following keys/values:
205              
206             =over 4
207              
208             =item * quantity => $quantity
209              
210             C<quantity> defaults to 1 if not supplied.
211              
212             =back
213              
214             The lowest of L<Interchange6::Schema::Result::PriceModifier/price> and
215             L<Interchange6::Schema::Result::Product/price>.
216              
217             For products with variants this is the lowest variant selling_price.
218              
219             Value is placed in the column C<selling_price>.
220              
221             If L<Schema/current_user> is defined then any roles assigned to that
222             user will be included in the search of
223             L<Interchange6::Schema::Result::PriceModifier>.
224              
225             =cut
226              
227             sub with_lowest_selling_price {
228 35     35 1 602076 my ( $self, $args ) = @_;
229              
230 35 100       185 if ( defined($args) ) {
231 13 100       144 $self->throw_exception(
232             "argument to with_lowest_selling_price must be a hash reference")
233             unless ref($args) eq "HASH";
234             }
235              
236 33 100       208 $args->{quantity} = 1 unless defined $args->{quantity};
237              
238 33         197 my $schema = $self->result_source->schema;
239              
240 33         815 my $today = $schema->format_datetime(DateTime->today);
241              
242             # start building the search condition
243              
244             my $search_cond = {
245             'start_date' => [ undef, { '<=', $today } ],
246             'end_date' => [ undef, { '>=', $today } ],
247             'quantity' => { '<=' => $args->{quantity} },
248 33         30111 'roles_id' => undef,
249             };
250              
251 33 100       945 if ( my $user = $schema->current_user ) {
252              
253             # add roles_id condition
254              
255             $search_cond->{roles_id} = [
256             undef,
257             {
258 28         808 -in => $schema->resultset('UserRole')
259             ->search( { users_id => $user->id } )
260             ->get_column('roles_id')->as_query
261             }
262             ];
263             }
264              
265             # most db engines have 'least' but SQLite has 'min'
266              
267 33         80345 my $least = 'least';
268 33 50       2932 $least = 'min' if $schema->storage->sqlt_type eq 'SQLite';
269              
270             # much hoop jumping required to make sure we don't trip over nulls
271             #
272             # see:
273             # https://dev.mysql.com/doc/refman/5.0/en/comparison-operators.html#function_least
274             #
275             # which states:
276             # Before MySQL 5.0.13, LEAST() returns NULL only if all arguments are NULL.
277             # As of 5.0.13, it returns NULL if any argument is NULL.
278             #
279             # Complete madness!
280             #
281             # Compare to the sanity of PostgreSQL:
282             # NULL values in the list are ignored. The result will be NULL only if all
283             # the expressions evaluate to NULL.
284              
285 33         2907 my $variant_price_modifiers =
286             $self->correlate('variants')
287             ->search_related( 'price_modifiers', $search_cond )->get_column('price')
288             ->min_rs->as_query;
289              
290 33         590223 my $variant_prices =
291             $self->correlate('variants')->get_column('price')->min_rs->as_query;
292              
293 33         140227 my $self_price_modifiers =
294             $self->correlate('price_modifiers')->search( $search_cond )
295             ->get_column('price')->min_rs->as_query;
296              
297 33         248358 my $return = $self->search(
298             undef,
299             {
300             '+select' => [
301             {
302             coalesce => [
303             {
304             $least => [
305             {
306             coalesce => [
307             $variant_price_modifiers,
308             $variant_prices
309             ]
310             },
311             $variant_prices
312             ]
313             },
314             {
315             coalesce =>
316             [ $self_price_modifiers, $self->me('price') ],
317              
318             },
319             ],
320             -as => 'selling_price'
321             }
322             ],
323             '+as' => ['selling_price'],
324             }
325             );
326 33         32599 return $return;
327             }
328              
329             =head2 with_highest_price
330              
331             For canonical products with no variants and for variant products
332             C<highest_price> is always undef. For canonical products that have variants
333             this is the highest of L<Interchange6::Schema::Result::Product/price> of
334             the variants.
335              
336             =cut
337              
338             sub with_highest_price {
339 5     5 1 30 my $self = shift;
340              
341 5         27 my $schema = $self->result_source->schema;
342              
343 5         75 return $self->search(
344             undef,
345             {
346             '+columns' => {
347             highest_price => {
348             coalesce => [
349             $self->correlate('variants')->get_column('price')
350             ->max_rs->as_query,
351             $self->me('price')
352             ]
353             }
354             }
355             }
356             );
357             }
358              
359             =head2 with_variant_count
360              
361             Adds column C<variant_count> which is a count of variants of each product.
362              
363             =cut
364              
365             sub with_variant_count {
366 80     80 1 172291 my $self = shift;
367 80         648 return $self->search(
368             undef,
369             {
370             '+columns' => {
371             variant_count =>
372             $self->correlate('variants')->count_rs->as_query
373             }
374             }
375             );
376             }
377              
378             1;