panier 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 +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
|