File Coverage

blib/lib/Dancer/Plugin/Interchange6/Cart.pm
Criterion Covered Total %
statement 39 161 24.2
branch 0 42 0.0
condition n/a
subroutine 13 25 52.0
pod 3 3 100.0
total 55 231 23.8


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