-
Notifications
You must be signed in to change notification settings - Fork 1
/
id.go
230 lines (201 loc) · 6.27 KB
/
id.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
package vat
import (
"database/sql/driver"
"errors"
"fmt"
"strings"
"unicode"
"github.com/domonda/go-types/country"
"github.com/domonda/go-types/strutil"
)
// MOSSSchemaVATCountryCode or the VAT Mini One Stop Shop (MOSS) is an optional scheme that allows you
// to account for VAT - normally due in multiple EU countries – in just one EU country. Check out:
// https://europa.eu/youreurope/business/taxation/vat/vat-digital-services-moss-scheme/index_en.htm
const MOSSSchemaVATCountryCode = "EU"
// ID is a european VAT ID.
// ID implements the database/sql.Scanner and database/sql/driver.Valuer interfaces,
// returning errors when the ID is not valid and can't be normalized.
// Use NullableID to read and write SQL NULL values.
type ID string
// NormalizeVATID returns str as normalized VAT ID or an error.
func NormalizeVATID(str string) (ID, error) {
return ID(str).Normalized()
}
// StringIsVATID returns if a string can be parsed as VATID.
func StringIsVATID(str string) bool {
return ID(str).Valid()
}
// BytesAreVATID returns if a byte string is a valid VAT ID
func BytesAreVATID(str []byte) bool {
return ID(str).Valid()
}
func isVATIDSplitRune(r rune) bool {
return unicode.IsSpace(r) || r == ':'
}
func isVATIDTrimRune(r rune) bool {
return unicode.IsPunct(r)
}
// NormalizedUnchecked returns a generic normalized version of ID without performing any format checks.
// func (id ID) NormalizedUnchecked() ID {
// return ID(strings.ToUpper(strutil.RemoveRunesString(string(id), unicode.IsSpace, unicode.IsPunct)))
// }
// Normalized returns the id in normalized form,
// or an error if the VAT ID is not valid.
func (id ID) Normalized() (ID, error) {
normalized := ID(strings.ToUpper(strutil.RemoveRunesString(string(id), unicode.IsSpace, unicode.IsPunct)))
// Check length
if len(normalized) < IDMinLength {
return "", fmt.Errorf("VAT ID %q is too short", string(id))
}
if len(normalized) > IDMaxLength {
return "", fmt.Errorf("VAT ID %q is too long", string(id))
}
// Check country code
countryCode := country.Code(normalized[:2])
if countryCode != MOSSSchemaVATCountryCode && !countryCode.Valid() {
return "", fmt.Errorf("VAT ID %q has an invalid country code: %q", string(id), string(countryCode))
}
// Check format with country specific regex
regex, ok := idRegex[countryCode]
if !ok {
return "", fmt.Errorf("VAT ID %q has an unsupported country code: %q", string(id), string(countryCode))
}
if !regex.MatchString(string(normalized)) {
return "", fmt.Errorf("VAT ID %q has an invalid format", string(id))
}
// Test checkFunc-sum if a function is available for the country
checkFunc, ok := checkSumFuncs[countryCode]
if ok && !checkFunc(id, normalized) {
return "", fmt.Errorf("VAT ID %q has an invalid check-sum", string(id))
}
return normalized, nil
}
// NormalizedOrNull returns the id in normalized form
// or Null if the VAT ID is not valid.
func (id ID) NormalizedOrNull() NullableID {
normalized, err := id.Normalized()
if err != nil {
return Null
}
return NullableID(normalized)
}
// Valid returns if id is a valid VAT ID,
// ignoring normalization.
func (id ID) Valid() bool {
_, err := id.Normalized()
return err == nil
}
// ValidAndNormalized returns if id is a valid and normalized VAT ID.
func (id ID) ValidAndNormalized() bool {
norm, err := id.Normalized()
return err == nil && id == norm
}
// Validate returns an error if id is not a valid VAT ID,
// ignoring normalization.
func (id ID) Validate() error {
_, err := id.Normalized()
return err
}
// ValidateIsNormalized returns an error if id is not a valid and normalized VAT ID.
func (id ID) ValidateIsNormalized() error {
norm, err := id.Normalized()
if err != nil {
return err
}
if id != norm {
return fmt.Errorf("VAT ID is valid but not normalized: %q", string(id))
}
return nil
}
// Nullable returns the id as NullableID
func (id ID) Nullable() NullableID {
return NullableID(id)
}
// CountryCode returns the country.Code of the VAT ID,
// or country.Invalid if the id is not valid.
// For a VAT Mini One Stop Shop (MOSS) ID that begins with "EU"
// the EU's capital Brussels' country Belgum's
// code country.BE will be returned.
// See also ID.IsMOSS.
func (id ID) CountryCode() country.Code {
norm, err := id.Normalized()
if err != nil {
return country.Invalid
}
code := country.Code(norm[:2])
if code == MOSSSchemaVATCountryCode {
// MOSS VAT begins with "EU" - Europe is not a country
return country.BE
}
return code
}
// IsMOSS returns true if the ID follows the
// VAT Mini One Stop Shop (MOSS) schema beginning with "EU".
func (id ID) IsMOSS() bool {
norm, err := id.Normalized()
if err != nil {
return false
}
return norm[:2] == MOSSSchemaVATCountryCode
}
// Number returns the number part after the country code of the VAT ID,
// or and empty string if the id is not valid.
func (id ID) Number() string {
norm, err := id.Normalized()
if err != nil {
return ""
}
return string(norm[2:])
}
// String returns the normalized ID if possible,
// else it will be returned unchanged as string.
// String implements the fmt.Stringer interface.
func (id ID) String() string {
norm, err := id.Normalized()
if err != nil {
return string(id)
}
return string(norm)
}
// ScanString tries to parse and assign the passed
// source string as value of the implementing type.
//
// If validate is true, the source string is checked
// for validity before it is assigned to the type.
//
// If validate is false and the source string
// can still be assigned in some non-normalized way
// it will be assigned without returning an error.
func (id *ID) ScanString(source string, validate bool) error {
newID, err := ID(source).Normalized()
if err != nil {
if validate {
return err
}
newID = ID(source)
}
*id = newID
return nil
}
// Scan implements the database/sql.Scanner interface.
func (id *ID) Scan(value any) error {
switch x := value.(type) {
case string:
*id = ID(x)
case []byte:
*id = ID(x)
case nil:
return errors.New("can't scan SQL NULL as vat.ID")
default:
return fmt.Errorf("can't scan SQL value of type %T as vat.ID", value)
}
return nil
}
// Value implements the driver database/sql/driver.Valuer interface.
func (id ID) Value() (driver.Value, error) {
normalized, err := id.Normalized()
if err != nil {
return string(id), nil
}
return string(normalized), nil
}