File Coverage

blib/lib/Dancer2/Plugin/Interchange6/Cart.pm
Criterion Covered Total %
statement 132 133 99.2
branch 27 36 75.0
condition n/a
subroutine 23 23 100.0
pod 2 2 100.0
total 184 194 94.8


line stmt bran cond sub pod time code
1 1     1   16990114 use utf8;
  1         1  
  1         5  
2              
3             package Dancer2::Plugin::Interchange6::Cart;
4              
5             =head1 NAME
6              
7             Dancer2::Plugin::Interchange6::Cart
8              
9             =head1 DESCRIPTION
10              
11             Extends L<Interchange6::Cart> to tie cart to L<Interchange6::Schema::Result::Cart>.
12              
13             =cut
14              
15 1     1   36 use strict;
  1         2  
  1         15  
16 1     1   2 use warnings;
  1         1  
  1         27  
17              
18 1     1   365 use Interchange6::Types -types;
  1         61744  
  1         8  
19 1     1   5272 use JSON::MaybeXS ();
  1         904  
  1         20  
20 1     1   373 use MooseX::CoverableModifiers;
  1         4041  
  1         6  
21 1     1   103 use Module::Runtime 'use_module';
  1         2  
  1         10  
22              
23 1     1   44 use Moo;
  1         2  
  1         8  
24             extends 'Interchange6::Cart';
25 1     1   390 use namespace::clean;
  1         1  
  1         10  
26              
27             =head1 ATTRIBUTES
28              
29             See L<Interchange6::Cart/ATTRIBUTES> for a full list of attributes
30             inherited by this module.
31              
32             =head2 plugin
33              
34             Dancer2::Plugin::Interchange6 plugin instance.
35              
36             =cut
37              
38             has plugin => (
39             is => 'ro',
40             required => 1,
41             );
42              
43             =head2
44              
45             Dancer2 app instance for L</plugin>.
46              
47             =cut
48              
49             has app => (
50             is => 'ro',
51             lazy => 1,
52             default => sub { $_[0]->plugin->app },
53             );
54              
55             =head2 database
56              
57             The database name as defined in the L<Dancer2::Plugin::DBIC> configuration.
58              
59             Defaults to 'default'.
60              
61             =cut
62              
63             has database => (
64             is => 'ro',
65             isa => Str,
66             default => 'default',
67             );
68              
69             =head2 dbic_cart
70              
71             =cut
72              
73             has dbic_cart => (
74             is => 'lazy',
75             isa => InstanceOf['Interchange6::Schema::Result::Cart'],
76             );
77              
78             sub _build_dbic_cart {
79 69     69   1108 my $self = shift;
80              
81 69         453 my $cart = $self->schema->resultset('Cart')->find_or_new(
82             {
83             name => $self->name,
84             sessions_id => $self->sessions_id,
85             },
86             { key => 'carts_name_sessions_id' }
87             );
88              
89 69 100       167517 if ( $cart->in_storage ) {
90 51         1903 $self->app->log( 'debug', "Existing cart: ",
91             $cart->carts_id, " ", $cart->name, "." );
92             }
93             else {
94 18         690 $cart->insert;
95 18         54035 $self->app->log( 'debug', "New cart ", $cart->carts_id, " ",
96             $cart->name, "." );
97             }
98 69         35223 return $cart;
99             }
100              
101             =head2 dbic_cart_products
102              
103             L</dbic_cart> related resultset C<cart_products> with prefetched C<product>.
104              
105             =cut
106              
107             has dbic_cart_products => (
108             is => 'lazy',
109             isa => InstanceOf['DBIx::Class::ResultSet'],
110             );
111              
112             sub _build_dbic_cart_products {
113 69     69   2152 return shift->dbic_cart->related_resultset('cart_products')->search(
114             undef,
115             {
116             prefetch => 'product'
117             }
118             );
119             }
120              
121             =head2 schema
122              
123             DBIC schema for L</database>.
124              
125             =cut
126              
127             has schema => (
128             is => 'ro',
129             required => 1,
130             );
131              
132             =head2 id
133              
134             Extends inherited L<Interchange6::Cart/id> attribute.
135              
136             Defaults to C<id> of L</dbic_cart>.
137              
138             =cut
139              
140             has '+id' => (
141             is => 'lazy',
142             );
143              
144             sub _build_id {
145             return shift->dbic_cart->id,
146 11     11   20556 }
147              
148             =head2 product_class
149              
150             Inherited. Default is L<Dancer2::Plugin::Interchange6::Cart::Product>.
151              
152             =cut
153              
154             has '+product_class' => (
155             default => __PACKAGE__ . '::Product',
156             );
157              
158             =head2 sessions_id
159              
160             Extends inherited sessions_id attribute.
161              
162             Defaults to C<< session->id >>.
163              
164             =cut
165              
166             has '+sessions_id' => (
167             is => 'ro',
168             required => 1,
169             );
170              
171             =head1 METHODS
172              
173             See L<Interchange6::Cart/METHODS> for a full list of methods inherited by
174             this module.
175              
176             =head2 BUILD
177              
178             Load existing cart from the database along with any products it contains.
179              
180             =cut
181              
182             sub BUILD {
183 69     69 1 55990 my $self = shift;
184 69         181 my ( @products, $roles );
185              
186 69         1203 my $rset = $self->dbic_cart_products->order_by( 'cart_position',
187             'cart_products_id' );
188              
189 69         96032 my $json = JSON::MaybeXS->new;
190              
191 69         1377 while ( my $record = $rset->next ) {
192              
193 67 50       492781 push @products,
194             {
195             dbic_product => $record->product,
196             id => $record->cart_products_id,
197             sku => $record->sku,
198             canonical_sku => $record->product->canonical_sku,
199             name => $record->product->name,
200             quantity => $record->quantity,
201             price => $record->product->price,
202             uri => $record->product->uri,
203             weight => $record->product->weight,
204             combine => $record->combine,
205             $record->extra ? ( extra => $json->decode( $record->extra ) ) : (),
206             };
207             }
208              
209             # use seed to avoid hooks
210 69         259031 $self->seed( \@products );
211             }
212              
213             =head1 METHODS
214              
215             =head2 add
216              
217             Add one or more products to the cart.
218              
219             Possible arguments:
220              
221             =over
222              
223             =item * single product sku (scalar value)
224              
225             =item * hashref with keys 'sku' and 'quantity' (quantity is optional and defaults to 1)
226              
227             =item * an array reference of either of the above
228              
229             =back
230              
231             In list context returns an array of L<Interchange6::Cart::Product>s and in scalar context returns an array reference of the same.
232              
233             =cut
234              
235             around 'add' => sub {
236 28     28   5755 my ( $orig, $self, $args ) = @_;
237 28         60 my ( @products, @ret );
238              
239             # convert to array reference if we don't already have one
240 28 50       171 $args = [$args] unless ref($args) eq 'ARRAY';
241              
242 28         669 $self->plugin->execute_plugin_hook( 'before_cart_add_validate', $self,
243             $args );
244              
245             # basic validation + add each validated arg to @args
246              
247 28         105098 foreach my $arg (@$args) {
248              
249             # make sure we have hasref
250 28 100       140 unless ( ref($arg) eq 'HASH' ) {
251 1         3 $arg = { sku => $arg };
252             }
253              
254             die "Attempt to add product to cart without sku failed."
255 28 50       115 unless defined $arg->{sku};
256              
257 28         239 my $result = $self->schema->resultset('Product')->find( $arg->{sku} );
258              
259 28 50       121070 die "Product with sku '$arg->{sku}' does not exist."
260             unless defined $result;
261              
262             my $product = {
263             dbic_product => $result,
264             name => $result->name,
265             price => $result->price,
266             sku => $result->sku,
267             canonical_sku => $result->canonical_sku,
268             uri => $result->uri,
269             weight => $result->weight,
270             quantity => defined $arg->{quantity} ? $arg->{quantity} : 1,
271 28 100       1083 defined $arg->{extra} ? ( extra => $arg->{extra} ) : (),
    100          
272             };
273              
274 28 100       3589 if ( defined $arg->{combine} ) {
    50          
275 5         20 $product->{combine} = $arg->{combine};
276             }
277             elsif ( $result->can('combine') ) {
278             # the db product has combine attribute (not guaranteed if schema
279             # is not IC6S)
280 23         395 $product->{combine} = $result->combine;
281             }
282             else {
283 0         0 $product->{combine} = 1;
284             }
285              
286 28         268 push @products, $product;
287             }
288              
289 28         592 $self->plugin->execute_plugin_hook( 'before_cart_add', $self, \@products );
290              
291             # add products to cart
292              
293 28         20476 my $json = JSON::MaybeXS->new( pretty => 0, convert_blessed => 1 );
294              
295 28         652 foreach my $product ( @products ) {
296              
297             # bubble up the add
298 28         155 my $ret = $orig->( $self, $product );
299              
300             # update or create in db taking notice of 'should_combine_by_sku'
301            
302 27         27000 my $cart_product;
303 27 100       103 if ( $ret->should_combine_by_sku ) {
304 22         514 $cart_product = $self->dbic_cart_products->search(
305             { 'me.sku' => $ret->sku, 'me.combine' => 1 },
306             { rows => 1 } )->single;
307             }
308              
309 27 100       201068 if ( $cart_product ) {
310 6         338 $cart_product->update({ quantity => $ret->quantity });
311             }
312             else {
313 21         999 $cart_product = $self->dbic_cart_products->create(
314             {
315             sku => $ret->sku,
316             quantity => $ret->quantity,
317             cart_position => 0,
318             combine => $ret->should_combine_by_sku,
319             extra => $json->encode( $ret->extra ),
320             }
321             );
322             }
323              
324 27         95021 push @ret, $ret;
325             }
326              
327 27         1399 $self->plugin->execute_plugin_hook( 'after_cart_add', $self, \@ret );
328              
329 27 50       189226 return wantarray ? @ret : \@ret;
330             };
331              
332             =head2 clear
333              
334             Removes all products from the cart.
335              
336             =cut
337              
338             around clear => sub {
339 2     2   45 my ( $orig, $self ) = @_;
340              
341 2         40 $self->plugin->execute_plugin_hook( 'before_cart_clear', $self );
342              
343 2         7836 $orig->( $self, @_ );
344              
345             # delete all products from this cart
346 2         415 $self->dbic_cart_products->delete_all;
347              
348 2         20575 $self->plugin->execute_plugin_hook( 'after_cart_clear', $self );
349              
350 2         2372 return;
351             };
352              
353             =head2 load_saved_products
354              
355             Pulls old cart items into current cart - used after user login.
356              
357             =cut
358              
359             sub load_saved_products {
360 5     5 1 9 my $self = shift;
361              
362             # should not be called unless user is logged in
363 5 50       31 return unless $self->users_id;
364              
365             # find old carts and see if they have products we should move into
366             # our new cart
367              
368 5         38 my $old_carts = $self->schema->resultset('Cart')->search(
369             {
370             'me.name' => $self->name,
371             'me.users_id' => $self->users_id,
372             'me.sessions_id' => [ undef, { '!=', $self->sessions_id } ],
373             },
374             {
375             prefetch => 'cart_products',
376             }
377             );
378              
379 5         2285 my $json = JSON::MaybeXS->new;
380              
381 5         191 while ( my $cart = $old_carts->next ) {
382              
383 2         22847 my $cart_products = $cart->cart_products;
384 2         362 while ( my $cart_product = $cart_products->next ) {
385              
386 5 50       278 $self->add(
387             {
388             sku => $cart_product->sku,
389             quantity => $cart_product->quantity,
390             combine => $cart_product->combine,
391             $cart_product->extra
392             ? ( extra => $json->decode($cart_product->extra) )
393             : (),
394             }
395             );
396              
397             }
398             }
399              
400             # delete the old carts (cascade deletes related cart products)
401 5         20002 $old_carts->delete;
402             }
403              
404             =head2 remove
405              
406             Remove single product from the cart. Takes SKU of product to identify
407             the product.
408              
409             =cut
410              
411             around remove => sub {
412 4     4   100 my ( $orig, $self, $arg ) = @_;
413              
414 4         95 $self->plugin->execute_plugin_hook( 'before_cart_remove_validate', $self,
415             $arg );
416              
417 4     7   24640 my $index = $self->product_index( sub { $_->sku eq $arg } );
  7         204  
418              
419 4 100       33 die "Product sku not found in cart: $arg." unless $index >= 0;
420              
421 3         72 $self->plugin->execute_plugin_hook( 'before_cart_remove', $self, $arg );
422              
423 3         2245 my $ret = $orig->( $self, $arg );
424              
425 3         1928 $self->dbic_cart_products->search( { 'me.sku' => $ret->sku } )->delete;
426              
427 3         35840 $self->plugin->execute_plugin_hook( 'after_cart_remove', $self, $arg );
428              
429 3         3124 return $ret;
430             };
431              
432             =head2 rename
433              
434             Rename this cart. This is the writer method for L<Interchange6::Cart/name>.
435              
436             Arguments: new name
437              
438             Returns: new name
439              
440             =cut
441              
442             around rename => sub {
443 2     2   133 my ( $orig, $self, $new_name ) = @_;
444              
445 2         8 my $old_name = $self->name;
446              
447 2         40 $self->plugin->execute_plugin_hook( 'before_cart_rename',
448             $self, $old_name, $new_name );
449              
450 2         1627 my $ret = $orig->( $self, $new_name );
451              
452 2         126 $self->dbic_cart->update( { name => $ret } );
453              
454 2         5178 $self->plugin->execute_plugin_hook( 'after_cart_rename',
455             $self, $old_name, $ret );
456              
457 2         1198 return $ret;
458             };
459              
460             sub _find_and_update {
461 1     1   2 my ( $self, $sku, $new_product ) = @_;
462              
463 1         18 $self->dbic_cart_products->search(
464             {
465             'me.sku' => $sku
466             }
467             )->update($new_product);
468             }
469              
470             =head2 set_sessions_id
471              
472             Writer method for L<Interchange6::Cart/sessions_id>.
473              
474             =cut
475              
476             around set_sessions_id => sub {
477 4     4   120 my ( $orig, $self, $arg ) = @_;
478              
479 4         82 $self->plugin->execute_plugin_hook( 'before_cart_set_sessions_id', $self,
480             $arg );
481              
482 4         31078 my $ret = $orig->( $self, $arg );
483              
484 1         35 $self->app->log( 'debug',
485             "Change sessions_id of cart " . $self->id . " to: ", $arg );
486              
487 1         446 $self->dbic_cart->update({ sessions_id => $arg });
488              
489 1         1987 $self->plugin->execute_plugin_hook( 'after_cart_set_sessions_id', $ret,
490             $arg );
491              
492 1         574 return $ret;
493             };
494              
495             =head2 set_users_id
496              
497             Writer method for L<Interchange6::Cart/users_id>.
498              
499             =cut
500              
501             around set_users_id => sub {
502 5     5   335 my ( $orig, $self, $arg ) = @_;
503              
504 5         132 $self->plugin->execute_plugin_hook( 'before_cart_set_users_id', $self,
505             $arg );
506              
507 5         32501 $self->app->log( 'debug',
508             "Change users_id of cart " . $self->id . " to: $arg" );
509              
510 5         2447 my $ret = $orig->( $self, $arg );
511              
512 5         175 $self->dbic_cart->update( { users_id => $arg } );
513              
514 5         10652 $self->plugin->execute_plugin_hook( 'after_cart_set_users_id', $ret, $arg );
515              
516 5         3106 return $ret;
517             };
518              
519             =head2 update
520              
521             Update quantity of products in the cart.
522              
523             Parameters are pairs of SKUs and quantities, e.g.
524              
525             $cart->update(9780977920174 => 5,
526             9780596004927 => 3);
527              
528             Triggers before_cart_update and after_cart_update hooks.
529              
530             A quantity of zero is equivalent to removing this product,
531             so in this case the remove hooks will be invoked instead
532             of the update hooks.
533              
534             Returns updated products that are still in the cart. Products removed
535             via quantity 0 or products for which quantity has not changed will not
536             be returned.
537              
538             =cut
539              
540             around update => sub {
541 3     3   66 my ( $orig, $self, @args ) = @_;
542 3         5 my ( @products, $product, $new_product, $count );
543              
544 3         15 ARGS: while ( @args > 0 ) {
545              
546 3         7 my $sku = shift @args;
547 3         6 my $qty = shift @args;
548              
549 3 50       20 die "Bad quantity argument to update: $qty" unless $qty =~ /^\d+$/;
550              
551 3 100       17 if ( $qty == 0 ) {
552              
553             # do remove instead of update
554 1         21 $self->remove($sku);
555 1         45 next ARGS;
556             }
557              
558 2         51 $self->plugin->execute_plugin_hook( 'before_cart_update',
559             $self, $sku, $qty );
560              
561 2         12355 my ($ret) = $orig->( $self, $sku => $qty );
562              
563 1         134 $self->_find_and_update( $sku, { quantity => $qty } );
564              
565 1         12564 $self->plugin->execute_plugin_hook( 'after_cart_update',
566             $ret, $sku, $qty );
567             }
568             };
569              
570             =head1 HOOKS
571              
572             The following hooks are available:
573              
574             =over 4
575              
576             =item before_cart_add_validate
577              
578             Executed in L</add> before arguments are validated as being valid. Hook
579             receives the following arguments:
580              
581             Receives: $cart, \%args
582              
583             The args are those that were passed to L<add>.
584              
585             Example:
586              
587             hook before_cart_add_validate => sub {
588             my ( $cart, $args ) = @_;
589             foreach my $arg ( @$args ) {
590             my $sku = ref($arg) eq 'HASH' ? $arg->{sku} : $arg;
591             die "bad product" if $sku eq "bad sku";
592             }
593             }
594              
595             =item before_cart_add
596              
597             Called in L</add> immediately before the products are added to the cart.
598              
599             Receives: $cart, \@products
600              
601             The products arrary ref contains simple hash references that will be passed
602             to L<Interchange6::Cart::Product/new>.
603              
604             =item after_cart_add
605              
606             Called in L</add> after products have been added to the cart.
607              
608             Receives: $cart, \@products
609              
610             The products arrary ref contains L<Interchange6::Cart::Product>s.
611              
612             =item before_cart_remove_validate
613              
614             Called at start of L</remove> before arg has been validated.
615              
616             Receives: $cart, $sku
617              
618             =item before_cart_remove
619              
620             Called in L</remove> before validated product is removed from cart.
621              
622             Receives: $cart, $sku
623              
624             =item after_cart_remove
625              
626             Called in L</remove> after product has been removed from cart.
627              
628             Receives: $cart, $sku
629              
630             =item before_cart_update
631              
632             Executed for each pair of sku/quantity passed to L<update> before the update is performed.
633              
634             Receives: $cart, $sku, $quantity
635              
636             A quantity of zero is equivalent to removing this product,
637             so in this case the remove hooks will be invoked instead
638             of the update hooks.
639              
640             =item after_cart_update
641              
642             Executed for each pair of sku/quantity passed to L<update> after the update is performed.
643              
644             Receives: $product, $sku, $quantity
645              
646             Where C<$product> is the L<Interchange6::Cart::Product> returned from
647             L<Interchange6::Cart::Product/update>.
648              
649             A quantity of zero is equivalent to removing this product,
650             so in this case the remove hooks will be invoked instead
651             of the update hooks.
652              
653             =item before_cart_clear
654              
655             Executed in L</clear> before the clear is performed.
656              
657             Receives: $cart
658              
659             =item after_cart_clear
660              
661             Executed in L</clear> after the clear is performed.
662              
663             Receives: $cart
664              
665             =item before_cart_set_users_id
666              
667             Executed in L<set_users_id> before users_id is updated.
668              
669             Receives: $cart, $userid
670              
671             =item after_cart_set_users_id
672              
673             Executed in L<set_users_id> after users_id is updated.
674              
675             Receives: $new_usersid, $requested_userid
676              
677             =item before_cart_set_sessions_id
678              
679             Executed in L<set_sessions_id> before sessions_id is updated.
680              
681             Receives: $cart, $sessionid
682              
683             =item after_cart_set_sessions_id
684              
685             Executed in L<set_sessions_id> after sessions_id is updated.
686              
687             Receives: $cart, $sessionid
688              
689             =item before_cart_rename
690              
691             Executed in L</rename> before cart L<Interchange6::Cart/name> is updated.
692              
693             Receives: $cart, $old_name, $new_name
694              
695             =item after_cart_rename
696              
697             Executed in L</rename> after cart L<Interchange6::Cart/name> is updated.
698              
699             Receives: $cart, $old_name, $new_name
700              
701             =back
702              
703             =head1 AUTHORS
704              
705             Stefan Hornburg (Racke), <racke@linuxia.de>
706             Peter Mottram (SysPete), <peter@sysnix.com>
707              
708             =head1 LICENSE AND COPYRIGHT
709              
710             Copyright 2011-2016 Stefan Hornburg (Racke) <racke@linuxia.de>.
711              
712             This program is free software; you can redistribute it and/or modify it
713             under the terms of either: the GNU General Public License as published
714             by the Free Software Foundation; or the Artistic License.
715              
716             See http://dev.perl.org/licenses/ for more information.
717              
718             =cut
719              
720             1;