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.
Files changed (39) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +22 -0
  3. data/Gemfile +3 -0
  4. data/LICENSE.txt +22 -0
  5. data/README.md +33 -0
  6. data/Rakefile +6 -0
  7. data/bin/panier +14 -0
  8. data/docs/application.md +9 -0
  9. data/docs/domain_model.md +21 -0
  10. data/docs/examples.md +45 -0
  11. data/docs/implementation_notes.md +39 -0
  12. data/lib/panier/application/cli.rb +72 -0
  13. data/lib/panier/application/input_reader.rb +41 -0
  14. data/lib/panier/application/sales_tax_service.rb +28 -0
  15. data/lib/panier/decorators/decorator.rb +25 -0
  16. data/lib/panier/decorators/receipt_decorator.rb +29 -0
  17. data/lib/panier/domain/line_item.rb +85 -0
  18. data/lib/panier/domain/product.rb +33 -0
  19. data/lib/panier/domain/product_service.rb +47 -0
  20. data/lib/panier/domain/receipt.rb +40 -0
  21. data/lib/panier/domain/round_up_rounding.rb +38 -0
  22. data/lib/panier/domain/tax_class.rb +28 -0
  23. data/lib/panier/version.rb +4 -0
  24. data/lib/panier.rb +22 -0
  25. data/panier.gemspec +29 -0
  26. data/spec/factories/line_item.rb +17 -0
  27. data/spec/factories/product.rb +16 -0
  28. data/spec/factories/receipt.rb +21 -0
  29. data/spec/factories/tax_class.rb +8 -0
  30. data/spec/panier/application/input_reader_spec.rb +21 -0
  31. data/spec/panier/application/sales_tax_service_spec.rb +83 -0
  32. data/spec/panier/decorators/receipt_decorator_spec.rb +24 -0
  33. data/spec/panier/domain/line_item_spec.rb +100 -0
  34. data/spec/panier/domain/product_spec.rb +28 -0
  35. data/spec/panier/domain/receipt_spec.rb +33 -0
  36. data/spec/panier/domain/round_up_rounding_spec.rb +26 -0
  37. data/spec/panier/domain/tax_class_spec.rb +21 -0
  38. data/spec/spec_helper.rb +14 -0
  39. 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
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
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
@@ -0,0 +1,6 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new
5
+
6
+ task default: :spec
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
@@ -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