File Coverage

blib/lib/Dancer2/Plugin/Interchange6/Cart.pm
Criterion Covered Total %
statement 128 128 100.0
branch 20 26 76.9
condition n/a
subroutine 22 22 100.0
pod 2 2 100.0
total 172 178 96.6


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