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