stamp_duty 0.1.0 → 0.3.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 1147690e21ebe12467c191b40afbd6f29e4c4c90
4
- data.tar.gz: 63fe7bd8fabb23dc64f76c71d030b821b3a7a765
2
+ SHA256:
3
+ metadata.gz: 4b724c57600f6cb367e2442010a0348105300dee3042f767774e2a62073455b4
4
+ data.tar.gz: 1e27b0ef5cb82fce0268381f513fe2c4254d42987913aeb412b4260be2ee9a1a
5
5
  SHA512:
6
- metadata.gz: dfaed9758a2773e05453e368f6d97a244350203973256c2f849acbb6315f802373420690cd025ea68d424f01e3db41ac3e9915fe8ba04b35b7fec8fd3a22611a
7
- data.tar.gz: 26763a41182056a87a64a2fbeeae8187b3341504bad4d74113680f9141d75b91fbaf9c11c8f41c165d5ca318e288d74711a39498ced0ccd30d6464d9ce17ba31
6
+ metadata.gz: 645e90479a5bb67c1f23088a2158da065b6d3880fda3e32fc3410995f4f54388a2f4659a713ce47687d2ce04dfd8b43927d98b7c59bba57e31c96a03d60f0268
7
+ data.tar.gz: e25fb5b60caa8799e6d0c70384fc9feb707b3af5427a24b3d55f96b843b20cf4d7646d43d8df3ac127df94e5d0b283fc3d3a31bc30d71339e0ffb932032b1b98
data/CHANGELOG.md ADDED
@@ -0,0 +1,52 @@
1
+ # Changelog
2
+
3
+ ## 0.3.0
4
+
5
+ ### Breaking
6
+
7
+ - `StampDuty.for` returns an immutable `StampDuty::Calculation`. `ResidentialCalculator`
8
+ and `BandSelector` are removed.
9
+ - `Band` and `BandAmount` are immutable value objects built with keyword arguments.
10
+ `BandAmount` takes the band's taxable amount instead of `price:`.
11
+ - Band descriptions use £ and HMRC-style wording ("£125,001 to £250,000", "Above £1,500,000").
12
+
13
+ ### Added
14
+
15
+ - First-time buyer relief (`first_time_buyer: true`).
16
+ - Higher rates for additional properties (`additional_property: true`).
17
+ - Non-UK resident surcharge (`non_resident: true`).
18
+ - `to_h` on results.
19
+ - RBS type signatures.
20
+
21
+ ### Changed
22
+
23
+ - Releases published from GitHub Actions via RubyGems trusted publishing.
24
+ - Dependabot for bundler and GitHub Actions.
25
+ - Linting with standard; bundler-audit in CI.
26
+ - Actions pinned to exact versions.
27
+
28
+ ## 0.2.0
29
+
30
+ ### Breaking
31
+
32
+ - Requires Ruby 3.3 or later.
33
+ - `stamp_duty` is rounded down to the whole pound, matching HMRC.
34
+ - Invalid prices (non-numeric, `nil`, negative, NaN, infinite) raise `ArgumentError`.
35
+ Numeric strings such as `"275000"` are no longer accepted.
36
+
37
+ ### Fixed
38
+
39
+ - Gem failed to load on Ruby 3.x (`uninitialized constant Forwardable`).
40
+ - Calling `calculate` more than once doubled the result.
41
+ - `stamp_duty` returned 0 unless `calculate` had been called first; results are now computed lazily.
42
+
43
+ ### Changed
44
+
45
+ - Declared `bigdecimal` as a runtime dependency (bundled gem from Ruby 3.4).
46
+ - Updated development dependencies (rake 13, minitest 5.25, simplecov 0.22); dropped mocha and pry.
47
+ - Require MFA for gem pushes (`rubygems_mfa_required`).
48
+ - CI moved from Travis to GitHub Actions.
49
+
50
+ ## 0.1.0
51
+
52
+ - Initial release.
data/CONTEXT.md ADDED
@@ -0,0 +1,46 @@
1
+ # StampDuty
2
+
3
+ Calculates Stamp Duty Land Tax owed on a residential property purchase in England and Northern Ireland.
4
+
5
+ ## Language
6
+
7
+ **Stamp Duty**:
8
+ Stamp Duty Land Tax (SDLT) owed on a purchase, rounded down to the whole pound.
9
+ _Avoid_: LBTT, LTT (Scottish and Welsh equivalents, out of scope)
10
+
11
+ **Purchase Price**:
12
+ The amount paid for the property, on which Stamp Duty is charged. Never negative.
13
+ _Avoid_: value, consideration
14
+
15
+ **Band**:
16
+ A slice of the Purchase Price between a lower and optional upper bound, taxed at a single percentage rate.
17
+ _Avoid_: bracket, tier
18
+
19
+ **Taxable Amount**:
20
+ The part of the Purchase Price that falls within one Band.
21
+
22
+ **Band Amount**:
23
+ The tax due on a Band's Taxable Amount.
24
+
25
+ **Calculation**:
26
+ The result for one purchase: its Purchase Price, Band Amounts and Stamp Duty.
27
+
28
+ **First-Time Buyer Relief**:
29
+ Reduced Bands for buyers who have never owned a home, available only when the Purchase Price is £500,000 or less.
30
+
31
+ **Surcharge**:
32
+ Percentage points added to every Band's rate. Two exist: the **Additional Property** surcharge (buyer will own more than one home) and the **Non-Resident** surcharge (buyer not UK resident). Neither applies below £40,000.
33
+ _Avoid_: higher rates (ambiguous with the higher Bands)
34
+
35
+ ## Relationships
36
+
37
+ - A **Purchase Price** falls into one or more **Bands**, starting from the lowest
38
+ - **First-Time Buyer Relief** replaces the standard **Bands**; **Surcharges** add to whichever **Bands** apply
39
+ - A **First-Time Buyer** cannot also be buying an **Additional Property**
40
+ - Each **Band** the price reaches produces one **Band Amount**
41
+ - **Stamp Duty** is the sum of the **Band Amounts**, rounded down to the pound
42
+
43
+ ## Flagged ambiguities
44
+
45
+ - "Stamp duty" colloquially covers Scottish LBTT and Welsh LTT; here it means SDLT for England and Northern Ireland only.
46
+ - Rates are those in force from 1 April 2025; purchases completed earlier used different rates.
data/Gemfile CHANGED
@@ -1,4 +1,4 @@
1
- source 'https://rubygems.org'
1
+ source "https://rubygems.org"
2
2
 
3
3
  # Specify your gem's dependencies in stamp_duty.gemspec
4
4
  gemspec
data/README.md CHANGED
@@ -1,7 +1,12 @@
1
1
  # StampDuty
2
2
 
3
- StampDuty is a stamp duty calculator for the UK property market. Currently only
4
- for residential property, and doesn't include the extra 3% for owning multiple properties
3
+ StampDuty is a Stamp Duty Land Tax (SDLT) calculator for residential property
4
+ in England and Northern Ireland, using the rates in force from 1 April 2025.
5
+ It supports first-time buyer relief, the higher rates for additional
6
+ properties and the non-UK resident surcharge. Scotland (LBTT) and Wales (LTT)
7
+ are not covered.
8
+
9
+ Requires Ruby 3.3 or later.
5
10
 
6
11
  ## Installation
7
12
 
@@ -22,47 +27,70 @@ Or install it yourself as:
22
27
  ## Usage
23
28
 
24
29
  ```ruby
25
- calc = StampDuty.for(275000)
26
- calc.calculate
30
+ calc = StampDuty.for(275_000)
27
31
 
28
- # .to_s("F") as returns a big decimal
29
- calc.stamp_duty.to_s("F") # => 3750.0
32
+ # a BigDecimal, rounded down to the whole pound as HMRC does
33
+ calc.stamp_duty.to_s("F") # => "3750.0"
30
34
  ```
31
35
 
32
- You can also can get each the amount in each band
36
+ Pass options for reliefs and surcharges:
37
+
38
+ ```ruby
39
+ StampDuty.for(400_000, first_time_buyer: true) # 0% to £300k, 5% to £500k
40
+ StampDuty.for(300_000, additional_property: true) # +5% on every band
41
+ StampDuty.for(300_000, non_resident: true) # +2% on every band
42
+ ```
43
+
44
+ - First-time buyer relief does not apply above £500,000; standard rates are used instead.
45
+ - Surcharges only apply to purchases of £40,000 or more.
46
+ - `first_time_buyer` and `additional_property` cannot both be true.
47
+
48
+ The price must be a non-negative, finite number (Integer, BigDecimal, Rational
49
+ or Float) and the options must be `true` or `false`. Anything else raises
50
+ `ArgumentError`.
51
+
52
+ The tax due in each band is available too:
33
53
 
34
54
  ```ruby
35
55
  calc.band_amounts.each do |ba|
36
- puts ba.description
37
- puts ba.amount.to_s("F")
38
- puts ba.percentage_rate
56
+ puts ba.description
57
+ puts ba.amount.to_s("F")
58
+ puts ba.percentage_rate
39
59
  end
40
60
  ```
41
61
 
42
- Which would return
43
-
44
- Up to 125,000
45
- 0.0
62
+ ```
63
+ Up to £125,000
64
+ 0.0
46
65
  0
47
-
48
- Above 125,000 and up to 250,000
49
- 2500.0
66
+ £125,001 to £250,000
67
+ 2500.0
50
68
  2
51
-
52
- Above 250,000 and up to 925,000
53
- 1250.0
69
+ £250,001 to £925,000
70
+ 1250.0
54
71
  5
72
+ ```
73
+
74
+ Results are immutable. `calc.to_h` returns a plain hash for serialising.
75
+ `calc.calculate` is kept for compatibility with 0.1.0 and returns the same
76
+ result.
55
77
 
78
+ Type signatures are included in `sig/`.
56
79
 
57
80
  ## Development
58
81
 
59
82
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
60
83
 
61
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
84
+ To install this gem onto your local machine, run `bundle exec rake install`.
85
+
86
+ To release a new version, update the version number in `version.rb` and the
87
+ CHANGELOG, merge to master, then run the **Release** workflow from the Actions
88
+ tab. It publishes to [rubygems.org](https://rubygems.org) using trusted
89
+ publishing, so no API key is stored anywhere.
62
90
 
63
91
  ## Contributing
64
92
 
65
- Bug reports and pull requests are welcome on GitHub at https://github.com/jdexx/stamp_duty.
93
+ Bug reports and pull requests are welcome on GitHub at https://github.com/j-dexx/stamp_duty.
66
94
 
67
95
 
68
96
  ## License
data/Rakefile CHANGED
@@ -1,10 +1,16 @@
1
1
  require "bundler/gem_tasks"
2
2
  require "rake/testtask"
3
+ require "standard/rake"
3
4
 
4
5
  Rake::TestTask.new(:test) do |t|
5
6
  t.libs << "test"
6
7
  t.libs << "lib"
7
- t.test_files = FileList['test/**/*_test.rb']
8
+ t.test_files = FileList["test/**/*_test.rb"]
8
9
  end
9
10
 
10
- task :default => :test
11
+ desc "Validate RBS signatures"
12
+ task :rbs do
13
+ sh "rbs", "-r", "bigdecimal", "-r", "forwardable", "-I", "sig", "validate"
14
+ end
15
+
16
+ task default: %i[test standard rbs]
@@ -1,31 +1,38 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module StampDuty
2
- class Band
3
- attr_reader :lower_bound, :upper_bound, :percentage_rate
4
+ # A slice of the purchase price, from lower_bound up to upper_bound (nil for
5
+ # no limit), taxed at percentage_rate.
6
+ Band = Data.define(:lower_bound, :upper_bound, :percentage_rate) do
7
+ def decimal_percentage_rate
8
+ percentage_rate.to_d / 100
9
+ end
4
10
 
5
- def initialize(lower_bound, upper_bound, percentage_rate)
6
- @lower_bound = lower_bound
7
- @upper_bound = upper_bound
8
- @percentage_rate = percentage_rate
11
+ # The part of price that falls within this band.
12
+ def taxable_amount(price)
13
+ top = upper_bound.nil? ? price : [price, upper_bound].min
14
+ [top - lower_bound, 0].max
9
15
  end
10
16
 
11
- def description
12
- return "Up to #{ formatted_upper_bound }" if lower_bound.zero?
13
- return "Above #{ formatted_lower_bound }+" if upper_bound.nil?
14
- "Above #{ formatted_lower_bound } and up to #{ formatted_upper_bound }"
17
+ def applies_to?(price)
18
+ price > lower_bound
15
19
  end
16
20
 
17
- def decimal_percentage_rate
18
- percentage_rate.to_d / 100
21
+ def with_surcharge(percentage_points)
22
+ with(percentage_rate: percentage_rate + percentage_points)
19
23
  end
20
24
 
21
- private
25
+ def description
26
+ return "Up to #{format_pounds(upper_bound)}" if lower_bound.zero?
27
+ return "Above #{format_pounds(lower_bound)}" if upper_bound.nil?
22
28
 
23
- def formatted_lower_bound
24
- lower_bound.to_s.reverse.scan(/\d{1,3}/).join(",").reverse
29
+ "#{format_pounds(lower_bound + 1)} to #{format_pounds(upper_bound)}"
25
30
  end
26
31
 
27
- def formatted_upper_bound
28
- upper_bound.to_s.reverse.scan(/\d{1,3}/).join(",").reverse
32
+ private
33
+
34
+ def format_pounds(amount)
35
+ "£#{amount.to_i.to_s.reverse.scan(/\d{1,3}/).join(",").reverse}"
29
36
  end
30
37
  end
31
38
  end
@@ -1,24 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "forwardable"
4
+
1
5
  module StampDuty
2
- class BandAmount
6
+ # The tax due on the part of a purchase price that falls within one band.
7
+ BandAmount = Data.define(:band, :taxable_amount) do
3
8
  extend Forwardable
4
9
 
5
- def_delegators :@band, :lower_bound, :percentage_rate, :decimal_percentage_rate, :description
6
- attr_reader :price, :band
10
+ def_delegators :band, :lower_bound, :upper_bound, :percentage_rate, :decimal_percentage_rate, :description
7
11
 
8
- def initialize(price:, band:)
9
- @price = price
10
- @band = band
12
+ def self.for(band, price)
13
+ new(band: band, taxable_amount: band.taxable_amount(price))
11
14
  end
12
15
 
13
16
  def amount
14
- @amount ||= decimal_percentage_rate * rateable_amount
17
+ decimal_percentage_rate * taxable_amount
15
18
  end
16
19
 
17
- private
18
-
19
- def rateable_amount
20
- @rateable_amount ||= price - lower_bound
20
+ def to_h
21
+ {
22
+ description: description,
23
+ lower_bound: lower_bound,
24
+ upper_bound: upper_bound,
25
+ percentage_rate: percentage_rate,
26
+ taxable_amount: taxable_amount,
27
+ amount: amount
28
+ }
21
29
  end
22
-
23
30
  end
24
31
  end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StampDuty
4
+ # The result of a stamp duty calculation. Band amounts are ordered lowest
5
+ # band first; stamp_duty is rounded down to the whole pound, as HMRC does.
6
+ Calculation = Data.define(:purchase_price, :band_amounts, :stamp_duty) do
7
+ def self.build(purchase_price, bands)
8
+ band_amounts = bands
9
+ .select { |band| band.applies_to?(purchase_price) }
10
+ .sort_by(&:lower_bound)
11
+ .map { |band| BandAmount.for(band, purchase_price) }
12
+ .freeze
13
+ total = band_amounts.sum(BigDecimal(0), &:amount).round(0, BigDecimal::ROUND_FLOOR)
14
+
15
+ new(purchase_price: purchase_price, band_amounts: band_amounts, stamp_duty: total)
16
+ end
17
+
18
+ # Kept for compatibility with 0.1.0; results are calculated up front.
19
+ def calculate
20
+ self
21
+ end
22
+
23
+ def to_h
24
+ {
25
+ purchase_price: purchase_price,
26
+ stamp_duty: stamp_duty,
27
+ band_amounts: band_amounts.map(&:to_h)
28
+ }
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StampDuty
4
+ # Residential SDLT rates for England and Northern Ireland from 1 April 2025.
5
+ # https://www.gov.uk/stamp-duty-land-tax/residential-property-rates
6
+ module Rates
7
+ STANDARD = [
8
+ Band.new(lower_bound: 0, upper_bound: 125_000, percentage_rate: 0),
9
+ Band.new(lower_bound: 125_000, upper_bound: 250_000, percentage_rate: 2),
10
+ Band.new(lower_bound: 250_000, upper_bound: 925_000, percentage_rate: 5),
11
+ Band.new(lower_bound: 925_000, upper_bound: 1_500_000, percentage_rate: 10),
12
+ Band.new(lower_bound: 1_500_000, upper_bound: nil, percentage_rate: 12)
13
+ ].freeze
14
+
15
+ FIRST_TIME_BUYER = [
16
+ Band.new(lower_bound: 0, upper_bound: 300_000, percentage_rate: 0),
17
+ Band.new(lower_bound: 300_000, upper_bound: 500_000, percentage_rate: 5)
18
+ ].freeze
19
+
20
+ # First-time buyer relief cannot be claimed above this price.
21
+ FIRST_TIME_BUYER_MAX_PRICE = 500_000
22
+
23
+ # Surcharges only apply to purchases of at least this price.
24
+ SURCHARGE_MIN_PRICE = 40_000
25
+
26
+ ADDITIONAL_PROPERTY_SURCHARGE = 5
27
+ NON_RESIDENT_SURCHARGE = 2
28
+
29
+ def self.bands_for(price, first_time_buyer:, additional_property:, non_resident:)
30
+ if first_time_buyer && additional_property
31
+ raise ArgumentError, "first-time buyer relief cannot apply to an additional property"
32
+ end
33
+
34
+ bands = (first_time_buyer && price <= FIRST_TIME_BUYER_MAX_PRICE) ? FIRST_TIME_BUYER : STANDARD
35
+ surcharge = 0
36
+ if price >= SURCHARGE_MIN_PRICE
37
+ surcharge += ADDITIONAL_PROPERTY_SURCHARGE if additional_property
38
+ surcharge += NON_RESIDENT_SURCHARGE if non_resident
39
+ end
40
+ surcharge.zero? ? bands : bands.map { |band| band.with_surcharge(surcharge) }
41
+ end
42
+ end
43
+ end
@@ -1,3 +1,3 @@
1
1
  module StampDuty
2
- VERSION = "0.1.0"
2
+ VERSION = "0.3.0"
3
3
  end
data/lib/stamp_duty.rb CHANGED
@@ -1,14 +1,41 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "bigdecimal"
2
4
  require "bigdecimal/util"
3
5
  require "stamp_duty/version"
4
6
  require "stamp_duty/band"
5
- require "stamp_duty/band_selector"
6
7
  require "stamp_duty/band_amount"
7
- require "stamp_duty/residential_calculator"
8
+ require "stamp_duty/rates"
9
+ require "stamp_duty/calculation"
8
10
 
9
11
  module StampDuty
10
- def self.for(price)
11
- bands = StampDuty::BandSelector.new(price).bands
12
- StampDuty::ResidentialCalculator.new(price, bands)
12
+ # Calculates Stamp Duty Land Tax on a residential purchase in England or
13
+ # Northern Ireland.
14
+ def self.for(price, first_time_buyer: false, additional_property: false, non_resident: false)
15
+ price = normalize_price(price)
16
+ bands = Rates.bands_for(
17
+ price,
18
+ first_time_buyer: boolean!(first_time_buyer, :first_time_buyer),
19
+ additional_property: boolean!(additional_property, :additional_property),
20
+ non_resident: boolean!(non_resident, :non_resident)
21
+ )
22
+ Calculation.build(price, bands)
23
+ end
24
+
25
+ def self.normalize_price(price)
26
+ unless price.is_a?(Numeric) && price.real?
27
+ raise ArgumentError, "price must be a number, got #{price.inspect}"
28
+ end
29
+ raise ArgumentError, "price must be finite, got #{price.inspect}" unless price.finite?
30
+ raise ArgumentError, "price must not be negative, got #{price.inspect}" if price.negative?
31
+
32
+ price.is_a?(Rational) ? price.to_d(20) : price.to_d
33
+ end
34
+
35
+ def self.boolean!(value, name)
36
+ return value if value == true || value == false
37
+
38
+ raise ArgumentError, "#{name} must be true or false, got #{value.inspect}"
13
39
  end
40
+ private_class_method :normalize_price, :boolean!
14
41
  end
@@ -0,0 +1,62 @@
1
+ module StampDuty
2
+ VERSION: String
3
+
4
+ type price = Integer | Float | Rational | BigDecimal
5
+
6
+ def self.for: (price price, ?first_time_buyer: bool, ?additional_property: bool, ?non_resident: bool) -> Calculation
7
+
8
+ class Band
9
+ attr_reader lower_bound: Integer
10
+ attr_reader upper_bound: Integer?
11
+ attr_reader percentage_rate: Integer
12
+
13
+ def self.new: (lower_bound: Integer, upper_bound: Integer?, percentage_rate: Integer) -> Band
14
+
15
+ def decimal_percentage_rate: () -> BigDecimal
16
+ def taxable_amount: (BigDecimal | Integer price) -> (BigDecimal | Integer)
17
+ def applies_to?: (BigDecimal | Integer price) -> bool
18
+ def with_surcharge: (Integer percentage_points) -> Band
19
+ def description: () -> String
20
+ def with: (?lower_bound: Integer, ?upper_bound: Integer?, ?percentage_rate: Integer) -> Band
21
+ def to_h: () -> { lower_bound: Integer, upper_bound: Integer?, percentage_rate: Integer }
22
+ end
23
+
24
+ class BandAmount
25
+ attr_reader band: Band
26
+ attr_reader taxable_amount: BigDecimal | Integer
27
+
28
+ def self.new: (band: Band, taxable_amount: BigDecimal | Integer) -> BandAmount
29
+ def self.for: (Band band, BigDecimal | Integer price) -> BandAmount
30
+
31
+ def lower_bound: () -> Integer
32
+ def upper_bound: () -> Integer?
33
+ def percentage_rate: () -> Integer
34
+ def decimal_percentage_rate: () -> BigDecimal
35
+ def description: () -> String
36
+ def amount: () -> BigDecimal
37
+ def to_h: () -> Hash[Symbol, untyped]
38
+ end
39
+
40
+ class Calculation
41
+ attr_reader purchase_price: BigDecimal
42
+ attr_reader band_amounts: Array[BandAmount]
43
+ attr_reader stamp_duty: BigDecimal
44
+
45
+ def self.new: (purchase_price: BigDecimal, band_amounts: Array[BandAmount], stamp_duty: BigDecimal) -> Calculation
46
+ def self.build: (BigDecimal purchase_price, Array[Band] bands) -> Calculation
47
+
48
+ def calculate: () -> self
49
+ def to_h: () -> Hash[Symbol, untyped]
50
+ end
51
+
52
+ module Rates
53
+ STANDARD: Array[Band]
54
+ FIRST_TIME_BUYER: Array[Band]
55
+ FIRST_TIME_BUYER_MAX_PRICE: Integer
56
+ SURCHARGE_MIN_PRICE: Integer
57
+ ADDITIONAL_PROPERTY_SURCHARGE: Integer
58
+ NON_RESIDENT_SURCHARGE: Integer
59
+
60
+ def self.bands_for: (BigDecimal price, first_time_buyer: bool, additional_property: bool, non_resident: bool) -> Array[Band]
61
+ end
62
+ end
data/stamp_duty.gemspec CHANGED
@@ -1,28 +1,35 @@
1
- # coding: utf-8
2
- lib = File.expand_path('../lib', __FILE__)
1
+ lib = File.expand_path("../lib", __FILE__)
3
2
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
- require 'stamp_duty/version'
3
+ require "stamp_duty/version"
5
4
 
6
5
  Gem::Specification.new do |spec|
7
- spec.name = "stamp_duty"
8
- spec.version = StampDuty::VERSION
9
- spec.authors = ["James Doyley"]
10
- spec.email = ["jdoyley@gmail.com"]
6
+ spec.name = "stamp_duty"
7
+ spec.version = StampDuty::VERSION
8
+ spec.authors = ["James Doyley"]
9
+ spec.email = ["jdoyley@gmail.com"]
11
10
 
12
- spec.summary = %q{ A UK proerty stamp duty calculator }
13
- spec.homepage = "https://github.com/j-dexx/stamp_duty"
14
- spec.license = "MIT"
11
+ spec.summary = " A UK property stamp duty calculator "
12
+ spec.homepage = "https://github.com/j-dexx/stamp_duty"
13
+ spec.license = "MIT"
15
14
 
16
- spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
17
- spec.bindir = "exe"
18
- spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
15
+ spec.metadata = {
16
+ "source_code_uri" => spec.homepage,
17
+ "changelog_uri" => "#{spec.homepage}/blob/master/CHANGELOG.md",
18
+ "rubygems_mfa_required" => "true"
19
+ }
20
+
21
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features|bin|\.github)/}) }
22
+ spec.bindir = "exe"
23
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
19
24
  spec.require_paths = ["lib"]
20
- spec.required_ruby_version = "~> 2.1"
25
+ spec.required_ruby_version = ">= 3.3"
26
+
27
+ spec.add_dependency "bigdecimal", ">= 3.1", "< 5"
21
28
 
22
- spec.add_development_dependency "bundler", "~> 1.11"
23
- spec.add_development_dependency "rake", "~> 10.0"
24
- spec.add_development_dependency "minitest", "~> 5.0"
25
- spec.add_development_dependency "simplecov", "~> 0.11.2"
26
- spec.add_development_dependency "mocha", "~> 1.1.0"
27
- spec.add_development_dependency "pry"
29
+ spec.add_development_dependency "rake", "~> 13.0"
30
+ spec.add_development_dependency "minitest", "~> 6.0"
31
+ spec.add_development_dependency "simplecov", "~> 1.3"
32
+ spec.add_development_dependency "standard", "~> 1.56"
33
+ spec.add_development_dependency "bundler-audit", "~> 0.9"
34
+ spec.add_development_dependency "rbs", "~> 4.2"
28
35
  end
metadata CHANGED
@@ -1,100 +1,118 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: stamp_duty
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - James Doyley
8
- autorequire:
9
8
  bindir: exe
10
9
  cert_chain: []
11
- date: 2016-04-27 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
- name: bundler
13
+ name: bigdecimal
15
14
  requirement: !ruby/object:Gem::Requirement
16
15
  requirements:
17
- - - "~>"
16
+ - - ">="
18
17
  - !ruby/object:Gem::Version
19
- version: '1.11'
20
- type: :development
18
+ version: '3.1'
19
+ - - "<"
20
+ - !ruby/object:Gem::Version
21
+ version: '5'
22
+ type: :runtime
21
23
  prerelease: false
22
24
  version_requirements: !ruby/object:Gem::Requirement
23
25
  requirements:
24
- - - "~>"
26
+ - - ">="
25
27
  - !ruby/object:Gem::Version
26
- version: '1.11'
28
+ version: '3.1'
29
+ - - "<"
30
+ - !ruby/object:Gem::Version
31
+ version: '5'
27
32
  - !ruby/object:Gem::Dependency
28
33
  name: rake
29
34
  requirement: !ruby/object:Gem::Requirement
30
35
  requirements:
31
36
  - - "~>"
32
37
  - !ruby/object:Gem::Version
33
- version: '10.0'
38
+ version: '13.0'
34
39
  type: :development
35
40
  prerelease: false
36
41
  version_requirements: !ruby/object:Gem::Requirement
37
42
  requirements:
38
43
  - - "~>"
39
44
  - !ruby/object:Gem::Version
40
- version: '10.0'
45
+ version: '13.0'
41
46
  - !ruby/object:Gem::Dependency
42
47
  name: minitest
43
48
  requirement: !ruby/object:Gem::Requirement
44
49
  requirements:
45
50
  - - "~>"
46
51
  - !ruby/object:Gem::Version
47
- version: '5.0'
52
+ version: '6.0'
48
53
  type: :development
49
54
  prerelease: false
50
55
  version_requirements: !ruby/object:Gem::Requirement
51
56
  requirements:
52
57
  - - "~>"
53
58
  - !ruby/object:Gem::Version
54
- version: '5.0'
59
+ version: '6.0'
55
60
  - !ruby/object:Gem::Dependency
56
61
  name: simplecov
57
62
  requirement: !ruby/object:Gem::Requirement
58
63
  requirements:
59
64
  - - "~>"
60
65
  - !ruby/object:Gem::Version
61
- version: 0.11.2
66
+ version: '1.3'
62
67
  type: :development
63
68
  prerelease: false
64
69
  version_requirements: !ruby/object:Gem::Requirement
65
70
  requirements:
66
71
  - - "~>"
67
72
  - !ruby/object:Gem::Version
68
- version: 0.11.2
73
+ version: '1.3'
69
74
  - !ruby/object:Gem::Dependency
70
- name: mocha
75
+ name: standard
71
76
  requirement: !ruby/object:Gem::Requirement
72
77
  requirements:
73
78
  - - "~>"
74
79
  - !ruby/object:Gem::Version
75
- version: 1.1.0
80
+ version: '1.56'
76
81
  type: :development
77
82
  prerelease: false
78
83
  version_requirements: !ruby/object:Gem::Requirement
79
84
  requirements:
80
85
  - - "~>"
81
86
  - !ruby/object:Gem::Version
82
- version: 1.1.0
87
+ version: '1.56'
83
88
  - !ruby/object:Gem::Dependency
84
- name: pry
89
+ name: bundler-audit
85
90
  requirement: !ruby/object:Gem::Requirement
86
91
  requirements:
87
- - - ">="
92
+ - - "~>"
88
93
  - !ruby/object:Gem::Version
89
- version: '0'
94
+ version: '0.9'
90
95
  type: :development
91
96
  prerelease: false
92
97
  version_requirements: !ruby/object:Gem::Requirement
93
98
  requirements:
94
- - - ">="
99
+ - - "~>"
95
100
  - !ruby/object:Gem::Version
96
- version: '0'
97
- description:
101
+ version: '0.9'
102
+ - !ruby/object:Gem::Dependency
103
+ name: rbs
104
+ requirement: !ruby/object:Gem::Requirement
105
+ requirements:
106
+ - - "~>"
107
+ - !ruby/object:Gem::Version
108
+ version: '4.2'
109
+ type: :development
110
+ prerelease: false
111
+ version_requirements: !ruby/object:Gem::Requirement
112
+ requirements:
113
+ - - "~>"
114
+ - !ruby/object:Gem::Version
115
+ version: '4.2'
98
116
  email:
99
117
  - jdoyley@gmail.com
100
118
  executables: []
@@ -102,42 +120,42 @@ extensions: []
102
120
  extra_rdoc_files: []
103
121
  files:
104
122
  - ".gitignore"
105
- - ".travis.yml"
123
+ - CHANGELOG.md
124
+ - CONTEXT.md
106
125
  - Gemfile
107
126
  - LICENSE.txt
108
127
  - README.md
109
128
  - Rakefile
110
- - bin/console
111
- - bin/setup
112
129
  - lib/stamp_duty.rb
113
130
  - lib/stamp_duty/band.rb
114
131
  - lib/stamp_duty/band_amount.rb
115
- - lib/stamp_duty/band_selector.rb
116
- - lib/stamp_duty/residential_calculator.rb
132
+ - lib/stamp_duty/calculation.rb
133
+ - lib/stamp_duty/rates.rb
117
134
  - lib/stamp_duty/version.rb
135
+ - sig/stamp_duty.rbs
118
136
  - stamp_duty.gemspec
119
137
  homepage: https://github.com/j-dexx/stamp_duty
120
138
  licenses:
121
139
  - MIT
122
- metadata: {}
123
- post_install_message:
140
+ metadata:
141
+ source_code_uri: https://github.com/j-dexx/stamp_duty
142
+ changelog_uri: https://github.com/j-dexx/stamp_duty/blob/master/CHANGELOG.md
143
+ rubygems_mfa_required: 'true'
124
144
  rdoc_options: []
125
145
  require_paths:
126
146
  - lib
127
147
  required_ruby_version: !ruby/object:Gem::Requirement
128
148
  requirements:
129
- - - "~>"
149
+ - - ">="
130
150
  - !ruby/object:Gem::Version
131
- version: '2.1'
151
+ version: '3.3'
132
152
  required_rubygems_version: !ruby/object:Gem::Requirement
133
153
  requirements:
134
154
  - - ">="
135
155
  - !ruby/object:Gem::Version
136
156
  version: '0'
137
157
  requirements: []
138
- rubyforge_project:
139
- rubygems_version: 2.5.1
140
- signing_key:
158
+ rubygems_version: 4.0.20
141
159
  specification_version: 4
142
- summary: A UK proerty stamp duty calculator
160
+ summary: A UK property stamp duty calculator
143
161
  test_files: []
data/.travis.yml DELETED
@@ -1,4 +0,0 @@
1
- language: ruby
2
- rvm:
3
- - 2.3.0
4
- before_install: gem install bundler -v 1.11.2
data/bin/console DELETED
@@ -1,14 +0,0 @@
1
- #!/usr/bin/env ruby
2
-
3
- require "bundler/setup"
4
- require "stamp_duty"
5
-
6
- # You can add fixtures and/or initialization code here to make experimenting
7
- # with your gem easier. You can also use a different console, if you like.
8
-
9
- # (If you use this, don't forget to add pry to your Gemfile!)
10
- # require "pry"
11
- # Pry.start
12
-
13
- require "irb"
14
- IRB.start
data/bin/setup DELETED
@@ -1,8 +0,0 @@
1
- #!/usr/bin/env bash
2
- set -euo pipefail
3
- IFS=$'\n\t'
4
- set -vx
5
-
6
- bundle install
7
-
8
- # Do any other automated setup that you need to do here
@@ -1,35 +0,0 @@
1
- module StampDuty
2
- class BandSelector
3
- BAND_DATA = [
4
- { lower_bound: 0, upper_bound: 125000, percentage_rate: 0 },
5
- { lower_bound: 125000, upper_bound: 250000, percentage_rate: 2 },
6
- { lower_bound: 250000, upper_bound: 925000, percentage_rate: 5 },
7
- { lower_bound: 925000, upper_bound: 1500000, percentage_rate: 10 },
8
- { lower_bound: 1500000, percentage_rate: 12 }
9
- ]
10
-
11
- attr_reader :price
12
-
13
- def initialize(price)
14
- @price = price
15
- end
16
-
17
- def bands
18
- sorted_band_data.map{ |band_data| build_band(band_data) }
19
- end
20
-
21
- private
22
-
23
- def build_band(band_data)
24
- StampDuty::Band.new(band_data[:lower_bound], band_data[:upper_bound], band_data[:percentage_rate])
25
- end
26
-
27
- def sorted_band_data
28
- band_data_for_price.sort_by{ |band| band[:lower_band] }.reverse!
29
- end
30
-
31
- def band_data_for_price
32
- BAND_DATA.select{ |band| price > band[:lower_bound] }
33
- end
34
- end
35
- end
@@ -1,32 +0,0 @@
1
- module StampDuty
2
- class ResidentialCalculator
3
-
4
- attr_reader :purchase_price, :band_amounts, :bands
5
-
6
- def initialize(purchase_price, bands)
7
- @purchase_price = purchase_price.to_d
8
- @bands = bands
9
- @band_amounts = []
10
- end
11
-
12
- def stamp_duty
13
- band_amounts.map(&:amount).reduce(0, :+)
14
- end
15
-
16
- def calculate
17
- price = purchase_price
18
- while price > 0
19
- bands.each do |band|
20
- add_band_amount(band, price)
21
- price = band.lower_bound
22
- end
23
- end
24
- end
25
-
26
- private
27
-
28
- def add_band_amount(band, price)
29
- @band_amounts.unshift(StampDuty::BandAmount.new(price: price, band: band))
30
- end
31
- end
32
- end