salestax 0.0.1 → 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/CONTRIBUTING.md +27 -0
- data/LICENSE +24 -0
- data/README.md +272 -5
- data/bin/salestax +1 -1
- data/salestax.gemspec +19 -0
- metadata +4 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c8b0453465806dd4fc525a7f0488247d479005b6
|
4
|
+
data.tar.gz: d2749e2f79a6b87b351ffd412a14f4894218b710
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e4b6272bbc957e80a9695ad788bf30b6471e44140c258bea00a4fd782d82f4ae1b85423e4ea75382d933f0b159f7fa0cf6a43fd61672300df95f61a20035699d
|
7
|
+
data.tar.gz: afb0ea18e520db5e7e92565f0849b71aea83d5f5d93ef42d998b58a1804595c31b078a0707c4a444e39d54c62657644e0e84b5a9c6f2500f3fba578ea266e681
|
data/.gitignore
CHANGED
@@ -0,0 +1 @@
|
|
1
|
+
.gem
|
data/CONTRIBUTING.md
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
## How to contribute
|
2
|
+
|
3
|
+
Fork, then clone the repo:
|
4
|
+
|
5
|
+
git clone git@github.com:your-username/sales-tax.git
|
6
|
+
|
7
|
+
Check you ruby and development dependencies versions:
|
8
|
+
|
9
|
+
$ ruby -v
|
10
|
+
|
11
|
+
$ gem list
|
12
|
+
|
13
|
+
Make sure the tests pass:
|
14
|
+
|
15
|
+
rspec
|
16
|
+
|
17
|
+
Make your change. Add tests for your change. Make the tests pass:
|
18
|
+
|
19
|
+
rspec
|
20
|
+
|
21
|
+
Push to your fork and submit a pull request. Some things that are equal parts necessary/epic:
|
22
|
+
|
23
|
+
* Write tests.
|
24
|
+
|
25
|
+
* Follow a sensible style guide. My pick is [this one](https://github.com/bbatsov/ruby-style-guide).
|
26
|
+
|
27
|
+
* Write a [good commit message](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html).
|
data/LICENSE
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
This is free and unencumbered software released into the public domain.
|
2
|
+
|
3
|
+
Anyone is free to copy, modify, publish, use, compile, sell, or
|
4
|
+
distribute this software, either in source code form or as a compiled
|
5
|
+
binary, for any purpose, commercial or non-commercial, and by any
|
6
|
+
means.
|
7
|
+
|
8
|
+
In jurisdictions that recognize copyright laws, the author or authors
|
9
|
+
of this software dedicate any and all copyright interest in the
|
10
|
+
software to the public domain. We make this dedication for the benefit
|
11
|
+
of the public at large and to the detriment of our heirs and
|
12
|
+
successors. We intend this dedication to be an overt act of
|
13
|
+
relinquishment in perpetuity of all present and future rights to this
|
14
|
+
software under copyright law.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
19
|
+
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
20
|
+
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
21
|
+
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
22
|
+
OTHER DEALINGS IN THE SOFTWARE.
|
23
|
+
|
24
|
+
For more information, please refer to <http://unlicense.org>
|
data/README.md
CHANGED
@@ -3,15 +3,135 @@ Sales Tax
|
|
3
3
|
|
4
4
|
A basic sales tax calculator.
|
5
5
|
|
6
|
-
##
|
6
|
+
## Installation
|
7
7
|
|
8
|
-
|
8
|
+
via RubyGems:
|
9
|
+
|
10
|
+
$ gem install salestax
|
11
|
+
|
12
|
+
|
13
|
+
## Usage
|
14
|
+
|
15
|
+
Pipe in a file with items:
|
16
|
+
|
17
|
+
$ salestax < path/to/file
|
18
|
+
|
19
|
+
An example file with items:
|
20
|
+
|
21
|
+
1, book, 12.49
|
22
|
+
1, music CD, 14.99
|
23
|
+
1, chocolate bar, 0.85
|
24
|
+
|
25
|
+
To which the calculator will print:
|
26
|
+
|
27
|
+
1, book, 12.49
|
28
|
+
1, music CD, 16.49
|
29
|
+
1, chocolate bar, 0.85
|
30
|
+
|
31
|
+
Sales Taxes: 1.50
|
32
|
+
Total: 29.83
|
33
|
+
|
34
|
+
More example files can be found at `data/`
|
35
|
+
|
36
|
+
## Dependencies
|
37
|
+
|
38
|
+
ruby version ~> 2.1.0p0
|
39
|
+
|
40
|
+
To check your version run:
|
41
|
+
|
42
|
+
$ ruby -v
|
43
|
+
|
44
|
+
To learn how to install ruby visit [ruby-lang.org/en/installation/](https://www.ruby-lang.org/en/installation/)
|
45
|
+
|
46
|
+
|
47
|
+
## Troubleshooting
|
48
|
+
|
49
|
+
### Development environment
|
50
|
+
|
51
|
+
* OSX 10.8.5, ruby 2.1.2p95
|
52
|
+
|
53
|
+
Development dependencies:
|
54
|
+
|
55
|
+
rspec ~> 3.1
|
56
|
+
|
57
|
+
To install them along the gem:
|
58
|
+
|
59
|
+
$ gem install --dev salestax
|
60
|
+
|
61
|
+
### Compatible environments
|
62
|
+
|
63
|
+
* Ubuntu 12.04 x32, ruby 2.1.0p0
|
64
|
+
|
65
|
+
### Incompatible environments
|
66
|
+
|
67
|
+
* ruby < 2.1.0
|
68
|
+
|
69
|
+
### Tests
|
70
|
+
|
71
|
+
To run the specs:
|
72
|
+
|
73
|
+
$ rspec
|
74
|
+
|
75
|
+
To run just the integration tests:
|
76
|
+
|
77
|
+
$ rspec spec/features
|
78
|
+
|
79
|
+
## Overview
|
80
|
+
|
81
|
+
The application is designed to read items from `$stdin` and print the receipt to `$stdout`.
|
82
|
+
|
83
|
+
### Input Format
|
84
|
+
|
85
|
+
Items feed into the program are expected to be comma-separated values with the following format:
|
86
|
+
|
87
|
+
<quantity>, <description>, <unit_price>
|
88
|
+
|
89
|
+
Example:
|
90
|
+
|
91
|
+
1, book, 12.49
|
92
|
+
|
93
|
+
Please note the blank space between the commas and `<description>` / `<unit_price>`, since it is required.
|
94
|
+
|
95
|
+
Each row in the input represents a line item of the receipt.
|
96
|
+
|
97
|
+
#### Quantity
|
98
|
+
|
99
|
+
Quantity is expected to be a whole value, thus only digits are allowed.
|
100
|
+
|
101
|
+
#### Description
|
102
|
+
|
103
|
+
Anything but a comma character is allowed in the description. They must contain at least one word character (letter, number, underscore).
|
104
|
+
|
105
|
+
* Imported items should contain 'imported' in the description.
|
106
|
+
|
107
|
+
* Food items should contain 'chocolate' in the description.
|
108
|
+
|
109
|
+
* Medicine items should contain 'headache' in the description.
|
110
|
+
|
111
|
+
* Book items should contain 'book' in the description.
|
9
112
|
|
10
|
-
|
113
|
+
#### Unit price
|
11
114
|
|
12
|
-
|
115
|
+
Unit price is expected to be a decimal value, though it can be a whole one as well. A '.' is expected as the fractional separator.
|
13
116
|
|
14
|
-
|
117
|
+
### Output Format
|
118
|
+
|
119
|
+
A receipt will be printed 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.
|
120
|
+
|
121
|
+
Example:
|
122
|
+
|
123
|
+
1, book, 12.49
|
124
|
+
1, music CD, 16.49
|
125
|
+
1, chocolate bar, 0.85
|
126
|
+
|
127
|
+
Sales Taxes: 1.50
|
128
|
+
Total: 29.83
|
129
|
+
|
130
|
+
### Taxing rules
|
131
|
+
|
132
|
+
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.
|
133
|
+
|
134
|
+
The rounding rules for sales tax are that for a tax rate of n%, a shelf price of p, it contains np/100 (rounded up to the nearest 0.05) amount of sales tax.
|
15
135
|
|
16
136
|
### Examples
|
17
137
|
|
@@ -65,3 +185,150 @@ Output:
|
|
65
185
|
|
66
186
|
Sales Taxes: 6.70
|
67
187
|
Total: 74.68
|
188
|
+
|
189
|
+
## Design
|
190
|
+
|
191
|
+
The application flows in the following way:
|
192
|
+
|
193
|
+
#### 1 Launch
|
194
|
+
|
195
|
+
$ salestax < path/to/file
|
196
|
+
|
197
|
+
##### 1.1 bin/salestax
|
198
|
+
|
199
|
+
The `salestax` in the command is a ruby executable in your load path. This executable contains the following lines:
|
200
|
+
|
201
|
+
```ruby
|
202
|
+
require 'sales_tax'
|
203
|
+
SalesTax::Application.new.run
|
204
|
+
```
|
205
|
+
|
206
|
+
The first line requires the file `./lib/sales_tax.rb` which loads the library. The second line creates and instance of the application.
|
207
|
+
|
208
|
+
##### 1.2 lib/sales_tax/application.rb
|
209
|
+
|
210
|
+
The `#run` method reads in items from the input source (defaults to `$stdin`) and passes them to the parser. The parser returns (might) a structured item which is added to the receipt.
|
211
|
+
|
212
|
+
```ruby
|
213
|
+
def run
|
214
|
+
loop do
|
215
|
+
raw_input = input.gets
|
216
|
+
break unless raw_input
|
217
|
+
raw_input.chomp!
|
218
|
+
line_item = parser.parse(raw_input)
|
219
|
+
receipt.add_item(line_item) if line_item
|
220
|
+
end
|
221
|
+
puts receipt.print
|
222
|
+
end
|
223
|
+
```
|
224
|
+
|
225
|
+
##### 1.2.1 lib/sales_tax/line_item/parser.rb
|
226
|
+
|
227
|
+
`parser.parse()` first calls a `@input_validator` function which checks the formating of the input. It the pre-process the input into a basic hash composed of the quantity, description and price of the item. It finally delegates the heavy lifting to the augmenter function which is in charge of extending the hash with actual taxing info.
|
228
|
+
|
229
|
+
```ruby
|
230
|
+
def parse(_in)
|
231
|
+
@input_validator.call(_in)
|
232
|
+
@augmenter.call(@pre_processor.call(_in))
|
233
|
+
end
|
234
|
+
```
|
235
|
+
|
236
|
+
The `@augmenter` function delegates the actual augmenting feature to the augmenters, which in this case are hard-coded. The augmenters need to respond to the `#augment` message and either return the augmented hash or `nil` if they do not understand it.
|
237
|
+
|
238
|
+
```ruby
|
239
|
+
@augmenter = lambda do |_in|
|
240
|
+
augmented = nil
|
241
|
+
[LineItem::Book, LineItem::Medicine, LineItem::Food].each do |augmenter|
|
242
|
+
augmented = augmenter.augment(_in)
|
243
|
+
break if augmented
|
244
|
+
end
|
245
|
+
augmented || LineItem::Base.augment(_in)
|
246
|
+
end
|
247
|
+
```
|
248
|
+
|
249
|
+
This function will return the first augmented hash it encounters or whatever `LineItem::Base.augment(_in)` returns.
|
250
|
+
|
251
|
+
##### 1.2.1.1 lib/sales_tax/line_item/base.rb
|
252
|
+
|
253
|
+
```ruby
|
254
|
+
def self.augment(attributes)
|
255
|
+
new(attributes).to_hash if attributes[:description] =~ description_matcher
|
256
|
+
end
|
257
|
+
```
|
258
|
+
|
259
|
+
This is the base method which `LineItem::(Food/Book/Medicine)` inherit. It tries to recognize the description in the hash, and if successful it will augment it.
|
260
|
+
|
261
|
+
Augmenting it will actually involve a long series of messages, which I will not cover here. All of them are contained within `lib/sales_tax/line_item/base.rb` and `lib/sales_tax/accountable.rb`
|
262
|
+
|
263
|
+
#### 1.2.2 lib/sales_tax/receipt.rb
|
264
|
+
|
265
|
+
Next line in `Application#run` is:
|
266
|
+
|
267
|
+
```ruby
|
268
|
+
receipt.add_item(line_item) if line_item
|
269
|
+
```
|
270
|
+
|
271
|
+
This simply adds the `line_item`, which is actually a hash, to a @items array in the receipt.
|
272
|
+
|
273
|
+
```ruby
|
274
|
+
def add_item(item)
|
275
|
+
items << item
|
276
|
+
end
|
277
|
+
```
|
278
|
+
|
279
|
+
This will be done for every line item. After all items have been added, the application prints the receipt:
|
280
|
+
|
281
|
+
```ruby
|
282
|
+
puts receipt.print
|
283
|
+
```
|
284
|
+
|
285
|
+
Printing will return a string with all the items printed out, as well as totals and sales tax totals.
|
286
|
+
|
287
|
+
```ruby
|
288
|
+
def print
|
289
|
+
_print = ''
|
290
|
+
_print << print_items
|
291
|
+
_print << "\n"
|
292
|
+
_print << print_totals
|
293
|
+
_print
|
294
|
+
end
|
295
|
+
```
|
296
|
+
|
297
|
+
#### 2 Exit
|
298
|
+
|
299
|
+
After the receipt is printed, the application exits.
|
300
|
+
|
301
|
+
|
302
|
+
## Discussion
|
303
|
+
|
304
|
+
My main design decisions / concerns in no specific order are:
|
305
|
+
|
306
|
+
* `Accountable` depends on a `Taxable` role which only has a private interface. This is weird. A object should not depend on a private interface. The alternative here is to make that interface public (`#category`, `#imported?`, `#unit_price_str`), but that would make the `LineItem::Base` class to expose much more than I like. Another possible solution would be to make `Accountable` a class instead of a module, so that these messages can be passed as arguments. I liked the module approach better, which ended up bitting.
|
307
|
+
|
308
|
+
* There is a pending test for `LineItem::Parser.parse` which should test that the method delegates the augmentation part of it. This behavior is only covered by the integration test. Implementing this test would mean having the dependencies injected into the parser, which is better and that is why the pending test is left there.
|
309
|
+
|
310
|
+
* `Receipt#print` loops two times through the `@items` array. One for printing the items, the second for calculating the totals. I found this to be more readable since it allowed me to break the method into sub-routines.
|
311
|
+
|
312
|
+
* The quantity property of items is ignored since there is not enough information in the spec to decide what should be done with it. There is a [issue hosted on github](https://github.com/matiasanaya/sales-tax/issues/1) for this.
|
313
|
+
|
314
|
+
* I found input inconsistencies which I did not know how to manage. The decision was to re-format the input and open [another issue](https://github.com/matiasanaya/sales-tax/issues/2)
|
315
|
+
|
316
|
+
### Subtleties
|
317
|
+
|
318
|
+
In `spec/shared_examples_for_line_item_base.rb` the following line:
|
319
|
+
|
320
|
+
```
|
321
|
+
it { expect(described_class).to respond_to :augment }
|
322
|
+
```
|
323
|
+
prints:
|
324
|
+
|
325
|
+
SalesTax::LineItem::Base
|
326
|
+
behaves like a line item
|
327
|
+
the public interface
|
328
|
+
should respond to #augment
|
329
|
+
|
330
|
+
This should read `should respond to .augment` since it is a method on the class.
|
331
|
+
|
332
|
+
## Contributing
|
333
|
+
|
334
|
+
View [CONTRIBUTING.md](https://github.com/matiasanaya/sales-tax/blob/master/CONTRIBUTING.md)
|
data/bin/salestax
CHANGED
data/salestax.gemspec
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
Gem::Specification.new do |spec|
|
2
|
+
spec.name = 'salestax'
|
3
|
+
spec.version = '0.0.2'
|
4
|
+
spec.authors = ['Matias Anaya']
|
5
|
+
spec.email = ['matiasanaya@gmail.com']
|
6
|
+
spec.summary = %q{A basic sales tax calculator}
|
7
|
+
spec.description = %q{A basic sales tax calculator utility, that accepts a list of items and prints a receipt with taxes.}
|
8
|
+
spec.homepage = 'https://github.com/matiasanaya/sales-tax'
|
9
|
+
spec.license = 'UNLICENSE'
|
10
|
+
|
11
|
+
spec.files = `git ls-files -z`.split("\x0")
|
12
|
+
spec.executables = ['salestax']
|
13
|
+
spec.test_files = spec.files.grep(%r{^(spec)/})
|
14
|
+
spec.require_paths = ['lib']
|
15
|
+
|
16
|
+
spec.required_ruby_version = '~> 2.1'
|
17
|
+
|
18
|
+
spec.add_development_dependency 'rspec', '~> 3.1'
|
19
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: salestax
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Matias Anaya
|
@@ -34,6 +34,8 @@ extensions: []
|
|
34
34
|
extra_rdoc_files: []
|
35
35
|
files:
|
36
36
|
- ".gitignore"
|
37
|
+
- CONTRIBUTING.md
|
38
|
+
- LICENSE
|
37
39
|
- README.md
|
38
40
|
- bin/salestax
|
39
41
|
- data/example_input_a.txt
|
@@ -48,6 +50,7 @@ files:
|
|
48
50
|
- lib/sales_tax/line_item/medicine.rb
|
49
51
|
- lib/sales_tax/line_item/parser.rb
|
50
52
|
- lib/sales_tax/receipt.rb
|
53
|
+
- salestax.gemspec
|
51
54
|
- spec/accountable_spec.rb
|
52
55
|
- spec/features/sales_tax_spec.rb
|
53
56
|
- spec/line_item_base_spec.rb
|