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 +4 -4
- data/CHANGELOG.md +25 -0
- data/CONTEXT.md +18 -3
- data/Gemfile +1 -1
- data/README.md +41 -27
- 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 +21 -6
- data/sig/stamp_duty.rbs +62 -0
- data/stamp_duty.gemspec +17 -14
- metadata +52 -13
- data/lib/stamp_duty/band_selector.rb +0 -35
- data/lib/stamp_duty/residential_calculator.rb +0 -38
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
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
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
|
|
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
|
|
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
|
-
-
|
|
46
|
+
- Rates are those in force from 1 April 2025; purchases completed earlier used different rates.
|
data/Gemfile
CHANGED
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
|
|
5
|
-
|
|
6
|
-
|
|
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(
|
|
30
|
+
calc = StampDuty.for(275_000)
|
|
30
31
|
|
|
31
|
-
#
|
|
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
|
-
|
|
36
|
-
or Float). Anything else raises `ArgumentError`:
|
|
36
|
+
Pass options for reliefs and surcharges:
|
|
37
37
|
|
|
38
38
|
```ruby
|
|
39
|
-
StampDuty.for(
|
|
40
|
-
StampDuty.for(
|
|
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
|
-
|
|
44
|
-
|
|
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
|
-
|
|
52
|
+
The tax due in each band is available too:
|
|
47
53
|
|
|
48
54
|
```ruby
|
|
49
55
|
calc.band_amounts.each do |ba|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
56
|
+
puts ba.description
|
|
57
|
+
puts ba.amount.to_s("F")
|
|
58
|
+
puts ba.percentage_rate
|
|
53
59
|
end
|
|
54
60
|
```
|
|
55
61
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
0.0
|
|
62
|
+
```
|
|
63
|
+
Up to £125,000
|
|
64
|
+
0.0
|
|
60
65
|
0
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
2500.0
|
|
66
|
+
£125,001 to £250,000
|
|
67
|
+
2500.0
|
|
64
68
|
2
|
|
65
|
-
|
|
66
|
-
|
|
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`.
|
|
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[
|
|
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,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/
|
|
8
|
+
require "stamp_duty/rates"
|
|
9
|
+
require "stamp_duty/calculation"
|
|
9
10
|
|
|
10
11
|
module StampDuty
|
|
11
|
-
|
|
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 =
|
|
14
|
-
|
|
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
|
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,16 +1,16 @@
|
|
|
1
|
-
lib = File.expand_path(
|
|
1
|
+
lib = File.expand_path("../lib", __FILE__)
|
|
2
2
|
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
3
|
-
require
|
|
3
|
+
require "stamp_duty/version"
|
|
4
4
|
|
|
5
5
|
Gem::Specification.new do |spec|
|
|
6
|
-
spec.name
|
|
7
|
-
spec.version
|
|
8
|
-
spec.authors
|
|
9
|
-
spec.email
|
|
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
|
|
12
|
-
spec.homepage
|
|
13
|
-
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"
|
|
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
|
|
22
|
-
spec.bindir
|
|
23
|
-
spec.executables
|
|
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", "~>
|
|
31
|
-
spec.add_development_dependency "simplecov", "~>
|
|
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.
|
|
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
13
|
name: bigdecimal
|
|
@@ -50,29 +49,70 @@ dependencies:
|
|
|
50
49
|
requirements:
|
|
51
50
|
- - "~>"
|
|
52
51
|
- !ruby/object:Gem::Version
|
|
53
|
-
version: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
75
|
-
|
|
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/
|
|
93
|
-
- lib/stamp_duty/
|
|
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:
|
|
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
|