File Coverage

blib/lib/Interchange6/Cart/Product.pm
Criterion Covered Total %
statement 41 41 100.0
branch 12 12 100.0
condition n/a
subroutine 16 16 100.0
pod 3 3 100.0
total 72 72 100.0


line stmt bran cond sub pod time code
1             package Interchange6::Cart::Product;
2              
3 4     4   2428 use Interchange6::Types -types;
  4         5  
  4         83  
4              
5 4     4   15842 use Moo;
  4         6  
  4         26  
6 4     4   1319 use MooX::HandlesVia;
  4         5  
  4         27  
7 4     4   385 use MooseX::CoverableModifiers;
  4         13  
  4         30  
8             with 'Interchange6::Role::Costs';
9 4     4   461 use namespace::clean;
  4         5  
  4         51  
10              
11             =head1 NAME
12              
13             Interchange6::Cart::Product - Cart product class for Interchange6 Shop Machine
14              
15             =head1 DESCRIPTION
16              
17             Cart product class for L.
18              
19             See L for details of cost attributes and methods.
20              
21             =head1 ATTRIBUTES
22              
23             See also L.
24              
25             Each cart product has the following attributes:
26              
27             =head2 id
28              
29             Can be used by subclasses, e.g. primary key value for cart products in the database.
30              
31             =cut
32              
33             has id => (
34             is => 'ro',
35             isa => Int,
36             );
37              
38             =head2 cart
39              
40             A reference to the Cart object that this Cart::Product belongs to.
41              
42             =over
43              
44             =item Writer: C
45              
46             =back
47              
48             =cut
49              
50             has cart => (
51             is => 'ro',
52             isa => Maybe[Cart],
53             default => undef,
54             writer => 'set_cart',
55             weak_ref => 1,
56             );
57              
58             =head2 name
59              
60             Product name is required.
61              
62             =cut
63              
64             has name => (
65             is => 'ro',
66             isa => NonEmptyStr,
67             required => 1,
68             );
69              
70             =head2 price
71              
72             Product price is required and a positive number or zero.
73              
74             Price is required, because you want to maintain the price that was valid at the time of adding to the cart. Should the price in the shop change in the meantime, it will maintain this price.
75              
76             =over
77              
78             =item Writer: C
79              
80             =back
81              
82             =cut
83              
84             has price => (
85             is => 'ro',
86             isa => PositiveOrZeroNum,
87             required => 1,
88             writer => 'set_price',
89             );
90              
91             =head2 selling_price
92              
93             Selling price is the price after group pricing, tier pricing or promotional discounts have been applied. If it is not set then it defaults to L.
94              
95             =over
96              
97             =item Writer: C
98              
99             =back
100              
101             =cut
102              
103             has selling_price => (
104             is => 'lazy',
105             isa => PositiveOrZeroNum,
106             writer => 'set_selling_price',
107             );
108              
109             sub _build_selling_price {
110 16     16   2457 my $self = shift;
111 16         250 return $self->price;
112             }
113              
114             =head2 discount_percent
115              
116             This is the integer discount percentage calculated from the difference
117             between L and L. This attribute should not normally
118             be set since as it is a calculated value.
119              
120             L is cleared if either L or
121             L methods are called.
122              
123             =cut
124              
125             has discount_percent => (
126             is => 'lazy',
127             clearer => 1
128             );
129              
130             sub _build_discount_percent {
131 3     3   1164 my $self = shift;
132 3 100       41 return 0 if $self->price == $self->selling_price;
133 1         20 return int( ( $self->price - $self->selling_price ) / $self->price * 100 );
134             }
135              
136             after 'set_price', 'set_selling_price' => sub {
137 5     5   5366 shift->clear_discount_percent;
138             };
139              
140             =head2 quantity
141              
142             Product quantity is optional and has to be a natural number greater
143             than zero. Default for quantity is 1.
144              
145             =cut
146              
147             has quantity => (
148             is => 'ro',
149             # https://github.com/interchange/Interchange6/issues/28
150             # Tupe::Tiny::XS sometimes incorrectly passes Int assertion for
151             # non-integer values such as 2.3 so we can't just do:
152             # isa => PositiveInt
153             # but if we stringify the value then things work as expected. Huh?
154             # Also prevent uninitialized warning in case value is undef.
155             isa => sub {
156 4     4   3483 no warnings 'uninitialized';
  4         5  
  4         289  
157             PositiveInt->assert_valid("$_[0]");
158             },
159             default => 1,
160             writer => 'set_quantity',
161             );
162              
163             after set_quantity => sub {
164 11     11   434 my $self = shift;
165 11         101 $self->clear_subtotal;
166 11         1220 $self->clear_total;
167             };
168              
169             =head2 sku
170              
171             Unique product identifier is required.
172              
173             =cut
174              
175             has sku => (
176             is => 'ro',
177             isa => NonEmptyStr,
178             required => 1,
179             );
180              
181             =head2 canonical_sku
182              
183             If this product is a variant of a "parent" product then C
184             is the sku of the parent product.
185              
186             =cut
187              
188             has canonical_sku => (
189             is => 'ro',
190             default => undef,
191             );
192              
193             =head2 subtotal
194              
195             Subtotal calculated as L * L. Lazy set via builder.
196              
197             =cut
198              
199             has subtotal => (
200             is => 'lazy',
201             isa => Num,
202             clearer => 1,
203             predicate => 1,
204             );
205              
206             sub _build_subtotal {
207 23     23   1995 my $self = shift;
208 23         269 return sprintf( "%.2f", $self->selling_price * $self->quantity);
209             }
210              
211             =head2 uri
212              
213             Product uri
214              
215             =cut
216              
217             has uri => (
218             is => 'ro',
219             isa => Str,
220             );
221              
222             =head2 weight
223              
224             Weight of quantity 1 of this product.
225              
226             =cut
227              
228             has weight => (
229             is => 'ro',
230             isa => Num,
231             writer => 'set_weight',
232             );
233              
234             =head2 extra
235              
236             Hash reference of extra things the cart product might want to store such as:
237              
238             =over
239              
240             =item * variant attributes in order to be able to change variant within cart
241              
242             =item * simple attributes to allow display of them within cart
243              
244             =back
245              
246             =cut
247              
248             has extra => (
249             is => 'ro',
250             isa => HashRef,
251             default => sub { {} },
252             handles_via => 'Hash',
253             handles => {
254             get_extra => 'get',
255             set_extra => 'set',
256             delete_extra => 'delete',
257             keys_extra => 'keys',
258             clear_extra => 'clear',
259             exists_extra => 'exists',
260             defined_extra => 'defined',
261             },
262             );
263              
264             =head2 combine
265              
266             Indicate whether products with the same SKU should be combined in the Cart
267              
268             =over
269              
270             =item Writer: C
271              
272             =back
273              
274             =cut
275            
276             has combine => (
277             is => 'ro',
278             isa => CodeRef | Bool,
279             default => 1,
280             );
281              
282             =head1 METHODS
283              
284             See also L.
285              
286             =head2 L methods
287              
288             =over
289              
290             =item * get_extra($key, $key2, $key3...)
291              
292             See L
293              
294             =item * set_extra($key => $value, $key2 => $value2...)
295              
296             See L
297              
298             =item * delete_extra($key, $key2, $key3...)
299              
300             See L
301              
302             =item * keys_extra
303              
304             See L
305              
306             =item * clear_extra
307              
308             See L
309              
310             =item * exists_extra($key)
311              
312             See L
313              
314             =item * defined_extra($key)
315              
316             See L
317              
318             =back
319              
320             =head2 L methods
321              
322             =over
323              
324             =item * clear_subtotal
325              
326             Clears L.
327              
328             =item * has_subtotal
329              
330             predicate on L.
331              
332             =back
333              
334             =head2 is_variant
335              
336             Returns 1 if L is defined else 0.
337              
338             =cut
339              
340             sub is_variant {
341 2 100   2 1 11 return defined shift->canonical_sku ? 1 : 0;
342             }
343              
344             =head2 is_canonical
345              
346             Returns 0 if L is defined else 1.
347              
348             =cut
349              
350             sub is_canonical {
351 2 100   2 1 257 return defined shift->canonical_sku ? 0 : 1;
352             }
353              
354             =head2 should_combine_by_sku
355              
356             Determines whether a product should be combined by sku based on the
357             value of L.
358              
359             If L isa CodeRef the result of applying that CodeRef is returned
360             otherwise:
361             Returns 0 if a product should not be combined
362             Returns 1 if a product should be combined
363              
364             =cut
365            
366             sub should_combine_by_sku {
367 19     19 1 23 my ($self) = @_;
368              
369 19 100       67 return $self->combine->()
370             if ref $self->combine eq 'CODE';
371              
372 18         65 return $self->combine;
373             }
374              
375             # after cost changes we need to clear the cart subtotal/total
376             # our own total is handled by the Costs role
377              
378             after 'clear_costs', 'cost_set', 'apply_cost', 'set_quantity' => sub {
379 15     15   746 my $self = shift;
380 15 100       179 if ( $self->cart ) {
381 10         167 $self->cart->clear_subtotal;
382 10         308 $self->cart->clear_total;
383             }
384             };
385             after 'set_quantity', 'set_weight' => sub {
386 11     11   185 my $self = shift;
387 11 100       97 if ( $self->cart ) {
388 6         78 $self->cart->clear_weight;
389             }
390             };
391              
392             1;