Compare commits

..

13 Commits

8 changed files with 370 additions and 0 deletions

41
cache/cache.go vendored Normal file
View File

@@ -0,0 +1,41 @@
package cache
import (
"errors"
"fmt"
"os"
"time"
"github.com/gabdlr/api-cuit-go/utils"
)
const cacheDir = "./.cache"
func cacheIsOld(fPath string) bool {
fInfo, _ := os.Stat(fPath)
fCreationDate := fInfo.ModTime()
oneYearAgo := (time.Now()).AddDate(-1, 0, 0)
isOld := fCreationDate.Before(oneYearAgo)
if isOld {
os.Remove(fPath)
}
return isOld
}
func Search(cuit string) ([]byte, error) {
cuit = utils.StandardizeCuit(cuit)
fPath := fmt.Sprintf("%s/%s.json", cacheDir, cuit)
f, err := os.ReadFile(fPath)
if err == nil {
if cacheIsOld(fPath) {
return []byte{0}, errors.New("cached file expired")
}
return f, nil
}
return []byte{0}, err
}
func Save(cuit string, cuitInfo []byte) {
cuit = utils.StandardizeCuit(cuit)
os.WriteFile(fmt.Sprintf("%s/%s.json", cacheDir, cuit), cuitInfo, 0644)
}

153
cuit/search.go Normal file
View File

@@ -0,0 +1,153 @@
package cuit
import (
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"strings"
"github.com/gabdlr/api-cuit-go/utils"
)
type Address struct {
Provincia string `json:"provincia"`
Localidad string `json:"localidad"`
Domicilio string `json:"domicilio"`
PisoDeptoOfi string `json:"pisoDeptoOfi"`
CodigoPostal int `json:"codigoPostal"`
EstadoDeDomicilio string `json:"estadoDeDomicilio"`
}
type Society struct {
RazonSocial string `json:"razonSocial"`
Cuit string `json:"cuit"`
TipoSocietario string `json:"tipoSocietario"`
FechaDeContrato string `json:"fechaDeContrato"`
}
type CuitInfo struct {
Sociedad Society `json:"sociedad"`
DomicilioFiscal Address `json:"domicilioFiscal"`
DomicilioLegal Address `json:"domicilioLegal"`
FechaActualizacion string `json:"fechaActualizacion"`
}
type SocietyRegister struct {
Cuit string `json:"cuit"`
Razon_social string `json:"razon_social"`
Fecha_contrato_social string `json:"fecha_contrato_social"`
Tipo_societario string `json:"tipo_societario"`
Numero_inscripcion string `json:"numero_inscripcion"`
Fecha_actualizacion string `json:"fecha_actualizacion"`
Domicilio_fiscal string `json:"domicilio_fiscal"`
Df_provincia string `json:"df_provincia"`
Df_localidad string `json:"df_localidad"`
Df_calle string `json:"df_calle"`
Df_numero string `json:"df_numero"`
Df_piso string `json:"df_piso"`
Df_departamento string `json:"df_departamento"`
Df_domicilio string `json:"df_domicilio"`
Df_cp int `json:"df_cp"`
Df_estado_domicilio string `json:"df_estado_domicilio"`
Domicilio_legal string `json:"domicilio_legal"`
Dl_provincia string `json:"dl_provincia"`
Dl_localidad string `json:"dl_localidad"`
Dl_calle string `json:"dl_calle"`
Dl_numero string `json:"dl_numero"`
Dl_piso string `json:"dl_piso"`
Dl_departamento string `json:"dl_departamento"`
Dl_domicilio string `json:"dl_domicilio"`
Dl_cp int `json:"dl_cp"`
Dl_estado_domicilio string `json:"dl_estado_domicilio"`
Fecha_corte string `json:"fecha_corte"`
Desc_actividad string `json:"desc_actividad"`
}
const htmlOfInterestStart = `Drupal.settings.registroSociedades.allItems = `
const htmlOfInterestEnd = `}];
`
const exitSignal = "No se encuentran resultados"
func Search(cuit string) ([]byte, error) {
url := fmt.Sprintf("https://argentina.gob.ar/justicia/registro-nacional-sociedades?cuit=%s&razon=", utils.StandardizeCuit(cuit))
res, err := http.Get(url)
if err != nil {
return []byte{0}, err
}
defer res.Body.Close()
body, err := io.ReadAll(res.Body)
if err != nil {
return []byte{0}, err
}
cuitInfo, err := parseResponse(string(body))
if err != nil {
return []byte{0}, err
}
cuitInfoJSON, err := json.Marshal(cuitInfo)
if err != nil {
return []byte{0}, err
}
return cuitInfoJSON, nil
}
func updateCuitInfo(cuitInfo *CuitInfo, rawRegister *SocietyRegister) {
cuitInfo.Sociedad.RazonSocial = rawRegister.Razon_social
cuitInfo.Sociedad.Cuit = rawRegister.Cuit
cuitInfo.Sociedad.TipoSocietario = rawRegister.Tipo_societario
cuitInfo.Sociedad.FechaDeContrato = rawRegister.Fecha_contrato_social
cuitInfo.DomicilioFiscal.Provincia = rawRegister.Df_provincia
cuitInfo.DomicilioFiscal.Localidad = rawRegister.Df_localidad
cuitInfo.DomicilioFiscal.Domicilio = rawRegister.Df_domicilio
cuitInfo.DomicilioFiscal.PisoDeptoOfi = rawRegister.Df_piso + ", " + rawRegister.Df_departamento
cuitInfo.DomicilioFiscal.CodigoPostal = rawRegister.Df_cp
cuitInfo.DomicilioFiscal.EstadoDeDomicilio = rawRegister.Df_estado_domicilio
cuitInfo.DomicilioLegal.Provincia = rawRegister.Dl_provincia
cuitInfo.DomicilioLegal.Localidad = rawRegister.Dl_localidad
cuitInfo.DomicilioLegal.Domicilio = rawRegister.Dl_domicilio
cuitInfo.DomicilioLegal.PisoDeptoOfi = rawRegister.Dl_piso + ", " + rawRegister.Dl_departamento
cuitInfo.DomicilioLegal.CodigoPostal = rawRegister.Dl_cp
cuitInfo.DomicilioLegal.EstadoDeDomicilio = rawRegister.Dl_estado_domicilio
cuitInfo.FechaActualizacion = rawRegister.Fecha_actualizacion
}
func parseResponse(html string) (CuitInfo, error) {
var cuitInfo CuitInfo
notFoundErr := "información no disponible"
jsonParseErr := "ocurrió un error"
if strings.Contains(html, exitSignal) {
return cuitInfo, errors.New(notFoundErr)
}
startPosition := strings.Index(html, htmlOfInterestStart)
endPosition := strings.Index(html, htmlOfInterestEnd)
if startPosition != -1 && endPosition != -1 {
info := html[startPosition+len(htmlOfInterestStart) : endPosition+2]
registers := []SocietyRegister{}
parseError := json.Unmarshal([]byte(info), &registers)
if parseError != nil {
return cuitInfo, errors.New(jsonParseErr)
}
if len(registers) > 0 {
updateCuitInfo(&cuitInfo, &registers[0])
return cuitInfo, nil
} else {
return cuitInfo, errors.New(notFoundErr)
}
}
return cuitInfo, errors.New(notFoundErr)
}

80
cuit/validations.go Normal file
View File

@@ -0,0 +1,80 @@
package cuit
import (
"regexp"
"strconv"
"github.com/gabdlr/api-cuit-go/utils"
)
const CUIT_REGEX = `^([\d]{2}-[\d]{8}-[\d]{1}|[\d]{11})$`
var CUIT_TYPES = map[uint8]bool{
30: true,
33: true,
34: true,
}
func IsValid(cuit string) (isValidCuit bool) {
if validateFormat(cuit) {
cuit = utils.StandardizeCuit(cuit)
if validateCuitType(cuit) {
isValidCuit = validateWithVerifierDigit(cuit)
}
}
return isValidCuit
}
func validateWithVerifierDigit(cuit string) bool {
verificationResult := false
toVerify := utils.ReverseStringWithBuffer(cuit[:10])
weightUpResult := 0
weightUpFactorCounter := -1
weightUpCheckFactor := []int{2, 3, 4, 5, 6, 7}
verifierDigit, err := strconv.Atoi(cuit[len(cuit)-1:])
if err != nil {
return false
}
for i := range 10 {
if i%6 == 0 {
weightUpFactorCounter += 1
}
weightUp, err := strconv.Atoi(string(toVerify[i]))
if err != nil {
return verificationResult
}
weightUpResult += weightUp * weightUpCheckFactor[i-6*weightUpFactorCounter]
}
mod11WeightupResult := 11 - (weightUpResult % 11)
switch mod11WeightupResult {
case 11:
verificationResult = verifierDigit == 0
case 10:
verificationResult = verifierDigit == 9
default:
verificationResult = verifierDigit == mod11WeightupResult
}
return verificationResult
}
func validateCuitType(cuit string) bool {
validationResult := false
cuitType, err := strconv.Atoi(cuit[:2])
if err == nil {
validationResult = CUIT_TYPES[uint8(cuitType)]
}
return validationResult
}
func validateFormat(cuit string) bool {
regexExp, err := regexp.Compile(CUIT_REGEX)
if err == nil {
return regexExp.MatchString(cuit)
}
return false
}

15
main.go Normal file
View File

@@ -0,0 +1,15 @@
package main
import (
"net/http"
"os"
)
func main() {
_, err := os.Stat("./.cache")
if err != nil {
os.Mkdir("./.cache", 0700)
}
http.HandleFunc("/", RequestHandler)
http.ListenAndServe(":3333", nil)
}

View File

@@ -3,12 +3,14 @@ package rate_limit
import (
"encoding/gob"
"os"
"strings"
"time"
)
const TIMEFRAME = 60
func TimeLeft(addr string) int64 {
addr = (strings.Split(addr, ":"))[0]
timeLeft := int64(0)
file, err := os.OpenFile("addr_table.gob", os.O_RDWR|os.O_CREATE, 0644)

57
request_handler.go Normal file
View File

@@ -0,0 +1,57 @@
package main
import (
"encoding/json"
"fmt"
"net/http"
"github.com/gabdlr/api-cuit-go/cache"
"github.com/gabdlr/api-cuit-go/cuit"
"github.com/gabdlr/api-cuit-go/rate_limit"
)
type CuitError struct {
Error string `json:"error"`
}
const NO_SEARCH_ARG = "Sin argumento de búsqueda"
const INVALID_CUIT = "CUIT inválido"
func RequestHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
errorResponse := &CuitError{Error: "ocurrió un error"}
argument := r.URL.Path
if len(argument) == 1 {
errorResponse.Error = NO_SEARCH_ARG
} else {
argument = argument[1:]
timeLeft := rate_limit.TimeLeft(r.RemoteAddr)
if timeLeft > 0 {
errorResponse.Error = fmt.Sprintf("recurso no disponible, debe esperar %v segundos", timeLeft)
} else {
if cuit.IsValid(argument) {
cRes, cErr := cache.Search(argument)
if cErr == nil {
w.Write(cRes)
return
}
res, err := cuit.Search(argument)
if err == nil {
cache.Save(argument, res)
w.Write(res)
return
} else {
errorResponse.Error = err.Error()
}
} else {
errorResponse.Error = INVALID_CUIT
}
}
}
jsonResponse, _ := json.Marshal(errorResponse)
w.Write(jsonResponse)
}

10
utils/formatters.go Normal file
View File

@@ -0,0 +1,10 @@
package utils
import "strings"
func StandardizeCuit(cuit string) string {
if len(cuit) > 11 {
cuit = strings.ReplaceAll(cuit, "-", "")
}
return cuit
}

12
utils/strings.go Normal file
View File

@@ -0,0 +1,12 @@
package utils
import "bytes"
func ReverseStringWithBuffer(input string) string {
var buffer bytes.Buffer
length := len(input) - 1
for i := length; i >= 0; i-- {
buffer.WriteByte(input[i])
}
return buffer.String()
}