Skip to content
This repository was archived by the owner on Oct 12, 2026. It is now read-only.
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
12 changes: 12 additions & 0 deletions docs/docs.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,18 @@ const docTemplate = `{
"name": "pix",
"in": "query"
},
{
"type": "string",
"description": "State registration; requires uf",
"name": "ie",
"in": "query"
},
{
"type": "string",
"description": "Two-letter state code qualifying ie",
"name": "uf",
"in": "query"
},
{
"type": "string",
"description": "Name to be validated",
Expand Down
12 changes: 12 additions & 0 deletions docs/swagger.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,18 @@
"name": "pix",
"in": "query"
},
{
"type": "string",
"description": "State registration; requires uf",
"name": "ie",
"in": "query"
},
{
"type": "string",
"description": "Two-letter state code qualifying ie",
"name": "uf",
"in": "query"
},
{
"type": "string",
"description": "Name to be validated",
Expand Down
8 changes: 8 additions & 0 deletions docs/swagger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,14 @@ paths:
in: query
name: pix
type: string
- description: State registration; requires uf
in: query
name: ie
type: string
- description: Two-letter state code qualifying ie
in: query
name: uf
type: string
- description: Name to be validated
in: query
name: name
Expand Down
7 changes: 6 additions & 1 deletion handlers/validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import (
// @Param titulo query string false "Voter registration number to be validated"
// @Param placa query string false "Vehicle plate, old or Mercosul pattern"
// @Param pix query string false "PIX key in any of its five forms"
// @Param ie query string false "State registration; requires uf"
// @Param uf query string false "Two-letter state code qualifying ie"
// @Param name query string false "Name to be validated"
// @Param telephone query string false "Phone number to be validated"
// @Param phone query string false "Phone number to be validated"
Expand Down Expand Up @@ -63,6 +65,9 @@ func ValidateHandler(w http.ResponseWriter, r *http.Request) {
} else if documento := r.URL.Query().Get("documento"); documento != "" {
isValid, sanitizedValue, message := utils.ValidateDocument(documento)
response = createResponse("documento", documento, sanitizedValue, isValid, message, start, false)
} else if ie := r.URL.Query().Get("ie"); ie != "" {
isValid, sanitizedValue, message := utils.ValidateInscricaoEstadual(ie, r.URL.Query().Get("uf"))
response = createResponse("ie", ie, sanitizedValue, isValid, message, start, false)
} else if pis := r.URL.Query().Get("pis"); pis != "" {
isValid, sanitizedValue, message := utils.ValidatePIS(pis)
response = createResponse("pis", pis, sanitizedValue, isValid, message, start, false)
Expand Down Expand Up @@ -111,7 +116,7 @@ func validationCount(r *http.Request) int {
if strings.TrimSpace(r.URL.Query().Get("email")) != "" {
count++
}
for _, key := range []string{"cnpj", "documento", "pis", "titulo", "placa", "pix"} {
for _, key := range []string{"cnpj", "documento", "pis", "titulo", "placa", "pix", "ie"} {
if strings.TrimSpace(r.URL.Query().Get(key)) != "" {
count++
}
Expand Down
38 changes: 38 additions & 0 deletions handlers/validate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -177,3 +177,41 @@ func TestValidateHandlerNewDocuments(t *testing.T) {
})
}
}

func TestValidateHandlerInscricaoEstadual(t *testing.T) {
tests := []struct {
name string
query string
wantStatus int
wantValid bool
}{
{"valid_with_state", "ie=0100482300112&uf=AC", http.StatusOK, true},
{"valid_elsewhere", "ie=0100482300112&uf=SP", http.StatusUnprocessableEntity, false},
{"missing_state", "ie=0100482300112", http.StatusUnprocessableEntity, false},
{"unknown_state", "ie=0100482300112&uf=XX", http.StatusUnprocessableEntity, false},
{"exempt", "ie=ISENTO&uf=SP", http.StatusOK, true},
// uf qualifies ie rather than being a validation of its own, so it must
// not push the request over the one-parameter limit.
{"uf_is_not_a_second_validation", "ie=0100482300112&uf=AC", http.StatusOK, true},
{"ie_with_cpf", "ie=0100482300112&uf=AC&cpf=529.982.247-25", http.StatusBadRequest, false},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
recorder := httptest.NewRecorder()
ValidateHandler(recorder, httptest.NewRequest(http.MethodGet, "/validate?"+tt.query, nil))

if recorder.Code != tt.wantStatus {
t.Fatalf("expected status %d, got %d: %s", tt.wantStatus, recorder.Code, recorder.Body.String())
}

var response models.ValidationResponse
if err := json.Unmarshal(recorder.Body.Bytes(), &response); err != nil {
t.Fatalf("could not decode the response: %v", err)
}
if response.IsValid != tt.wantValid {
t.Fatalf("expected is_valid=%v, got %v (%s)", tt.wantValid, response.IsValid, response.Message)
}
})
}
}
98 changes: 98 additions & 0 deletions utils/ie_arithmetic.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package utils

import "strings"

// The arithmetic every state registration is built from.
//
// Twenty-seven states, twenty-seven roteiros de crítica — but almost all of
// them are the same three steps with different constants: take a prefix of the
// digits, multiply by descending weights, and fold the remainder into a check
// digit. Keeping those steps here is what lets most states be four lines
// instead of forty.

// descendingWeights produces the weight series the SINTEGRA documents print
// above the digits: counting down from start, and wrapping back to 9 after 2.
//
// descendingWeights(4, 11) is 4 3 2 9 8 7 6 5 4 3 2 — the series Acre's roteiro
// shows, and the reason the wrap exists at all.
func descendingWeights(start, size int) []int {
weights := make([]int, 0, size)

for len(weights) < size {
weights = append(weights, start)
if start == 2 {
start = 10
}
start--
}

return weights
}

// weightedTotal multiplies each digit by its weight and sums, stopping at
// whichever of the two runs out first.
func weightedTotal(digits string, weights []int) int {
total := 0

for i := 0; i < len(weights) && i < len(digits); i++ {
total += int(digits[i]-'0') * weights[i]
}

return total
}

// mod11Check is the rule most states share: eleven minus the remainder, with
// remainders of 0 and 1 both yielding zero because the alternative needs two
// characters.
func mod11Check(total int) int {
if remainder := total % 11; remainder >= 2 {
return 11 - remainder
}
return 0
}

// mod10Check is the same idea in base ten, used by Bahia for the registrations
// whose leading digit puts them in the module-10 group.
func mod10Check(total int) int {
if remainder := total % 10; remainder != 0 {
return 10 - remainder
}
return 0
}

// mod11TimesTen covers the states whose roteiro multiplies the sum by ten
// before taking the remainder — Alagoas and Rio Grande do Norte — where a
// remainder of 10 yields zero.
func mod11TimesTen(total int) int {
if remainder := total * 10 % 11; remainder != 10 {
return remainder
}
return 0
}

// hasPrefix reports whether the registration starts with any of the state
// codes given. An empty list means the state does not fix its opening digits.
func hasPrefix(ie string, prefixes []string) bool {
if len(prefixes) == 0 {
return true
}

for _, prefix := range prefixes {
if strings.HasPrefix(ie, prefix) {
return true
}
}

return false
}

// digitsOnly reports whether every character is a digit, which every state
// requires except São Paulo, whose rural registrations begin with a P.
func digitsOnly(ie string) bool {
for i := 0; i < len(ie); i++ {
if ie[i] < '0' || ie[i] > '9' {
return false
}
}
return len(ie) > 0
}
Loading
Loading