1package Test::Tax;
2
3use DateTime;
4use Test::Exception;
5use Test::MockTime qw( :all );
6use Test::Roo::Role;
7
8test 'tax tests' => sub {
9
10    my $self = shift;
11
12    my ( %countries, %states, $rset, @data, $result, %data, $data, $tax );
13
14    my $dt = DateTime->now;
15
16    # fixtures
17    $self->countries;
18    $self->states;
19
20    cmp_ok( $self->taxes->count, '==', 37, "37 taxes in the table" );
21
22    # some country-level taxes + EU reverse charge
23
24#<<<
25@data = (
26    [ 'DE VAT Reduced', 'DE VAT Reduced Rate',          7, '1983-07-01', 'DE' ],
27    [ 'DE VAT Exempt',  'DE VAT Exempt',                0, '1968-01-01', 'DE' ],
28    [ 'MT VAT Reduced', 'Malta VAT Reduced Rate',       5, '1995-01-01', 'MT' ],
29    [ 'MT VAT Hotel',   'Malta VAT Hotel Accomodation', 7, '2011-01-01', 'MT' ],
30    [ 'MT VAT Exempt',  'Malta VAT Exempt',             0, '1995-01-01', 'MT' ],
31    [ 'GB VAT Reduced', 'GB VAT Reduced Rate',          5, '1997-09-01', 'GB' ],
32    [ 'GB VAT Exempt',  'GB VAT Exempt',                0, '1973-04-01', 'GB' ],
33    [ 'EU reverse charge', 'EU B2B reverse charge',    0, '2000-01-01', undef ],
34    [ 'CA GST',         'Canada Goods and Service Tax', 5, '2008-01-01', 'CA' ],
35);
36#>>>
37
38    lives_ok(
39        sub {
40            $result = $self->taxes->populate(
41                [
42                    [
43                        'tax_name', 'description',
44                        'percent',  'valid_from',
45                        'country_iso_code',
46                    ],
47                    @data
48                ]
49            );
50        },
51        "Populate tax table"
52    );
53
54    cmp_ok( $self->taxes->count, '==', 46, "46 taxes in the table" );
55
56    # test some incorrect tax entries
57
58    $data = {
59        tax_name         => 'IE VAT Standard',
60        description      => 'Ireland VAT Standard Rate',
61        country_iso_code => 'FooBar',
62        percent          => 21,
63        valid_from       => '2010-01-01',
64        valid_to         => '2011-12-31'
65    };
66    throws_ok(
67        sub { $self->taxes->create($data) },
68        qr/iso_code not valid/,
69        "Fail create with bad country_iso_code"
70    );
71
72    cmp_ok( $self->taxes->count, '==', 46, "46 taxes in the table" );
73
74    # create an old IE rate
75
76    $data = {
77        tax_name         => 'IE VAT Standard',
78        description      => 'Ireland VAT Standard Rate',
79        country_iso_code => 'IE',
80        percent          => 21,
81        valid_from       => '2010-01-01',
82        valid_to         => '2011-12-31'
83    };
84
85    lives_ok(
86        sub { $result = $self->taxes->create($data) },
87        "Create previous IE VAT Standard rate"
88    );
89
90    cmp_ok( $self->taxes->count, '==', 47, "47 taxes in the table" );
91
92    throws_ok(
93        sub { $result = $self->taxes->create($data) },
94        qr/overlaps existing date range/,
95        "Fail to create identical tax"
96    );
97
98    cmp_ok( $self->taxes->count, '==', 47, "47 taxes in the table" );
99
100    $data->{valid_from} = '2011-01-01';
101    $data->{valid_to}   = undef;
102    throws_ok(
103        sub { $result = $self->taxes->create($data) },
104        qr/overlaps existing date range/,
105        "Fail to create valid_from in tax 1 and valid_to undef"
106    );
107
108    $data->{valid_from} = '2013-01-01';
109    throws_ok(
110        sub { $result = $self->taxes->create($data) },
111        qr/overlaps existing date range/,
112        "Fail to create valid_from in tax 2 and valid_to undef"
113    );
114
115    $data->{valid_from} = '2009-01-01';
116    throws_ok(
117        sub { $result = $self->taxes->create($data) },
118        qr/overlaps existing date range/,
119        "Fail to create valid_from before tax 1 and valid_to undef"
120    );
121
122    $data->{valid_to} = '2010-01-01';
123    throws_ok(
124        sub { $result = $self->taxes->create($data) },
125        qr/overlaps existing date range/,
126        "Fail to create valid_from before tax 1 and valid_to in tax 1"
127    );
128
129    $data->{valid_from} = '2011-01-01';
130    $data->{valid_to}   = '2013-01-01';
131    throws_ok(
132        sub { $result = $self->taxes->create($data) },
133        qr/overlaps existing date range/,
134        "Fail to create valid_from in tax 1 and valid_to in tax 2"
135    );
136
137    $data->{valid_from} = '2011-01-01';
138    $data->{valid_to}   = '2011-01-01';
139    throws_ok(
140        sub { $result = $self->taxes->create($data) },
141        qr/valid_to is not later than valid_from/,
142        "Fail to create valid_from eq valid_to"
143    );
144
145    $data->{valid_from} = '2011-01-01';
146    $data->{valid_to}   = '2010-01-01';
147    throws_ok(
148        sub { $result = $self->taxes->create($data) },
149        qr/valid_to is not later than valid_from/,
150        "Fail to create valid_from > valid_to"
151    );
152
153    cmp_ok( $self->taxes->count, '==', 47, "47 taxes in the table" );
154
155    # calculate tax
156
157    throws_ok(
158        sub { $tax = $self->taxes->current_tax() },
159        qr/tax_name not supplied/,
160        "Exception if no tax_name supplied"
161    );
162
163    throws_ok(
164        sub { $tax = $self->taxes->current_tax('FooBar') },
165        qr/not found.*FooBar/,
166        "Exception if tax_name not found"
167    );
168
169    lives_ok( sub { $tax = $self->taxes->current_tax('MT VAT Standard') },
170        "Get current MT tax" );
171
172    cmp_ok( $tax->calculate( { price => 13.47, tax_included => 1 } ),
173        '==', 2.05, "Tax on gross 13.47 should be 2.05" );
174
175    cmp_ok( $tax->calculate( { price => 13.47, tax_included => 0 } ),
176        '==', 2.42, "Tax on nett 13.47 should be 2.42" );
177
178    lives_ok( sub { $tax = $self->taxes->current_tax('IE VAT Standard') },
179        "Get current IE tax" );
180
181    cmp_ok( $tax->calculate( { price => 13.47, tax_included => 0 } ),
182        '==', 3.10, "Tax on nett 13.47 should be 3.10" );
183
184    set_absolute_time('2011-01-01T00:00:00Z');
185
186        lives_ok( sub { $tax = $self->taxes->current_tax('IE VAT Standard') },
187            "Get IE tax for this historical date" );
188
189        cmp_ok( $tax->calculate( { price => 13.47, tax_included => 0 } ),
190            '==', 2.83, "Tax on nett 13.47 should be 2.83" );
191
192    restore_time();
193
194    lives_ok( sub { $tax = $self->taxes->current_tax('IE VAT Standard') },
195        "Get current IE tax" );
196
197    cmp_ok( $tax->calculate( { price => 13.47, tax_included => 0 } ),
198        '==', 3.10, "Tax on nett 13.47 should be 3.10" );
199
200    # mock time to before any valid ranges
201
202    set_absolute_time('1950-01-01T00:00:00Z');
203
204    throws_ok(
205        sub { $tax = $self->taxes->current_tax('IE VAT Standard') },
206        qr/not found.*IE VAT/,
207        "Exception when tax not found for current date"
208    );
209
210    restore_time();
211
212    # some weird decimal_places/ceil/floor taxes
213
214    $data = {
215        tax_name         => 'testing',
216        description      => 'description',
217        country_iso_code => 'IE',
218        percent          => 21.333,
219        valid_from       => '2010-01-01',
220        decimal_places   => 2,
221    };
222    lives_ok( sub { $tax = $self->taxes->create($data) },
223        "Create 21.33% decimal_places 2" );
224    cmp_ok( $tax->calculate( { price => 13.47 } ),
225        '==', 2.87, "Tax on nett 13.47 should be 2.87" );
226
227    lives_ok( sub { $tax->rounding('f') }, "set rounding floor" );
228    cmp_ok( $tax->rounding, 'eq', 'f', "rounding is f" );
229    cmp_ok( $tax->calculate( { price => 13.47 } ),
230        '==', 2.87, "Tax on nett 13.47 should be 2.87" );
231
232    lives_ok( sub { $tax->rounding('c') }, "set rounding ceiling" );
233    cmp_ok( $tax->rounding, 'eq', 'c', "rounding is c" );
234    cmp_ok( $tax->calculate( { price => 13.47 } ),
235        '==', 2.88, "Tax on nett 13.47 should be 2.88" );
236
237    lives_ok( sub { $tax->rounding(undef) }, "set rounding default" );
238    is( $tax->rounding, undef, "rounding is undef" );
239    lives_ok( sub { $tax->decimal_places(3) }, "set decimal_places 3" );
240    cmp_ok( $tax->calculate( { price => 13.47 } ),
241        '==', 2.874, "Tax on nett 13.47 should be 2.874" );
242
243    lives_ok( sub { $tax->rounding('f') }, "set rounding floor" );
244    cmp_ok( $tax->calculate( { price => 13.47 } ),
245        '==', 2.873, "Tax on nett 13.47 should be 2.873" );
246
247    lives_ok( sub { $tax->rounding('c') }, "set rounding ceiling" );
248    cmp_ok( $tax->calculate( { price => 13.47 } ),
249        '==', 2.874, "Tax on nett 13.47 should be 2.874" );
250
251    # invalid/missing price
252
253    throws_ok( sub { $tax->calculate( { price => "qw" } ) },
254        qr/price.*not.*valid.*qw/, "Exception on invalid price" );
255    throws_ok(
256        sub { $tax->calculate( { price => undef } ) },
257        qr/price is missing/,
258        "Exception on undef price"
259    );
260    throws_ok(
261        sub { $tax->calculate( {} ) },
262        qr/price is missing/,
263        "Exception on no price"
264    );
265
266    # rounding input checks
267
268    $data = {
269        tax_name       => '1',
270        description    => 'description',
271        percent        => 21.333,
272        valid_from     => '2010-01-01',
273        rounding       => 'c',
274        decimal_places => 2,
275    };
276    lives_ok( sub { $tax = $self->taxes->create($data) },
277        "new tax with rounding c" );
278    cmp_ok( $tax->rounding, 'eq', 'c', "rounding is c" );
279
280    my $taxid = $tax->taxes_id;
281
282    throws_ok(
283        sub { $tax->update( { rounding => 2 } ) },
284        qr/value for rounding not/,
285        "fail rounding 2"
286    );
287
288    lives_ok( sub { $tax = $self->taxes->find($taxid) },
289        "reload tax from database" );
290    cmp_ok( $tax->rounding, 'eq', 'c', "rounding is still c" );
291
292    lives_ok( sub { $tax->update( { rounding => 'C' } ) },
293        "set rounding to C" );
294    cmp_ok( $tax->rounding, 'eq', 'c', "rounding is c" );
295    lives_ok( sub { $tax->update( { rounding => 'F' } ) },
296        "set rounding to F" );
297    cmp_ok( $tax->rounding, 'eq', 'f', "rounding is f" );
298
299    # exception when impossible rounding value found in database
300
301    lives_ok {
302        $self->ic6s_schema->storage->dbh_do(
303            sub {
304                my ( $storage, $dbh ) = @_;
305                $dbh->do(q| UPDATE taxes SET rounding='x' WHERE tax_name='1' |);
306            }
307        );
308    }
309    "change rounding to illegal value 'x'";
310
311    lives_ok( sub { $rset = $self->taxes->search( { tax_name => '1' } ) },
312        "search for tax with bad rounding in db" );
313
314    cmp_ok( $rset->count, '==', 1, "found it" );
315
316    $tax = $rset->next;
317    cmp_ok( $tax->rounding, 'eq', 'x', "rounding is x" );
318
319    throws_ok(
320        sub { $tax->calculate( { price => 13.47 } ) },
321        qr/rounding value from database is invalid/,
322        "Throws rounding value from database is invalid"
323    );
324
325    lives_ok( sub { $self->clear_taxes }, "clear_taxes" );
326
327};
328
3291;
330