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 +5 -5
- data/CHANGELOG.md +52 -0
- data/CONTEXT.md +46 -0
- data/Gemfile +1 -1
- data/README.md +50 -22
- data/Rakefile +8 -2
- data/lib/stamp_duty/band.rb +24 -17
- data/lib/stamp_duty/band_amount.rb +19 -12
- data/lib/stamp_duty/calculation.rb +31 -0
- data/lib/stamp_duty/rates.rb +43 -0
- data/lib/stamp_duty/version.rb +1 -1
- data/lib/stamp_duty.rb +32 -5
- data/sig/stamp_duty.rbs +62 -0
- data/stamp_duty.gemspec +27 -20
- metadata +55 -37
- data/.travis.yml +0 -4
- data/bin/console +0 -14
- data/bin/setup +0 -8
- data/lib/stamp_duty/band_selector.rb +0 -35
- data/lib/stamp_duty/residential_calculator.rb +0 -32
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
|
-
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 4b724c57600f6cb367e2442010a0348105300dee3042f767774e2a62073455b4
|
|
4
|
+
data.tar.gz: 1e27b0ef5cb82fce0268381f513fe2c4254d42987913aeb412b4260be2ee9a1a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
data/README.md
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
# StampDuty
|
|
2
2
|
|
|
3
|
-
StampDuty is a
|
|
4
|
-
|
|
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(
|
|
26
|
-
calc.calculate
|
|
30
|
+
calc = StampDuty.for(275_000)
|
|
27
31
|
|
|
28
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
56
|
+
puts ba.description
|
|
57
|
+
puts ba.amount.to_s("F")
|
|
58
|
+
puts ba.percentage_rate
|
|
39
59
|
end
|
|
40
60
|
```
|
|
41
61
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
0.0
|
|
62
|
+
```
|
|
63
|
+
Up to £125,000
|
|
64
|
+
0.0
|
|
46
65
|
0
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
2500.0
|
|
66
|
+
£125,001 to £250,000
|
|
67
|
+
2500.0
|
|
50
68
|
2
|
|
51
|
-
|
|
52
|
-
|
|
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`.
|
|
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/
|
|
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[
|
|
8
|
+
t.test_files = FileList["test/**/*_test.rb"]
|
|
8
9
|
end
|
|
9
10
|
|
|
10
|
-
|
|
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]
|
data/lib/stamp_duty/band.rb
CHANGED
|
@@ -1,31 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module StampDuty
|
|
2
|
-
|
|
3
|
-
|
|
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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
|
12
|
-
|
|
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
|
|
18
|
-
percentage_rate
|
|
21
|
+
def with_surcharge(percentage_points)
|
|
22
|
+
with(percentage_rate: percentage_rate + percentage_points)
|
|
19
23
|
end
|
|
20
24
|
|
|
21
|
-
|
|
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
|
-
|
|
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
|
-
|
|
28
|
-
|
|
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
|
-
|
|
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
|
|
6
|
-
attr_reader :price, :band
|
|
10
|
+
def_delegators :band, :lower_bound, :upper_bound, :percentage_rate, :decimal_percentage_rate, :description
|
|
7
11
|
|
|
8
|
-
def
|
|
9
|
-
|
|
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
|
-
|
|
17
|
+
decimal_percentage_rate * taxable_amount
|
|
15
18
|
end
|
|
16
19
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
data/lib/stamp_duty/version.rb
CHANGED
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/
|
|
8
|
+
require "stamp_duty/rates"
|
|
9
|
+
require "stamp_duty/calculation"
|
|
8
10
|
|
|
9
11
|
module StampDuty
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
data/sig/stamp_duty.rbs
ADDED
|
@@ -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
|
-
|
|
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
|
|
3
|
+
require "stamp_duty/version"
|
|
5
4
|
|
|
6
5
|
Gem::Specification.new do |spec|
|
|
7
|
-
spec.name
|
|
8
|
-
spec.version
|
|
9
|
-
spec.authors
|
|
10
|
-
spec.email
|
|
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
|
|
13
|
-
spec.homepage
|
|
14
|
-
spec.license
|
|
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.
|
|
17
|
-
|
|
18
|
-
|
|
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 = "
|
|
25
|
+
spec.required_ruby_version = ">= 3.3"
|
|
26
|
+
|
|
27
|
+
spec.add_dependency "bigdecimal", ">= 3.1", "< 5"
|
|
21
28
|
|
|
22
|
-
spec.add_development_dependency "
|
|
23
|
-
spec.add_development_dependency "
|
|
24
|
-
spec.add_development_dependency "
|
|
25
|
-
spec.add_development_dependency "
|
|
26
|
-
spec.add_development_dependency "
|
|
27
|
-
spec.add_development_dependency "
|
|
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.
|
|
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:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
13
12
|
- !ruby/object:Gem::Dependency
|
|
14
|
-
name:
|
|
13
|
+
name: bigdecimal
|
|
15
14
|
requirement: !ruby/object:Gem::Requirement
|
|
16
15
|
requirements:
|
|
17
|
-
- - "
|
|
16
|
+
- - ">="
|
|
18
17
|
- !ruby/object:Gem::Version
|
|
19
|
-
version: '1
|
|
20
|
-
|
|
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
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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:
|
|
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:
|
|
73
|
+
version: '1.3'
|
|
69
74
|
- !ruby/object:Gem::Dependency
|
|
70
|
-
name:
|
|
75
|
+
name: standard
|
|
71
76
|
requirement: !ruby/object:Gem::Requirement
|
|
72
77
|
requirements:
|
|
73
78
|
- - "~>"
|
|
74
79
|
- !ruby/object:Gem::Version
|
|
75
|
-
version: 1.
|
|
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.
|
|
87
|
+
version: '1.56'
|
|
83
88
|
- !ruby/object:Gem::Dependency
|
|
84
|
-
name:
|
|
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
|
-
|
|
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
|
-
-
|
|
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/
|
|
116
|
-
- lib/stamp_duty/
|
|
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
|
-
|
|
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: '
|
|
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
|
-
|
|
139
|
-
rubygems_version: 2.5.1
|
|
140
|
-
signing_key:
|
|
158
|
+
rubygems_version: 4.0.20
|
|
141
159
|
specification_version: 4
|
|
142
|
-
summary: A UK
|
|
160
|
+
summary: A UK property stamp duty calculator
|
|
143
161
|
test_files: []
|
data/.travis.yml
DELETED
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,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
|