steuer 0.2.0.pre.alpha
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 +7 -0
- data/LICENSE +21 -0
- data/README.md +212 -0
- data/lib/steuer/errors.rb +8 -0
- data/lib/steuer/state_mapping.rb +208 -0
- data/lib/steuer/steuernummer.rb +303 -0
- data/lib/steuer/version.rb +5 -0
- data/lib/steuer.rb +13 -0
- metadata +163 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 359109ea7cce6dd33e9fed32d083742ad20b27f97e84b3d89c4bdf73aa281dd4
|
|
4
|
+
data.tar.gz: 4fa8e2834415fc5e041a7f00ea13eac12d4340f62aed2a6168e1fac2aa6b80f2
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 7c4805f2ff71eed6e1bcf2b85335226dab136107b82718fa9d223d8c83b8884b13817ba9a4dce30403bb60cac163f227f1527413b911b6f0245bc1c0748b2739
|
|
7
|
+
data.tar.gz: 126840ae3f0888c8dfc1d1eb0f0e717686b6f1b0a841fb9b18e54f3c9f408259b1576b1d2416e1428fd34529dde3460cd2a907a3d52f2143e22e3b266a461a90
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Steuer Contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
# Steuer
|
|
2
|
+
|
|
3
|
+
A Ruby gem for German tax system utilities, including Steuernummer (tax number) conversion between different formats and validation.
|
|
4
|
+
|
|
5
|
+
Based on the official specifications from the [German Wikipedia page on Steuernummer](https://de.wikipedia.org/wiki/Steuernummer#Deutschland).
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **Format Detection**: Automatically detects the format of German tax numbers
|
|
10
|
+
- **Format Conversion**: Convert between three different formats:
|
|
11
|
+
- Standard scheme (state-specific format, e.g., `93/815/08152`)
|
|
12
|
+
- Unified federal scheme (12-digit, e.g., `289381508152`)
|
|
13
|
+
- Unified federal scheme for electronic transmission (13-digit, e.g., `2893081508152`)
|
|
14
|
+
- **State Detection**: Automatically detects the German state (Bundesland) from tax numbers
|
|
15
|
+
- **Validation**: Validates tax numbers according to official patterns
|
|
16
|
+
- **Object-Oriented**: Clean, object-oriented API design
|
|
17
|
+
- **Extensible**: Designed to be extended with additional German tax utilities (VAT validation, etc.)
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
Add this line to your application's Gemfile:
|
|
22
|
+
|
|
23
|
+
```ruby
|
|
24
|
+
gem 'steuer'
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
And then execute:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
$ bundle install
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Or install it yourself as:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
$ gem install steuer
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Usage
|
|
40
|
+
|
|
41
|
+
### Basic Usage
|
|
42
|
+
|
|
43
|
+
```ruby
|
|
44
|
+
require 'steuer'
|
|
45
|
+
|
|
46
|
+
# Create a tax number object (auto-detects state from unambiguous formats)
|
|
47
|
+
tax_number = Steuer.steuernummer('93/815/08152') # Auto-detects Baden-Württemberg
|
|
48
|
+
|
|
49
|
+
# Check if valid
|
|
50
|
+
puts tax_number.valid? # => true
|
|
51
|
+
|
|
52
|
+
# Get information
|
|
53
|
+
puts tax_number.state_code # => "BW"
|
|
54
|
+
puts tax_number.state_name # => "Baden-Württemberg"
|
|
55
|
+
puts tax_number.format_type # => :standard
|
|
56
|
+
|
|
57
|
+
# Convert between formats
|
|
58
|
+
puts tax_number.to_federal_12 # => "289381508152"
|
|
59
|
+
puts tax_number.to_federal_13 # => "2893081508152"
|
|
60
|
+
puts tax_number.to_standard # => "93/815/08152"
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Auto-Detection vs Explicit State
|
|
64
|
+
|
|
65
|
+
```ruby
|
|
66
|
+
# ✅ Auto-detects from standard format (each state has unique pattern)
|
|
67
|
+
standard = Steuer.steuernummer('181/815/08155') # Auto-detects Bayern
|
|
68
|
+
puts standard.state_code # => "BY"
|
|
69
|
+
|
|
70
|
+
# ✅ Auto-detects from unambiguous federal prefixes
|
|
71
|
+
federal_12 = Steuer.steuernummer('289381508152') # Prefix '28' is unique to BW
|
|
72
|
+
puts federal_12.state_name # => "Baden-Württemberg"
|
|
73
|
+
|
|
74
|
+
# ❌ Requires explicit state for ambiguous prefixes
|
|
75
|
+
begin
|
|
76
|
+
Steuer.steuernummer('304881508155') # Prefix '3' shared by BB, SN, ST
|
|
77
|
+
rescue Steuer::UnsupportedStateError => e
|
|
78
|
+
puts e.message # => "Cannot determine state from tax number..."
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# ✅ Works with explicit state for ambiguous cases
|
|
82
|
+
ambiguous = Steuer.steuernummer('304881508155', state: 'BB')
|
|
83
|
+
puts ambiguous.state_code # => "BB"
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Ambiguous vs Unambiguous Prefixes
|
|
87
|
+
|
|
88
|
+
**✅ Unambiguous prefixes (auto-detected):**
|
|
89
|
+
|
|
90
|
+
- `28` → Baden-Württemberg
|
|
91
|
+
- `9` → Bayern
|
|
92
|
+
- `11` → Berlin
|
|
93
|
+
- `24` → Bremen
|
|
94
|
+
- `22` → Hamburg
|
|
95
|
+
- `26` → Hessen
|
|
96
|
+
- `23` → Niedersachsen
|
|
97
|
+
- `5` → Nordrhein-Westfalen
|
|
98
|
+
- `27` → Rheinland-Pfalz
|
|
99
|
+
- `1` → Saarland
|
|
100
|
+
- `21` → Schleswig-Holstein
|
|
101
|
+
|
|
102
|
+
**❌ Ambiguous prefixes (require explicit state):**
|
|
103
|
+
|
|
104
|
+
- `3` → Brandenburg, Sachsen, or Sachsen-Anhalt
|
|
105
|
+
- `4` → Mecklenburg-Vorpommern or Thüringen
|
|
106
|
+
|
|
107
|
+
### Error Handling
|
|
108
|
+
|
|
109
|
+
```ruby
|
|
110
|
+
begin
|
|
111
|
+
# Invalid format
|
|
112
|
+
Steuer.steuernummer('invalid')
|
|
113
|
+
rescue Steuer::InvalidTaxNumberError => e
|
|
114
|
+
puts "Invalid format: #{e.message}"
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
begin
|
|
118
|
+
# Unsupported state or undetectable
|
|
119
|
+
Steuer.steuernummer('99/999/99999')
|
|
120
|
+
rescue Steuer::UnsupportedStateError => e
|
|
121
|
+
puts "Unsupported state: #{e.message}"
|
|
122
|
+
end
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Supported German States
|
|
126
|
+
|
|
127
|
+
The gem supports all 16 German states (Bundesländer):
|
|
128
|
+
|
|
129
|
+
| State Code | State Name | Standard Format Example | 12-digit Example | 13-digit Example |
|
|
130
|
+
| ---------- | ---------------------- | ----------------------- | ---------------- | ---------------- |
|
|
131
|
+
| BW | Baden-Württemberg | `93/815/08152` | `289381508152` | `2893081508152` |
|
|
132
|
+
| BY | Bayern | `181/815/08155` | `918181508155` | `9181081508155` |
|
|
133
|
+
| BE | Berlin | `21/815/08150` | `112181508150` | `1121081508150` |
|
|
134
|
+
| BB | Brandenburg | `048/815/08155` | `304881508155` | `3048081508155` |
|
|
135
|
+
| HB | Bremen | `75/815/08152` | `247581508152` | `2475081508152` |
|
|
136
|
+
| HH | Hamburg | `02/815/08156` | `220281508156` | `2202081508156` |
|
|
137
|
+
| HE | Hessen | `013/815/08153` | `261381508153` | `2613081508153` |
|
|
138
|
+
| MV | Mecklenburg-Vorpommern | `79/815/08151` | `407981508151` | `4079081508151` |
|
|
139
|
+
| NI | Niedersachsen | `24/815/08151` | `232481508151` | `2324081508151` |
|
|
140
|
+
| NW | Nordrhein-Westfalen | `133/8150/8159` | `513381508159` | `5133081508159` |
|
|
141
|
+
| RP | Rheinland-Pfalz | `22/815/08154` | `272281508154` | `2722081508154` |
|
|
142
|
+
| SL | Saarland | `010/815/08182` | `101081508182` | `1010081508182` |
|
|
143
|
+
| SN | Sachsen | `201/815/08156` | `320181508156` | `3201081508156` |
|
|
144
|
+
| ST | Sachsen-Anhalt | `101/815/08153` | `310181508153` | `3101081508153` |
|
|
145
|
+
| SH | Schleswig-Holstein | `01/815/08155` | `210181508155` | `2101081508155` |
|
|
146
|
+
| TH | Thüringen | `151/815/08154` | `415181508154` | `4151081508154` |
|
|
147
|
+
|
|
148
|
+
## API Reference
|
|
149
|
+
|
|
150
|
+
### `Steuer.steuernummer(tax_number, state_code = nil)`
|
|
151
|
+
|
|
152
|
+
Creates a new `Steuer::Steuernummer` object.
|
|
153
|
+
|
|
154
|
+
**Parameters:**
|
|
155
|
+
|
|
156
|
+
- `tax_number` (String): The tax number in any supported format
|
|
157
|
+
- `state_code` (String, optional): The German state code (auto-detected if not provided)
|
|
158
|
+
|
|
159
|
+
**Returns:** `Steuer::Steuernummer` instance
|
|
160
|
+
|
|
161
|
+
### `Steuer::Steuernummer` Methods
|
|
162
|
+
|
|
163
|
+
#### Instance Methods
|
|
164
|
+
|
|
165
|
+
- `valid?` - Returns `true` if the tax number is valid
|
|
166
|
+
- `to_federal_12` - Converts to 12-digit unified federal scheme
|
|
167
|
+
- `to_federal_13` - Converts to 13-digit electronic transmission format
|
|
168
|
+
- `to_standard` - Converts to standard state-specific format
|
|
169
|
+
- `state_code` - Returns the German state code (e.g., "BW")
|
|
170
|
+
- `state_name` - Returns the full state name (e.g., "Baden-Württemberg")
|
|
171
|
+
- `format_type` - Returns the detected format (`:standard`, `:federal_12`, or `:federal_13`)
|
|
172
|
+
- `original_input` - Returns the original input string
|
|
173
|
+
|
|
174
|
+
## Development
|
|
175
|
+
|
|
176
|
+
After checking out the repo, run:
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
bundle install
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
To run the tests:
|
|
183
|
+
|
|
184
|
+
```bash
|
|
185
|
+
bundle exec rspec
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
## Contributing
|
|
189
|
+
|
|
190
|
+
1. Fork it
|
|
191
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
|
192
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
|
193
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
|
194
|
+
5. Create new Pull Request
|
|
195
|
+
|
|
196
|
+
## Future Features
|
|
197
|
+
|
|
198
|
+
This gem is designed to be extensible. Planned features include:
|
|
199
|
+
|
|
200
|
+
- German VAT number validation
|
|
201
|
+
- Steuer-ID (tax identification number) handling
|
|
202
|
+
- Wirtschafts-Identifikationsnummer support
|
|
203
|
+
- Additional German tax system utilities
|
|
204
|
+
|
|
205
|
+
## References
|
|
206
|
+
|
|
207
|
+
- [German Steuernummer Wikipedia Page](https://de.wikipedia.org/wiki/Steuernummer#Deutschland)
|
|
208
|
+
- [Bundeszentralamt für Steuern](https://www.bzst.de/)
|
|
209
|
+
|
|
210
|
+
## License
|
|
211
|
+
|
|
212
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Steuer
|
|
4
|
+
module StateMapping
|
|
5
|
+
# Mapping from state codes to their configuration
|
|
6
|
+
# Based on https://de.wikipedia.org/wiki/Steuernummer#Deutschland
|
|
7
|
+
STATES = {
|
|
8
|
+
'BW' => {
|
|
9
|
+
name: 'Baden-Württemberg',
|
|
10
|
+
standard_pattern: %r{^(\d{2})/(\d{3})/(\d{5})$},
|
|
11
|
+
federal_12_prefix: '28',
|
|
12
|
+
federal_13_prefix: '28',
|
|
13
|
+
federal_13_zero_position: 4, # Insert 0 at position 4 (after FF)
|
|
14
|
+
},
|
|
15
|
+
'BY' => {
|
|
16
|
+
name: 'Bayern',
|
|
17
|
+
standard_pattern: %r{^(\d{3})/(\d{3})/(\d{5})$},
|
|
18
|
+
federal_12_prefix: '9',
|
|
19
|
+
federal_13_prefix: '9',
|
|
20
|
+
federal_13_zero_position: 4, # Insert 0 at position 4 (after FFF)
|
|
21
|
+
},
|
|
22
|
+
'BE' => {
|
|
23
|
+
name: 'Berlin',
|
|
24
|
+
standard_pattern: %r{^(\d{2})/(\d{3})/(\d{5})$},
|
|
25
|
+
federal_12_prefix: '11',
|
|
26
|
+
federal_13_prefix: '11',
|
|
27
|
+
federal_13_zero_position: 4, # Insert 0 at position 4 (after FF)
|
|
28
|
+
},
|
|
29
|
+
'BB' => {
|
|
30
|
+
name: 'Brandenburg',
|
|
31
|
+
standard_pattern: %r{^(\d{3})/(\d{3})/(\d{5})$},
|
|
32
|
+
federal_12_prefix: '3',
|
|
33
|
+
federal_13_prefix: '3',
|
|
34
|
+
federal_13_zero_position: 4, # Insert 0 at position 4 (after FFF)
|
|
35
|
+
},
|
|
36
|
+
'HB' => {
|
|
37
|
+
name: 'Bremen',
|
|
38
|
+
standard_pattern: %r{^(\d{2})/(\d{3})/(\d{5})$},
|
|
39
|
+
federal_12_prefix: '24',
|
|
40
|
+
federal_13_prefix: '24',
|
|
41
|
+
federal_13_zero_position: 4, # Insert 0 at position 4 (after FF)
|
|
42
|
+
},
|
|
43
|
+
'HH' => {
|
|
44
|
+
name: 'Hamburg',
|
|
45
|
+
standard_pattern: %r{^(\d{2})/(\d{3})/(\d{5})$},
|
|
46
|
+
federal_12_prefix: '22',
|
|
47
|
+
federal_13_prefix: '22',
|
|
48
|
+
federal_13_zero_position: 4, # Insert 0 at position 4 (after FF)
|
|
49
|
+
},
|
|
50
|
+
'HE' => {
|
|
51
|
+
name: 'Hessen',
|
|
52
|
+
standard_pattern: %r{^0(1[3-9])/(\d{3})/(\d{5})$}, # More specific: 013-019 range, capture 13-19
|
|
53
|
+
federal_12_prefix: '26',
|
|
54
|
+
federal_13_prefix: '26',
|
|
55
|
+
federal_13_zero_position: 4, # Insert 0 at position 4 (after FF)
|
|
56
|
+
},
|
|
57
|
+
'MV' => {
|
|
58
|
+
name: 'Mecklenburg-Vorpommern',
|
|
59
|
+
standard_pattern: %r{^(\d{3})/(\d{3})/(\d{5})$},
|
|
60
|
+
federal_12_prefix: '4',
|
|
61
|
+
federal_13_prefix: '4',
|
|
62
|
+
federal_13_zero_position: 4, # Insert 0 at position 4 (after FFF)
|
|
63
|
+
},
|
|
64
|
+
'NI' => {
|
|
65
|
+
name: 'Niedersachsen',
|
|
66
|
+
standard_pattern: %r{^(\d{2})/(\d{3})/(\d{5})$},
|
|
67
|
+
federal_12_prefix: '23',
|
|
68
|
+
federal_13_prefix: '23',
|
|
69
|
+
federal_13_zero_position: 4, # Insert 0 at position 4 (after FF)
|
|
70
|
+
},
|
|
71
|
+
'NW' => {
|
|
72
|
+
name: 'Nordrhein-Westfalen',
|
|
73
|
+
standard_pattern: %r{^(\d{3})/(\d{4})/(\d{4})$},
|
|
74
|
+
federal_12_prefix: '5',
|
|
75
|
+
federal_13_prefix: '5',
|
|
76
|
+
federal_13_zero_position: 4, # Insert 0 at position 4 (after FFF)
|
|
77
|
+
},
|
|
78
|
+
'RP' => {
|
|
79
|
+
name: 'Rheinland-Pfalz',
|
|
80
|
+
standard_pattern: %r{^(\d{2})/(\d{3})/(\d{5})$},
|
|
81
|
+
federal_12_prefix: '27',
|
|
82
|
+
federal_13_prefix: '27',
|
|
83
|
+
federal_13_zero_position: 4, # Insert 0 at position 4 (after FF)
|
|
84
|
+
},
|
|
85
|
+
'SL' => {
|
|
86
|
+
name: 'Saarland',
|
|
87
|
+
standard_pattern: %r{^(01[0-2])/(\d{3})/(\d{5})$}, # Specific to 010-012 for Saarland
|
|
88
|
+
federal_12_prefix: '1',
|
|
89
|
+
federal_13_prefix: '1',
|
|
90
|
+
federal_13_zero_position: 4, # Insert 0 at position 4 (after FFF)
|
|
91
|
+
},
|
|
92
|
+
'SN' => {
|
|
93
|
+
name: 'Sachsen',
|
|
94
|
+
standard_pattern: %r{^(\d{3})/(\d{3})/(\d{5})$},
|
|
95
|
+
federal_12_prefix: '3',
|
|
96
|
+
federal_13_prefix: '3',
|
|
97
|
+
federal_13_zero_position: 4, # Insert 0 at position 4 (after FFF)
|
|
98
|
+
},
|
|
99
|
+
'ST' => {
|
|
100
|
+
name: 'Sachsen-Anhalt',
|
|
101
|
+
standard_pattern: %r{^(\d{3})/(\d{3})/(\d{5})$},
|
|
102
|
+
federal_12_prefix: '3',
|
|
103
|
+
federal_13_prefix: '3',
|
|
104
|
+
federal_13_zero_position: 4, # Insert 0 at position 4 (after FFF)
|
|
105
|
+
},
|
|
106
|
+
'SH' => {
|
|
107
|
+
name: 'Schleswig-Holstein',
|
|
108
|
+
standard_pattern: %r{^(\d{2})/(\d{3})/(\d{5})$},
|
|
109
|
+
federal_12_prefix: '21',
|
|
110
|
+
federal_13_prefix: '21',
|
|
111
|
+
federal_13_zero_position: 4, # Insert 0 at position 4 (after FF)
|
|
112
|
+
},
|
|
113
|
+
'TH' => {
|
|
114
|
+
name: 'Thüringen',
|
|
115
|
+
standard_pattern: %r{^(\d{3})/(\d{3})/(\d{5})$},
|
|
116
|
+
federal_12_prefix: '4',
|
|
117
|
+
federal_13_prefix: '4',
|
|
118
|
+
federal_13_zero_position: 4, # Insert 0 at position 4 (after FFF)
|
|
119
|
+
},
|
|
120
|
+
}.freeze
|
|
121
|
+
class << self
|
|
122
|
+
def find_state_by_standard_format(tax_number)
|
|
123
|
+
# Try more specific patterns first to avoid conflicts with general patterns
|
|
124
|
+
# Priority: character classes with ranges > specific digit patterns > general digit patterns
|
|
125
|
+
sorted_states = STATES.sort_by do |_, config|
|
|
126
|
+
pattern_str = config[:standard_pattern].source
|
|
127
|
+
if pattern_str.include?('[') # Character classes like [0-2] or [3-9]
|
|
128
|
+
0
|
|
129
|
+
elsif pattern_str.include?('0(') || pattern_str.start_with?('^0') # Patterns starting with specific digit
|
|
130
|
+
1
|
|
131
|
+
else # General patterns like (\d{3})
|
|
132
|
+
2
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
sorted_states.each do |code, config|
|
|
137
|
+
return code if tax_number.match?(config[:standard_pattern])
|
|
138
|
+
end
|
|
139
|
+
nil
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def find_state_by_federal_12(tax_number)
|
|
143
|
+
return unless tax_number.length == 12
|
|
144
|
+
|
|
145
|
+
# Try exact prefix matches, longest first to avoid conflicts
|
|
146
|
+
# e.g., '28' should match before '2', '11' before '1'
|
|
147
|
+
sorted_states = STATES.sort_by { |_, config| -config[:federal_12_prefix].length }
|
|
148
|
+
|
|
149
|
+
sorted_states.each do |code, config|
|
|
150
|
+
prefix = config[:federal_12_prefix]
|
|
151
|
+
|
|
152
|
+
# Check if tax number starts with this exact prefix
|
|
153
|
+
next unless tax_number.start_with?(prefix)
|
|
154
|
+
|
|
155
|
+
# For single digit prefixes, ensure we don't match longer numbers
|
|
156
|
+
if prefix.length == 1
|
|
157
|
+
# For single digit, check that the next digit doesn't make it a longer prefix
|
|
158
|
+
next_char = tax_number[prefix.length]
|
|
159
|
+
return nil if next_char.nil? # Invalid tax number
|
|
160
|
+
|
|
161
|
+
# Skip if this would create a longer prefix that exists
|
|
162
|
+
longer_prefix = prefix + next_char
|
|
163
|
+
has_longer_prefix = STATES.any? { |_, c| c[:federal_12_prefix] == longer_prefix }
|
|
164
|
+
next if has_longer_prefix
|
|
165
|
+
|
|
166
|
+
# Additional validation: reject obviously invalid combinations
|
|
167
|
+
# For prefix '9' (Bayern), '99' is not a valid federal tax number start
|
|
168
|
+
if prefix == '9' && next_char == '9'
|
|
169
|
+
return nil
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
return code
|
|
174
|
+
end
|
|
175
|
+
nil
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def find_state_by_federal_13(tax_number)
|
|
179
|
+
return unless tax_number.length == 13
|
|
180
|
+
|
|
181
|
+
# Try exact prefix matches, longest first to avoid conflicts
|
|
182
|
+
sorted_states = STATES.sort_by { |_, config| -config[:federal_13_prefix].length }
|
|
183
|
+
|
|
184
|
+
sorted_states.each do |code, config|
|
|
185
|
+
prefix = config[:federal_13_prefix]
|
|
186
|
+
|
|
187
|
+
# Check if tax number starts with this exact prefix
|
|
188
|
+
next unless tax_number.start_with?(prefix)
|
|
189
|
+
|
|
190
|
+
# For single digit prefixes, ensure we don't match longer numbers
|
|
191
|
+
if prefix.length == 1
|
|
192
|
+
# For single digit, check that the next digit doesn't make it a longer prefix
|
|
193
|
+
next_char = tax_number[prefix.length]
|
|
194
|
+
return nil if next_char.nil? # Invalid tax number
|
|
195
|
+
|
|
196
|
+
# Skip if this would create a longer prefix that exists
|
|
197
|
+
longer_prefix = prefix + next_char
|
|
198
|
+
has_longer_prefix = STATES.any? { |_, c| c[:federal_13_prefix] == longer_prefix }
|
|
199
|
+
next if has_longer_prefix
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
return code
|
|
203
|
+
end
|
|
204
|
+
nil
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
end
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'state_mapping'
|
|
4
|
+
|
|
5
|
+
module Steuer
|
|
6
|
+
class Steuernummer
|
|
7
|
+
attr_reader :original_input, :state_code
|
|
8
|
+
|
|
9
|
+
def initialize(tax_number, state: nil)
|
|
10
|
+
@original_input = tax_number.to_s.strip
|
|
11
|
+
@state_code = normalize_state_code(state) || auto_detect_state
|
|
12
|
+
|
|
13
|
+
validate_tax_number!
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def valid?
|
|
17
|
+
return false if @original_input.empty?
|
|
18
|
+
return false unless format_type
|
|
19
|
+
return false unless @state_code
|
|
20
|
+
|
|
21
|
+
case format_type
|
|
22
|
+
when :standard
|
|
23
|
+
validate_standard_format
|
|
24
|
+
when :federal_12
|
|
25
|
+
validate_federal_12_format
|
|
26
|
+
when :federal_13
|
|
27
|
+
validate_federal_13_format
|
|
28
|
+
else
|
|
29
|
+
false
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def to_federal_12
|
|
34
|
+
return unless valid?
|
|
35
|
+
|
|
36
|
+
case format_type
|
|
37
|
+
when :standard
|
|
38
|
+
convert_standard_to_federal_12
|
|
39
|
+
when :federal_12
|
|
40
|
+
normalized_input
|
|
41
|
+
when :federal_13
|
|
42
|
+
convert_federal_13_to_federal_12
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def to_federal_13
|
|
47
|
+
return unless valid?
|
|
48
|
+
|
|
49
|
+
case format_type
|
|
50
|
+
when :standard
|
|
51
|
+
convert_standard_to_federal_13
|
|
52
|
+
when :federal_12
|
|
53
|
+
convert_federal_12_to_federal_13
|
|
54
|
+
when :federal_13
|
|
55
|
+
normalized_input
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def to_standard
|
|
60
|
+
return unless valid?
|
|
61
|
+
|
|
62
|
+
case format_type
|
|
63
|
+
when :standard
|
|
64
|
+
@original_input
|
|
65
|
+
when :federal_12
|
|
66
|
+
convert_federal_12_to_standard
|
|
67
|
+
when :federal_13
|
|
68
|
+
convert_federal_13_to_standard
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def state_name
|
|
73
|
+
return unless @state_code
|
|
74
|
+
|
|
75
|
+
StateMapping::STATES[@state_code][:name]
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def format_type
|
|
79
|
+
@format_type ||= if normalized_input.include?('/')
|
|
80
|
+
:standard
|
|
81
|
+
elsif normalized_input.length == 12 && normalized_input.match?(/^\d{12}$/)
|
|
82
|
+
:federal_12
|
|
83
|
+
elsif normalized_input.length == 13 && normalized_input.match?(/^\d{13}$/)
|
|
84
|
+
:federal_13
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
@format_type
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
private
|
|
91
|
+
|
|
92
|
+
def normalized_input
|
|
93
|
+
@normalized_input_input ||= if @original_input.include?('/') || @original_input.include?('-')
|
|
94
|
+
@original_input.gsub(%r{[^0-9/\-]}, '').tr('-', '/')
|
|
95
|
+
else
|
|
96
|
+
@original_input.gsub(/[^0-9]/, '')
|
|
97
|
+
end
|
|
98
|
+
@normalized_input_input
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def validate_tax_number!
|
|
102
|
+
unless format_type
|
|
103
|
+
raise InvalidTaxNumberError, "Invalid tax number format: #{@original_input}"
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Auto-detect state if not provided
|
|
107
|
+
if @state_code.nil?
|
|
108
|
+
raise UnsupportedStateError,
|
|
109
|
+
"Cannot determine state from tax number #{@original_input}. Please provide state parameter."
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
unless StateMapping::STATES.key?(@state_code)
|
|
113
|
+
raise UnsupportedStateError, "Unsupported state: #{@state_code}"
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Validate that the tax number matches the state's expected pattern
|
|
117
|
+
unless valid_for_state?
|
|
118
|
+
raise InvalidTaxNumberError, "Tax number #{@original_input} is not valid for state #{@state_code}"
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def normalize_state_code(state)
|
|
123
|
+
return if state.nil? || state.to_s.strip.empty?
|
|
124
|
+
|
|
125
|
+
state_str = state.to_s.strip.upcase
|
|
126
|
+
|
|
127
|
+
return state_str if StateMapping::STATES.key?(state_str)
|
|
128
|
+
|
|
129
|
+
StateMapping::STATES.find { |_code, config| config[:name].upcase == state_str }&.first || state_str
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def auto_detect_state
|
|
133
|
+
case format_type
|
|
134
|
+
when :standard
|
|
135
|
+
StateMapping.find_state_by_standard_format(normalized_input)
|
|
136
|
+
when :federal_12
|
|
137
|
+
# Only auto-detect if prefix is unambiguous
|
|
138
|
+
detected = StateMapping.find_state_by_federal_12(normalized_input)
|
|
139
|
+
return detected if detected && unambiguous_federal_prefix?(detected, normalized_input, :federal_12)
|
|
140
|
+
|
|
141
|
+
nil
|
|
142
|
+
when :federal_13
|
|
143
|
+
# Only auto-detect if prefix is unambiguous
|
|
144
|
+
detected = StateMapping.find_state_by_federal_13(normalized_input)
|
|
145
|
+
return detected if detected && unambiguous_federal_prefix?(detected, normalized_input, :federal_13)
|
|
146
|
+
|
|
147
|
+
nil
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def unambiguous_federal_prefix?(detected_state, tax_number, format_type)
|
|
152
|
+
prefix_key = format_type == :federal_12 ? :federal_12_prefix : :federal_13_prefix
|
|
153
|
+
detected_prefix = StateMapping::STATES[detected_state][prefix_key]
|
|
154
|
+
|
|
155
|
+
# Count how many states share this prefix
|
|
156
|
+
matching_states = StateMapping::STATES.count do |_, config|
|
|
157
|
+
config[prefix_key] == detected_prefix
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Only unambiguous if exactly one state has this prefix
|
|
161
|
+
matching_states == 1
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def valid_for_state?
|
|
165
|
+
return false unless @state_code && StateMapping::STATES.key?(@state_code)
|
|
166
|
+
|
|
167
|
+
case format_type
|
|
168
|
+
when :standard
|
|
169
|
+
validate_standard_format
|
|
170
|
+
when :federal_12
|
|
171
|
+
validate_federal_12_format
|
|
172
|
+
when :federal_13
|
|
173
|
+
validate_federal_13_format
|
|
174
|
+
else
|
|
175
|
+
false
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def validate_standard_format
|
|
180
|
+
config = StateMapping::STATES[@state_code]
|
|
181
|
+
normalized_input.match?(config[:standard_pattern])
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def validate_federal_12_format
|
|
185
|
+
return false unless normalized_input.length == 12
|
|
186
|
+
return false unless normalized_input.match?(/^\d{12}$/)
|
|
187
|
+
|
|
188
|
+
config = StateMapping::STATES[@state_code]
|
|
189
|
+
normalized_input.start_with?(config[:federal_12_prefix])
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def validate_federal_13_format
|
|
193
|
+
return false unless normalized_input.length == 13
|
|
194
|
+
return false unless normalized_input.match?(/^\d{13}$/)
|
|
195
|
+
|
|
196
|
+
config = StateMapping::STATES[@state_code]
|
|
197
|
+
normalized_input.start_with?(config[:federal_13_prefix])
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def convert_standard_to_federal_12
|
|
201
|
+
config = StateMapping::STATES[@state_code]
|
|
202
|
+
|
|
203
|
+
match = normalized_input.match(config[:standard_pattern])
|
|
204
|
+
return unless match
|
|
205
|
+
|
|
206
|
+
parts = match.captures
|
|
207
|
+
|
|
208
|
+
case @state_code
|
|
209
|
+
when 'NW' # Nordrhein-Westfalen has different structure (FFF/BBBB/UUUP)
|
|
210
|
+
fff, bbbb, uuup = parts
|
|
211
|
+
"#{config[:federal_12_prefix]}#{fff}#{bbbb}#{uuup}"
|
|
212
|
+
when 'HE' # Hessen (has leading zero in standard format)
|
|
213
|
+
if parts.length == 3
|
|
214
|
+
ff_or_fff, bbb, uuuup = parts
|
|
215
|
+
# Remove leading zero from Finanzamt code for federal format
|
|
216
|
+
finanzamt = ff_or_fff.sub(/^0/, '')
|
|
217
|
+
"#{config[:federal_12_prefix]}#{finanzamt}#{bbb}#{uuuup}"
|
|
218
|
+
end
|
|
219
|
+
else
|
|
220
|
+
if parts.length == 3
|
|
221
|
+
ff_or_fff, bbb, uuuup = parts
|
|
222
|
+
"#{config[:federal_12_prefix]}#{ff_or_fff}#{bbb}#{uuuup}"
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def convert_standard_to_federal_13
|
|
228
|
+
federal_12 = convert_standard_to_federal_12
|
|
229
|
+
return unless federal_12
|
|
230
|
+
|
|
231
|
+
config = StateMapping::STATES[@state_code]
|
|
232
|
+
insert_position = config[:federal_13_zero_position]
|
|
233
|
+
|
|
234
|
+
result = federal_12.dup
|
|
235
|
+
result.insert(insert_position, '0')
|
|
236
|
+
result
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def convert_federal_12_to_federal_13
|
|
240
|
+
config = StateMapping::STATES[@state_code]
|
|
241
|
+
insert_position = config[:federal_13_zero_position]
|
|
242
|
+
|
|
243
|
+
result = normalized_input.dup
|
|
244
|
+
result.insert(insert_position, '0')
|
|
245
|
+
result
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def convert_federal_13_to_federal_12
|
|
249
|
+
config = StateMapping::STATES[@state_code]
|
|
250
|
+
remove_position = config[:federal_13_zero_position]
|
|
251
|
+
|
|
252
|
+
result = normalized_input.dup
|
|
253
|
+
result.slice!(remove_position)
|
|
254
|
+
result
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def convert_federal_12_to_standard
|
|
258
|
+
federal_12_to_standard_format(normalized_input)
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def convert_federal_13_to_standard
|
|
262
|
+
federal_12 = convert_federal_13_to_federal_12
|
|
263
|
+
return unless federal_12
|
|
264
|
+
|
|
265
|
+
federal_12_to_standard_format(federal_12)
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
def federal_12_to_standard_format(federal_12_string)
|
|
269
|
+
config = StateMapping::STATES[@state_code]
|
|
270
|
+
prefix = config[:federal_12_prefix]
|
|
271
|
+
|
|
272
|
+
# Remove the prefix
|
|
273
|
+
without_prefix = federal_12_string[prefix.length..-1]
|
|
274
|
+
|
|
275
|
+
case @state_code
|
|
276
|
+
when 'NW' # Nordrhein-Westfalen (FFF/BBBB/UUUP)
|
|
277
|
+
fff = without_prefix[0, 3]
|
|
278
|
+
bbbb = without_prefix[3, 4]
|
|
279
|
+
uuup = without_prefix[7, 4]
|
|
280
|
+
"#{fff}/#{bbbb}/#{uuup}"
|
|
281
|
+
when 'HE' # Hessen (has leading 0 in standard format)
|
|
282
|
+
ff = without_prefix[0, 2]
|
|
283
|
+
bbb = without_prefix[2, 3]
|
|
284
|
+
uuuup = without_prefix[5, 5]
|
|
285
|
+
"0#{ff}/#{bbb}/#{uuuup}"
|
|
286
|
+
else
|
|
287
|
+
if prefix.length == 1 # Single digit prefix states
|
|
288
|
+
fff = without_prefix[0, 3]
|
|
289
|
+
bbb = without_prefix[3, 3]
|
|
290
|
+
uuuup = without_prefix[6, 5]
|
|
291
|
+
"#{fff}/#{bbb}/#{uuuup}"
|
|
292
|
+
else # Two digit prefix states
|
|
293
|
+
ff = without_prefix[0, 2]
|
|
294
|
+
bbb = without_prefix[2, 3]
|
|
295
|
+
uuuup = without_prefix[5, 5]
|
|
296
|
+
"#{ff}/#{bbb}/#{uuuup}"
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
alias_method :to_elster, :to_federal_13
|
|
302
|
+
end
|
|
303
|
+
end
|
data/lib/steuer.rb
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'steuer/version'
|
|
4
|
+
require_relative 'steuer/steuernummer'
|
|
5
|
+
require_relative 'steuer/errors'
|
|
6
|
+
|
|
7
|
+
module Steuer
|
|
8
|
+
class << self
|
|
9
|
+
def steuernummer(tax_number, state: nil)
|
|
10
|
+
Steuernummer.new(tax_number, state: state)
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: steuer
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.2.0.pre.alpha
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Olumuyiwa Osiname
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2025-08-17 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: pry
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: 0.15.1
|
|
20
|
+
type: :development
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: 0.15.1
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: rake
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - "~>"
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '13.0'
|
|
34
|
+
type: :development
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - "~>"
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '13.0'
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: rspec
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - "~>"
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '3.0'
|
|
48
|
+
type: :development
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - "~>"
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '3.0'
|
|
55
|
+
- !ruby/object:Gem::Dependency
|
|
56
|
+
name: rubocop
|
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - "~>"
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: 1.56.0
|
|
62
|
+
type: :development
|
|
63
|
+
prerelease: false
|
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - "~>"
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: 1.56.0
|
|
69
|
+
- !ruby/object:Gem::Dependency
|
|
70
|
+
name: rubocop-factory_bot
|
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
|
72
|
+
requirements:
|
|
73
|
+
- - "~>"
|
|
74
|
+
- !ruby/object:Gem::Version
|
|
75
|
+
version: 2.24.0
|
|
76
|
+
type: :development
|
|
77
|
+
prerelease: false
|
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
79
|
+
requirements:
|
|
80
|
+
- - "~>"
|
|
81
|
+
- !ruby/object:Gem::Version
|
|
82
|
+
version: 2.24.0
|
|
83
|
+
- !ruby/object:Gem::Dependency
|
|
84
|
+
name: rubocop-performance
|
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
|
86
|
+
requirements:
|
|
87
|
+
- - "~>"
|
|
88
|
+
- !ruby/object:Gem::Version
|
|
89
|
+
version: 1.19.0
|
|
90
|
+
type: :development
|
|
91
|
+
prerelease: false
|
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
93
|
+
requirements:
|
|
94
|
+
- - "~>"
|
|
95
|
+
- !ruby/object:Gem::Version
|
|
96
|
+
version: 1.19.0
|
|
97
|
+
- !ruby/object:Gem::Dependency
|
|
98
|
+
name: rubocop-rspec
|
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
|
100
|
+
requirements:
|
|
101
|
+
- - "~>"
|
|
102
|
+
- !ruby/object:Gem::Version
|
|
103
|
+
version: 2.24.0
|
|
104
|
+
type: :development
|
|
105
|
+
prerelease: false
|
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
107
|
+
requirements:
|
|
108
|
+
- - "~>"
|
|
109
|
+
- !ruby/object:Gem::Version
|
|
110
|
+
version: 2.24.0
|
|
111
|
+
- !ruby/object:Gem::Dependency
|
|
112
|
+
name: rubocop-shopify
|
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
|
114
|
+
requirements:
|
|
115
|
+
- - "~>"
|
|
116
|
+
- !ruby/object:Gem::Version
|
|
117
|
+
version: '2.14'
|
|
118
|
+
type: :development
|
|
119
|
+
prerelease: false
|
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
121
|
+
requirements:
|
|
122
|
+
- - "~>"
|
|
123
|
+
- !ruby/object:Gem::Version
|
|
124
|
+
version: '2.14'
|
|
125
|
+
description: A Ruby gem for German tax system utilities including Steuernummer conversion
|
|
126
|
+
between formats, VAT validation, and other tax-related functionality
|
|
127
|
+
email:
|
|
128
|
+
- oluosiname@gmail.com
|
|
129
|
+
executables: []
|
|
130
|
+
extensions: []
|
|
131
|
+
extra_rdoc_files: []
|
|
132
|
+
files:
|
|
133
|
+
- LICENSE
|
|
134
|
+
- README.md
|
|
135
|
+
- lib/steuer.rb
|
|
136
|
+
- lib/steuer/errors.rb
|
|
137
|
+
- lib/steuer/state_mapping.rb
|
|
138
|
+
- lib/steuer/steuernummer.rb
|
|
139
|
+
- lib/steuer/version.rb
|
|
140
|
+
homepage: https://github.com/oluosiname/steuer
|
|
141
|
+
licenses:
|
|
142
|
+
- MIT
|
|
143
|
+
metadata: {}
|
|
144
|
+
post_install_message:
|
|
145
|
+
rdoc_options: []
|
|
146
|
+
require_paths:
|
|
147
|
+
- lib
|
|
148
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
149
|
+
requirements:
|
|
150
|
+
- - ">="
|
|
151
|
+
- !ruby/object:Gem::Version
|
|
152
|
+
version: 3.3.0
|
|
153
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
154
|
+
requirements:
|
|
155
|
+
- - ">="
|
|
156
|
+
- !ruby/object:Gem::Version
|
|
157
|
+
version: '0'
|
|
158
|
+
requirements: []
|
|
159
|
+
rubygems_version: 3.5.3
|
|
160
|
+
signing_key:
|
|
161
|
+
specification_version: 4
|
|
162
|
+
summary: German tax system utilities - tax numbers, VAT validation, and more
|
|
163
|
+
test_files: []
|