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
30 changes: 30 additions & 0 deletions docs/docs.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,36 @@ const docTemplate = `{
"name": "cnpj",
"in": "query"
},
{
"type": "string",
"description": "CPF or CNPJ, whichever the digits describe",
"name": "documento",
"in": "query"
},
{
"type": "string",
"description": "PIS/PASEP/NIT/NIS to be validated",
"name": "pis",
"in": "query"
},
{
"type": "string",
"description": "Voter registration number to be validated",
"name": "titulo",
"in": "query"
},
{
"type": "string",
"description": "Vehicle plate, old or Mercosul pattern",
"name": "placa",
"in": "query"
},
{
"type": "string",
"description": "PIX key in any of its five forms",
"name": "pix",
"in": "query"
},
{
"type": "string",
"description": "Name to be validated",
Expand Down
30 changes: 30 additions & 0 deletions docs/swagger.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,36 @@
"name": "cnpj",
"in": "query"
},
{
"type": "string",
"description": "CPF or CNPJ, whichever the digits describe",
"name": "documento",
"in": "query"
},
{
"type": "string",
"description": "PIS/PASEP/NIT/NIS to be validated",
"name": "pis",
"in": "query"
},
{
"type": "string",
"description": "Voter registration number to be validated",
"name": "titulo",
"in": "query"
},
{
"type": "string",
"description": "Vehicle plate, old or Mercosul pattern",
"name": "placa",
"in": "query"
},
{
"type": "string",
"description": "PIX key in any of its five forms",
"name": "pix",
"in": "query"
},
{
"type": "string",
"description": "Name to be validated",
Expand Down
20 changes: 20 additions & 0 deletions docs/swagger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,26 @@ paths:
in: query
name: cnpj
type: string
- description: CPF or CNPJ, whichever the digits describe
in: query
name: documento
type: string
- description: PIS/PASEP/NIT/NIS to be validated
in: query
name: pis
type: string
- description: Voter registration number to be validated
in: query
name: titulo
type: string
- description: Vehicle plate, old or Mercosul pattern
in: query
name: placa
type: string
- description: PIX key in any of its five forms
in: query
name: pix
type: string
- description: Name to be validated
in: query
name: name
Expand Down
26 changes: 24 additions & 2 deletions handlers/validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ import (
// @Param email query string false "Email to be validated"
// @Param cpf query string false "CPF to be validated"
// @Param cnpj query string false "CNPJ to be validated"
// @Param documento query string false "CPF or CNPJ, whichever the digits describe"
// @Param pis query string false "PIS/PASEP/NIT/NIS to be validated"
// @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 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 @@ -55,6 +60,21 @@ func ValidateHandler(w http.ResponseWriter, r *http.Request) {
} else if cnpj := r.URL.Query().Get("cnpj"); cnpj != "" {
isValid, sanitizedValue, message := utils.ValidateCNPJ(cnpj)
response = createResponse("cnpj", cnpj, sanitizedValue, isValid, message, start, false)
} 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 pis := r.URL.Query().Get("pis"); pis != "" {
isValid, sanitizedValue, message := utils.ValidatePIS(pis)
response = createResponse("pis", pis, sanitizedValue, isValid, message, start, false)
} else if titulo := r.URL.Query().Get("titulo"); titulo != "" {
isValid, sanitizedValue, message := utils.ValidateTituloEleitor(titulo)
response = createResponse("titulo", titulo, sanitizedValue, isValid, message, start, false)
} else if placa := r.URL.Query().Get("placa"); placa != "" {
isValid, sanitizedValue, message := utils.ValidatePlate(placa)
response = createResponse("placa", placa, sanitizedValue, isValid, message, start, false)
} else if pix := r.URL.Query().Get("pix"); pix != "" {
isValid, sanitizedValue, message := utils.ValidatePixKey(pix)
response = createResponse("pix", pix, sanitizedValue, isValid, message, start, false)
} else if name := r.URL.Query().Get("name"); name != "" {
isValid, sanitizedValue, message := utils.ValidateName(name)
response = createResponse("name", name, sanitizedValue, isValid, message, start, false)
Expand Down Expand Up @@ -91,8 +111,10 @@ func validationCount(r *http.Request) int {
if strings.TrimSpace(r.URL.Query().Get("email")) != "" {
count++
}
if strings.TrimSpace(r.URL.Query().Get("cnpj")) != "" {
count++
for _, key := range []string{"cnpj", "documento", "pis", "titulo", "placa", "pix"} {
if strings.TrimSpace(r.URL.Query().Get(key)) != "" {
count++
}
}
if strings.TrimSpace(r.URL.Query().Get("cpf")) != "" {
count++
Expand Down
45 changes: 45 additions & 0 deletions handlers/validate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,3 +132,48 @@ func TestValidateHandlerCNPJ(t *testing.T) {
})
}
}

func TestValidateHandlerNewDocuments(t *testing.T) {
tests := []struct {
name string
query string
wantStatus int
wantValid bool
wantKey string
}{
{"documento_cpf", "documento=529.982.247-25", http.StatusOK, true, "documento"},
{"documento_cnpj", "documento=33.000.167/0001-01", http.StatusOK, true, "documento"},
{"pis", "pis=12056874107", http.StatusOK, true, "pis"},
{"pis_invalid", "pis=12056412547", http.StatusUnprocessableEntity, false, "pis"},
{"placa_mercosul", "placa=ABC1D23", http.StatusOK, true, "placa"},
{"placa_invalid", "placa=AB1CD23", http.StatusUnprocessableEntity, false, "placa"},
{"pix_email", "pix=jeferson@example.com", http.StatusOK, true, "pix"},
{"pix_invalid", "pix=minha-chave", http.StatusUnprocessableEntity, false, "pix"},
// Each new parameter must join the one-per-request rule, or it becomes
// a way to smuggle a second validation past the guard.
{"two_new_parameters", "pis=12056874107&placa=ABC1D23", http.StatusBadRequest, false, ""},
{"new_parameter_with_cpf", "pix=jeferson@example.com&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)
}
if tt.wantKey != "" && response.ParameterKey != tt.wantKey {
t.Fatalf("expected parameter_key %q, got %q", tt.wantKey, response.ParameterKey)
}
})
}
}
38 changes: 38 additions & 0 deletions utils/checkdigit.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package utils

// Most Brazilian document numbers end in check digits computed the same way: a
// weighted sum of the preceding digits, taken modulo 11. Only the weights
// differ. Keeping that arithmetic in one place is what stops each new document
// from arriving with its own copy of the same loop — and its own chance of
// getting the edge case wrong.

// mod11Digit returns the check digit for digits weighted by weights.
//
// The remainder maps to 11 minus itself, except that 10 and 11 both mean zero —
// a check digit is a single character, so the two remainders that would need two
// are folded onto 0. CPF, CNPJ, PIS and título de eleitor all share this rule.
func mod11Digit(digits string, weights []int) int {
sum := 0
for i, weight := range weights {
sum += int(digits[i]-'0') * weight
}

if digit := 11 - sum%11; digit < 10 {
return digit
}
return 0
}

// allSameDigit reports a run of one repeated digit.
//
// Every mod-11 document has repeated-digit values that satisfy the arithmetic —
// 111.111.111-11 computes correctly — so they have to be excluded by name
// rather than by the check digits.
func allSameDigit(digits string) bool {
for i := 1; i < len(digits); i++ {
if digits[i] != digits[0] {
return false
}
}
return len(digits) > 0
}
Loading
Loading