stamp_duty 0.2.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
2
  SHA256:
3
- metadata.gz: 71cfaf89bad2686407f8d31d767f30656f89c390f82fcaf05f53385df1d975d5
4
- data.tar.gz: cd2f4a70c7409c3c7462f7280de4b192e6fc645a87b5b8679bb1b9ed70d1fcb8
3
+ metadata.gz: 4b724c57600f6cb367e2442010a0348105300dee3042f767774e2a62073455b4
4
+ data.tar.gz: 1e27b0ef5cb82fce0268381f513fe2c4254d42987913aeb412b4260be2ee9a1a
5
5
  SHA512:
6
- metadata.gz: 722ef366bd8f96c5ee09fcb843a67ffe939c59c4232aef779f07b57f76a2e3f7d340561e5b5281e2364934afa350acf43c1f48d6fa4ce2eb41c90a349cfabd10
7
- data.tar.gz: 589f6094772da6ba947131c0fef3e5d7c569cbd9727bc7f4cf9ae7dd06daed5c3e38c8dbe7e6336e47af0d2e575688c5f21f2cd94b571e2cb2a0b80d4aba500d
6
+ metadata.gz: 645e90479a5bb67c1f23088a2158da065b6d3880fda3e32fc3410995f4f54388a2f4659a713ce47687d2ce04dfd8b43927d98b7c59bba57e31c96a03d60f0268
7
+ data.tar.gz: e25fb5b60caa8799e6d0c70384fc9feb707b3af5427a24b3d55f96b843b20cf4d7646d43d8df3ac127df94e5d0b283fc3d3a31bc30d71339e0ffb932032b1b98
data/CHANGELOG.md CHANGED
@@ -1,5 +1,30 @@
1
1
  # Changelog
2
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
+
3
28
  ## 0.2.0
4
29
 
5
30
  ### Breaking
data/CONTEXT.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # StampDuty
2
2
 
3
- Calculates Stamp Duty Land Tax owed on a residential property purchase in England and Northern Ireland at the standard rates.
3
+ Calculates Stamp Duty Land Tax owed on a residential property purchase in England and Northern Ireland.
4
4
 
5
5
  ## Language
6
6
 
@@ -16,16 +16,31 @@ _Avoid_: value, consideration
16
16
  A slice of the Purchase Price between a lower and optional upper bound, taxed at a single percentage rate.
17
17
  _Avoid_: bracket, tier
18
18
 
19
+ **Taxable Amount**:
20
+ The part of the Purchase Price that falls within one Band.
21
+
19
22
  **Band Amount**:
20
- The tax due on the part of a Purchase Price that falls within one Band.
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)
21
34
 
22
35
  ## Relationships
23
36
 
24
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**
25
40
  - Each **Band** the price reaches produces one **Band Amount**
26
41
  - **Stamp Duty** is the sum of the **Band Amounts**, rounded down to the pound
27
42
 
28
43
  ## Flagged ambiguities
29
44
 
30
45
  - "Stamp duty" colloquially covers Scottish LBTT and Welsh LTT; here it means SDLT for England and Northern Ireland only.
31
- - Surcharges (additional properties, non-residents) and reliefs (first-time buyers) are not modelled.
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,9 +1,10 @@
1
1
  # StampDuty
2
2
 
3
3
  StampDuty is a Stamp Duty Land Tax (SDLT) calculator for residential property
4
- in England and Northern Ireland. It uses the standard residential rates and
5
- doesn't include surcharges (additional properties, non-residents) or reliefs
6
- (first-time buyers). Scotland (LBTT) and Wales (LTT) are not covered.
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.
7
8
 
8
9
  Requires Ruby 3.3 or later.
9
10
 
@@ -26,53 +27,66 @@ Or install it yourself as:
26
27
  ## Usage
27
28
 
28
29
  ```ruby
29
- calc = StampDuty.for(275000)
30
+ calc = StampDuty.for(275_000)
30
31
 
31
- # returns a BigDecimal, rounded down to the whole pound as HMRC does
32
- 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"
33
34
  ```
34
35
 
35
- The price must be a non-negative, finite number (Integer, BigDecimal, Rational
36
- or Float). Anything else raises `ArgumentError`:
36
+ Pass options for reliefs and surcharges:
37
37
 
38
38
  ```ruby
39
- StampDuty.for("275000") # => ArgumentError
40
- StampDuty.for(-1) # => ArgumentError
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
41
42
  ```
42
43
 
43
- `calc.calculate` still exists for backwards compatibility, but results are
44
- computed on first access so calling it is no longer necessary.
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`.
45
51
 
46
- You can also can get each the amount in each band
52
+ The tax due in each band is available too:
47
53
 
48
54
  ```ruby
49
55
  calc.band_amounts.each do |ba|
50
- puts ba.description
51
- puts ba.amount.to_s("F")
52
- puts ba.percentage_rate
56
+ puts ba.description
57
+ puts ba.amount.to_s("F")
58
+ puts ba.percentage_rate
53
59
  end
54
60
  ```
55
61
 
56
- Which would return
57
-
58
- Up to 125,000
59
- 0.0
62
+ ```
63
+ Up to £125,000
64
+ 0.0
60
65
  0
61
-
62
- Above 125,000 and up to 250,000
63
- 2500.0
66
+ £125,001 to £250,000
67
+ 2500.0
64
68
  2
65
-
66
- Above 250,000 and up to 925,000
67
- 1250.0
69
+ £250,001 to £925,000
70
+ 1250.0
68
71
  5
72
+ ```
69
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.
77
+
78
+ Type signatures are included in `sig/`.
70
79
 
71
80
  ## Development
72
81
 
73
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.
74
83
 
75
- 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.
76
90
 
77
91
  ## Contributing
78
92
 
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.2.0"
2
+ VERSION = "0.3.0"
3
3
  end
data/lib/stamp_duty.rb CHANGED
@@ -1,17 +1,25 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "bigdecimal"
2
4
  require "bigdecimal/util"
3
- require "forwardable"
4
5
  require "stamp_duty/version"
5
6
  require "stamp_duty/band"
6
- require "stamp_duty/band_selector"
7
7
  require "stamp_duty/band_amount"
8
- require "stamp_duty/residential_calculator"
8
+ require "stamp_duty/rates"
9
+ require "stamp_duty/calculation"
9
10
 
10
11
  module StampDuty
11
- def self.for(price)
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)
12
15
  price = normalize_price(price)
13
- bands = StampDuty::BandSelector.new(price).bands
14
- StampDuty::ResidentialCalculator.new(price, bands)
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)
15
23
  end
16
24
 
17
25
  def self.normalize_price(price)
@@ -23,4 +31,11 @@ module StampDuty
23
31
 
24
32
  price.is_a?(Rational) ? price.to_d(20) : price.to_d
25
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}"
39
+ end
40
+ private_class_method :normalize_price, :boolean!
26
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,16 +1,16 @@
1
- lib = File.expand_path('../lib', __FILE__)
1
+ lib = File.expand_path("../lib", __FILE__)
2
2
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
- require 'stamp_duty/version'
3
+ require "stamp_duty/version"
4
4
 
5
5
  Gem::Specification.new do |spec|
6
- spec.name = "stamp_duty"
7
- spec.version = StampDuty::VERSION
8
- spec.authors = ["James Doyley"]
9
- 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"]
10
10
 
11
- spec.summary = %q{ A UK property stamp duty calculator }
12
- spec.homepage = "https://github.com/j-dexx/stamp_duty"
13
- 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"
14
14
 
15
15
  spec.metadata = {
16
16
  "source_code_uri" => spec.homepage,
@@ -18,15 +18,18 @@ Gem::Specification.new do |spec|
18
18
  "rubygems_mfa_required" => "true"
19
19
  }
20
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) }
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) }
24
24
  spec.require_paths = ["lib"]
25
25
  spec.required_ruby_version = ">= 3.3"
26
26
 
27
27
  spec.add_dependency "bigdecimal", ">= 3.1", "< 5"
28
28
 
29
29
  spec.add_development_dependency "rake", "~> 13.0"
30
- spec.add_development_dependency "minitest", "~> 5.25"
31
- spec.add_development_dependency "simplecov", "~> 0.22"
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"
32
35
  end
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: stamp_duty
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.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: 2026-09-28 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
13
  name: bigdecimal
@@ -50,29 +49,70 @@ dependencies:
50
49
  requirements:
51
50
  - - "~>"
52
51
  - !ruby/object:Gem::Version
53
- version: '5.25'
52
+ version: '6.0'
54
53
  type: :development
55
54
  prerelease: false
56
55
  version_requirements: !ruby/object:Gem::Requirement
57
56
  requirements:
58
57
  - - "~>"
59
58
  - !ruby/object:Gem::Version
60
- version: '5.25'
59
+ version: '6.0'
61
60
  - !ruby/object:Gem::Dependency
62
61
  name: simplecov
63
62
  requirement: !ruby/object:Gem::Requirement
64
63
  requirements:
65
64
  - - "~>"
66
65
  - !ruby/object:Gem::Version
67
- version: '0.22'
66
+ version: '1.3'
68
67
  type: :development
69
68
  prerelease: false
70
69
  version_requirements: !ruby/object:Gem::Requirement
71
70
  requirements:
72
71
  - - "~>"
73
72
  - !ruby/object:Gem::Version
74
- version: '0.22'
75
- description:
73
+ version: '1.3'
74
+ - !ruby/object:Gem::Dependency
75
+ name: standard
76
+ requirement: !ruby/object:Gem::Requirement
77
+ requirements:
78
+ - - "~>"
79
+ - !ruby/object:Gem::Version
80
+ version: '1.56'
81
+ type: :development
82
+ prerelease: false
83
+ version_requirements: !ruby/object:Gem::Requirement
84
+ requirements:
85
+ - - "~>"
86
+ - !ruby/object:Gem::Version
87
+ version: '1.56'
88
+ - !ruby/object:Gem::Dependency
89
+ name: bundler-audit
90
+ requirement: !ruby/object:Gem::Requirement
91
+ requirements:
92
+ - - "~>"
93
+ - !ruby/object:Gem::Version
94
+ version: '0.9'
95
+ type: :development
96
+ prerelease: false
97
+ version_requirements: !ruby/object:Gem::Requirement
98
+ requirements:
99
+ - - "~>"
100
+ - !ruby/object:Gem::Version
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'
76
116
  email:
77
117
  - jdoyley@gmail.com
78
118
  executables: []
@@ -89,9 +129,10 @@ files:
89
129
  - lib/stamp_duty.rb
90
130
  - lib/stamp_duty/band.rb
91
131
  - lib/stamp_duty/band_amount.rb
92
- - lib/stamp_duty/band_selector.rb
93
- - lib/stamp_duty/residential_calculator.rb
132
+ - lib/stamp_duty/calculation.rb
133
+ - lib/stamp_duty/rates.rb
94
134
  - lib/stamp_duty/version.rb
135
+ - sig/stamp_duty.rbs
95
136
  - stamp_duty.gemspec
96
137
  homepage: https://github.com/j-dexx/stamp_duty
97
138
  licenses:
@@ -100,7 +141,6 @@ metadata:
100
141
  source_code_uri: https://github.com/j-dexx/stamp_duty
101
142
  changelog_uri: https://github.com/j-dexx/stamp_duty/blob/master/CHANGELOG.md
102
143
  rubygems_mfa_required: 'true'
103
- post_install_message:
104
144
  rdoc_options: []
105
145
  require_paths:
106
146
  - lib
@@ -115,8 +155,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
115
155
  - !ruby/object:Gem::Version
116
156
  version: '0'
117
157
  requirements: []
118
- rubygems_version: 3.5.3
119
- signing_key:
158
+ rubygems_version: 4.0.20
120
159
  specification_version: 4
121
160
  summary: A UK property stamp duty calculator
122
161
  test_files: []
@@ -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_bound] }
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,38 +0,0 @@
1
- module StampDuty
2
- class ResidentialCalculator
3
-
4
- attr_reader :purchase_price, :bands
5
-
6
- def initialize(purchase_price, bands)
7
- @purchase_price = StampDuty.normalize_price(purchase_price)
8
- @bands = bands
9
- end
10
-
11
- # Total tax, rounded down to the whole pound as HMRC does.
12
- def stamp_duty
13
- @stamp_duty ||= band_amounts.map(&:amount).reduce(BigDecimal(0), :+).round(0, BigDecimal::ROUND_FLOOR)
14
- end
15
-
16
- # Band amounts ordered lowest band first.
17
- def band_amounts
18
- @band_amounts ||= build_band_amounts
19
- end
20
-
21
- # Kept for backwards compatibility; results are computed lazily.
22
- def calculate
23
- band_amounts
24
- self
25
- end
26
-
27
- private
28
-
29
- def build_band_amounts
30
- price = purchase_price
31
- bands.sort_by { |band| -band.lower_bound }.map do |band|
32
- band_amount = StampDuty::BandAmount.new(price: price, band: band)
33
- price = band.lower_bound
34
- band_amount
35
- end.reverse
36
- end
37
- end
38
- end