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