1use utf8;
2
3package Interchange6::Schema::ResultSet::Product;
4
5=head1 NAME
6
7Interchange6::Schema::ResultSet::Product
8
9=cut
10
11=head1 SYNOPSIS
12
13Provides extra accessor methods for L<Interchange6::Schema::Result::Product>
14
15=cut
16
17use strict;
18use warnings;
19use mro 'c3';
20
21use parent 'Interchange6::Schema::ResultSet';
22
23=head1 METHODS
24
25See also L<DBIx::Class::Helper::ResultSet::Shortcut> which is loaded by this
26result set.
27
28=head2 active
29
30Returns all rows where L<Interchange6::Schema::Result::Product/active> is true.
31
32=cut
33
34sub active {
35    return $_[0]->search({ $_[0]->me('active') => 1 });
36}
37
38=head2 canonical_only
39
40Returns all rows where L<Interchange6::Schema::Result::Product/canonical_sku>
41is null, i.e. only canonical products.
42
43=cut
44
45sub canonical_only {
46    return $_[0]->search({ $_[0]->me('canonical_sku') => undef });
47}
48
49=head2 listing
50
51This 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
60Though in addition if you pass in arguments these are passed through to the
61appropriate with_* method so you can do:
62
63  $self->listing({ quantity => 10 })
64
65And 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
76sub listing {
77    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      )->with_highest_price->with_quantity_in_stock->with_variant_count;
85}
86
87=head2 with_average_rating
88
89Adds C<average_rating> column which is available to order_by clauses and
90whose value can be retrieved via
91L<Interchange6::Schema::Result::Product/average_rating>.
92
93This is the average rating across all public and approved product reviews or
94undef if there are no reviews. Product reviews are only related to canonical
95products so for variants the value returned is that of the canonical product.
96
97=cut
98
99sub with_average_rating {
100    my $self = shift;
101
102    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
137Prefetch related active L<Interchange6::Schema::Result::Media> where
138L<Interchange6::Schema::Result::MediaType/type> is C<$type>.
139
140C<$type> defaults to C<image> if not provided.
141
142=cut
143
144sub with_media {
145    my $self = shift;
146    my $type = defined $_[0] ? $_[0] : 'image';
147
148    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
162Adds C<quantity_in_stock> column which is available to order_by clauses and
163whose value can be retrieved via
164L<Interchange6::Schema::Result::Product/quantity_in_stock>.
165
166The value is retrieved is L<Interchange6::Schema::Result::Inventory/quantity>.
167
168For a product variant and for a canonical product with no variants the
169quantity returned is for the product itself.
170
171For a canonical (parent) product the quantity returned is the total for all its
172variants.
173
174=cut
175
176sub with_quantity_in_stock {
177    my $self = shift;
178
179    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
204Arguments should be given as a hash reference with the following keys/values:
205
206=over 4
207
208=item * quantity => $quantity
209
210C<quantity> defaults to 1 if not supplied.
211
212=back
213
214The lowest of L<Interchange6::Schema::Result::PriceModifier/price> and
215L<Interchange6::Schema::Result::Product/price>.
216
217For products with variants this is the lowest variant selling_price.
218
219Value is placed in the column C<selling_price>.
220
221If L<Schema/current_user> is defined then any roles assigned to that
222user will be included in the search of
223L<Interchange6::Schema::Result::PriceModifier>.
224
225=cut
226
227sub with_lowest_selling_price {
228    my ( $self, $args ) = @_;
229
230    if ( defined($args) ) {
231        $self->throw_exception(
232            "argument to with_lowest_selling_price must be a hash reference")
233          unless ref($args) eq "HASH";
234    }
235
236    $args->{quantity} = 1 unless defined $args->{quantity};
237
238    my $schema = $self->result_source->schema;
239
240    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        'roles_id'   => undef,
249    };
250
251    if ( my $user = $schema->current_user ) {
252
253        # add roles_id condition
254
255        $search_cond->{roles_id} = [
256            undef,
257            {
258                -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    my $least = 'least';
268    $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    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    my $variant_prices =
291      $self->correlate('variants')->get_column('price')->min_rs->as_query;
292
293    my $self_price_modifiers =
294      $self->correlate('price_modifiers')->search( $search_cond )
295      ->get_column('price')->min_rs->as_query;
296
297    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    return $return;
327}
328
329=head2 with_highest_price
330
331For canonical products with no variants and for variant products
332C<highest_price> is always undef. For canonical products that have variants
333this is the highest of L<Interchange6::Schema::Result::Product/price> of
334the variants.
335
336=cut
337
338sub with_highest_price {
339    my $self = shift;
340
341    my $schema = $self->result_source->schema;
342
343    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
361Adds column C<variant_count> which is a count of variants of each product.
362
363=cut
364
365sub with_variant_count {
366    my $self = shift;
367    return $self->search(
368        undef,
369        {
370            '+columns' => {
371                variant_count =>
372                  $self->correlate('variants')->count_rs->as_query
373            }
374        }
375    );
376}
377
3781;
379