File Coverage

blib/lib/Interchange6/Cart.pm
Criterion Covered Total %
statement 67 67 100.0
branch 6 6 100.0
condition n/a
subroutine 18 18 100.0
pod 3 3 100.0
total 94 94 100.0


line stmt bran cond sub pod time code
1             # Interchange6::Cart - Interchange6 cart class
2              
3             package Interchange6::Cart;
4              
5             =head1 NAME
6              
7             Interchange6::Cart - Cart class for Interchange6 Shop Machine
8              
9             =cut
10              
11 3     3   73035 use strict;
  3         6  
  3         77  
12 3     3   17 use Carp;
  3         7  
  3         223  
13 3     3   3506 use DateTime;
  3         437656  
  3         111  
14 3     3   1757 use Interchange6::Cart::Product;
  3         13  
  3         104  
15 3     3   26 use Scalar::Util 'blessed';
  3         6  
  3         124  
16 3     3   13 use Try::Tiny;
  3         6  
  3         148  
17 3     3   15 use Moo;
  3         4  
  3         25  
18 3     3   905 use MooseX::CoverableModifiers;
  3         6  
  3         32  
19 3     3   391 use MooX::HandlesVia;
  3         6  
  3         21  
20 3     3   339 use Types::Standard qw/ArrayRef InstanceOf Str/;
  3         6  
  3         28  
21 3     3   2041 use Types::Common::String qw/NonEmptyStr/;
  3         5  
  3         26  
22              
23             with 'Interchange6::Role::Costs';
24              
25 3     3   1128 use namespace::clean;
  3         6  
  3         29  
26              
27             =head1 DESCRIPTION
28              
29             Generic cart class for L.
30              
31             =head1 SYNOPSIS
32              
33             my $cart = Interchange6::Cart->new();
34              
35             $cart->add( sku => 'ABC', name => 'Foo', price => 23.45 );
36              
37             $cart->update( sku => 'ABC', quantity => 3 );
38              
39             my $product = Interchange::Cart::Product->new( ... );
40              
41             $cart->add($product);
42              
43             $cart->apply_cost( ... );
44              
45             my $total = $cart->total;
46              
47             =head1 ATTRIBUTES
48              
49             See also L.
50              
51             =head2 id
52              
53             Cart id can be used for subclasses, e.g. primary key value for carts in the database.
54              
55             =over
56              
57             =item Writer: C
58              
59             =back
60              
61             =cut
62              
63             has id => (
64             is => 'ro',
65             isa => Str,
66             writer => 'set_id',
67             );
68              
69             =head2 name
70              
71             The cart name. Default is 'main'.
72              
73             =over
74              
75             =item Writer: C
76              
77             =back
78              
79             =cut
80              
81             has name => (
82             is => 'ro',
83             isa => NonEmptyStr,
84             default => 'main',
85             writer => 'rename',
86             );
87              
88             =head2 products
89              
90             Called without args returns a hash reference of L.
91              
92             Anything passed in as a value on object instantiation is ignored. To load
93             products into a cart the preferred methods are L and L which
94             make sure appropriate arguements are passed.
95              
96             =cut
97              
98             has products => (
99             # rwp allows us to clear out products in seed via _set_products
100             # without disturbing what subclasses might expect of clear
101             is => 'rwp',
102             isa => ArrayRef [ InstanceOf ['Interchange6::Cart::Product'] ],
103             default => sub { [] },
104             handles_via => 'Array',
105             handles => {
106             clear => 'clear',
107             count => 'count',
108             is_empty => 'is_empty',
109             product_get => 'get',
110             product_index => 'first_index',
111             products_array => 'elements',
112             product_delete => 'delete',
113             product_push => 'push',
114             product_set => 'set',
115             },
116             init_arg => undef,
117             );
118              
119             =head2 sessions_id
120              
121             The session ID for the cart.
122              
123             =over
124              
125             =item Writer: C
126              
127             =back
128              
129             =cut
130              
131             has sessions_id => (
132             is => 'ro',
133             isa => Str,
134             clearer => 1,
135             writer => 'set_sessions_id',
136             );
137              
138             =head2 subtotal
139              
140             Returns current cart subtotal excluding costs.
141              
142             =cut
143              
144             has subtotal => (
145             is => 'lazy',
146             clearer => 1,
147             predicate => 1,
148             );
149              
150             sub _build_subtotal {
151 25     25   887 my $self = shift;
152              
153 25         41 my $subtotal = 0;
154              
155 25         476 map { $subtotal += $_->total } $self->products_array;
  40         1910  
156              
157 25         874 return sprintf( "%.2f", $subtotal );
158             }
159              
160             after 'add', 'clear', 'product_push', 'product_set', 'product_delete', 'remove',
161             'seed', 'update' => sub {
162              
163             my $self = shift;
164             $self->clear_subtotal;
165             $self->clear_weight;
166             };
167              
168             after 'clear_subtotal' => sub {
169             shift->clear_total;
170 41     41   1907 };
171              
172             =head2 users_id
173              
174             The user id of the logged in user.
175              
176             =over
177              
178             =item Writer: C
179              
180             =back
181              
182             =cut
183              
184             has users_id => (
185             is => 'ro',
186             isa => Str,
187             writer => 'set_users_id',
188             );
189              
190             =head2 weight
191              
192             Returns total weight of all products in the cart. If all products have
193             unedfined weight then this returns undef.
194              
195             =cut
196              
197             has weight => (
198             is => 'lazy',
199             clearer => 1,
200             predicate => 1,
201             );
202              
203             sub _build_weight {
204             my $self = shift;
205 17     17   9206 my $weight;
206 17         28  
207             map { $weight += $_->weight * $_->quantity }
208 25         1066 grep { defined $_->weight } $self->products_array;
209 17         356  
  25         761  
210             return $weight ? $weight : 0;
211 17 100       908 }
212              
213             =head1 METHODS
214              
215             See also L.
216              
217             =head2 clear
218              
219             Removes all products from the cart.
220              
221             =head2 count
222              
223             Returns the number of different products in the shopping cart. If you have 5 apples and 6 pears it will return 2 (2 different products).
224              
225             =head2 is_empty
226              
227             Return boolean 1 or 0 depending on whether the cart is empty or not.
228              
229             =head2 product_delete($index)
230              
231             Deletes the product at the specified index.
232              
233             =head2 product_get($index)
234              
235             Returns the product at the specified index.
236              
237             =head2 product_index( sub {...})
238              
239             This method returns the index of the first matching product in the cart. The matching is done with a subroutine reference you pass to this method. The subroutine will be called against each element in the array until one matches or all elements have been checked.
240              
241             This method requires a single argument.
242              
243             my $index = $cart->product_index( sub { $_->sku eq 'ABC' } );
244              
245             =head2 product_push($product)
246              
247             Like Perl's normal C this adds the supplied L
248             to L.
249              
250             =head2 product_set($index, $product)
251              
252             Sets the product at the specified index in L to the supplied
253             L.
254              
255             =head2 products_array
256              
257             Returns an array of Interchange::Cart::Product(s)
258              
259             =head2 new
260              
261             Inherited method. Returns a new Cart object.
262              
263             =head2 add($product)
264              
265             Add product to the cart. Returns product in case of success.
266              
267             The product is an L or a hash (reference) of product attributes that would be passed to Interchange6::Cart::Product->new().
268              
269             =cut
270              
271             sub add {
272             my $self = shift;
273             my $product = $_[0];
274             my ( $index, $oldproduct, $update );
275              
276             if ( !defined $product ) {
277             die "undefined argument passed to add";
278             }
279             elsif ( blessed($product) ) {
280             die "product argument is not an Interchange6::Cart::Product"
281             unless ( $product->isa('Interchange6::Cart::Product') );
282             }
283             else {
284              
285             my %args;
286              
287             if ( @_ % 2 ) {
288             if ( ref($product) eq 'HASH' ) {
289              
290             # copy args
291             %args = %{$product};
292             }
293             else {
294             die "argument to add should be hash or hashref";
295             }
296             }
297             else {
298              
299             %args = @_;
300             }
301              
302             $product = 'Interchange6::Cart::Product'->new(%args);
303             }
304              
305             # cart may already contain an product with the same sku
306             # if so then we add quantity to existing product otherwise we add new product
307              
308             $index = $self->product_index( sub { $_->sku eq $product->sku } );
309              
310             if ( $index >= 0 ) {
311              
312             # product already exists in cart so we need to add new quantity to old
313              
314             $oldproduct = $self->product_get($index);
315              
316             $product->set_quantity( $oldproduct->quantity + $product->quantity );
317              
318             $self->product_set( $index, $product );
319              
320             $update = 1;
321             }
322             else {
323              
324             # a new product for this cart
325              
326             $product->set_cart($self);
327             $self->product_push($product);
328             }
329              
330             return $product;
331             }
332              
333             =head2 find
334              
335             Searches for an cart product with the given SKU.
336             Returns cart product in case of sucess or undef on failure.
337              
338             if ($product = $cart->find(9780977920174)) {
339             print "Quantity: $product->{quantity}.\n";
340             }
341              
342             =cut
343              
344             sub find {
345             my ( $self, $sku ) = @_;
346 11     11 1 18  
347             for my $cartproduct ( $self->products_array ) {
348 11         205 if ( $sku eq $cartproduct->sku ) {
349 16 100       558 return $cartproduct;
350 10         34 }
351             }
352              
353             return undef;
354 1         5 }
355              
356             =head2 has_subtotal
357              
358             predicate on L.
359              
360             =head2 has_total
361              
362             predicate on L.
363              
364             =head2 has_weight
365              
366             predicate on L.
367              
368             =head2 quantity
369              
370             Returns the sum of the quantity of all products in the shopping cart,
371             which is commonly used as number of products. If you have 5 apples and 6 pears it will return 11.
372              
373             print 'Products in your cart: ', $cart->quantity, "\n";
374              
375             =cut
376              
377             sub quantity {
378             my $self = shift;
379 19     19 1 17839 my $qty = 0;
380 19         30  
381             map { $qty += $_->quantity } $self->products_array;
382 19         471  
  26         2266  
383             return $qty;
384 19         988 }
385              
386             =head2 remove($sku)
387              
388             Remove product from the cart. Takes SKU of product to identify the product.
389              
390             =cut
391              
392             sub remove {
393             my ( $self, $arg ) = @_;
394              
395             die "no argument passed to remove" unless defined $arg;
396              
397             my $index = $self->product_index( sub { $_->sku eq $arg } );
398              
399             die "sku $arg not found in cart" unless $index >= 0;
400              
401             # remove product from our array
402             my $ret = $self->product_delete($index);
403             die "remove sku $arg failed" unless defined $ret;
404              
405             return $ret;
406             }
407              
408             =head2 seed $product_ref
409              
410             Seeds products within the cart from $product_ref.
411              
412             B use with caution since any existing products in the cart will be lost.
413              
414             $cart->seed([
415             { sku => 'BMX2015', price => 20, quantity = 1 },
416             { sku => 'KTM2018', price => 400, quantity = 5 },
417             { sku => 'DBF2020', price => 200, quantity = 5 },
418             ]);
419              
420             If any product fails to be added (for example bad product args) then an
421             exception is thrown and no products will be added to cart.
422              
423             On success returns L.
424              
425             =cut
426              
427             sub seed {
428             my ( $self, $product_ref ) = @_;
429 4     4 1 2287  
430             die "argument to seed must be an array reference"
431 4 100       29 unless ref($product_ref) eq 'ARRAY';
432              
433             # we can't use ->clear since subclasses might wrap that to do
434             # interesting things like remove products from cart in database
435             $self->_set_products([]);
436 3         61  
437             my @products;
438 3         967 for my $args ( @{$product_ref} ) {
439 3         8 push @products, Interchange6::Cart::Product->new($args);
  3         9  
440 6         863 }
441              
442             # if we got here then all products are good so put them in the cart
443             $self->product_push(@products);
444 2         401  
445             return $self->products;
446 2         71 }
447              
448             =head2 update
449              
450             Update quantity of products in the cart.
451              
452             Parameters are pairs of SKUs and quantities, e.g.
453              
454             $cart->update(9780977920174 => 5,
455             9780596004927 => 3);
456              
457             A quantity of zero is equivalent to removing this product.
458              
459             Returns an array of updated products that are still in the cart.
460             Products removed via quantity 0 or products for which quantity has not
461             changed will not be returned.
462              
463             =cut
464              
465             sub update {
466             my ( $self, @args ) = @_;
467             my ( $sku, $qty, $product, $update, @products );
468              
469             ARGS: while ( @args > 0 ) {
470             $sku = shift @args;
471             $qty = shift @args;
472              
473             die "sku not defined in arg to update" unless defined $sku;
474              
475             die "quantity not supplied as arg to update for sku $sku"
476             unless defined $qty;
477              
478             unless ( $product = $self->find($sku) ) {
479             die "Product for $sku not found in cart.";
480             }
481              
482             if ( $qty == 0 ) {
483             $self->remove($sku);
484             next;
485             }
486              
487             # jump to next product if quantity stays the same
488             next ARGS if $qty == $product->quantity;
489              
490             $product->set_quantity($qty);
491             push @products, $product;
492             }
493             return @products;
494             }
495              
496             =head1 AUTHORS
497              
498             Stefan Hornburg (Racke),
499             Peter Mottram (SysPete),
500              
501             =head1 LICENSE AND COPYRIGHT
502              
503             Copyright 2011-2015 Stefan Hornburg (Racke) .
504              
505             This program is free software; you can redistribute it and/or modify it
506             under the terms of either: the GNU General Public License as published
507             by the Free Software Foundation; or the Artistic License.
508              
509             See http://dev.perl.org/licenses/ for more information.
510              
511             =cut
512              
513             1;