Compare commits

..

10 Commits

8 changed files with 375 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)
}

161
cuit/search.go Normal file
View File

@@ -0,0 +1,161 @@
package cuit
import (
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"strings"
"sync"
"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 string `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"`
NumeroRegistroLocal string `json:"numeroRegistroLocal"`
}
type CuitInfo struct {
Sociedad Society `json:"sociedad"`
DomicilioFiscal Address `json:"domicilioFiscal"`
DomicilioLegal Address `json:"domicilioLegal"`
FechaActualizacion string `json:"fechaActualizacion"`
}
const htmlOfInterestStart = `<tbody`
const htmlOfInterestEnd = `</tbody`
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 searchElements(s, startMarker, endMarker string) []string {
elements := make([]string, 0)
thereAreMoreElements := true
for thereAreMoreElements {
startElement := strings.Index(s, startMarker)
endElement := strings.Index(s, endMarker)
if startElement > -1 || endElement > -1 {
elements = append(elements, s[startElement+len(startMarker):endElement])
s = s[endElement+len(endMarker):]
} else {
thereAreMoreElements = false
}
}
return elements
}
func searchParagraphElements(s string) []string {
return searchElements(s, "<p>", "</p>")
}
func updateCuitInfo(cuitInfo *CuitInfo, keyElements []string, wg *sync.WaitGroup) {
go func() {
defer wg.Done()
sociedadElements := searchParagraphElements(keyElements[0])
if len(sociedadElements) == 5 {
cuitInfo.Sociedad.RazonSocial = sociedadElements[0]
cuitInfo.Sociedad.Cuit = sociedadElements[1]
cuitInfo.Sociedad.TipoSocietario = sociedadElements[2]
cuitInfo.Sociedad.FechaDeContrato = sociedadElements[3]
cuitInfo.Sociedad.NumeroRegistroLocal = sociedadElements[4]
}
}()
go func() {
defer wg.Done()
domicilioFiscalElements := searchParagraphElements(keyElements[1])
if len(domicilioFiscalElements) == 6 {
cuitInfo.DomicilioFiscal.Provincia = domicilioFiscalElements[0]
cuitInfo.DomicilioFiscal.Localidad = domicilioFiscalElements[1]
cuitInfo.DomicilioFiscal.Domicilio = domicilioFiscalElements[2]
cuitInfo.DomicilioFiscal.PisoDeptoOfi = domicilioFiscalElements[3]
cuitInfo.DomicilioFiscal.CodigoPostal = domicilioFiscalElements[4]
cuitInfo.DomicilioFiscal.EstadoDeDomicilio = domicilioFiscalElements[5]
}
}()
go func() {
defer wg.Done()
domicilioLegalElements := searchParagraphElements(keyElements[2])
if len(domicilioLegalElements) == 6 {
cuitInfo.DomicilioLegal.Provincia = domicilioLegalElements[0]
cuitInfo.DomicilioLegal.Localidad = domicilioLegalElements[1]
cuitInfo.DomicilioLegal.Domicilio = domicilioLegalElements[2]
cuitInfo.DomicilioLegal.PisoDeptoOfi = domicilioLegalElements[3]
cuitInfo.DomicilioLegal.CodigoPostal = domicilioLegalElements[4]
cuitInfo.DomicilioLegal.EstadoDeDomicilio = domicilioLegalElements[5]
}
}()
go func() {
defer wg.Done()
fechaActualizacionElements := searchParagraphElements(keyElements[3])
if len(fechaActualizacionElements) == 1 {
cuitInfo.FechaActualizacion = fechaActualizacionElements[0]
}
}()
}
func parseResponse(html string) (cuitInfo CuitInfo, err error) {
notFoundErr := "información no disponible"
if strings.Contains(html, exitSignal) {
err = errors.New(notFoundErr)
return cuitInfo, err
}
startPosition := strings.Index(html, htmlOfInterestStart)
endPosition := strings.Index(html, htmlOfInterestEnd)
if startPosition != -1 && endPosition != -1 {
info := html[startPosition:endPosition]
startMarker := "<td"
endMarker := "</td"
keyElements := searchElements(info, startMarker, endMarker)
if len(keyElements) == 4 {
var wg sync.WaitGroup
wg.Add(4)
updateCuitInfo(&cuitInfo, keyElements, &wg)
wg.Wait()
}
} else {
err = errors.New(notFoundErr)
}
return cuitInfo, err
}

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 := weightUpResult % 11
switch mod11WeightupResult {
case 11:
verificationResult = verifierDigit == 0
case 10:
verificationResult = verifierDigit == 9
default:
verificationResult = verifierDigit == 11-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 ( import (
"encoding/gob" "encoding/gob"
"os" "os"
"strings"
"time" "time"
) )
const TIMEFRAME = 60 const TIMEFRAME = 60
func TimeLeft(addr string) int64 { func TimeLeft(addr string) int64 {
addr = (strings.Split(addr, ":"))[0]
timeLeft := int64(0) timeLeft := int64(0)
file, err := os.OpenFile("addr_table.gob", os.O_RDWR|os.O_CREATE, 0644) file, err := os.OpenFile("addr_table.gob", os.O_RDWR|os.O_CREATE, 0644)

54
request_handler.go Normal file
View File

@@ -0,0 +1,54 @@
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"
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()
}
}
}
}
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()
}