salestax 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +0 -0
- data/README.md +67 -0
- data/bin/salestax +5 -0
- data/data/example_input_a.txt +3 -0
- data/data/example_input_b.txt +2 -0
- data/data/example_input_c.txt +4 -0
- data/lib/sales_tax.rb +1 -0
- data/lib/sales_tax/accountable.rb +57 -0
- data/lib/sales_tax/application.rb +27 -0
- data/lib/sales_tax/line_item/base.rb +45 -0
- data/lib/sales_tax/line_item/book.rb +15 -0
- data/lib/sales_tax/line_item/food.rb +15 -0
- data/lib/sales_tax/line_item/medicine.rb +15 -0
- data/lib/sales_tax/line_item/parser.rb +42 -0
- data/lib/sales_tax/receipt.rb +66 -0
- data/spec/accountable_spec.rb +64 -0
- data/spec/features/sales_tax_spec.rb +60 -0
- data/spec/line_item_base_spec.rb +14 -0
- data/spec/line_item_book_spec.rb +14 -0
- data/spec/line_item_food_spec.rb +14 -0
- data/spec/line_item_medicine_spec.rb +14 -0
- data/spec/line_item_parser_spec.rb +71 -0
- data/spec/receipt_spec.rb +101 -0
- data/spec/shared_examples_for_accountable.rb +9 -0
- data/spec/shared_examples_for_line_item_base.rb +72 -0
- data/spec/shared_examples_for_taxable.rb +16 -0
- metadata +97 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: a0fd7c247bcbb8a37acc5defcc9a57b095a9f0b4
|
4
|
+
data.tar.gz: c2741cf4cbaa3dffbe9f1f2367aa5ce1fe45fe40
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 6bb6e03211201492c8f448b7e9094e5c50e1863f5dacfb30c2e06294bc961d22a4364c9e6d7f432d6ecb22e0317a0e7171ec8a44208580b84ab11b81bc3b00b6
|
7
|
+
data.tar.gz: 11346abbfd2b1a2127ea88e70cb928802ee7aeabaa1ce64c4b50826cdef649efdd54447cc8f89a7f017d690efd442e22960ba78863fb040099a9ef094cb23b40
|
data/.gitignore
ADDED
File without changes
|
data/README.md
ADDED
@@ -0,0 +1,67 @@
|
|
1
|
+
Sales Tax
|
2
|
+
==========
|
3
|
+
|
4
|
+
A basic sales tax calculator.
|
5
|
+
|
6
|
+
## Description
|
7
|
+
|
8
|
+
Basic sales tax is applicable at a rate of 10% on all goods, except books, food, and medical products that are exempt. Import duty is an additional sales tax applicable on all imported goods at a rate of 5%, with no exemptions.
|
9
|
+
|
10
|
+
When I purchase items I receive a receipt that lists the name of all the items and their price (including tax), finishing with the total cost of the items, and the total amounts of sales taxes paid. The rounding rules for sales tax are that for a tax rate of n%, a shelf price of p contains (np/100 rounded up to the nearest 0.05) amount of sales tax.
|
11
|
+
|
12
|
+
Write an application that prints out the receipt details for these shopping carts; this application should be written in Ruby [1] and use Rspec [2] to test inputs and the expected outputs. The output should be to standard out or CSV.
|
13
|
+
|
14
|
+
Proper object orientated design is important. Each row in the input represents a line item of the receipt.
|
15
|
+
|
16
|
+
### Examples
|
17
|
+
|
18
|
+
#### Example A
|
19
|
+
|
20
|
+
Input:
|
21
|
+
|
22
|
+
1, book, 12.49
|
23
|
+
1, music CD, 14.99
|
24
|
+
1, chocolate bar, 0.85
|
25
|
+
|
26
|
+
Output:
|
27
|
+
|
28
|
+
1, book, 12.49
|
29
|
+
1, music CD, 16.49
|
30
|
+
1, chocolate bar, 0.85
|
31
|
+
|
32
|
+
Sales Taxes: 1.50
|
33
|
+
Total: 29.83
|
34
|
+
|
35
|
+
#### Example B
|
36
|
+
|
37
|
+
Input:
|
38
|
+
|
39
|
+
1, imported box of chocolates, 10.00
|
40
|
+
1, imported bottle of perfume, 47.50
|
41
|
+
|
42
|
+
Output:
|
43
|
+
|
44
|
+
1, imported box of chocolates, 10.50
|
45
|
+
1, imported bottle of perfume, 54.65
|
46
|
+
|
47
|
+
Sales Taxes: 7.65
|
48
|
+
Total: 65.15
|
49
|
+
|
50
|
+
#### Example C
|
51
|
+
|
52
|
+
Input:
|
53
|
+
|
54
|
+
1, imported bottle of perfume, 27.99
|
55
|
+
1, bottle of perfume, 18.99
|
56
|
+
1, packet of headache pills, 9.75
|
57
|
+
1, box of imported chocolates, 11.25
|
58
|
+
|
59
|
+
Output:
|
60
|
+
|
61
|
+
1, imported bottle of perfume, 32.19
|
62
|
+
1, bottle of perfume, 20.89
|
63
|
+
1, packet of headache pills, 9.75
|
64
|
+
1, box of imported chocolates, 11.85
|
65
|
+
|
66
|
+
Sales Taxes: 6.70
|
67
|
+
Total: 74.68
|
data/bin/salestax
ADDED
data/lib/sales_tax.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require_relative 'sales_tax/application'
|
@@ -0,0 +1,57 @@
|
|
1
|
+
require 'bigdecimal'
|
2
|
+
|
3
|
+
module SalesTax
|
4
|
+
module Accountable
|
5
|
+
def to_hash
|
6
|
+
accountable_hash
|
7
|
+
end
|
8
|
+
|
9
|
+
protected
|
10
|
+
|
11
|
+
def accountable_hash
|
12
|
+
{
|
13
|
+
unit_sales_tax: unit_sales_tax.to_s('F'),
|
14
|
+
total_unit_price: (unit_price + unit_sales_tax).to_s('F')
|
15
|
+
}
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def unit_sales_tax
|
21
|
+
round_tax(unit_price * sales_tax_rate)
|
22
|
+
end
|
23
|
+
|
24
|
+
def unit_price
|
25
|
+
BigDecimal(unit_price_str)
|
26
|
+
end
|
27
|
+
|
28
|
+
def round_tax(tax)
|
29
|
+
round_factor = BigDecimal('1')/BigDecimal('0.05')
|
30
|
+
(tax * round_factor).ceil/round_factor
|
31
|
+
end
|
32
|
+
|
33
|
+
def sales_tax_rate
|
34
|
+
basic_sales_tax_rate + import_duty_sales_tax_rate
|
35
|
+
end
|
36
|
+
|
37
|
+
def import_duty_sales_tax_rate
|
38
|
+
imported? ? reference_import_duty_sales_tax_rate : BigDecimal('0')
|
39
|
+
end
|
40
|
+
|
41
|
+
def basic_sales_tax_rate
|
42
|
+
basic_sales_tax_exempt? ? BigDecimal('0') : reference_basic_sales_tax_rate
|
43
|
+
end
|
44
|
+
|
45
|
+
def reference_basic_sales_tax_rate
|
46
|
+
BigDecimal('0.1')
|
47
|
+
end
|
48
|
+
|
49
|
+
def reference_import_duty_sales_tax_rate
|
50
|
+
BigDecimal('0.05')
|
51
|
+
end
|
52
|
+
|
53
|
+
def basic_sales_tax_exempt?
|
54
|
+
[:food, :medicine, :book].include?(category)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require_relative 'line_item/parser'
|
2
|
+
require_relative 'receipt'
|
3
|
+
|
4
|
+
module SalesTax
|
5
|
+
class Application
|
6
|
+
def initialize(args = {})
|
7
|
+
@input = args[:input] || $stdin
|
8
|
+
@parser = LineItem::Parser
|
9
|
+
@receipt = Receipt.new
|
10
|
+
end
|
11
|
+
|
12
|
+
def run
|
13
|
+
loop do
|
14
|
+
raw_input = input.gets
|
15
|
+
break unless raw_input
|
16
|
+
raw_input.chomp!
|
17
|
+
line_item = parser.parse(raw_input)
|
18
|
+
receipt.add_item(line_item) if line_item
|
19
|
+
end
|
20
|
+
puts receipt.print
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
attr_reader :input, :parser, :receipt
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require_relative '../accountable'
|
2
|
+
|
3
|
+
module SalesTax
|
4
|
+
module LineItem
|
5
|
+
class Base
|
6
|
+
include SalesTax::Accountable
|
7
|
+
|
8
|
+
def self.augment(attributes)
|
9
|
+
new(attributes).to_hash if attributes[:description] =~ description_matcher
|
10
|
+
end
|
11
|
+
|
12
|
+
def initialize(args = {})
|
13
|
+
@quantity_str = args[:quantity] || ''
|
14
|
+
@description = args[:description]
|
15
|
+
@unit_price_str = args[:unit_price] || ''
|
16
|
+
end
|
17
|
+
|
18
|
+
def to_hash
|
19
|
+
accountable_hash.merge(
|
20
|
+
{
|
21
|
+
quantity: quantity_str,
|
22
|
+
description: description,
|
23
|
+
unit_price: unit_price_str
|
24
|
+
}
|
25
|
+
)
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
attr_reader :quantity_str, :description, :unit_price_str
|
31
|
+
|
32
|
+
def self.description_matcher
|
33
|
+
/\w+/
|
34
|
+
end
|
35
|
+
|
36
|
+
def category
|
37
|
+
:other
|
38
|
+
end
|
39
|
+
|
40
|
+
def imported?
|
41
|
+
/imported/.match(description) ? true : false
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require_relative 'base'
|
2
|
+
require_relative 'book'
|
3
|
+
require_relative 'food'
|
4
|
+
require_relative 'medicine'
|
5
|
+
|
6
|
+
module SalesTax
|
7
|
+
module LineItem
|
8
|
+
module Parser
|
9
|
+
module_function
|
10
|
+
|
11
|
+
FormatError = Class.new(StandardError)
|
12
|
+
|
13
|
+
@input_validator = lambda do |_in|
|
14
|
+
raise FormatError unless _in =~ /\A\d+,[^,]*\w+, \d+(.\d)?\d*\z/
|
15
|
+
end
|
16
|
+
|
17
|
+
@pre_processor = lambda do |_in|
|
18
|
+
quantity_str, description, unit_price_str = _in.split(',')
|
19
|
+
|
20
|
+
{
|
21
|
+
quantity: quantity_str,
|
22
|
+
description: description.strip,
|
23
|
+
unit_price: unit_price_str.strip
|
24
|
+
}
|
25
|
+
end
|
26
|
+
|
27
|
+
@augmenter = lambda do |_in|
|
28
|
+
augmented = nil
|
29
|
+
[LineItem::Book, LineItem::Medicine, LineItem::Food].each do |augmenter|
|
30
|
+
augmented = augmenter.augment(_in)
|
31
|
+
break if augmented
|
32
|
+
end
|
33
|
+
augmented || LineItem::Base.augment(_in)
|
34
|
+
end
|
35
|
+
|
36
|
+
def parse(_in)
|
37
|
+
@input_validator.call(_in)
|
38
|
+
@augmenter.call(@pre_processor.call(_in))
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
module SalesTax
|
2
|
+
class Receipt
|
3
|
+
def initialize(args = {})
|
4
|
+
@items = args[:items] || []
|
5
|
+
end
|
6
|
+
|
7
|
+
def add_item(item)
|
8
|
+
items << item
|
9
|
+
end
|
10
|
+
|
11
|
+
def print
|
12
|
+
_print = ''
|
13
|
+
_print << print_items
|
14
|
+
_print << "\n"
|
15
|
+
_print << print_totals
|
16
|
+
_print
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
attr_accessor :items
|
22
|
+
|
23
|
+
def print_items
|
24
|
+
_items = ''
|
25
|
+
items.each { |i| _items << print_item(i) }
|
26
|
+
_items
|
27
|
+
end
|
28
|
+
|
29
|
+
def print_item(item)
|
30
|
+
taxed_price_str = format_to_money_string(item[:total_unit_price])
|
31
|
+
"#{item[:quantity]}, #{item[:description]}, #{taxed_price_str}\n"
|
32
|
+
end
|
33
|
+
|
34
|
+
def print_totals
|
35
|
+
total, sales_taxes_total = calculate_totals
|
36
|
+
_totals = ''
|
37
|
+
_totals << print_sales_taxes_total(sales_taxes_total)
|
38
|
+
_totals << print_total(total)
|
39
|
+
_totals
|
40
|
+
end
|
41
|
+
|
42
|
+
def calculate_totals
|
43
|
+
total = BigDecimal('0')
|
44
|
+
sales_taxes_total = BigDecimal('0')
|
45
|
+
items.each do |item|
|
46
|
+
total += BigDecimal(item[:total_unit_price])
|
47
|
+
sales_taxes_total += BigDecimal(item[:unit_sales_tax])
|
48
|
+
end
|
49
|
+
[total, sales_taxes_total]
|
50
|
+
end
|
51
|
+
|
52
|
+
def print_sales_taxes_total(bd)
|
53
|
+
st_total = format_to_money_string(bd.to_s('F'))
|
54
|
+
"Sales Taxes: #{st_total}\n"
|
55
|
+
end
|
56
|
+
|
57
|
+
def print_total(bd)
|
58
|
+
total = format_to_money_string(bd.to_s('F'))
|
59
|
+
"Total: #{total}\n"
|
60
|
+
end
|
61
|
+
|
62
|
+
def format_to_money_string(str)
|
63
|
+
sprintf("%.2f", str)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
require_relative '../lib/sales_tax/accountable'
|
2
|
+
require_relative 'shared_examples_for_accountable'
|
3
|
+
require_relative 'shared_examples_for_taxable'
|
4
|
+
|
5
|
+
RSpec.describe SalesTax::Accountable do
|
6
|
+
AccountableDouble = Struct.new(:unit_price_str, :imported?, :category) do
|
7
|
+
include SalesTax::Accountable
|
8
|
+
end
|
9
|
+
|
10
|
+
let(:accountable) {
|
11
|
+
AccountableDouble.new('11.25', true, :other)
|
12
|
+
}
|
13
|
+
|
14
|
+
describe 'AccountableDouble' do
|
15
|
+
let(:object) { accountable }
|
16
|
+
it_behaves_like 'a accountable'
|
17
|
+
it_behaves_like 'a taxable'
|
18
|
+
end
|
19
|
+
|
20
|
+
describe '#to_hash' do
|
21
|
+
describe '[:total_unit_price]' do
|
22
|
+
subject { accountable.to_hash[:unit_sales_tax] }
|
23
|
+
it { is_expected.to eq '1.7'}
|
24
|
+
end
|
25
|
+
|
26
|
+
describe '[:unit_sales_tax]'do
|
27
|
+
subject {
|
28
|
+
AccountableDouble.new('11.25', imported?, category).to_hash[:unit_sales_tax]
|
29
|
+
}
|
30
|
+
|
31
|
+
context 'when basic exempt' do
|
32
|
+
['food', 'medicine', 'book'].each do |cat|
|
33
|
+
context "when #{cat}" do
|
34
|
+
let(:category) { cat.to_sym }
|
35
|
+
|
36
|
+
context 'when local' do
|
37
|
+
let(:imported?) { false }
|
38
|
+
it { is_expected.to eq '0.0' }
|
39
|
+
end
|
40
|
+
|
41
|
+
context 'when imported' do
|
42
|
+
let(:imported?) { true }
|
43
|
+
it { is_expected.to eq '0.6' }
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
context 'when basic applicable' do
|
50
|
+
let(:category) { :other }
|
51
|
+
|
52
|
+
context 'when local' do
|
53
|
+
let(:imported?) { false }
|
54
|
+
it { is_expected.to eq '1.15' }
|
55
|
+
end
|
56
|
+
|
57
|
+
context 'when imported' do
|
58
|
+
let(:imported?) { true }
|
59
|
+
it { is_expected.to eq '1.7' }
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require_relative '../../lib/sales_tax/application'
|
2
|
+
|
3
|
+
RSpec.describe SalesTax::Application do
|
4
|
+
|
5
|
+
let(:app) { lambda { |f| SalesTax::Application.new(input: f).run } }
|
6
|
+
|
7
|
+
context 'when cart is provided' do
|
8
|
+
|
9
|
+
shared_examples 'a receipt printer' do |cart, receipt|
|
10
|
+
it 'prints the correct receipt' do
|
11
|
+
expect { app.call(cart) }.to output(receipt).to_stdout
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
context 'from data/example_input_a.txt' do
|
16
|
+
cart = File.open('data/example_input_a.txt', 'r')
|
17
|
+
|
18
|
+
correct_receipt = <<-END.gsub(/^\s+\|/, '')
|
19
|
+
|1, book, 12.49
|
20
|
+
|1, music CD, 16.49
|
21
|
+
|1, chocolate bar, 0.85
|
22
|
+
|
|
23
|
+
|Sales Taxes: 1.50
|
24
|
+
|Total: 29.83
|
25
|
+
END
|
26
|
+
|
27
|
+
it_behaves_like 'a receipt printer', cart, correct_receipt
|
28
|
+
end
|
29
|
+
|
30
|
+
context 'from data/example_input_b.txt' do
|
31
|
+
cart = File.open('data/example_input_b.txt', 'r')
|
32
|
+
|
33
|
+
correct_receipt = <<-END.gsub(/^\s+\|/, '')
|
34
|
+
|1, imported box of chocolates, 10.50
|
35
|
+
|1, imported bottle of perfume, 54.65
|
36
|
+
|
|
37
|
+
|Sales Taxes: 7.65
|
38
|
+
|Total: 65.15
|
39
|
+
END
|
40
|
+
|
41
|
+
it_behaves_like 'a receipt printer', cart, correct_receipt
|
42
|
+
end
|
43
|
+
|
44
|
+
context 'from data/example_input_c.txt' do
|
45
|
+
cart = File.open('data/example_input_c.txt', 'r')
|
46
|
+
|
47
|
+
correct_receipt = <<-END.gsub(/^\s+\|/, '')
|
48
|
+
|1, imported bottle of perfume, 32.19
|
49
|
+
|1, bottle of perfume, 20.89
|
50
|
+
|1, packet of headache pills, 9.75
|
51
|
+
|1, box of imported chocolates, 11.85
|
52
|
+
|
|
53
|
+
|Sales Taxes: 6.70
|
54
|
+
|Total: 74.68
|
55
|
+
END
|
56
|
+
|
57
|
+
it_behaves_like 'a receipt printer', cart, correct_receipt
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
require_relative '../lib/sales_tax/line_item/base'
|
2
|
+
require_relative 'shared_examples_for_line_item_base'
|
3
|
+
|
4
|
+
RSpec.describe SalesTax::LineItem::Base do
|
5
|
+
matching_h = {
|
6
|
+
description: 'anything'
|
7
|
+
}
|
8
|
+
|
9
|
+
non_matching_h = {
|
10
|
+
description: ''
|
11
|
+
}
|
12
|
+
|
13
|
+
it_behaves_like 'a line item', :other, matching_h, non_matching_h
|
14
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
require_relative '../lib/sales_tax/line_item/book'
|
2
|
+
require_relative 'shared_examples_for_line_item_base'
|
3
|
+
|
4
|
+
RSpec.describe SalesTax::LineItem::Book do
|
5
|
+
matching_h = {
|
6
|
+
description: 'some books'
|
7
|
+
}
|
8
|
+
|
9
|
+
non_matching_h = {
|
10
|
+
description: 'this is not readable'
|
11
|
+
}
|
12
|
+
|
13
|
+
it_behaves_like 'a line item', :book, matching_h, non_matching_h
|
14
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
require_relative '../lib/sales_tax/line_item/food'
|
2
|
+
require_relative 'shared_examples_for_line_item_base'
|
3
|
+
|
4
|
+
RSpec.describe SalesTax::LineItem::Food do
|
5
|
+
matching_h = {
|
6
|
+
description: 'some chocolate fun'
|
7
|
+
}
|
8
|
+
|
9
|
+
non_matching_h = {
|
10
|
+
description: 'non food product'
|
11
|
+
}
|
12
|
+
|
13
|
+
it_behaves_like 'a line item', :food, matching_h, non_matching_h
|
14
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
require_relative '../lib/sales_tax/line_item/medicine'
|
2
|
+
require_relative 'shared_examples_for_line_item_base'
|
3
|
+
|
4
|
+
RSpec.describe SalesTax::LineItem::Medicine do
|
5
|
+
matching_h = {
|
6
|
+
description: 'epic headache relief'
|
7
|
+
}
|
8
|
+
|
9
|
+
non_matching_h = {
|
10
|
+
description: 'not medical stuff'
|
11
|
+
}
|
12
|
+
|
13
|
+
it_behaves_like 'a line item', :medicine, matching_h, non_matching_h
|
14
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
require_relative '../lib/sales_tax/line_item/parser'
|
2
|
+
|
3
|
+
RSpec.describe SalesTax::LineItem::Parser do
|
4
|
+
let(:parser) { SalesTax::LineItem::Parser }
|
5
|
+
|
6
|
+
shared_examples 'a correct parser' do |_in, _out|
|
7
|
+
it 'does the job' do
|
8
|
+
expect(parser.parse(_in)).to include _out
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
shared_examples 'a complaining parser' do |_in|
|
13
|
+
it 'screams' do
|
14
|
+
expect{ parser.parse(_in) }.to raise_exception SalesTax::LineItem::Parser::FormatError
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
it "asks for augmenting of it's parsing"
|
19
|
+
|
20
|
+
context 'when invalid input' do
|
21
|
+
context 'with missing arguments' do
|
22
|
+
it_behaves_like 'a complaining parser',
|
23
|
+
'1, description'
|
24
|
+
end
|
25
|
+
|
26
|
+
context 'with too many arguments' do
|
27
|
+
it_behaves_like 'a complaining parser',
|
28
|
+
'1, description, 12.49, 1'
|
29
|
+
end
|
30
|
+
|
31
|
+
context 'with arguments in wrong order' do
|
32
|
+
it_behaves_like 'a complaining parser',
|
33
|
+
'12.49, description, 1'
|
34
|
+
end
|
35
|
+
|
36
|
+
context 'with no description' do
|
37
|
+
it_behaves_like 'a complaining parser',
|
38
|
+
'1, , 12.49'
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
context 'when valid input' do
|
43
|
+
context 'control' do
|
44
|
+
it_behaves_like 'a correct parser',
|
45
|
+
'1, description, 12.49',
|
46
|
+
{
|
47
|
+
quantity: '1',
|
48
|
+
description: 'description',
|
49
|
+
unit_price: '12.49'
|
50
|
+
}
|
51
|
+
end
|
52
|
+
|
53
|
+
context 'with quantity' do
|
54
|
+
it_behaves_like 'a correct parser',
|
55
|
+
'5, description, 0.0',
|
56
|
+
{quantity: '5'}
|
57
|
+
end
|
58
|
+
|
59
|
+
context 'with description' do
|
60
|
+
it_behaves_like 'a correct parser',
|
61
|
+
'0, a imported description, 0.0',
|
62
|
+
{description: 'a imported description'}
|
63
|
+
end
|
64
|
+
|
65
|
+
context 'with unit price' do
|
66
|
+
it_behaves_like 'a correct parser',
|
67
|
+
'0, description, 12.49',
|
68
|
+
{unit_price: '12.49'}
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
require_relative '../lib/sales_tax/receipt'
|
2
|
+
|
3
|
+
RSpec.describe SalesTax::Receipt do
|
4
|
+
let(:receipt) { SalesTax::Receipt.new }
|
5
|
+
|
6
|
+
describe 'the public interface' do
|
7
|
+
subject{ receipt }
|
8
|
+
it { is_expected.to respond_to :add_item, :print }
|
9
|
+
end
|
10
|
+
|
11
|
+
describe '#add_item' do
|
12
|
+
it 'adds item to items list' do
|
13
|
+
item = Object.new
|
14
|
+
receipt.add_item(item)
|
15
|
+
expect(receipt.instance_variable_get(:@items)).to include(item)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
describe '#print' do
|
20
|
+
subject { receipt.print }
|
21
|
+
it { is_expected.to be_instance_of String }
|
22
|
+
|
23
|
+
context 'in general' do
|
24
|
+
let(:receipt) {
|
25
|
+
items = [
|
26
|
+
{
|
27
|
+
quantity: '1',
|
28
|
+
description: 'a item',
|
29
|
+
unit_price: '11.1',
|
30
|
+
unit_sales_tax: '0',
|
31
|
+
total_unit_price: '11.1'
|
32
|
+
},
|
33
|
+
]
|
34
|
+
|
35
|
+
SalesTax::Receipt.new(items: items)
|
36
|
+
}
|
37
|
+
|
38
|
+
it 'formats numbers to two decimal places' do
|
39
|
+
expect(subject).to match /11\.10\n\n.+0\.00/
|
40
|
+
end
|
41
|
+
|
42
|
+
it 'matches exactly' do
|
43
|
+
expect(subject).to match <<-END.gsub(/^\s+\|/, '')
|
44
|
+
|1, a item, 11.10
|
45
|
+
|
|
46
|
+
|Sales Taxes: 0.00
|
47
|
+
|Total: 11.10
|
48
|
+
END
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
context 'with two items' do
|
53
|
+
let(:receipt) {
|
54
|
+
items = [
|
55
|
+
{
|
56
|
+
quantity: '1',
|
57
|
+
description: 'a description',
|
58
|
+
unit_price: '12.49',
|
59
|
+
unit_sales_tax: '0.65',
|
60
|
+
total_unit_price: '13.14'
|
61
|
+
},
|
62
|
+
{
|
63
|
+
quantity: '1',
|
64
|
+
description: 'another description',
|
65
|
+
unit_price: '18.99',
|
66
|
+
unit_sales_tax: '1.9',
|
67
|
+
total_unit_price: '20.89'
|
68
|
+
}
|
69
|
+
]
|
70
|
+
|
71
|
+
SalesTax::Receipt.new(items: items)
|
72
|
+
}
|
73
|
+
|
74
|
+
it 'prints every item' do
|
75
|
+
expect(subject).to match /a description.+\n.+another description/
|
76
|
+
end
|
77
|
+
|
78
|
+
it 'prints taxed prices' do
|
79
|
+
expect(subject).to match /13\.14\n.+20\.89/
|
80
|
+
end
|
81
|
+
|
82
|
+
it 'prints a sales taxes total' do
|
83
|
+
expect(subject).to match /2\.55/
|
84
|
+
end
|
85
|
+
|
86
|
+
it 'prints a total' do
|
87
|
+
expect(subject).to match /34\.03/
|
88
|
+
end
|
89
|
+
|
90
|
+
it 'matches exactly' do
|
91
|
+
expect(subject).to match <<-END.gsub(/^\s+\|/, '')
|
92
|
+
|1, a description, 13.14
|
93
|
+
|1, another description, 20.89
|
94
|
+
|
|
95
|
+
|Sales Taxes: 2.55
|
96
|
+
|Total: 34.03
|
97
|
+
END
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
RSpec.shared_examples 'a line item' do |category, matching_h, non_matching_h|
|
2
|
+
let(:object) {
|
3
|
+
described_class.new(
|
4
|
+
quantity: '1',
|
5
|
+
description: description,
|
6
|
+
unit_price: '1'
|
7
|
+
)
|
8
|
+
}
|
9
|
+
|
10
|
+
describe 'the public interface' do
|
11
|
+
it { expect(described_class).to respond_to :augment }
|
12
|
+
let(:description) { '' }
|
13
|
+
it { expect(object).to respond_to :to_hash }
|
14
|
+
end
|
15
|
+
|
16
|
+
describe '.augment' do
|
17
|
+
context 'with a matching hash' do
|
18
|
+
it 'returns a augmented hash' do
|
19
|
+
expect(described_class.augment(matching_h)).to be_instance_of Hash
|
20
|
+
end
|
21
|
+
end
|
22
|
+
context 'with non-matching hash' do
|
23
|
+
it 'returns nil' do
|
24
|
+
expect(described_class.augment(non_matching_h)).to be_nil
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
context 'with a standard description' do
|
30
|
+
let(:description) { 'a description' }
|
31
|
+
|
32
|
+
it_behaves_like 'a accountable'
|
33
|
+
it_behaves_like 'a taxable'
|
34
|
+
|
35
|
+
describe '#to_hash' do
|
36
|
+
subject { object.to_hash }
|
37
|
+
|
38
|
+
let(:expected_hash) {
|
39
|
+
{
|
40
|
+
quantity: '1',
|
41
|
+
description: 'a description'
|
42
|
+
}
|
43
|
+
}
|
44
|
+
|
45
|
+
it { is_expected.to be_instance_of Hash }
|
46
|
+
it { is_expected.to include expected_hash }
|
47
|
+
end
|
48
|
+
|
49
|
+
describe '#category' do
|
50
|
+
it 'returns the correct symbol' do
|
51
|
+
expect(object.send(:category)).to eq category
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
describe '#imported?' do
|
57
|
+
subject { object.send(:imported?) }
|
58
|
+
|
59
|
+
context 'when imported' do
|
60
|
+
let(:description) {'a imported description'}
|
61
|
+
it { is_expected.to be }
|
62
|
+
|
63
|
+
let(:description) {'imported description'}
|
64
|
+
it { is_expected.to be }
|
65
|
+
end
|
66
|
+
|
67
|
+
context 'when local' do
|
68
|
+
let(:description) {'a local description'}
|
69
|
+
it { is_expected.to be_falsey }
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
RSpec.shared_examples 'a taxable' do
|
2
|
+
describe '#unit_price_str' do
|
3
|
+
subject { object.send(:unit_price_str) }
|
4
|
+
it { is_expected.to be_instance_of String }
|
5
|
+
end
|
6
|
+
|
7
|
+
describe '#imported?' do
|
8
|
+
subject { object.send(:imported?) }
|
9
|
+
it { is_expected.to satisfy { |v| v.instance_of?(TrueClass) || v.instance_of?(FalseClass) } }
|
10
|
+
end
|
11
|
+
|
12
|
+
describe '#category' do
|
13
|
+
subject { object.send(:category) }
|
14
|
+
it { is_expected.to be_instance_of Symbol }
|
15
|
+
end
|
16
|
+
end
|
metadata
ADDED
@@ -0,0 +1,97 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: salestax
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Matias Anaya
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-09-16 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rspec
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '3.1'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '3.1'
|
27
|
+
description: A basic sales tax calculator utility, that accepts a list of items and
|
28
|
+
prints a receipt with taxes.
|
29
|
+
email:
|
30
|
+
- matiasanaya@gmail.com
|
31
|
+
executables:
|
32
|
+
- salestax
|
33
|
+
extensions: []
|
34
|
+
extra_rdoc_files: []
|
35
|
+
files:
|
36
|
+
- ".gitignore"
|
37
|
+
- README.md
|
38
|
+
- bin/salestax
|
39
|
+
- data/example_input_a.txt
|
40
|
+
- data/example_input_b.txt
|
41
|
+
- data/example_input_c.txt
|
42
|
+
- lib/sales_tax.rb
|
43
|
+
- lib/sales_tax/accountable.rb
|
44
|
+
- lib/sales_tax/application.rb
|
45
|
+
- lib/sales_tax/line_item/base.rb
|
46
|
+
- lib/sales_tax/line_item/book.rb
|
47
|
+
- lib/sales_tax/line_item/food.rb
|
48
|
+
- lib/sales_tax/line_item/medicine.rb
|
49
|
+
- lib/sales_tax/line_item/parser.rb
|
50
|
+
- lib/sales_tax/receipt.rb
|
51
|
+
- spec/accountable_spec.rb
|
52
|
+
- spec/features/sales_tax_spec.rb
|
53
|
+
- spec/line_item_base_spec.rb
|
54
|
+
- spec/line_item_book_spec.rb
|
55
|
+
- spec/line_item_food_spec.rb
|
56
|
+
- spec/line_item_medicine_spec.rb
|
57
|
+
- spec/line_item_parser_spec.rb
|
58
|
+
- spec/receipt_spec.rb
|
59
|
+
- spec/shared_examples_for_accountable.rb
|
60
|
+
- spec/shared_examples_for_line_item_base.rb
|
61
|
+
- spec/shared_examples_for_taxable.rb
|
62
|
+
homepage: https://github.com/matiasanaya/sales-tax
|
63
|
+
licenses:
|
64
|
+
- UNLICENSE
|
65
|
+
metadata: {}
|
66
|
+
post_install_message:
|
67
|
+
rdoc_options: []
|
68
|
+
require_paths:
|
69
|
+
- lib
|
70
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
71
|
+
requirements:
|
72
|
+
- - "~>"
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: '2.1'
|
75
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
76
|
+
requirements:
|
77
|
+
- - ">="
|
78
|
+
- !ruby/object:Gem::Version
|
79
|
+
version: '0'
|
80
|
+
requirements: []
|
81
|
+
rubyforge_project:
|
82
|
+
rubygems_version: 2.2.2
|
83
|
+
signing_key:
|
84
|
+
specification_version: 4
|
85
|
+
summary: A basic sales tax calculator
|
86
|
+
test_files:
|
87
|
+
- spec/accountable_spec.rb
|
88
|
+
- spec/features/sales_tax_spec.rb
|
89
|
+
- spec/line_item_base_spec.rb
|
90
|
+
- spec/line_item_book_spec.rb
|
91
|
+
- spec/line_item_food_spec.rb
|
92
|
+
- spec/line_item_medicine_spec.rb
|
93
|
+
- spec/line_item_parser_spec.rb
|
94
|
+
- spec/receipt_spec.rb
|
95
|
+
- spec/shared_examples_for_accountable.rb
|
96
|
+
- spec/shared_examples_for_line_item_base.rb
|
97
|
+
- spec/shared_examples_for_taxable.rb
|