Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ linters:
- comments
- std-error-handling
rules:
# Lookup datasets: extracting repeated labels into constants makes them
# harder to review and maintain without changing runtime behavior.
- path: (bik|kpp)/data.go
linters:
- goconst
- linters:
- staticcheck
text: "ST1003"
Expand Down
12 changes: 10 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,11 @@ Go library for validation and generation of Russian official document codes.
| OGRN | Primary State Registration Number (ОГРН) | + | + |
| OGRNIP | Primary State Registration Number for IE (ОГРНИП) | + | + |
| SNILS | Insurance Individual Account Number (СНИЛС) | + | + |
| OKATO | Russian Classification of Administrative Territories | - | - |
| OKATO | Russian Classification of Administrative Territories | +¹ | - |

¹ OKATO validation checks the format and level ranges only; existence lookup and
generation are not implemented yet and return `okato.ErrNotImplemented`. OKATO is
not part of the top-level `Validate`/`Generate` dispatch — use the `okato` package directly.

## Requirements

Expand Down Expand Up @@ -58,11 +62,15 @@ if !isValid {
```go
import (
"fmt"
"log"

docs_code "github.com/sshaplygin/docs-code"
)

code := docs_code.Generate(docs_code.INN)
code, err := docs_code.Generate(docs_code.INN)
if err != nil {
log.Fatal(err)
}
fmt.Println("Generated INN:", code)
```

Expand Down
4 changes: 2 additions & 2 deletions bik/bik_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ func TestValidate(t *testing.T) {
for i, tc := range testCases {
isValid, err := Validate(tc.Code)
if err != nil {
require.ErrorAs(t, err, &tc.Error, fmt.Sprintf("invalid test case %d: input: %s", i, tc.Code))
require.ErrorIs(t, err, tc.Error, fmt.Sprintf("invalid test case %d: input: %s", i, tc.Code))
} else {
require.NoError(t, err, fmt.Sprintf("invalid test case %d: input: %s", i, tc.Code))
}
Expand Down Expand Up @@ -87,7 +87,7 @@ func TestValidate(t *testing.T) {
for i, tc := range testCases {
isValid, err := Validate(tc.Code)
if err != nil {
require.ErrorAs(t, err, &tc.Error, fmt.Sprintf("invalid test case %d: input: %s", i, tc.Code))
require.ErrorIs(t, err, tc.Error, fmt.Sprintf("invalid test case %d: input: %s", i, tc.Code))
} else {
require.Empty(t, err, fmt.Sprintf("invalid test case %d: input: %s", i, tc.Code))
}
Expand Down
8 changes: 4 additions & 4 deletions bik/data.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ package bik
var countryCodes []CountryCode

var supportedCountryCodes = map[CountryCode]string{
DirectParticipationCounty: "Участник платежной системы с прямым участием",
IndirectParticipationCounty: "Участник платежной системы с косвенным участием",
NotMemberClientCBRF: "Клиент Банка России, не являющийся участником платежной системы",
RussiaCountryCode: "Код Российской Федерации",
DirectParticipationCountry: "Участник платежной системы с прямым участием",
IndirectParticipationCountry: "Участник платежной системы с косвенным участием",
NotMemberClientCBRF: "Клиент Банка России, не являющийся участником платежной системы",
RussiaCountryCode: "Код Российской Федерации",
}

var existsBIKs = map[string]string{
Expand Down
24 changes: 6 additions & 18 deletions bik/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,12 @@ const (
unspecifiedCountryCode = "Неопределенный код страны"
)

var (
// DirectParticipationCounty - участник платежной системы с прямым участием
DirectParticipationCounty CountryCode = 0
const (
// DirectParticipationCountry - участник платежной системы с прямым участием
DirectParticipationCountry CountryCode = 0

// IndirectParticipationCounty - участник платежной системы с косвенным участием
IndirectParticipationCounty CountryCode = 1
// IndirectParticipationCountry - участник платежной системы с косвенным участием
IndirectParticipationCountry CountryCode = 1

// NotMemberClientCBRF - клиент Банка России, не являющийся участником платежной системы
NotMemberClientCBRF CountryCode = 2
Expand Down Expand Up @@ -158,19 +158,7 @@ type BIKStruct struct {
lastNumber LastAccountNumbers
}

// generateOptions TODO
type generateOptions struct {
}

type GenerateOpt func(options *generateOptions)

func NewBIK(opts ...GenerateOpt) *BIKStruct {
var options generateOptions

for _, o := range opts {
o(&options)
}

func NewBIK() *BIKStruct {
return &BIKStruct{
country: GenerateCountryCode(),
territoryCode: okato.GenerateStateCode(),
Expand Down
7 changes: 7 additions & 0 deletions errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package docs_code

import "errors"

// ErrUnsupportedDocType is returned by Validate and Generate when the provided
// DocType is not one of the supported document types.
var ErrUnsupportedDocType = errors.New("unsupported document type")
70 changes: 70 additions & 0 deletions fts/fts_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package fts

import (
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestParseTaxRegionCode(t *testing.T) {
trc, err := ParseTaxRegionCode("7746")
require.NoError(t, err)

assert.Equal(t, "7746", trc.String())
assert.Equal(t, []int{7, 7, 4, 6}, trc.Ints())
}

func TestParseTaxRegionCode_Invalid(t *testing.T) {
_, err := ParseTaxRegionCode("77XX")
require.Error(t, err)
}

func TestTaxRegionCode_Nil(t *testing.T) {
var trc *TaxRegionCode

assert.Nil(t, trc.Ints())
assert.False(t, trc.IsValid())
assert.Equal(t, "0000", trc.String())
assert.Equal(t, SupportedTaxDepartments[0].Name, trc.GetName())
}

func TestConstitutionRegionCode(t *testing.T) {
const adygea ConstitutionRegionCode = 1

assert.True(t, adygea.IsValid())
assert.Equal(t, "01", adygea.String())
assert.Equal(t, SupportedRegionsCodes[adygea], adygea.GetName())
assert.Equal(t, []int{0, 1}, adygea.Ints())
}

func TestConstitutionRegionCode_Invalid(t *testing.T) {
const unknown ConstitutionRegionCode = 100

assert.False(t, unknown.IsValid())
// Unknown/out-of-range subjects fall back to the "other territories" code.
assert.Equal(t, "99", unknown.String())
}

func TestRegionTaxServiceNumber_String(t *testing.T) {
assert.Equal(t, "05", RegionTaxServiceNumber(5).String())
assert.Equal(t, "42", RegionTaxServiceNumber(42).String())
}

func TestGenerateConstitutionSubjectCode_AlwaysValid(t *testing.T) {
for range 100 {
require.True(t, GenerateConstitutionSubjectCode().IsValid())
}
}

func TestGenerateTaxRegionCode_RoundTrip(t *testing.T) {
for range 100 {
trc := GenerateTaxRegionCode()
require.NotNil(t, trc)
require.True(t, trc.IsValid())

parsed, err := ParseTaxRegionCode(trc.String())
require.NoError(t, err)
assert.Equal(t, trc.Ints(), parsed.Ints())
}
}
6 changes: 3 additions & 3 deletions generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import (

type GenerateFunc func() string

func Generate(docType DocType) string {
func Generate(docType DocType) (string, error) {
var callFunc GenerateFunc
switch docType {
case BIK:
Expand All @@ -29,8 +29,8 @@ func Generate(docType DocType) string {
}

if callFunc == nil {
panic("not implemented method")
return "", ErrUnsupportedDocType
}

return callFunc()
return callFunc(), nil
}
9 changes: 5 additions & 4 deletions generate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,16 @@ import (
)

func Test_Generate(t *testing.T) {
inn := Generate(INN)
inn, err := Generate(INN)
require.NoError(t, err)

isValid, err := Validate(INN, inn)
require.NoError(t, err)

require.True(t, isValid)
}

func Test_Generate_Unsupported(t *testing.T) {
require.Panics(t, func() {
Generate(DocType(100500))
})
_, err := Generate(DocType(100500))
require.ErrorIs(t, err, ErrUnsupportedDocType)
}
4 changes: 2 additions & 2 deletions inn/inn.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,10 @@ func Generate() string {

// GenerateLegal generate legal type inn string value
func GenerateLegal() string {
return NewINN(_supportedTypes[utils.Random(0, len(_supportedTypes)-1)]).String()
return NewINN(Legal).String()
}

// GeneratePhysical generate physical type inn string value
func GeneratePhysical() string {
return NewINN(INNType(Physical)).String()
return NewINN(Physical).String()
}
4 changes: 2 additions & 2 deletions inn/inn_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ func TestValidate(t *testing.T) {
isValid, err := Validate(tc.Code)
assert.Equal(t, tc.IsValid, isValid, tc.Code)
if err != nil {
assert.ErrorAs(t, err, &tc.Error, fmt.Sprintf("invalid test case %d: input: %s", i, tc.Code))
assert.ErrorIs(t, err, tc.Error, fmt.Sprintf("invalid test case %d: input: %s", i, tc.Code))
} else {
assert.Empty(t, err, fmt.Sprintf("invalid test case %d: input: %s", i, tc.Code))
}
Expand Down Expand Up @@ -84,7 +84,7 @@ func TestValidate(t *testing.T) {
isValid, err := Validate(tc.Code)
assert.Equal(t, tc.IsValid, isValid, tc.Code)
if err != nil {
assert.ErrorAs(t, err, &tc.Error, fmt.Sprintf("invalid test case %d: input: %s", i, tc.Code))
assert.ErrorIs(t, err, tc.Error, fmt.Sprintf("invalid test case %d: input: %s", i, tc.Code))
} else {
assert.Empty(t, err, fmt.Sprintf("invalid test case %d: input: %s", i, tc.Code))
}
Expand Down
16 changes: 12 additions & 4 deletions inn/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ const (
ForeignLegal
)

// foreignLegalPrefix is the fixed tax region prefix of a foreign legal-entity INN.
const foreignLegalPrefix = "9909"

var _supportedTypes = []INNType{
Physical,
Legal,
Expand All @@ -69,7 +72,7 @@ func (sn *SerialNumber) Ints() []int {
return res
}

func GenerateSerailNumber(innType INNType) SerialNumber {
func GenerateSerialNumber(innType INNType) SerialNumber {
if innType == Physical {
return SerialNumber{
val: int(utils.RandomDigits(physicalSerialNumberLength)),
Expand Down Expand Up @@ -124,7 +127,13 @@ type INNStruct struct {

func NewINN(innType INNType) *INNStruct {
taxRegionCode := fts.GenerateTaxRegionCode()
serialNumber := GenerateSerailNumber(innType)
if innType == ForeignLegal {
// Foreign legal-entity INNs always start with the fixed 9909 prefix,
// so the random tax region code must not be used for them.
taxRegionCode, _ = fts.ParseTaxRegionCode(foreignLegalPrefix)
}

serialNumber := GenerateSerialNumber(innType)

return &INNStruct{
taxRegionCode: taxRegionCode,
Expand Down Expand Up @@ -154,8 +163,7 @@ func ParseINN(inn string) (*INNStruct, error) {
snlen = legalSerialNumberLength
t = Legal
parseIdx = len(inn) - 1
const foreignLegalStartWith = "9909"
if inn[0:4] == foreignLegalStartWith {
if inn[0:4] == foreignLegalPrefix {
t = ForeignLegal
}
}
Expand Down
6 changes: 3 additions & 3 deletions kpp/kpp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ func TestValidate(t *testing.T) {
isValid, err := Validate(tc.Code)
assert.Equal(t, isValid, tc.IsValid, fmt.Sprintf("invalid test case %d: input: %s", i, tc.Code))
if err != nil {
assert.ErrorAs(t, err, &tc.Error, fmt.Sprintf("invalid test case %d: input: %s", i, tc.Code))
assert.ErrorIs(t, err, tc.Error, fmt.Sprintf("invalid test case %d: input: %s", i, tc.Code))
} else {
assert.Empty(t, err, fmt.Sprintf("invalid test case %d: input: %s", i, tc.Code))
}
Expand Down Expand Up @@ -87,7 +87,7 @@ func TestValidate(t *testing.T) {
isValid, err := Validate(tc.Code)
assert.Equal(t, isValid, tc.IsValid, fmt.Sprintf("invalid test case %d: input: %s", i, tc.Code))
if err != nil {
assert.ErrorAs(t, err, &tc.Error, fmt.Sprintf("invalid test case %d: input: %s", i, tc.Code))
assert.ErrorIs(t, err, tc.Error, fmt.Sprintf("invalid test case %d: input: %s", i, tc.Code))
} else {
assert.Empty(t, err, fmt.Sprintf("invalid test case %d: input: %s", i, tc.Code))
}
Expand Down Expand Up @@ -115,7 +115,7 @@ func TestValidate(t *testing.T) {
isValid, err := Validate(tc.Code)
assert.Equal(t, isValid, tc.IsValid, fmt.Sprintf("invalid test case %d: input: %s", i, tc.Code))
if err != nil {
assert.ErrorAs(t, err, &tc.Error, fmt.Sprintf("invalid test case %d: input: %s", i, tc.Code))
assert.ErrorIs(t, err, tc.Error, fmt.Sprintf("invalid test case %d: input: %s", i, tc.Code))
} else {
assert.Empty(t, err, fmt.Sprintf("invalid test case %d: input: %s", i, tc.Code))
}
Expand Down
4 changes: 2 additions & 2 deletions kpp/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,15 +72,15 @@ func ParseKPP(kpp string) (*KPPStruct, error) {
return nil, fmt.Errorf("parse tax region code raw %s: %w", packageName, err)
}

servialNumberArr, err := utils.StrToArr(kpp[6:])
serialNumberArr, err := utils.StrToArr(kpp[6:])
if err != nil {
return nil, fmt.Errorf("parse serial number code raw %s: %w", packageName, err)
}

return &KPPStruct{
taxRegionCode: taxRegionCode,
reasonCode: RegistrationReason(kpp[4:6]),
serialNumber: SerialNumber(utils.SliceToInt(servialNumberArr)),
serialNumber: SerialNumber(utils.SliceToInt(serialNumberArr)),
}, nil
}

Expand Down
6 changes: 6 additions & 0 deletions models/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,9 @@ type CommonError struct {
func (c *CommonError) Error() string {
return fmt.Sprintf("%s: %s", c.Method, c.Err.Error())
}

// Unwrap exposes the wrapped base error so callers can use errors.Is/errors.As
// to detect sentinels such as ErrInvalidLength and ErrInvalidValue.
func (c *CommonError) Unwrap() error {
return c.Err
}
Loading
Loading