File Coverage

blib/lib/Basket/Calc.pm
Criterion Covered Total %
statement 84 118 71.1
branch 32 54 59.2
condition 6 15 40.0
subroutine 10 11 90.9
pod 3 3 100.0
total 135 201 67.1


line stmt bran cond sub pod time code
1             package Basket::Calc;
2              
3 1     1   22110 use 5.010001;
  1         4  
  1         40  
4 1     1   2576 use Mouse;
  1         35739  
  1         5  
5 1     1   1135 use experimental 'smartmatch';
  1         808  
  1         6  
6              
7             # ABSTRACT: Basket/Cart calculation library with support for currency conversion, discounts and tax
8              
9             our $VERSION = '0.5'; # VERSION
10              
11 1     1   63 use Scalar::Util qw(looks_like_number);
  1         1  
  1         101  
12 1     1   891 use Finance::Currency::Convert::Yahoo;
  1         71994  
  1         46  
13 1     1   12 use Carp;
  1         2  
  1         1521  
14              
15              
16             has 'debug' => (
17             is => 'rw',
18             isa => 'Bool',
19             trigger => \&_set_debug,
20             lazy => 1,
21             default => sub { 0 },
22             );
23              
24             has 'items' => (
25             is => 'rw',
26             isa => 'ArrayRef',
27             clearer => 'empty_items',
28             );
29              
30             has 'discount' => (
31             is => 'rw',
32             isa => 'HashRef',
33             clearer => 'no_discount',
34             );
35              
36              
37             has 'currency' => (
38             is => 'rw',
39             isa => 'Str',
40             required => 1,
41             );
42              
43              
44             has 'tax' => (
45             is => 'rw',
46             isa => 'Num',
47             lazy => 1,
48             default => sub { 0 },
49             );
50              
51              
52             sub add_item {
53 8     8 1 4191 my ($self, $item) = @_;
54              
55             # make sure the input is sane
56 8 50       26 unless (ref $item eq 'HASH') {
57 0         0 carp "parameter has to be a HASHREF";
58 0         0 return;
59             }
60              
61 8         14 foreach my $key ('price') {
62 8 50 33     62 unless (exists $item->{$key} and $item->{$key}) {
63 0         0 carp "$key missing";
64 0         0 return;
65             }
66             }
67              
68 8 50       32 unless (looks_like_number($item->{price})) {
69 0         0 carp "'price' is not a number";
70 0         0 return;
71             }
72              
73             # calculate amount from quantity and price
74 8 100       21 if (exists $item->{quantity}) {
75 4 50 33     27 if (!looks_like_number($item->{quantity}) || ($item->{quantity} < 0)) {
76 0         0 carp "'quantity' is not a number or smaller than 0";
77 0         0 return;
78             }
79              
80 4         16 $item->{amount} = $item->{price} * $item->{quantity};
81             }
82             else {
83 4         9 $item->{amount} = $item->{price};
84 4         8 $item->{quantity} = 1;
85             }
86              
87 8 100 66     42 $item->{currency} = $self->currency
88             unless (exists $item->{currency} and $item->{currency});
89              
90             # convert currency if needed
91 8 100       30 if ($item->{currency} ne $self->currency) {
92 2         13 my $amount =
93             Finance::Currency::Convert::Yahoo::convert($item->{amount},
94             $item->{currency}, $self->currency);
95              
96 2 50       1606909 unless ($amount) {
97 0         0 carp "could not get "
98             . $item->{amount} . " "
99             . $item->{currency}
100             . " converted to "
101             . $self->currency;
102 0         0 return;
103             }
104              
105 2         10 $item->{orig_amount} = $item->{amount};
106 2         5 $item->{orig_currency} = $item->{currency};
107              
108 2         6 $item->{amount} = $amount;
109 2         17 $item->{currency} = $self->currency;
110             }
111              
112 8 50       32 print __PACKAGE__ . ' added item: ' . join(' ', %$item) . $/
113             if $self->debug;
114              
115 8 100       11 $self->items([ @{ $self->items || [] }, $item ]);
  8         65  
116              
117 8         27 return $item;
118             }
119              
120              
121             sub add_discount {
122 2     2 1 1071 my ($self, $discount) = @_;
123              
124             # make sure the input is sane
125 2 50       8 unless (ref $discount eq 'HASH') {
126 0         0 carp "parameter has to be a HASHREF";
127 0         0 return;
128             }
129              
130 2         5 foreach my $key ('type', 'value') {
131 4 50 33     25 unless (exists $discount->{$key} and $discount->{$key}) {
132 0         0 carp "'$key' missing";
133 0         0 return;
134             }
135             }
136              
137 2 50       13 unless ($discount->{type} =~ m/^(percent|amount)$/x) {
138 0         0 carp "'type' has to be either percent, or amount";
139 0         0 return;
140             }
141              
142 2 50       9 unless (looks_like_number($discount->{value})) {
143 0         0 carp "'value' is not a number";
144 0         0 return;
145             }
146              
147 2         28 given ($discount->{type}) {
148 2         12 when ('percent') {
149 1 50 33     14 if ($discount->{value} <= 0 or $discount->{value} > 1) {
150 0         0 carp "'percent' has to be a decimal between 0 and 1";
151 0         0 return;
152             }
153             }
154 1         3 when ('amount') {
155 1 50       7 $discount->{currency} = $self->currency
156             unless exists $discount->{currency};
157              
158             # convert currency if needed
159 1 50       8 if ($discount->{currency} ne $self->currency) {
160 0         0 my $amount = Finance::Currency::Convert::Yahoo::convert(
161             $discount->{value}, $discount->{currency}, $self->currency);
162              
163 0 0       0 unless ($amount) {
164 0         0 carp "could not get "
165             . $discount->{value} . " "
166             . $discount->{currency}
167             . " converted to "
168             . $self->currency;
169 0         0 return;
170             }
171              
172 0         0 $discount->{orig_value} = $discount->{value};
173 0         0 $discount->{orig_currency} = $discount->{currency};
174              
175 0         0 $discount->{value} = $amount;
176 0         0 $discount->{currency} = $self->currency;
177             }
178             }
179             }
180              
181 2 50       8 print __PACKAGE__ . ' added discount: ' . join(' ', %$discount) . $/
182             if $self->debug;
183              
184 2         10 $self->discount($discount);
185              
186 2         8 return $self->discount;
187             }
188              
189              
190             sub calculate {
191 3     3 1 537 my ($self) = @_;
192              
193 3 50       12 unless ($self->items) {
194 0         0 carp "no items added";
195 0         0 return;
196             }
197              
198 3         24 my $total = {
199             value => 0,
200             net => 0,
201             tax_amount => 0,
202             discount => 0,
203             };
204              
205 3 50       11 print __PACKAGE__ . " -- calculating totals --\n" if $self->debug;
206              
207             # calculate net
208 3         3 foreach my $item (@{ $self->items }) {
  3         10  
209 6 50       20 print __PACKAGE__ . ' item: ' . join(' ', %$item) . $/ if $self->debug;
210              
211 6         18 $total->{net} += $item->{amount};
212             }
213              
214 3         7 my $original_net = $total->{net};
215              
216             # apply discounts
217 3 100       19 if ($self->discount) {
218 2 50       9 print __PACKAGE__ . ' discount: ' . join(' ', %{ $self->discount }) . $/
  0         0  
219             if $self->debug;
220              
221 2         6 given ($self->discount->{type}) {
222 2         5 when ('percent') {
223 1         20 $total->{net} *= (1 - $self->discount->{value});
224             }
225 1         3 when ('amount') {
226 1         4 $total->{net} = $total->{net} - $self->discount->{value};
227 1 50       7 $total->{net} = 0 if $total->{net} < 0;
228             }
229             }
230             }
231              
232             # calculate tax
233 3         12 $total->{tax_amount} = $total->{net} * $self->tax;
234 3         6 $total->{value} = $total->{net} + $total->{tax_amount};
235 3         6 $total->{discount} = $original_net - $total->{net};
236              
237             # proper rounding and formatting
238 3         15 $total->{$_} = _round($total->{$_}) for keys %$total;
239              
240             # remind what the currency is that was requested
241 3         14 $total->{currency} = $self->currency;
242              
243 3 50       9 print __PACKAGE__ . ' total: ' . join(' ', %$total) . $/ if $self->debug;
244              
245 3         15 $self->empty_items;
246 3         12 $self->no_discount;
247              
248 3         9 return $total;
249             }
250              
251             sub _set_debug {
252 0     0   0 my ($self, $value, $some) = @_;
253              
254 0         0 $Finance::Currency::Convert::Yahoo::CHAT = $value;
255              
256 0         0 return;
257             }
258              
259             sub _round {
260 12     12   16 my ($float) = @_;
261              
262             # some stupid perl versions on some platforms can't round correctly and i
263             # don't want to use more modules
264 12 100       60 $float += 0.001 if ($float =~ m/\.[0-9]{2}5/);
265              
266 12         100 return sprintf('%.2f', sprintf('%.10f', $float)) + 0;
267             }
268              
269              
270             1; # End of Basket::Calc
271              
272             __END__