panier 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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