panier 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +22 -0
- data/Gemfile +3 -0
- data/LICENSE.txt +22 -0
- data/README.md +33 -0
- data/Rakefile +6 -0
- data/bin/panier +14 -0
- data/docs/application.md +9 -0
- data/docs/domain_model.md +21 -0
- data/docs/examples.md +45 -0
- data/docs/implementation_notes.md +39 -0
- data/lib/panier/application/cli.rb +72 -0
- data/lib/panier/application/input_reader.rb +41 -0
- data/lib/panier/application/sales_tax_service.rb +28 -0
- data/lib/panier/decorators/decorator.rb +25 -0
- data/lib/panier/decorators/receipt_decorator.rb +29 -0
- data/lib/panier/domain/line_item.rb +85 -0
- data/lib/panier/domain/product.rb +33 -0
- data/lib/panier/domain/product_service.rb +47 -0
- data/lib/panier/domain/receipt.rb +40 -0
- data/lib/panier/domain/round_up_rounding.rb +38 -0
- data/lib/panier/domain/tax_class.rb +28 -0
- data/lib/panier/version.rb +4 -0
- data/lib/panier.rb +22 -0
- data/panier.gemspec +29 -0
- data/spec/factories/line_item.rb +17 -0
- data/spec/factories/product.rb +16 -0
- data/spec/factories/receipt.rb +21 -0
- data/spec/factories/tax_class.rb +8 -0
- data/spec/panier/application/input_reader_spec.rb +21 -0
- data/spec/panier/application/sales_tax_service_spec.rb +83 -0
- data/spec/panier/decorators/receipt_decorator_spec.rb +24 -0
- data/spec/panier/domain/line_item_spec.rb +100 -0
- data/spec/panier/domain/product_spec.rb +28 -0
- data/spec/panier/domain/receipt_spec.rb +33 -0
- data/spec/panier/domain/round_up_rounding_spec.rb +26 -0
- data/spec/panier/domain/tax_class_spec.rb +21 -0
- data/spec/spec_helper.rb +14 -0
- metadata +207 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 6d71eb654f6c9e4f3d3812729a1ed98f61fd62de
|
4
|
+
data.tar.gz: 61e6d3d591c2f9f657213b8b64184c749930304d
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: f407468ecdcd83e83550a0145d97a69426bb87c821de09ffb74a5881cf07f6700388a2d194bc686b904a078cf9b30ae1907b25bb893bda7cf3d31794b98eaeb7
|
7
|
+
data.tar.gz: daf083bdab9397125c3b7b9bc3df7fe1105d64ded6d0856f3bfc2b9bf751162d6461c9f5d49554229c283a8c4fca97b156117490fbf6aeeabd6e6b67730d0ec7
|
data/.gitignore
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
*.gem
|
2
|
+
*.rbc
|
3
|
+
.bundle
|
4
|
+
.config
|
5
|
+
.yardoc
|
6
|
+
Gemfile.lock
|
7
|
+
InstalledFiles
|
8
|
+
_yardoc
|
9
|
+
coverage
|
10
|
+
doc/
|
11
|
+
lib/bundler/man
|
12
|
+
pkg
|
13
|
+
rdoc
|
14
|
+
spec/reports
|
15
|
+
test/tmp
|
16
|
+
test/version_tmp
|
17
|
+
tmp
|
18
|
+
*.bundle
|
19
|
+
*.so
|
20
|
+
*.o
|
21
|
+
*.a
|
22
|
+
mkmf.log
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2014 Luke Eller
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
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
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
# Panier
|
2
|
+
|
3
|
+
This gem demonstrates the calculation of sales taxes for various kinds of line items on a receipt.
|
4
|
+
|
5
|
+
## Documentation
|
6
|
+
|
7
|
+
- [Application](docs/application.md)
|
8
|
+
- [Domain model](docs/domain_model.md)
|
9
|
+
- [Implementation notes](docs/implementation_notes.md)
|
10
|
+
- [Examples](docs/examples.md)
|
11
|
+
|
12
|
+
## Requirements
|
13
|
+
|
14
|
+
* Ruby 2.1.2+
|
15
|
+
|
16
|
+
## Installation
|
17
|
+
|
18
|
+
Add this line to your application's Gemfile:
|
19
|
+
|
20
|
+
gem 'panier'
|
21
|
+
|
22
|
+
And then execute:
|
23
|
+
|
24
|
+
$ bundle
|
25
|
+
|
26
|
+
Or install it yourself as:
|
27
|
+
|
28
|
+
$ gem install panier
|
29
|
+
|
30
|
+
## Usage
|
31
|
+
|
32
|
+
TODO: Write usage instructions here
|
33
|
+
|
data/Rakefile
ADDED
data/bin/panier
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
if RUBY_VERSION >= '2.1.2'
|
4
|
+
$LOAD_PATH.unshift(File.dirname(File.realpath(__FILE__)) + '/../lib')
|
5
|
+
|
6
|
+
require 'panier'
|
7
|
+
|
8
|
+
cli = Panier::Application::CLI.new
|
9
|
+
|
10
|
+
exit cli.run
|
11
|
+
else
|
12
|
+
puts 'Panier supports only Ruby 2.1.2+'
|
13
|
+
exit(-1)
|
14
|
+
end
|
data/docs/application.md
ADDED
@@ -0,0 +1,9 @@
|
|
1
|
+
# Application
|
2
|
+
|
3
|
+
## Sales tax service
|
4
|
+
|
5
|
+
This service is the core application service, fulfilling the core requirement of accepting input in the form of a list of products and producing a receipt in CSV format.
|
6
|
+
|
7
|
+
## CLI
|
8
|
+
|
9
|
+
The command line interface provides an interactive console application, allowing users to enter input data directly and view a receipt.
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# Domain model
|
2
|
+
|
3
|
+
## Receipt
|
4
|
+
|
5
|
+
A receipt is a value object describing a payment that has been made by a shopper to a merchant in relation to an order.
|
6
|
+
|
7
|
+
## Line item
|
8
|
+
|
9
|
+
A line item is a value object representing a single line a receipt. It describes the item represented and contains a reference to it, has a quantity, a unit amount, a tax class and can calculate its total amount and total tax.
|
10
|
+
|
11
|
+
## Product
|
12
|
+
|
13
|
+
A product is a purchasable item with a price and one or more tax classes that allow taxes to be calculated accurately.
|
14
|
+
|
15
|
+
## Tax class
|
16
|
+
|
17
|
+
A tax class is a value object that describes a particular type of tax or duty applicable to products sold. It has a name and a rate.
|
18
|
+
|
19
|
+
Its association with a product means that the orders made for that product are subject to tax at the rate represented by the tax class.
|
20
|
+
|
21
|
+
References to tax classes are held by a line item and used in the calculation of tax for that item.
|
data/docs/examples.md
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
# Input
|
2
|
+
|
3
|
+
## Input 1
|
4
|
+
Quantity, Product, Price
|
5
|
+
1, book, 12.49
|
6
|
+
1, music CD, 14.99
|
7
|
+
1, chocolate bar, 0.85
|
8
|
+
|
9
|
+
## Input 2
|
10
|
+
Quantity, Product, Price
|
11
|
+
1, imported box of chocolates, 10.00
|
12
|
+
1, imported bottle of perfume, 47.50
|
13
|
+
|
14
|
+
## Input 3
|
15
|
+
Quantity, Product, Price
|
16
|
+
1, imported bottle of perfume, 27.99
|
17
|
+
1, bottle of perfume, 18.99
|
18
|
+
1, packet of headache pills, 9.75
|
19
|
+
1, box of imported chocolates, 11.25
|
20
|
+
|
21
|
+
# Output
|
22
|
+
|
23
|
+
## Output 1
|
24
|
+
1, book, 12.49
|
25
|
+
1, music CD, 16.49
|
26
|
+
1, chocolate bar, 0.85
|
27
|
+
|
28
|
+
Sales Taxes: 1.50
|
29
|
+
Total: 29.83
|
30
|
+
|
31
|
+
## Output 2
|
32
|
+
1, imported box of chocolates, 10.50
|
33
|
+
1, imported bottle of perfume, 54.65
|
34
|
+
|
35
|
+
Sales Taxes: 7.65
|
36
|
+
Total: 65.15
|
37
|
+
|
38
|
+
## Output 3
|
39
|
+
1, imported bottle of perfume, 32.19
|
40
|
+
1, bottle of perfume, 20.89
|
41
|
+
1, packet of headache pills, 9.75
|
42
|
+
1, box of imported chocolates, 11.85
|
43
|
+
|
44
|
+
Sales Taxes: 6.70
|
45
|
+
Total: 74.68
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# Implementation notes
|
2
|
+
|
3
|
+
## Style
|
4
|
+
|
5
|
+
Because no web interface is necessary to demonstrate the core application code, the application has been implemented as a standalone gem with a command line interface.
|
6
|
+
|
7
|
+
## Assumptions
|
8
|
+
|
9
|
+
With regard to the given coding exercise, the following assumptions have been made.
|
10
|
+
|
11
|
+
* We are interested in calculating tax for the purposes of generating an itemised receipt.
|
12
|
+
* We assume a preexisting product catalog, where each product contains one more more tax classes.
|
13
|
+
* Products may be selected by the shopper from the preexisting product catalog. This catalog is limited to the items in the test data. Unknown items are ignored.
|
14
|
+
* The product name is the same for input and output. Some changes to input data had to be made to allow for discrepancies. TODO: Name the changes.
|
15
|
+
* There are some products with the same name and different prices. We can assume that these items are available in various sizes.
|
16
|
+
|
17
|
+
## Design considerations
|
18
|
+
|
19
|
+
### Input
|
20
|
+
|
21
|
+
The input is considered to be a selection of products from a preexisting catalog. The items are looked from the internal product service based on name and price. If neither match, no item is returned.
|
22
|
+
|
23
|
+
### Order workflow
|
24
|
+
|
25
|
+
In a real world application, the order domain class would have more states. For the purposes of this demonstration, only two states are defined: _awaiting payment_ and _completed_.
|
26
|
+
|
27
|
+
### Line items
|
28
|
+
|
29
|
+
The line item keeps a reference to the product's price and tax classes so that if the mutable product should change, the line item is unaffected. This is important because once a contract has been established between a merchant and a shopper via an order, the terms of that contract should not be changed.
|
30
|
+
|
31
|
+
The tax rounding calculation originally took place within the line item class. It was decided that making the policy of tax rounding more explicit by representing it in a standalone strategy object would be more expressive.
|
32
|
+
|
33
|
+
### Product service
|
34
|
+
|
35
|
+
This is an in-memory implementation, useful for testing, which contains only the products from the given input data. A real-world implementation would be backed by a database or web service.
|
36
|
+
|
37
|
+
### Decorator
|
38
|
+
|
39
|
+
The receipt decorator was created to separate the concern of formatting a receipt for display in CSV format from the receipt domain object.
|
@@ -0,0 +1,72 @@
|
|
1
|
+
include Panier::Domain
|
2
|
+
|
3
|
+
module Panier
|
4
|
+
module Application
|
5
|
+
##
|
6
|
+
# A class responsible for handling the command line interface input.
|
7
|
+
#
|
8
|
+
class CLI
|
9
|
+
EXIT_SUCCESS = 0
|
10
|
+
EXIT_FAILURE = -1
|
11
|
+
|
12
|
+
def initialize
|
13
|
+
I18n.enforce_available_locales = false
|
14
|
+
@service = SalesTaxService.new
|
15
|
+
end
|
16
|
+
|
17
|
+
##
|
18
|
+
# The main application loop.
|
19
|
+
#
|
20
|
+
def run
|
21
|
+
begin
|
22
|
+
print_welcome
|
23
|
+
loop do
|
24
|
+
prompt_for_input
|
25
|
+
end
|
26
|
+
rescue SignalException, Interrupt
|
27
|
+
puts "\nExiting..."
|
28
|
+
end
|
29
|
+
|
30
|
+
EXIT_SUCCESS
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
##
|
36
|
+
# Prints a welcome message and instructions about how to quit.
|
37
|
+
#
|
38
|
+
def print_welcome
|
39
|
+
puts "Welcome to Panier.\n"
|
40
|
+
puts 'Press Ctrl+C to exit.'
|
41
|
+
end
|
42
|
+
|
43
|
+
##
|
44
|
+
# Asks the user for input and processes it when given.
|
45
|
+
#
|
46
|
+
def prompt_for_input
|
47
|
+
puts 'Enter some sample input, then leave a blank line to proceed.'
|
48
|
+
|
49
|
+
input = []
|
50
|
+
$stdin.each do |line|
|
51
|
+
break if line.nil? || line.chomp.empty?
|
52
|
+
input << line.chomp
|
53
|
+
end
|
54
|
+
|
55
|
+
input = input.join("\n")
|
56
|
+
process_input(input) unless input.empty?
|
57
|
+
end
|
58
|
+
|
59
|
+
##
|
60
|
+
# Given a complete set of input, prints a receipt.
|
61
|
+
#
|
62
|
+
def process_input(input)
|
63
|
+
begin
|
64
|
+
puts @service.evaluate_input(input)
|
65
|
+
rescue ArgumentError
|
66
|
+
$stderr.puts 'The input was invalid.'
|
67
|
+
end
|
68
|
+
puts
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'csv'
|
2
|
+
|
3
|
+
include Panier::Domain
|
4
|
+
|
5
|
+
module Panier
|
6
|
+
module Application
|
7
|
+
##
|
8
|
+
# The InputReader is responsible for parsing raw input data.
|
9
|
+
#
|
10
|
+
class InputReader
|
11
|
+
CELLS_PER_LINE = 3
|
12
|
+
HEADER = /quantity.*?,.*?product.*?,.*?price/i
|
13
|
+
|
14
|
+
def initialize(product_service = nil)
|
15
|
+
@product_service = product_service ||
|
16
|
+
Panier::Domain::ProductService.new
|
17
|
+
end
|
18
|
+
|
19
|
+
def parse_input(input)
|
20
|
+
line_items = input.lines.reject(&:blank?).map do |line|
|
21
|
+
parse_line(line)
|
22
|
+
end
|
23
|
+
line_items.reject(&:nil?)
|
24
|
+
end
|
25
|
+
|
26
|
+
def parse_line(line)
|
27
|
+
return nil if line.match(HEADER)
|
28
|
+
parsed = CSV.parse_line(line)
|
29
|
+
unless parsed.count == CELLS_PER_LINE
|
30
|
+
fail ArgumentError, 'invalid input'
|
31
|
+
end
|
32
|
+
quantity = Integer(parsed[0])
|
33
|
+
name = parsed[1].strip
|
34
|
+
price = Money.new(Float(parsed[2]) * 100)
|
35
|
+
|
36
|
+
product = @product_service.find_by_name_and_price(name, price)
|
37
|
+
product.present? ? LineItem.new(product, quantity) : nil
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# Encoding: utf-8
|
2
|
+
include Panier::Domain
|
3
|
+
|
4
|
+
module Panier
|
5
|
+
module Application
|
6
|
+
##
|
7
|
+
# This is an application layer service responsible for handling the
|
8
|
+
# use-case of taking a list of items and producing a receipt.
|
9
|
+
#
|
10
|
+
class SalesTaxService
|
11
|
+
def initialize(input_reader = nil)
|
12
|
+
@input_reader = input_reader || InputReader.new
|
13
|
+
end
|
14
|
+
|
15
|
+
##
|
16
|
+
# Accepts a list of products and produces a receipt.
|
17
|
+
#
|
18
|
+
# @param [String] input A list of products in CSV format.
|
19
|
+
# @param [String] A receipt in CSV format.
|
20
|
+
def evaluate_input(input)
|
21
|
+
line_items = @input_reader.parse_input(input)
|
22
|
+
receipt = Receipt.new(line_items)
|
23
|
+
decorator = Panier::Decorators::ReceiptDecorator.new(receipt)
|
24
|
+
decorator.to_csv
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# Encoding: utf-8
|
2
|
+
|
3
|
+
module Panier
|
4
|
+
module Decorators
|
5
|
+
##
|
6
|
+
# Defines basic behaviour common to decorators.
|
7
|
+
#
|
8
|
+
module Decorator
|
9
|
+
attr_reader :decorated
|
10
|
+
|
11
|
+
def initialize(decorated)
|
12
|
+
@decorated = decorated
|
13
|
+
end
|
14
|
+
|
15
|
+
def method_missing(symbol, *args, &block)
|
16
|
+
super unless @decorated.respond_to? symbol
|
17
|
+
@decorated.send(symbol, *args, &block)
|
18
|
+
end
|
19
|
+
|
20
|
+
def respond_to_missing?(name, include_private = false)
|
21
|
+
@decorated.respond_to?(name, include_private) || super
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# Encoding: utf-8
|
2
|
+
require 'csv'
|
3
|
+
|
4
|
+
module Panier
|
5
|
+
module Decorators
|
6
|
+
##
|
7
|
+
# Decorates a receipt with presentation-specific methods.
|
8
|
+
#
|
9
|
+
class ReceiptDecorator
|
10
|
+
include Decorator
|
11
|
+
|
12
|
+
##
|
13
|
+
# Generates CSV string expressing the details of the receipt.
|
14
|
+
#
|
15
|
+
# @return [String] CSV expressing the details of the receipt.
|
16
|
+
def to_csv
|
17
|
+
::CSV.generate do |csv|
|
18
|
+
decorated.line_items.each do |item|
|
19
|
+
csv << [item.quantity, " #{item.description}",
|
20
|
+
" #{item.total_amount_inc_tax}"]
|
21
|
+
end
|
22
|
+
csv << []
|
23
|
+
csv << ["Sales Taxes: #{total_tax}"]
|
24
|
+
csv << ["Total: #{total_amount}"]
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
# Encoding: utf-8
|
2
|
+
require 'money'
|
3
|
+
|
4
|
+
module Panier
|
5
|
+
module Domain
|
6
|
+
##
|
7
|
+
# A line item is a value object representing a single line of an order or
|
8
|
+
# receipt.
|
9
|
+
#
|
10
|
+
class LineItem
|
11
|
+
##
|
12
|
+
# The fractional value to which tax rounding calculations are made.
|
13
|
+
#
|
14
|
+
TAX_ROUNDING_VALUE = 5
|
15
|
+
|
16
|
+
attr_reader :product, :quantity, :unit_amount, :tax_classes, :description
|
17
|
+
|
18
|
+
##
|
19
|
+
# Initializes the line such that it represents the given quantity of
|
20
|
+
# products.
|
21
|
+
#
|
22
|
+
# @param product [Product] The product represented in the line item.
|
23
|
+
# @param quantity [Integer] The number of products represented.
|
24
|
+
def initialize(product, quantity)
|
25
|
+
@product = product
|
26
|
+
self.quantity = quantity
|
27
|
+
@rounding_strategy = RoundUpRounding.new(TAX_ROUNDING_VALUE)
|
28
|
+
@description = product.name
|
29
|
+
@unit_amount = product.price
|
30
|
+
@tax_classes = product.tax_classes.dup
|
31
|
+
end
|
32
|
+
|
33
|
+
##
|
34
|
+
# Calculates the total value of the line item.
|
35
|
+
#
|
36
|
+
def total_amount
|
37
|
+
unit_amount * quantity
|
38
|
+
end
|
39
|
+
|
40
|
+
##
|
41
|
+
# Calculates the total tax included in the line item.
|
42
|
+
#
|
43
|
+
def total_tax
|
44
|
+
unit_tax * quantity
|
45
|
+
end
|
46
|
+
|
47
|
+
##
|
48
|
+
# Calculates the total value of the line item including tax.
|
49
|
+
#
|
50
|
+
# @return [Money] The total value of the line item including tax.
|
51
|
+
def total_amount_inc_tax
|
52
|
+
unit_amount_inc_tax * quantity
|
53
|
+
end
|
54
|
+
|
55
|
+
##
|
56
|
+
# Calculates the value of a single unit including tax.
|
57
|
+
#
|
58
|
+
def unit_amount_inc_tax
|
59
|
+
unit_amount + unit_tax
|
60
|
+
end
|
61
|
+
|
62
|
+
##
|
63
|
+
# Calculates the tax applicable to one unit of the line item.
|
64
|
+
#
|
65
|
+
def unit_tax
|
66
|
+
tax = Money.new(0)
|
67
|
+
tax_classes.each do |tax_class|
|
68
|
+
class_tax = @rounding_strategy.round(tax_class.rate * unit_amount)
|
69
|
+
tax += class_tax
|
70
|
+
end
|
71
|
+
tax
|
72
|
+
end
|
73
|
+
|
74
|
+
private
|
75
|
+
|
76
|
+
def quantity=(quantity)
|
77
|
+
unless quantity.is_a? Integer
|
78
|
+
fail ArgumentError, ':quantity must be a whole number'
|
79
|
+
end
|
80
|
+
fail ArgumentError, ':quantity must be non-negative' if quantity < 0
|
81
|
+
@quantity = quantity
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# Encoding: utf-8
|
2
|
+
|
3
|
+
module Panier
|
4
|
+
module Domain
|
5
|
+
##
|
6
|
+
# A product is a purchasable item with a price and one or more tax classes
|
7
|
+
# that allow taxes to be calculated accurately.
|
8
|
+
#
|
9
|
+
class Product
|
10
|
+
attr_accessor :name, :tax_classes
|
11
|
+
attr_reader :price
|
12
|
+
|
13
|
+
##
|
14
|
+
# Initializes the product using the given name, price and optional tax
|
15
|
+
# classes.
|
16
|
+
#
|
17
|
+
# @param name [String]
|
18
|
+
# @param price [Money]
|
19
|
+
# @param tax_classes [Array]
|
20
|
+
#
|
21
|
+
def initialize(name, price, tax_classes = [])
|
22
|
+
@name = name
|
23
|
+
self.price = price
|
24
|
+
@tax_classes = tax_classes
|
25
|
+
end
|
26
|
+
|
27
|
+
def price=(price)
|
28
|
+
fail ArgumentError, ':price must be non-negative' if price.negative?
|
29
|
+
@price = price
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'money'
|
2
|
+
|
3
|
+
module Panier
|
4
|
+
module Domain
|
5
|
+
##
|
6
|
+
# The product service provides a means of finding and retrieving products
|
7
|
+
# from the product catalog.
|
8
|
+
#
|
9
|
+
# This is an in-memory implementation, useful for testing, which contains
|
10
|
+
# only the products from the given input data. A real-world implementation
|
11
|
+
# would be backed by a database or web service.
|
12
|
+
#
|
13
|
+
class ProductService
|
14
|
+
def initialize
|
15
|
+
@tax = TaxClass.new('Basic sales tax', 0.1)
|
16
|
+
@duty = TaxClass.new('Import duty', 0.05)
|
17
|
+
|
18
|
+
@products = product_data.map do |row|
|
19
|
+
Product.new(*row)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def product_data
|
24
|
+
[['book', Money.new(1249)],
|
25
|
+
['music CD', Money.new(1499), [@tax]],
|
26
|
+
['chocolate bar', Money.new(85)],
|
27
|
+
['imported box of chocolates', Money.new(1000), [@duty]],
|
28
|
+
['imported bottle of perfume', Money.new(4750), [@tax, @duty]],
|
29
|
+
['imported bottle of perfume', Money.new(2799), [@tax, @duty]],
|
30
|
+
['bottle of perfume', Money.new(1899), [@tax]],
|
31
|
+
['packet of headache pills', Money.new(975)],
|
32
|
+
['box of imported chocolates', Money.new(1125), [@duty]]]
|
33
|
+
end
|
34
|
+
|
35
|
+
##
|
36
|
+
# Finds a product matching the given name and price.
|
37
|
+
#
|
38
|
+
# @param [String] name
|
39
|
+
# @param [Money] price
|
40
|
+
def find_by_name_and_price(name, price)
|
41
|
+
@products.find do |product|
|
42
|
+
product.name == name && product.price == price
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# Encoding: utf-8
|
2
|
+
|
3
|
+
module Panier
|
4
|
+
module Domain
|
5
|
+
##
|
6
|
+
# A receipt is a value object describing a payment that has been made by a
|
7
|
+
# shopper to a merchant in relation to an order.
|
8
|
+
#
|
9
|
+
class Receipt
|
10
|
+
attr_reader :line_items
|
11
|
+
|
12
|
+
##
|
13
|
+
# Initializes the receipt with the given line items.
|
14
|
+
#
|
15
|
+
# @param line_items [Array] The line items to be represented on the
|
16
|
+
# receipt.
|
17
|
+
def initialize(line_items)
|
18
|
+
@line_items = line_items
|
19
|
+
end
|
20
|
+
|
21
|
+
##
|
22
|
+
# Calculates the total value of the receipt by adding together the total
|
23
|
+
# values of all line items.
|
24
|
+
#
|
25
|
+
# @return [Money] The total value of the receipt.
|
26
|
+
def total_amount
|
27
|
+
line_items.reduce(Money.zero) { |a, e| a + e.total_amount_inc_tax }
|
28
|
+
end
|
29
|
+
|
30
|
+
##
|
31
|
+
# Calculates the total tax present on the receipt by adding together the
|
32
|
+
# total tax of all line items.
|
33
|
+
#
|
34
|
+
# @return [Money] The total tax present on the receipt.
|
35
|
+
def total_tax
|
36
|
+
line_items.reduce(Money.zero) { |a, e| a + e.total_tax }
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'money'
|
2
|
+
|
3
|
+
module Panier
|
4
|
+
module Domain
|
5
|
+
##
|
6
|
+
# A money-rounding strategy that rounds fractional values up to the nearest
|
7
|
+
# increment. For example, $0.21 would be rounded up to $0.25.
|
8
|
+
#
|
9
|
+
class RoundUpRounding
|
10
|
+
##
|
11
|
+
# @param increment [Integer] The fractional value to which rounding
|
12
|
+
# calculations are made.
|
13
|
+
#
|
14
|
+
def initialize(increment = 5)
|
15
|
+
self.increment = increment
|
16
|
+
end
|
17
|
+
|
18
|
+
##
|
19
|
+
# Rounds a monetary value up to the nearest increment.
|
20
|
+
#
|
21
|
+
# @param value [Money] The amount of tax to be rounded.
|
22
|
+
#
|
23
|
+
def round(value)
|
24
|
+
unless value % @increment == Money.zero
|
25
|
+
value += Money.new(@increment) - value % @increment
|
26
|
+
end
|
27
|
+
value
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def increment=(increment)
|
33
|
+
fail ArgumentError ':increment must be non-negative' if increment < 0
|
34
|
+
@increment = increment
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|