Skip to content
Open
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
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,14 +3,14 @@
var countryCodes []CountryCode

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

var existsBIKs = map[string]string{
"041280103": `УФК по Астраханской области`,

Check failure on line 13 in bik/data.go

View workflow job for this annotation

GitHub Actions / build

string `УФК по Астраханской области` has 3 occurrences, make it a constant (goconst)
"046577906": `УРАЛЬСКИЙ ФИЛИАЛ АО "РАЙФФАЙЗЕНБАНК"`,
"045402704": `ФИЛИАЛ "БАНКА ЗАРЕЧЬЕ" (АО) В Г. ОРЛЕ`,
"040037471": `БАЙКОНУРСКИЙ ФИЛИАЛ АБ "РОССИЯ"`,
Expand All @@ -22,9 +22,9 @@
"048270002": `ПУ БАНКА РОССИИ МАТВЕЕВСКОЕ`,
"040816000": `РКЦ СОВЕТСКАЯ ГАВАНЬ`,
"215000042": `Избирательная комиссия Брянской области`,
"049626100": `УФК по Чеченской Республике`,

Check failure on line 25 in bik/data.go

View workflow job for this annotation

GitHub Actions / build

string `УФК по Чеченской Республике` has 2 occurrences, make it a constant (goconst)
"042299100": `УФК по Нижегородской области`,

Check failure on line 26 in bik/data.go

View workflow job for this annotation

GitHub Actions / build

string `УФК по Нижегородской области` has 2 occurrences, make it a constant (goconst)
"041122101": `УФК по Архангельской области и Ненецкому автономному округу`,

Check failure on line 27 in bik/data.go

View workflow job for this annotation

GitHub Actions / build

string `УФК по Архангельской области и Ненецкому автономному округу` has 2 occurrences, make it a constant (goconst)
"041236100": `УФК по Астраханской области`,
"044083000": `МРХ СЕВЕРО-ЗАПАДНОГО ГУ БАНКА РОССИИ`,
"205000024": `Избирательная комиссия Приморского края`,
Expand All @@ -34,13 +34,13 @@
"040396055": `НАЦИОНАЛЬНЫЙ БАНК РЕСПУБЛИКИ АБХАЗИЯ`,
"043510107": `СИМФЕРОПОЛЬСКИЙ ФИЛИАЛ АБ "РОССИЯ"`,
"046577409": `ЕКАТЕРИНБУРГСКИЙ ФИЛИАЛ БАНКА "ВЯТИЧ" (ПАО)`,
"041081100": `УФК по Амурской области`,

Check failure on line 37 in bik/data.go

View workflow job for this annotation

GitHub Actions / build

string `УФК по Амурской области` has 2 occurrences, make it a constant (goconst)
"044012000": `КЦ СЕВЕРО-ЗАПАДНОГО ГУ БАНКА РОССИИ`,
"045853000": `РКЦ ВЕЛИКИЕ ЛУКИ`,
"049246000": `РКЦ НИЖНЕКАМСК`,
"040703002": `ПУ БАНКА РОССИИ ГЕОРГИЕВСКОЕ`,
"042880002": `ПУ БАНКА РОССИИ ЗАВОДСКОЕ`,

Check failure on line 42 in bik/data.go

View workflow job for this annotation

GitHub Actions / build

string `ПУ БАНКА РОССИИ ЗАВОДСКОЕ` has 2 occurrences, make it a constant (goconst)
"000816041": `УФК по Хабаровскому краю`,

Check failure on line 43 in bik/data.go

View workflow job for this annotation

GitHub Actions / build

string `УФК по Хабаровскому краю` has 5 occurrences, make it a constant (goconst)
"042202886": `НИЖЕГОРОДСКИЙ Ф-Л ПАО АКБ "МЕТАЛЛИНВЕСТБАНК"`,
"040349550": `ФИЛИАЛ N8 ПАО КБ "ЦЕНТР-ИНВЕСТ"`,
"041203712": `ФИЛИАЛ В Г.АСТРАХАНИ АО "МБ БАНК"`,
Expand All @@ -56,7 +56,7 @@
"048718000": `РКЦ ВОРКУТА`,
"046482002": `ПУ БАНКА РОССИИ ОСТРОВНОЕ`,
"045004001": `СИБИРСКОЕ ГУ БАНКА РОССИИ`,
"044783002": `ПУ БАНКА РОССИИ ПОЛЯРНОЕ`,

Check failure on line 59 in bik/data.go

View workflow job for this annotation

GitHub Actions / build

string `ПУ БАНКА РОССИИ ПОЛЯРНОЕ` has 2 occurrences, make it a constant (goconst)
"044784002": `ПУ БАНКА РОССИИ ФЛОТСКОЕ`,
"044785002": `ПУ БАНКА РОССИИ ПОЛЯРНОЕ`,
"046521000": `РКЦ СЕРОВ`,
Expand All @@ -77,8 +77,8 @@
"046380002": `ПУ БАНКА РОССИИ ВОЛЖСКОЕ`,
"044584002": `ПУ БАНКА РОССИИ ОСТОЖЕНСКОЕ`,
"044674002": `ПУ БАНКА РОССИИ ЧЕХОВСКОЕ`,
"040703101": `УФК по Ставропольскому краю`,

Check failure on line 80 in bik/data.go

View workflow job for this annotation

GitHub Actions / build

string `УФК по Ставропольскому краю` has 3 occurrences, make it a constant (goconst)
"046709101": `УФК по Республике Крым`,

Check failure on line 81 in bik/data.go

View workflow job for this annotation

GitHub Actions / build

string `УФК по Республике Крым` has 3 occurrences, make it a constant (goconst)
"046709102": `УФК по г. Севастополю`,
"045080101": `УФК по Новосибирской области`,
"043081100": `УФК по Камчатскому краю`,
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
}
36 changes: 36 additions & 0 deletions models/errors_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package models

import (
"errors"
"fmt"
"testing"

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

func TestCommonError_Error(t *testing.T) {
err := &CommonError{Method: "inn", Err: ErrInvalidLength}
assert.Equal(t, "inn: invalid length", err.Error())
}

func TestCommonError_Unwrap(t *testing.T) {
base := ErrInvalidValue
err := &CommonError{Method: "bik", Err: base}

require.Equal(t, base, err.Unwrap())
}

func TestCommonError_ErrorsIs(t *testing.T) {
t.Run("direct", func(t *testing.T) {
err := &CommonError{Method: "kpp", Err: ErrInvalidLength}
assert.True(t, errors.Is(err, ErrInvalidLength))
assert.False(t, errors.Is(err, ErrInvalidValue))
})

t.Run("wrapped", func(t *testing.T) {
err := fmt.Errorf("parse model: %w", &CommonError{Method: "snils", Err: ErrInvalidValue})
assert.True(t, errors.Is(err, ErrInvalidValue))
assert.False(t, errors.Is(err, ErrInvalidLength))
})
}
Loading
Loading