sistema de facturacion

This commit is contained in:
anivarroa 2025-05-17 11:36:26 -04:00
commit ae0e2e7155
163 changed files with 34168 additions and 0 deletions

82
README.md Normal file
View File

@ -0,0 +1,82 @@
# comando para crear carpetas y archivos
mkdir -p daemonService/{cmd,config,models,db,notifications,services,api,scheduler} && touch daemonService/cmd/main.go daemonService/config/config.go daemonService/models/models.go daemonService/db/db.go daemonService/notifications/notifier.go daemonService/services/service.go daemonService/services/tipo_documento_identidad.go daemonService/api/handlers.go daemonService/api/server.go daemonService/scheduler/scheduler.go daemonService/config.yaml
myproject/
├── cmd/
│ └── main.go
├── config/
│ └── config.go
├── models/
│ └── models.go
├── db/
│ └── db.go
├── notifications/
│ └── notifier.go
├── services/
│ ├── service.go
│ └── tipo_documento_identidad.go
├── api/
│ ├── handlers.go
│ └── server.go
├── scheduler/
│ └── scheduler.go
└── config.yaml
.
├── configs
│ └── config.yaml
├── cmd
│ └── daemon
│ └── main.go
└── internal
├── api
│ └── router.go
├── config
│ └── config.go
├── db
│ └── db.go
├── notifications
│ └── notifications.go
├── services
│ ├── service.go
│ └── tipo_documento_identidad.go
└── soap
└── soap.go
# iniciar go
go mod init daemonService
go mod tidy
# para ejecutar directo
go run cmd/main.go
# eliminar archivos go.sum go.mod
go clean -modcache
rm -r go.sum go.mod
# 1. Compilar el código
go build -o daemon-services cmd/daemon/main.go
# para compilar en linux
env GOOS=linux GOARCH=amd64 go build -o daemon-services cmd/daemon/main.go
# 2. Ejecutar el servicio
./daemon-services
# Para ejecutarlo con opciones específicas:
# Usando un archivo de configuración específico
./daemon-services --config=mi-config.yaml
# Ejecutar una vez y salir
./daemon-services --run-once
# Ejecutar solo un servicio específico
./daemon-services --service=tipo_documento_identidad

8
api-soap-facturacion/.idea/.gitignore generated vendored Normal file
View File

@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="Go" enabled="true" />
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

8
api-soap-facturacion/.idea/modules.xml generated Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/api-soap-facturacion.iml" filepath="$PROJECT_DIR$/.idea/api-soap-facturacion.iml" />
</modules>
</component>
</project>

View File

@ -0,0 +1,133 @@
# Proyecto Go para consumo de SOAP APIs
## Estructura del proyecto
```
soap-api-client/
├── cmd/
│ └── api/
│ └── main.go
├── configs/
│ └── config.yaml
├── internal/
│ ├── api/
│ │ ├── handler.go
│ │ └── router.go
│ ├── config/
│ │ └── config.go
│ ├── database/
│ │ ├── postgres.go
│ │ └── models.go
│ ├── soap/
│ │ ├── client.go
│ │ ├── api1/
│ │ │ ├── client.go
│ │ │ ├── models.go
│ │ │ └── transformers.go
│ │ └── api2/
│ │ ├── client.go
│ │ ├── models.go
│ │ └── transformers.go
│ └── logger/
│ └── logger.go
├── pkg/
│ ├── errors/
│ │ └── errors.go
│ ├── soap/
│ │ └── soap.go
│ └── utils/
│ └── utils.go
├── go.mod
└── go.sum
```
## Inicialización del proyecto
```bash
mkdir -p soap-api-client/cmd/api
mkdir -p soap-api-client/configs
mkdir -p soap-api-client/internal/api
mkdir -p soap-api-client/internal/configs
mkdir -p soap-api-client/internal/database
mkdir -p soap-api-client/internal/soap/api1
mkdir -p soap-api-client/internal/soap/api2
mkdir -p soap-api-client/internal/logger
mkdir -p soap-api-client/pkg/errors
mkdir -p soap-api-client/pkg/soap
mkdir -p soap-api-client/pkg/utils
cd soap-api-client
go mod init github.com/yourusername/soap-api-client
```
## Comandos para la ejecución
```bash
# Iniciar la aplicación
go run cmd/api/main.go
# Construir la aplicación
go build -o soap-api-client cmd/api/main.go
# Ejecutar la aplicación construida
./soap-api-client
```
## Comandos para la ejecución
```bash
# Iniciar la aplicación
go run cmd/api/main.go
# Construir la aplicación
go build -o soap-api-client cmd/api/main.go
# Ejecutar la aplicación construida
./soap-api-client
```
## Ejemplos de uso
### Ejemplo de solicitud GetData a API1
```bash
curl -X POST http://localhost:9999/api/v1/api1/get-data \
-H "Content-Type: application/json" \
-d '{
"id": "12345",
"type": "CUSTOMER",
"date": "2023-12-01"
}'
```
### Ejemplo de solicitud de transacción a API2
```bash
curl -X POST http://localhost:9999/api/v1/api2/process-transaction \
-H "Content-Type: application/json" \
-d '{
"transactionId": "TX789012",
"accountId": "ACC123",
"amount": 100.50,
"currency": "USD",
"description": "Pago de servicio"
}'
```
### Ejemplo de obtención de parámetros
```bash
curl -X GET http://localhost:9999/api/v1/parameters
```
### Ejemplo de actualización de parámetro
```bash
curl -X PUT http://localhost:9999/api/v1/parameters/soap.api1.endpoint \
-H "Content-Type: application/json" \
-d '{
"value": "https://new-api1.example.com/soap",
"description": "Nuevo endpoint para API 1",
"active": true
}'
```

View File

@ -0,0 +1,165 @@
package main
import (
"api-soap-facturacion/internal/api"
"api-soap-facturacion/internal/config"
"api-soap-facturacion/internal/database"
"api-soap-facturacion/internal/logger"
"api-soap-facturacion/internal/soap"
"context"
"fmt"
"net/http"
"os"
"os/signal"
"syscall"
"time"
)
func main() {
// Inicializar configuración
cfg, err := config.LoadConfig()
if err != nil {
panic(fmt.Sprintf("Error al cargar la configuración: %v", err))
}
// Inicializar logger
log := logger.NewLogger(cfg)
//log.Info("Iniciando aplicación", logger.Field("app", cfg.App.Name))
log.Info("Iniciando aplicación", logger.NewField("app", cfg.App.Name))
// Conectar a la base de datos
db, err := database.NewPostgresConnection(cfg.Database)
if err != nil {
//log.Fatal("Error al conectar a la base de datos", logger.Field("error", err))
log.Fatal("Error al conectar a la base de datos", logger.NewField("error", err))
}
defer db.Close()
// Verificar conexión a la base de datos
if err := db.Ping(); err != nil {
log.Error("Error al cargar parámetros desde la base de datos", logger.NewField("error", err))
}
log.Info("Conexión a la base de datos establecida")
// Cargar parámetros desde la base de datos
params, err := database.LoadParameters(db)
if err != nil {
log.Info("Parámetros cargados desde la base de datos", logger.NewField("count", len(params)))
} else {
log.Info("Parámetros cargados desde la base de datos", logger.NewField("count", len(params)))
// Actualizar configuración con parámetros de la base de datos
cfg.MergeParameters(params)
}
// Inicializar cliente SOAP
soapClients := soap.NewSoapClients(cfg, log)
// Inicializar API
router := api.NewRouter(cfg, log, db, soapClients)
// Configurar servidor HTTP
srv := &http.Server{
Addr: fmt.Sprintf(":%d", cfg.App.Port),
Handler: router,
ReadTimeout: cfg.App.RequestTimeout * time.Second,
WriteTimeout: cfg.App.RequestTimeout * time.Second,
IdleTimeout: 120 * time.Second,
}
// Iniciar servidor en goroutine
go func() {
log.Info("Servidor iniciado", logger.NewField("port", cfg.App.Port))
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatal("Error al iniciar servidor", logger.NewField("error", err))
}
}()
// Configurar captura de señales para graceful shutdown
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Info("Apagando servidor...")
// Iniciar graceful shutdown
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
log.Fatal("Error al apagar servidor", logger.NewField("error", err))
}
log.Info("Servidor detenido correctamente")
}
//
//func main() {
// // Inicializar configuración
// cfg, err := config.LoadConfig()
// if err != nil {
// panic(fmt.Sprintf("Error al cargar la configuración: %v", err))
// }
//
// // Inicializar logger
// log := logger.NewLogger(cfg)
// //log.Info("Iniciando aplicación", logger.Field("app", cfg.App.Name))
// log.Info("Iniciando aplicación", logger.NewField("app", cfg.App.Name))
//
// // Conectar a la base de datos
// db, err := database.NewPostgresConnection(cfg.Database)
// if err != nil {
// //log.Fatal("Error al conectar a la base de datos", logger.Field("error", err))
// log.Fatal("Error al conectar a la base de datos", logger.NewField("error", err))
// }
// defer db.Close()
//
// // Verificar conexión a la base de datos
// if err := db.Ping(); err != nil {
// log.Error("Error al cargar parámetros desde la base de datos", logger.NewField("error", err))
// }
// log.Info("Conexión a la base de datos establecida")
//
// // Cargar parámetros desde la base de datos
// params, err := database.LoadParameters(db)
// if err != nil {
// log.Info("Parámetros cargados desde la base de datos", logger.NewField("count", len(params)))
// } else {
// log.Info("Parámetros cargados desde la base de datos", logger.NewField("count", len(params)))
// // Actualizar configuración con parámetros de la base de datos
// cfg.MergeParameters(params)
// }
//
// // Inicializar cliente SOAP
// soapClients := soap.NewSoapClients(cfg, log)
//
// // Inicializar API
// router := api.NewRouter(cfg, log, db, soapClients)
//
// // Configurar servidor HTTP
// srv := &http.Server{
// Addr: fmt.Sprintf(":%d", cfg.App.Port),
// Handler: router,
// ReadTimeout: cfg.App.RequestTimeout * time.Second,
// WriteTimeout: cfg.App.RequestTimeout * time.Second,
// IdleTimeout: 120 * time.Second,
// }
//
// // Iniciar servidor en goroutine
// go func() {
// log.Info("Servidor iniciado", logger.NewField("port", cfg.App.Port))
// if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
// log.Fatal("Error al iniciar servidor", logger.NewField("error", err))
// }
// }()
//
// // Configurar captura de señales para graceful shutdown
// quit := make(chan os.Signal, 1)
// signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
// <-quit
// log.Info("Apagando servidor...")
//
// // Iniciar graceful shutdown
// ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
// defer cancel()
// if err := srv.Shutdown(ctx); err != nil {
// log.Fatal("Error al apagar servidor", logger.NewField("error", err))
// }
// log.Info("Servidor detenido correctamente")
//}

View File

@ -0,0 +1,36 @@
app:
name: "api-facturacion-myapps"
port: 9999
environment: "development" # development, staging, production
log_level: "debug" # debug, info, warn, error
request_timeout: 30 # seconds
log_file_path: "./logs/api_facturacion.log"
log_max_size: 10 # Tamaño máximo en MB
log_max_backups: 5 # Número de archivos rotados a mantener
log_max_age: 30 # Días a mantener los archivos
log_compress: true # Comprimir archivos rotados
use_file_logger: true # Controla si usar archivo (true) o solo stdout (false)
database:
host: "localhost"
port: 5555
username: "facturacion_user"
password: "facturacion_pass"
database: "facturacion_bd"
max_open_conns: 10
max_idle_conns: 5
conn_max_lifetime: 1h
soap:
timeout: 15 # seconds
apis:
api1:
endpoint: "https://api1.example.com/soap"
username: ""
password: ""
timeout: 10 # seconds
api2:
endpoint: "https://api2.example.com/soap"
username: ""
password: ""
timeout: 20 # seconds

View File

@ -0,0 +1,56 @@
module api-soap-facturacion
go 1.23.9
require (
github.com/gin-gonic/gin v1.10.0
github.com/google/uuid v1.6.0
github.com/jackc/pgx/v5 v5.7.4
github.com/jmoiron/sqlx v1.4.0
github.com/pkg/errors v0.9.1
github.com/spf13/viper v1.20.1
go.uber.org/zap v1.27.0
gopkg.in/natefinch/lumberjack.v2 v2.2.1
)
require (
github.com/bytedance/sonic v1.11.6 // indirect
github.com/bytedance/sonic/loader v0.1.1 // indirect
github.com/cloudwego/base64x v0.1.4 // indirect
github.com/cloudwego/iasm v0.2.0 // indirect
github.com/fsnotify/fsnotify v1.8.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.20.0 // indirect
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
github.com/sagikazarmark/locafero v0.7.0 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/afero v1.12.0 // indirect
github.com/spf13/cast v1.7.1 // indirect
github.com/spf13/pflag v1.0.6 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
go.uber.org/multierr v1.10.0 // indirect
golang.org/x/arch v0.8.0 // indirect
golang.org/x/crypto v0.32.0 // indirect
golang.org/x/net v0.33.0 // indirect
golang.org/x/sync v0.10.0 // indirect
golang.org/x/sys v0.29.0 // indirect
golang.org/x/text v0.21.0 // indirect
google.golang.org/protobuf v1.36.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

144
api-soap-facturacion/go.sum Normal file
View File

@ -0,0 +1,144 @@
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8=
github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=
github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.7.4 h1:9wKznZrhWa2QiHL+NjTSPP6yjl3451BX3imWDnokYlg=
github.com/jackc/pgx/v5 v5.7.4/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo=
github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k=
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs=
github.com/spf13/afero v1.12.0/go.mod h1:ZTlWwG4/ahT8W7T0WQ5uYmjI9duaLQGy3Q2OAl4sk/4=
github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4=
github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk=
google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=

View File

@ -0,0 +1,66 @@
package api
import (
"api-soap-facturacion/internal/logger"
api1 "api-soap-facturacion/internal/soap/api"
soapPkg "api-soap-facturacion/pkg/soap"
"context"
"github.com/gin-gonic/gin"
"net/http"
"time"
)
// API1GetData maneja solicitudes GetData para API1
func (h *Handler) API1GetData(c *gin.Context) {
// Obtener logger
log := getLogger(c)
// Obtener contexto con timeout
ctx, cancel := context.WithTimeout(c.Request.Context(), h.config.App.RequestTimeout*time.Second)
defer cancel()
// Agregar ID de solicitud al contexto
requestID := c.GetHeader("X-Request-ID")
ctx = context.WithValue(ctx, soapPkg.RequestIDKey{}, requestID)
// Parsear solicitud
var requestData struct {
ID string `json:"id" binding:"required"`
Type string `json:"type" binding:"required"`
Date string `json:"date"`
}
if err := c.ShouldBindJSON(&requestData); err != nil {
log.Error("Error al parsear solicitud", logger.NewField("error", err))
c.JSON(http.StatusBadRequest, gin.H{
"error": "Error al parsear solicitud",
})
return
}
// Crear solicitud SOAP
request := api1.GetDataRequest{
Namespace: "http://api1.example.com/soap",
ID: requestData.ID,
Type: requestData.Type,
Date: requestData.Date,
}
// Llamar a API1
response, err := h.soapClients.API1.GetData(ctx, request)
if err != nil {
log.Error("Error al llamar a API1", logger.NewField("error", err))
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Error al realizar solicitud a API1",
})
return
}
// Retornar respuesta
c.JSON(http.StatusOK, gin.H{
"status": response.Status,
"resultCode": response.ResultCode,
"description": response.Description,
"data": response.Data,
})
}

View File

@ -0,0 +1,240 @@
package api
import (
"context"
"net/http"
"time"
"api-soap-facturacion/internal/config"
"api-soap-facturacion/internal/database"
"api-soap-facturacion/internal/logger"
"api-soap-facturacion/internal/soap"
"api-soap-facturacion/internal/soap/api2"
//"api-soap-facturacion/pkg/errors"
soapPkg "api-soap-facturacion/pkg/soap"
"github.com/gin-gonic/gin"
"github.com/jmoiron/sqlx"
)
// Handler maneja las solicitudes HTTP
type Handler struct {
config *config.Config
logger logger.Logger
db *sqlx.DB
soapClients *soap.Clients
}
// NewHandler crea un nuevo handler
func NewHandler(cfg *config.Config, log logger.Logger, db *sqlx.DB, soapClients *soap.Clients) *Handler {
return &Handler{
config: cfg,
logger: log,
db: db,
soapClients: soapClients,
}
}
// HealthCheck verifica el estado de la aplicación
func (h *Handler) HealthCheck(c *gin.Context) {
// Verificar conexión a la base de datos
dbErr := h.db.Ping()
status := "ok"
statusCode := http.StatusOK
if dbErr != nil {
status = "degraded"
statusCode = http.StatusServiceUnavailable
}
c.JSON(statusCode, gin.H{
"status": status,
"timestamp": time.Now().Format(time.RFC3339),
"version": "1.0.0",
"services": gin.H{
"database": dbErr == nil,
},
})
}
// API2ProcessTransaction maneja solicitudes de transacción para API2
func (h *Handler) API2ProcessTransaction(c *gin.Context) {
// Obtener logger
log := getLogger(c)
// Obtener contexto con timeout
ctx, cancel := context.WithTimeout(c.Request.Context(), h.config.App.RequestTimeout*time.Second)
defer cancel()
// Agregar ID de solicitud al contexto
requestID := c.GetHeader("X-Request-ID")
ctx = context.WithValue(ctx, soapPkg.RequestIDKey{}, requestID)
// Parsear solicitud
var requestData struct {
TransactionID string `json:"transactionId" binding:"required"`
AccountID string `json:"accountId" binding:"required"`
Amount float64 `json:"amount" binding:"required"`
Currency string `json:"currency" binding:"required"`
Description string `json:"description"`
}
if err := c.ShouldBindJSON(&requestData); err != nil {
log.Error("Error al parsear solicitud", logger.NewField("error", err))
c.JSON(http.StatusBadRequest, gin.H{
"error": "Error al parsear solicitud",
})
return
}
// Crear solicitud SOAP
request := api2.TransactionRequest{
Namespace: "http://api2.example.com/soap",
TransactionID: requestData.TransactionID,
AccountID: requestData.AccountID,
Amount: requestData.Amount,
Currency: requestData.Currency,
Description: requestData.Description,
}
// Llamar a API2
response, err := h.soapClients.API2.ProcessTransaction(ctx, request)
if err != nil {
log.Error("Error al llamar a API2", logger.NewField("error", err))
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Error al realizar solicitud a API2",
})
return
}
// Retornar respuesta
c.JSON(http.StatusOK, gin.H{
"resultCode": response.ResultCode,
"resultMessage": response.ResultMessage,
"transactionId": response.TransactionID,
"confirmationCode": response.ConfirmationCode,
"timestamp": response.Timestamp,
})
}
// GetAllParameters retorna todos los parámetros
func (h *Handler) GetAllParameters(c *gin.Context) {
// Obtener logger
log := getLogger(c)
// Consultar parámetros
var params []database.Parameter
query := `SELECT id, key, value, description, active, created_at, updated_at FROM parameters`
err := h.db.Select(&params, query)
if err != nil {
log.Error("Error al obtener parámetros", logger.NewField("error", err))
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Error al obtener parámetros",
})
return
}
// Retornar parámetros
c.JSON(http.StatusOK, params)
}
// GetParameter retorna un parámetro por clave
func (h *Handler) GetParameter(c *gin.Context) {
// Obtener logger
log := getLogger(c)
// Obtener clave
key := c.Param("key")
// Consultar parámetro
var param database.Parameter
query := `SELECT id, key, value, description, active, created_at, updated_at FROM parameters WHERE key = $1`
err := h.db.Get(&param, query, key)
if err != nil {
log.Error("Error al obtener parámetro",
logger.NewField("error", err),
logger.NewField("key", key),
)
c.JSON(http.StatusNotFound, gin.H{
"error": "Parámetro no encontrado",
})
return
}
// Retornar parámetro
c.JSON(http.StatusOK, param)
}
// UpdateParameter actualiza un parámetro
func (h *Handler) UpdateParameter(c *gin.Context) {
// Obtener logger
log := getLogger(c)
// Obtener clave
key := c.Param("key")
// Parsear solicitud
var requestData struct {
Value string `json:"value" binding:"required"`
Description string `json:"description"`
Active bool `json:"active"`
}
if err := c.ShouldBindJSON(&requestData); err != nil {
log.Error("Error al parsear solicitud", logger.NewField("error", err))
c.JSON(http.StatusBadRequest, gin.H{
"error": "Error al parsear solicitud",
})
return
}
// Actualizar parámetro
query := `
UPDATE parameters
SET value = $1, description = $2, active = $3, updated_at = NOW()
WHERE key = $4
RETURNING id, key, value, description, active, created_at, updated_at
`
var param database.Parameter
err := h.db.Get(&param, query,
requestData.Value,
requestData.Description,
requestData.Active,
key,
)
if err != nil {
log.Error("Error al actualizar parámetro",
logger.NewField("error", err),
logger.NewField("key", key),
)
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Error al actualizar parámetro",
})
return
}
// Retornar parámetro actualizado
c.JSON(http.StatusOK, param)
}
// getLogger obtiene el logger del contexto
func getLogger(c *gin.Context) logger.Logger {
if log, exists := c.Get("logger"); exists {
return log.(logger.Logger)
}
// Si no hay logger en el contexto, retornar un logger por defecto
return &defaultLogger{}
}
// defaultLogger es un logger por defecto
type defaultLogger struct{}
func (l *defaultLogger) Debug(msg string, fields ...logger.Field) {}
func (l *defaultLogger) Info(msg string, fields ...logger.Field) {}
func (l *defaultLogger) Warn(msg string, fields ...logger.Field) {}
func (l *defaultLogger) Error(msg string, fields ...logger.Field) {}
func (l *defaultLogger) Fatal(msg string, fields ...logger.Field) {}
func (l *defaultLogger) With(fields ...logger.Field) logger.Logger { return l }

View File

@ -0,0 +1 @@
package api

View File

@ -0,0 +1,129 @@
package api
import (
"net/http"
"api-soap-facturacion/internal/config"
"api-soap-facturacion/internal/logger"
"api-soap-facturacion/internal/soap"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/jmoiron/sqlx"
)
// NewRouter configura el router HTTP
func NewRouter(cfg *config.Config, log logger.Logger, db *sqlx.DB, soapClients *soap.Clients) *gin.Engine {
// Configurar modo de Gin según entorno
if cfg.App.Environment == "production" {
gin.SetMode(gin.ReleaseMode)
}
// Crear router
router := gin.New()
// Configurar middleware
router.Use(gin.Recovery())
router.Use(LoggerMiddleware(log))
router.Use(RequestIDMiddleware())
router.Use(CORSMiddleware())
// Configurar manejadores
handler := NewHandler(cfg, log, db, soapClients)
// Rutas de salud
router.GET("/health", handler.HealthCheck)
// Rutas API
api := router.Group("/api/v1")
{
// API1
api1 := api.Group("/api1")
{
api1.POST("/get-data", handler.API1GetData)
}
// API2
api2 := api.Group("/api2")
{
api2.POST("/process-transaction", handler.API2ProcessTransaction)
}
// Parámetros
params := api.Group("/parameters")
{
params.GET("", handler.GetAllParameters)
params.GET("/:key", handler.GetParameter)
params.PUT("/:key", handler.UpdateParameter)
}
}
return router
}
// LoggerMiddleware configura el middleware de logging
func LoggerMiddleware(log logger.Logger) gin.HandlerFunc {
return func(c *gin.Context) {
// Antes de la solicitud
path := c.Request.URL.Path
method := c.Request.Method
requestID := c.GetHeader("X-Request-ID")
// Crear logger para esta solicitud
reqLogger := log.With(
logger.NewField("request_id", requestID),
logger.NewField("path", path),
logger.NewField("method", method),
)
// Almacenar logger en el contexto
c.Set("logger", reqLogger)
reqLogger.Info("Solicitud iniciada")
// Procesar solicitud
c.Next()
// Después de la solicitud
status := c.Writer.Status()
reqLogger.Info("Solicitud completada",
logger.NewField("status", status),
)
}
}
// RequestIDMiddleware genera un ID único para cada solicitud
func RequestIDMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
// Si no hay ID de solicitud, generar uno
requestID := c.GetHeader("X-Request-ID")
if requestID == "" {
requestID = generateRequestID()
c.Request.Header.Set("X-Request-ID", requestID)
}
// Establecer ID de solicitud en la respuesta
c.Writer.Header().Set("X-Request-ID", requestID)
c.Next()
}
}
// CORSMiddleware configura CORS
func CORSMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
c.Writer.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
c.Writer.Header().Set("Access-Control-Allow-Headers", "Origin, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, X-Request-ID")
if c.Request.Method == "OPTIONS" {
c.AbortWithStatus(http.StatusOK)
return
}
c.Next()
}
}
// generateRequestID genera un ID único para una solicitud
func generateRequestID() string {
return "req-" + uuid.New().String()
}

View File

@ -0,0 +1,95 @@
package config
import (
"strings"
"time"
"github.com/spf13/viper"
)
// Config contiene toda la configuración de la aplicación
type Config struct {
App AppConfig `mapstructure:"app"`
Database DatabaseConfig `mapstructure:"database"`
Soap SoapConfig `mapstructure:"soap"`
}
// contiene la configuración general de la aplicación
type AppConfig struct {
Name string `mapstructure:"name"`
Port int `mapstructure:"port"`
Environment string `mapstructure:"environment"`
LogLevel string `mapstructure:"log_level"`
RequestTimeout time.Duration `mapstructure:"request_timeout"`
LogFilePath string `mapstructure:"log_file_path"`
LogMaxSize int `mapstructure:"log_max_size"`
LogMaxBackups int `mapstructure:"log_max_backups"`
LogMaxAge int `mapstructure:"log_max_age"`
LogCompress bool `mapstructure:"log_compress"`
}
// DatabaseConfig contiene la configuración de la base de datos
type DatabaseConfig struct {
Host string `mapstructure:"host"`
Port int `mapstructure:"port"`
Username string `mapstructure:"username"`
Password string `mapstructure:"password"`
Database string `mapstructure:"database"`
MaxOpenConns int `mapstructure:"max_open_conns"`
MaxIdleConns int `mapstructure:"max_idle_conns"`
ConnMaxLifetime time.Duration `mapstructure:"conn_max_lifetime"`
}
// SoapConfig contiene la configuración general para clientes SOAP
type SoapConfig struct {
Timeout time.Duration `mapstructure:"timeout"`
APIs map[string]APIConfig `mapstructure:"apis"`
}
// APIConfig contiene la configuración específica para cada API SOAP
type APIConfig struct {
Endpoint string `mapstructure:"endpoint"`
Username string `mapstructure:"username"`
Password string `mapstructure:"password"`
Timeout time.Duration `mapstructure:"timeout"`
}
// LoadConfig carga la configuración desde el archivo config.yaml
func LoadConfig() (*Config, error) {
viper.SetConfigName("config")
viper.SetConfigType("yaml")
viper.AddConfigPath("./configs")
viper.AddConfigPath(".")
viper.AutomaticEnv()
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
if err := viper.ReadInConfig(); err != nil {
return nil, err
}
var config Config
if err := viper.Unmarshal(&config); err != nil {
return nil, err
}
return &config, nil
}
// actualiza la configuración con parámetros de la base de datos
func (c *Config) MergeParameters(params map[string]string) {
// Ejemplo de sobrescritura de configuración con parámetros de BD
if endpoint, ok := params["soap.api1.endpoint"]; ok {
if api1Config, exists := c.Soap.APIs["api1"]; exists {
api1Config.Endpoint = endpoint
c.Soap.APIs["api1"] = api1Config
}
}
if endpoint, ok := params["soap.api2.endpoint"]; ok {
if api2Config, exists := c.Soap.APIs["api2"]; exists {
api2Config.Endpoint = endpoint
c.Soap.APIs["api2"] = api2Config
}
}
// Otros parámetros pueden ser añadidos según sea necesario
}

View File

@ -0,0 +1,75 @@
package database
import (
"time"
)
// Parameter representa un parámetro en la base de datos
type Parameter struct {
ID int `db:"id"`
Key string `db:"key"`
Value string `db:"value"`
Description string `db:"description"`
Active bool `db:"active"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
}
// Estructura para las solicitudes SOAP
type SoapRequest struct {
ID int `db:"id"`
RequestID string `db:"request_id"`
APIID string `db:"api_id"`
OperationName string `db:"operation_name"`
RequestBody string `db:"request_body"`
ResponseBody string `db:"response_body"`
StatusCode int `db:"status_code"`
Error string `db:"error"`
RequestTime time.Time `db:"request_time"`
ResponseTime time.Time `db:"response_time"`
ProcessingTime int64 `db:"processing_time"`
CreatedAt time.Time `db:"created_at"`
}
// Inicialización de la base de datos: script SQL
const DatabaseInitSQL = `
-- Tabla de parámetros
CREATE TABLE IF NOT EXISTS parameters (
id SERIAL PRIMARY KEY,
key VARCHAR(100) NOT NULL UNIQUE,
value TEXT NOT NULL,
description TEXT,
active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- Tabla para registro de solicitudes SOAP
CREATE TABLE IF NOT EXISTS soap_requests (
id SERIAL PRIMARY KEY,
request_id VARCHAR(36) NOT NULL,
api_id VARCHAR(50) NOT NULL,
operation_name VARCHAR(100) NOT NULL,
request_body TEXT NOT NULL,
response_body TEXT,
status_code INT,
error TEXT,
request_time TIMESTAMP WITH TIME ZONE NOT NULL,
response_time TIMESTAMP WITH TIME ZONE,
processing_time BIGINT,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- Índices
CREATE INDEX IF NOT EXISTS idx_soap_requests_request_id ON soap_requests(request_id);
CREATE INDEX IF NOT EXISTS idx_soap_requests_api_id ON soap_requests(api_id);
CREATE INDEX IF NOT EXISTS idx_soap_requests_operation_name ON soap_requests(operation_name);
CREATE INDEX IF NOT EXISTS idx_soap_requests_request_time ON soap_requests(request_time);
-- Insertar parámetros iniciales
INSERT INTO parameters (key, value, description)
VALUES
('soap.api1.endpoint', 'https://api1.example.com/soap', 'Endpoint for API 1'),
('soap.api2.endpoint', 'https://api2.example.com/soap', 'Endpoint for API 2')
ON CONFLICT (key) DO NOTHING;
`

View File

@ -0,0 +1,49 @@
package database
import (
"fmt"
"time"
"api-soap-facturacion/internal/config"
_ "github.com/jackc/pgx/v5/stdlib"
"github.com/jmoiron/sqlx"
)
// NewPostgresConnection crea una nueva conexión a PostgreSQL
func NewPostgresConnection(cfg config.DatabaseConfig) (*sqlx.DB, error) {
dsn := fmt.Sprintf(
"host=%s port=%d user=%s password=%s dbname=%s sslmode=disable",
cfg.Host, cfg.Port, cfg.Username, cfg.Password, cfg.Database,
)
db, err := sqlx.Connect("pgx", dsn)
if err != nil {
return nil, err
}
// Configurar pool de conexiones
db.SetMaxOpenConns(cfg.MaxOpenConns)
db.SetMaxIdleConns(cfg.MaxIdleConns)
db.SetConnMaxLifetime(cfg.ConnMaxLifetime * time.Hour)
return db, nil
}
// LoadParameters carga parámetros desde la base de datos
func LoadParameters(db *sqlx.DB) (map[string]string, error) {
var params []Parameter
query := `SELECT id, key, value, description, created_at, updated_at FROM parameters WHERE active = true`
err := db.Select(&params, query)
if err != nil {
return nil, err
}
// Convertir a map
result := make(map[string]string)
for _, param := range params {
result[param.Key] = param.Value
}
return result, nil
}

View File

@ -0,0 +1 @@
package logger

View File

@ -0,0 +1,874 @@
package logger
import (
"api-soap-facturacion/internal/config"
"encoding/json"
"fmt"
"go.uber.org/zap"
"go.uber.org/zap/buffer"
"go.uber.org/zap/zapcore"
lumberjack "gopkg.in/natefinch/lumberjack.v2"
"os"
"strings"
)
// Logger es una interfaz que define los métodos necesarios para el logging
type Logger interface {
Debug(msg string, fields ...Field)
Info(msg string, fields ...Field)
Warn(msg string, fields ...Field)
Error(msg string, fields ...Field)
Fatal(msg string, fields ...Field)
With(fields ...Field) Logger
}
// Field es un campo para agregar contexto a los logs
type Field struct {
Key string
Value interface{}
}
// zapLogger implementa la interfaz Logger usando zap
type zapLogger struct {
logger *zap.Logger
}
// CustomEncoder implementa la interfaz zapcore.Encoder
type CustomEncoder struct {
zapcore.Encoder
pool buffer.Pool
serviceName string
}
// NewCustomEncoder crea un nuevo encoder personalizado
func NewCustomEncoder(cfg zapcore.EncoderConfig, serviceName string) zapcore.Encoder {
return &CustomEncoder{
Encoder: zapcore.NewJSONEncoder(cfg),
pool: buffer.NewPool(),
serviceName: serviceName,
}
}
// EncodeEntry implementa el formato personalizado
// EncodeEntry implementa el formato personalizado
func (e *CustomEncoder) EncodeEntry(entry zapcore.Entry, fields []zapcore.Field) (*buffer.Buffer, error) {
line := e.pool.Get()
// Agregar nivel (INFO:, WARN:, etc.)
levelStr := entry.Level.CapitalString()
line.AppendString(levelStr)
line.AppendString(": ")
// Agregar timestamp
timestamp := entry.Time.Format("2006/01/02 15:04:05")
line.AppendString(timestamp)
line.AppendString(" ")
// Agregar información del archivo/línea si está disponible
if entry.Caller.Defined {
// Convertir la ruta a formato Unix para manejar uniformemente las barras
filePath := strings.ReplaceAll(entry.Caller.File, "\\", "/")
// Opciones para encontrar la parte relevante del path
possiblePrefixes := []string{
"/cmd/",
"/internal/",
"/pkg/",
"/api/",
"/app/",
"/src/",
}
// Buscar el prefijo más cercano al final del path
shortPath := filePath
for _, prefix := range possiblePrefixes {
if idx := strings.LastIndex(filePath, prefix); idx != -1 {
// Encontramos un prefijo estándar de Go
shortPath = filePath[idx+1:] // +1 para quitar el slash inicial
break
}
}
// Si no se encontró ningún prefijo, usar el último componente del path
if shortPath == filePath {
parts := strings.Split(filePath, "/")
if len(parts) > 0 {
shortPath = parts[len(parts)-1]
}
}
caller := fmt.Sprintf("%s:%d: ", shortPath, entry.Caller.Line)
line.AppendString(caller)
}
// Agregar nombre del servicio entre corchetes
line.AppendString("[")
line.AppendString(e.serviceName)
line.AppendString("] ")
// Agregar mensaje
line.AppendString(entry.Message)
// Procesar campos adicionales
if len(fields) > 0 {
// Usar encoder JSON para serializar los campos
encoder := zapcore.NewMapObjectEncoder()
// Agregar cada campo al encoder, excepto "service"
for _, field := range fields {
if field.Key != "service" {
field.AddTo(encoder)
}
}
// Solo mostramos los campos si hay alguno después de filtrar
if len(encoder.Fields) > 0 {
line.AppendString(" ")
fieldsJSON, err := json.Marshal(encoder.Fields)
if err == nil {
line.AppendString(string(fieldsJSON))
}
}
}
line.AppendString("\n")
return line, nil
}
//func (e *CustomEncoder) EncodeEntry(entry zapcore.Entry, fields []zapcore.Field) (*buffer.Buffer, error) {
// line := e.pool.Get()
//
// // Agregar nivel (INFO:, WARN:, etc.)
// levelStr := entry.Level.CapitalString()
// line.AppendString(levelStr)
// line.AppendString(": ")
//
// // Agregar timestamp
// timestamp := entry.Time.Format("2006/01/02 15:04:05")
// line.AppendString(timestamp)
// line.AppendString(" ")
//
// // Agregar información del archivo/línea si está disponible
// if entry.Caller.Defined {
// // Obtener solo la parte relevante de la ruta (después del último "api-soap-facturacion")
// filePath := entry.Caller.File
// if idx := strings.LastIndex(filePath, "api-soap-facturacion"); idx != -1 {
// // Si encontramos la cadena, tomamos lo que viene después más "api-soap-facturacion"
// filePath = filePath[idx:]
// } else {
// // Si no encontramos la cadena, intentamos tomar solo el nombre del archivo
// if idx := strings.LastIndex(filePath, "/"); idx != -1 {
// filePath = filePath[idx+1:]
// } else if idx := strings.LastIndex(filePath, "\\"); idx != -1 {
// filePath = filePath[idx+1:]
// }
// }
//
// caller := fmt.Sprintf("%s:%d: ", filePath, entry.Caller.Line)
// line.AppendString(caller)
// }
// // Agregar nombre del servicio entre corchetes
// line.AppendString("[")
// line.AppendString(e.serviceName)
// line.AppendString("] ")
//
// // Agregar mensaje
// line.AppendString(entry.Message)
//
// // Procesar campos adicionales
// if len(fields) > 0 {
// // Usar encoder JSON para serializar los campos
// encoder := zapcore.NewMapObjectEncoder()
//
// // Agregar cada campo al encoder, excepto "service"
// for _, field := range fields {
// if field.Key != "service" {
// field.AddTo(encoder)
// }
// }
//
// // Solo mostramos los campos si hay alguno después de filtrar
// if len(encoder.Fields) > 0 {
// line.AppendString(" ")
// fieldsJSON, err := json.Marshal(encoder.Fields)
// if err == nil {
// line.AppendString(string(fieldsJSON))
// }
// }
// }
//
// line.AppendString("\n")
// return line, nil
//}
//func (e *CustomEncoder) EncodeEntry(entry zapcore.Entry, fields []zapcore.Field) (*buffer.Buffer, error) {
// line := e.pool.Get()
//
// // Agregar nivel (INFO:, WARN:, etc.)
// levelStr := entry.Level.CapitalString()
// line.AppendString(levelStr)
// line.AppendString(": ")
//
// // Agregar timestamp
// timestamp := entry.Time.Format("2006/01/02 15:04:05")
// line.AppendString(timestamp)
// line.AppendString(" ")
//
// // Agregar información del archivo/línea si está disponible
// if entry.Caller.Defined {
// caller := fmt.Sprintf("%s:%d: ", entry.Caller.File, entry.Caller.Line)
// line.AppendString(caller)
// }
//
// // Agregar nombre del servicio entre corchetes
// line.AppendString("[")
// line.AppendString(e.serviceName)
// line.AppendString("] ")
//
// // Agregar mensaje
// line.AppendString(entry.Message)
//
// // Procesar campos adicionales
// if len(fields) > 0 {
// // Usar encoder JSON para serializar los campos
// encoder := zapcore.NewMapObjectEncoder()
//
// // Agregar cada campo al encoder, excepto "service"
// for _, field := range fields {
// if field.Key != "service" {
// field.AddTo(encoder)
// }
// }
//
// // Solo mostramos los campos si hay alguno después de filtrar
// if len(encoder.Fields) > 0 {
// line.AppendString(" ")
// fieldsJSON, err := json.Marshal(encoder.Fields)
// if err == nil {
// line.AppendString(string(fieldsJSON))
// }
// }
// }
//
// line.AppendString("\n")
// return line, nil
//}
// Clone implementa la interfaz Encoder
func (e *CustomEncoder) Clone() zapcore.Encoder {
return &CustomEncoder{
Encoder: e.Encoder.Clone(),
pool: e.pool,
serviceName: e.serviceName,
}
}
// NewLogger crea una nueva instancia de Logger
func NewLogger(cfg *config.Config) Logger {
// Configurar nivel de log
var level zapcore.Level
switch cfg.App.LogLevel {
case "debug":
level = zapcore.DebugLevel
case "info":
level = zapcore.InfoLevel
case "warn":
level = zapcore.WarnLevel
case "error":
level = zapcore.ErrorLevel
default:
level = zapcore.InfoLevel
}
// Configuración básica para el encoder
encoderConfig := zapcore.EncoderConfig{
TimeKey: "time",
LevelKey: "level",
NameKey: "logger",
CallerKey: "caller",
FunctionKey: zapcore.OmitKey,
MessageKey: "msg",
StacktraceKey: "stacktrace",
LineEnding: zapcore.DefaultLineEnding,
EncodeLevel: zapcore.CapitalLevelEncoder,
EncodeTime: zapcore.ISO8601TimeEncoder,
EncodeDuration: zapcore.SecondsDurationEncoder,
EncodeCaller: zapcore.ShortCallerEncoder,
}
// Crear el encoder personalizado
customEncoder := NewCustomEncoder(encoderConfig, cfg.App.Name)
// Configurar el output
var core zapcore.Core
if cfg.App.LogFilePath != "" {
// Si se usa archivo con lumberjack
logWriter := &lumberjack.Logger{
Filename: cfg.App.LogFilePath,
MaxSize: cfg.App.LogMaxSize,
MaxBackups: cfg.App.LogMaxBackups,
MaxAge: cfg.App.LogMaxAge,
Compress: cfg.App.LogCompress,
}
core = zapcore.NewCore(
customEncoder,
zapcore.AddSync(logWriter),
level,
)
} else {
// Solo usar stdout
core = zapcore.NewCore(
customEncoder,
zapcore.AddSync(os.Stdout),
level,
)
}
// Habilitar el caller para mostrar información del archivo y línea
zapLog := zap.New(core, zap.AddCaller(), zap.AddCallerSkip(1))
// Ya no agregamos el nombre del servicio como campo, ya que lo incluimos en el formato
// zapLog = zapLog.With(zap.String("service", cfg.App.Name))
return &zapLogger{logger: zapLog}
}
// NewField crea un campo para agregar a los logs
func NewField(key string, value interface{}) Field {
return Field{Key: key, Value: value}
}
// convertFields convierte nuestros campos a campos de zap
func (l *zapLogger) convertFields(fields []Field) []zap.Field {
zapFields := make([]zap.Field, len(fields))
for i, field := range fields {
zapFields[i] = zap.Any(field.Key, field.Value)
}
return zapFields
}
// Debug registra un mensaje con nivel Debug
func (l *zapLogger) Debug(msg string, fields ...Field) {
l.logger.Debug(msg, l.convertFields(fields)...)
}
// Info registra un mensaje con nivel Info
func (l *zapLogger) Info(msg string, fields ...Field) {
l.logger.Info(msg, l.convertFields(fields)...)
}
// Warn registra un mensaje con nivel Warn
func (l *zapLogger) Warn(msg string, fields ...Field) {
l.logger.Warn(msg, l.convertFields(fields)...)
}
// Error registra un mensaje con nivel Error
func (l *zapLogger) Error(msg string, fields ...Field) {
l.logger.Error(msg, l.convertFields(fields)...)
}
// Fatal registra un mensaje con nivel Fatal
func (l *zapLogger) Fatal(msg string, fields ...Field) {
l.logger.Fatal(msg, l.convertFields(fields)...)
}
// With retorna un nuevo logger con campos adicionales
func (l *zapLogger) With(fields ...Field) Logger {
return &zapLogger{
logger: l.logger.With(l.convertFields(fields)...),
}
}
//package logger
//
//import (
// "api-soap-facturacion/internal/config"
// "encoding/json"
// "fmt"
// "go.uber.org/zap"
// "go.uber.org/zap/zapcore"
// lumberjack "gopkg.in/natefinch/lumberjack.v2" // Usa este alias
// "os"
//)
//
//// Logger es una interfaz que define los métodos necesarios para el logging
//type Logger interface {
// Debug(msg string, fields ...Field)
// Info(msg string, fields ...Field)
// Warn(msg string, fields ...Field)
// Error(msg string, fields ...Field)
// Fatal(msg string, fields ...Field)
// With(fields ...Field) Logger
//}
//
//// Field es un campo para agregar contexto a los logs
//type Field struct {
// Key string
// Value interface{}
//}
//
//// zapLogger implementa la interfaz Logger usando zap
//type zapLogger struct {
// logger *zap.Logger
//}
//
//// CustomEncoder implementa la interfaz zapcore.Encoder
//type CustomEncoder struct {
// zapcore.Encoder
// pool buffer.Pool
// serviceName string
//}
//
//// NewCustomEncoder crea un nuevo encoder personalizado
//func NewCustomEncoder(cfg zapcore.EncoderConfig, serviceName string) zapcore.Encoder {
// return &CustomEncoder{
// Encoder: zapcore.NewJSONEncoder(cfg),
// pool: buffer.NewPool(),
// serviceName: serviceName,
// }
//}
//
//// EncodeEntry implementa el formato personalizado
//func (e *CustomEncoder) EncodeEntry(entry zapcore.Entry, fields []zapcore.Field) (*buffer.Buffer, error) {
// line := e.pool.Get()
//
// // Agregar nivel (INFO:, WARN:, etc.)
// levelStr := entry.Level.CapitalString()
// line.AppendString(levelStr)
// line.AppendString(": ")
//
// // Agregar timestamp
// timestamp := entry.Time.Format("2006/01/02 15:04:05")
// line.AppendString(timestamp)
// line.AppendString(" ")
//
// // Agregar información del archivo/línea si está disponible
// if entry.Caller.Defined {
// caller := fmt.Sprintf("%s:%d: ", entry.Caller.File, entry.Caller.Line)
// line.AppendString(caller)
// }
//
// // Agregar nombre del servicio entre corchetes
// line.AppendString("[")
// line.AppendString(e.serviceName)
// line.AppendString("] ")
//
// // Agregar mensaje
// line.AppendString(entry.Message)
//
// // Procesar campos adicionales
// if len(fields) > 0 {
// line.AppendString(" ")
// fieldsMap := make(map[string]interface{})
// for _, field := range fields {
// if field.Key != "service" {
// field.AddTo(fieldsMap)
// }
// }
//
// if len(fieldsMap) > 0 {
// fieldJSON, err := json.Marshal(fieldsMap)
// if err == nil {
// line.AppendString(string(fieldJSON))
// }
// }
// }
//
// line.AppendString("\n")
// return line, nil
//}
//
//// Clone implementa la interfaz Encoder
//func (e *CustomEncoder) Clone() zapcore.Encoder {
// return &CustomEncoder{
// Encoder: e.Encoder.Clone(),
// pool: e.pool,
// serviceName: e.serviceName,
// }
//}
//
//// NewLogger crea una nueva instancia de Logger tipo json
////func NewLogger(cfg *config.Config) Logger {
//// // Configurar nivel de log
//// var level zapcore.Level
//// switch cfg.App.LogLevel {
//// case "debug":
//// level = zapcore.DebugLevel
//// case "info":
//// level = zapcore.InfoLevel
//// case "warn":
//// level = zapcore.WarnLevel
//// case "error":
//// level = zapcore.ErrorLevel
//// default:
//// level = zapcore.InfoLevel
//// }
////
//// // Configurar encoderConfig
//// encoderConfig := zapcore.EncoderConfig{
//// TimeKey: "time",
//// LevelKey: "level",
//// NameKey: "logger",
//// CallerKey: "caller",
//// MessageKey: "msg",
//// StacktraceKey: "stacktrace",
//// LineEnding: zapcore.DefaultLineEnding,
//// EncodeLevel: zapcore.LowercaseLevelEncoder,
//// EncodeTime: zapcore.ISO8601TimeEncoder,
//// EncodeDuration: zapcore.SecondsDurationEncoder,
//// EncodeCaller: zapcore.ShortCallerEncoder,
//// }
////
//// // Crear escritores
//// var cores []zapcore.Core
////
//// // Siempre escribir a stdout
//// cores = append(cores, zapcore.NewCore(
//// zapcore.NewJSONEncoder(encoderConfig),
//// zapcore.AddSync(os.Stdout),
//// level,
//// ))
////
//// // Si se especificó una ruta de archivo de logs, configurar rotación
//// if cfg.App.LogFilePath != "" {
//// logWriter := &lumberjack.Logger{
//// Filename: cfg.App.LogFilePath,
//// MaxSize: cfg.App.LogMaxSize,
//// MaxBackups: cfg.App.LogMaxBackups,
//// MaxAge: cfg.App.LogMaxAge,
//// Compress: cfg.App.LogCompress,
//// }
////
//// cores = append(cores, zapcore.NewCore(
//// zapcore.NewJSONEncoder(encoderConfig),
//// zapcore.AddSync(logWriter),
//// level,
//// ))
//// }
////
//// // Combinar cores
//// core := zapcore.NewTee(cores...)
////
//// // Crear logger
//// zapLog := zap.New(core)
//// zapLog = zapLog.With(zap.String("service", cfg.App.Name))
////
//// return &zapLogger{logger: zapLog}
////}
//
//// fomato de texto mas flexible
//// NewLogger crea una nueva instancia de Logger
////func NewLogger(cfg *config.Config) Logger {
//// // Configurar nivel de log
//// var level zapcore.Level
//// switch cfg.App.LogLevel {
//// case "debug":
//// level = zapcore.DebugLevel
//// case "info":
//// level = zapcore.InfoLevel
//// case "warn":
//// level = zapcore.WarnLevel
//// case "error":
//// level = zapcore.ErrorLevel
//// default:
//// level = zapcore.InfoLevel
//// }
////
//// // Configurar encoderConfig para un formato más legible
//// encoderConfig := zapcore.EncoderConfig{
//// TimeKey: "time",
//// LevelKey: "level",
//// NameKey: "logger",
//// CallerKey: "caller",
//// FunctionKey: zapcore.OmitKey,
//// MessageKey: "msg",
//// StacktraceKey: "stacktrace",
//// LineEnding: zapcore.DefaultLineEnding,
//// EncodeLevel: zapcore.CapitalLevelEncoder, // INFO en lugar de info
//// EncodeTime: zapcore.TimeEncoderOfLayout("2006/01/02 15:04:05"), // Formato como 2025/05/15 00:13:07
//// EncodeDuration: zapcore.SecondsDurationEncoder,
//// EncodeCaller: zapcore.ShortCallerEncoder,
//// }
////
//// // Crear un formato personalizado
//// consoleEncoder := zapcore.NewConsoleEncoder(encoderConfig)
////
//// // Configurar el output final
//// var core zapcore.Core
//// if cfg.App.LogFilePath != "" && cfg.App.UseFileLogger {
//// // Si se usa archivo con lumberjack
//// logWriter := &lumberjack.Logger{
//// Filename: cfg.App.LogFilePath,
//// MaxSize: cfg.App.LogMaxSize,
//// MaxBackups: cfg.App.LogMaxBackups,
//// MaxAge: cfg.App.LogMaxAge,
//// Compress: cfg.App.LogCompress,
//// }
////
//// core = zapcore.NewCore(
//// consoleEncoder,
//// zapcore.AddSync(logWriter),
//// level,
//// )
//// } else {
//// // Solo usar stdout
//// core = zapcore.NewCore(
//// consoleEncoder,
//// zapcore.AddSync(os.Stdout),
//// level,
//// )
//// }
////
//// // Habilitar el caller para mostrar información del archivo y línea
//// zapLog := zap.New(core, zap.AddCaller(), zap.AddCallerSkip(1))
////
//// // Agregar nombre del servicio como prefijo constante
//// zapLog = zapLog.With(zap.String("service", cfg.App.Name))
////
//// return &zapLogger{logger: zapLog}
////}
//
//// NewLogger crea una nueva instancia de Logger
//func NewLogger(cfg *config.Config) Logger {
// // Configurar nivel de log
// var level zapcore.Level
// switch cfg.App.LogLevel {
// case "debug":
// level = zapcore.DebugLevel
// case "info":
// level = zapcore.InfoLevel
// case "warn":
// level = zapcore.WarnLevel
// case "error":
// level = zapcore.ErrorLevel
// default:
// level = zapcore.InfoLevel
// }
//
// // Configuración básica para el encoder
// encoderConfig := zapcore.EncoderConfig{
// TimeKey: "time",
// LevelKey: "level",
// NameKey: "logger",
// CallerKey: "caller",
// FunctionKey: zapcore.OmitKey,
// MessageKey: "msg",
// StacktraceKey: "stacktrace",
// LineEnding: zapcore.DefaultLineEnding,
// EncodeLevel: zapcore.CapitalLevelEncoder,
// EncodeTime: zapcore.ISO8601TimeEncoder,
// EncodeDuration: zapcore.SecondsDurationEncoder,
// EncodeCaller: zapcore.ShortCallerEncoder,
// }
//
// // Crear el encoder personalizado
// customEncoder := NewCustomEncoder(encoderConfig, cfg.App.Name)
//
// // Configurar el output
// var core zapcore.Core
// if cfg.App.LogFilePath != "" {
// // Si se usa archivo con lumberjack
// logWriter := &lumberjack.Logger{
// Filename: cfg.App.LogFilePath,
// MaxSize: cfg.App.LogMaxSize,
// MaxBackups: cfg.App.LogMaxBackups,
// MaxAge: cfg.App.LogMaxAge,
// Compress: cfg.App.LogCompress,
// }
//
// core = zapcore.NewCore(
// customEncoder,
// zapcore.AddSync(logWriter),
// level,
// )
// } else {
// // Solo usar stdout
// core = zapcore.NewCore(
// customEncoder,
// zapcore.AddSync(os.Stdout),
// level,
// )
// }
//
// // Habilitar el caller para mostrar información del archivo y línea
// zapLog := zap.New(core, zap.AddCaller())
//
// // Ya no agregamos el nombre del servicio como campo, ya que lo incluimos en el formato
// // zapLog = zapLog.With(zap.String("service", cfg.App.Name))
//
// return &zapLogger{logger: zapLog}
//}
//
//// NewField crea un campo para agregar a los logs
//func NewField(key string, value interface{}) Field {
// return Field{Key: key, Value: value}
//}
//
//// convertFields convierte nuestros campos a campos de zap
//func (l *zapLogger) convertFields(fields []Field) []zap.Field {
// zapFields := make([]zap.Field, len(fields))
// for i, field := range fields {
// zapFields[i] = zap.Any(field.Key, field.Value)
// }
// return zapFields
//}
//
//// Debug registra un mensaje con nivel Debug
//func (l *zapLogger) Debug(msg string, fields ...Field) {
// l.logger.Debug(msg, l.convertFields(fields)...)
//}
//
//// Info registra un mensaje con nivel Info
//func (l *zapLogger) Info(msg string, fields ...Field) {
// l.logger.Info(msg, l.convertFields(fields)...)
//}
//
//// Warn registra un mensaje con nivel Warn
//func (l *zapLogger) Warn(msg string, fields ...Field) {
// l.logger.Warn(msg, l.convertFields(fields)...)
//}
//
//// Error registra un mensaje con nivel Error
//func (l *zapLogger) Error(msg string, fields ...Field) {
// l.logger.Error(msg, l.convertFields(fields)...)
//}
//
//// Fatal registra un mensaje con nivel Fatal
//func (l *zapLogger) Fatal(msg string, fields ...Field) {
// l.logger.Fatal(msg, l.convertFields(fields)...)
//}
//
//// With retorna un nuevo logger con campos adicionales
////
//// func (l *zapLogger) With(fields ...Field) Logger {
//// return &zapLogger{
//// logger: l.logger.With(l.convertFields(fields)...),
//// }
//// }
//func (l *zapLogger) With(fields ...Field) Logger {
// return &zapLogger{
// logger: l.logger.With(l.convertFields(fields)...),
// }
//}
//
////package logger
////
////import (
//// "api-soap-facturacion/internal/config"
//// "go.uber.org/zap"
//// "go.uber.org/zap/zapcore"
////)
////
////// Logger es una interfaz que define los métodos necesarios para el logging
////type Logger interface {
//// Debug(msg string, fields ...Field)
//// Info(msg string, fields ...Field)
//// Warn(msg string, fields ...Field)
//// Error(msg string, fields ...Field)
//// Fatal(msg string, fields ...Field)
//// With(fields ...Field) Logger
////}
////
////// Field es un campo para agregar contexto a los logs
////type Field struct {
//// Key string
//// Value interface{}
////}
////
////// zapLogger implementa la interfaz Logger usando zap
////type zapLogger struct {
//// logger *zap.Logger
////}
////
////// NewLogger crea una nueva instancia de Logger
////func NewLogger(cfg *config.Config) Logger {
//// // Configurar nivel de log
//// var level zapcore.Level
//// switch cfg.App.LogLevel {
//// case "debug":
//// level = zapcore.DebugLevel
//// case "info":
//// level = zapcore.InfoLevel
//// case "warn":
//// level = zapcore.WarnLevel
//// case "error":
//// level = zapcore.ErrorLevel
//// default:
//// level = zapcore.InfoLevel
//// }
////
//// // Configuración de zap
//// zapConfig := zap.Config{
//// Level: zap.NewAtomicLevelAt(level),
//// Development: cfg.App.Environment == "development",
//// Encoding: "json",
//// OutputPaths: []string{"stdout"},
//// ErrorOutputPaths: []string{"stderr"},
//// EncoderConfig: zapcore.EncoderConfig{
//// TimeKey: "time",
//// LevelKey: "level",
//// NameKey: "logger",
//// CallerKey: "caller",
//// MessageKey: "msg",
//// StacktraceKey: "stacktrace",
//// LineEnding: zapcore.DefaultLineEnding,
//// EncodeLevel: zapcore.LowercaseLevelEncoder,
//// EncodeTime: zapcore.ISO8601TimeEncoder,
//// EncodeDuration: zapcore.SecondsDurationEncoder,
//// EncodeCaller: zapcore.ShortCallerEncoder,
//// },
//// }
////
//// // Agregar service name a todos los logs
//// logger, _ := zapConfig.Build()
//// logger = logger.With(zap.String("service", cfg.App.Name))
////
//// return &zapLogger{logger: logger}
////}
////
////// NewField crea un campo para agregar a los logs
////func NewField(key string, value interface{}) Field {
//// return Field{Key: key, Value: value}
////}
////
////// convertFields convierte nuestros campos a campos de zap
////func (l *zapLogger) convertFields(fields []Field) []zap.Field {
//// zapFields := make([]zap.Field, len(fields))
//// for i, field := range fields {
//// zapFields[i] = zap.Any(field.Key, field.Value)
//// }
//// return zapFields
////}
////
////// Debug registra un mensaje con nivel Debug
////func (l *zapLogger) Debug(msg string, fields ...Field) {
//// l.logger.Debug(msg, l.convertFields(fields)...)
////}
////
////// Info registra un mensaje con nivel Info
////func (l *zapLogger) Info(msg string, fields ...Field) {
//// l.logger.Info(msg, l.convertFields(fields)...)
////}
////
////// Warn registra un mensaje con nivel Warn
////func (l *zapLogger) Warn(msg string, fields ...Field) {
//// l.logger.Warn(msg, l.convertFields(fields)...)
////}
////
////// Error registra un mensaje con nivel Error
////func (l *zapLogger) Error(msg string, fields ...Field) {
//// l.logger.Error(msg, l.convertFields(fields)...)
////}
////
////// Fatal registra un mensaje con nivel Fatal
////func (l *zapLogger) Fatal(msg string, fields ...Field) {
//// l.logger.Fatal(msg, l.convertFields(fields)...)
////}
////
////// With retorna un nuevo logger con campos adicionales
////func (l *zapLogger) With(fields ...Field) Logger {
//// return &zapLogger{
//// logger: l.logger.With(l.convertFields(fields)...),
//// }
////}

View File

@ -0,0 +1,51 @@
package facturacionOperaciones
import "encoding/xml"
// SolicitudRegistroPuntoVenta representa los datos necesarios para registrar un punto de venta
type SolicitudRegistroPuntoVenta struct {
CodigoAmbiente string `xml:"codigoAmbiente"`
CodigoModalidad string `xml:"codigoModalidad"`
CodigoSistema string `xml:"codigoSistema"`
CodigoSucursal string `xml:"codigoSucursal"`
CodigoTipoPuntoVenta string `xml:"codigoTipoPuntoVenta"`
CUIS string `xml:"cuis"`
Descripcion string `xml:"descripcion"`
NIT string `xml:"nit"`
NombrePuntoVenta string `xml:"nombrePuntoVenta"`
}
// RegistroPuntoVentaRequest es la estructura de petición SOAP
type RegistroPuntoVentaRequest struct {
XMLName xml.Name `xml:"soapenv:Envelope"`
SoapEnv string `xml:"xmlns:soapenv,attr"`
Siat string `xml:"xmlns:siat,attr"`
Body struct {
RegistroPuntoVenta struct {
Solicitud SolicitudRegistroPuntoVenta
} `xml:"siat:registroPuntoVenta"`
} `xml:"soapenv:Body"`
}
// Mensaje representa un mensaje de la respuesta SOAP
type Mensaje struct {
Codigo string `xml:"codigo"`
Descripcion string `xml:"descripcion"`
}
// RespuestaRegistroPuntoVenta representa la respuesta del servicio SOAP
type RespuestaRegistroPuntoVenta struct {
CodigoPuntoVenta string `xml:"codigoPuntoVenta"`
MensajesList []Mensaje `xml:"mensajesList"`
Transaccion string `xml:"transaccion"`
}
// RegistroPuntoVentaResponse es la estructura de respuesta SOAP
type RegistroPuntoVentaResponse struct {
XMLName xml.Name `xml:"Envelope"`
Body struct {
RegistroPuntoVentaResponse struct {
Respuesta RespuestaRegistroPuntoVenta `xml:"RespuestaRegistroPuntoVenta"`
} `xml:"registroPuntoVentaResponse"`
} `xml:"Body"`
}

View File

@ -0,0 +1,93 @@
package api1
import (
"context"
"time"
"api-soap-facturacion/internal/config"
"api-soap-facturacion/internal/logger"
"api-soap-facturacion/pkg/errors"
"api-soap-facturacion/pkg/soap"
"github.com/google/uuid"
)
// Client es la interfaz para el cliente de API1
type Client interface {
GetData(ctx context.Context, request GetDataRequest) (*GetDataResponse, error)
// Agregar más métodos según sea necesario
}
// client implementa la interfaz Client
type client struct {
soapClient soap.Client
logger logger.Logger
}
// NewClient crea un nuevo cliente para API1
func NewClient(config config.APIConfig, log logger.Logger) Client {
// Configurar cliente SOAP
options := soap.ClientOptions{
Endpoint: config.Endpoint,
Username: config.Username,
Password: config.Password,
Timeout: config.Timeout * time.Second,
Headers: map[string]string{
"User-Agent": "soap-api-client/1.0",
},
}
return &client{
soapClient: soap.NewClient(options),
logger: log,
}
}
// GetData realiza una solicitud GetData a API1
func (c *client) GetData(ctx context.Context, request GetDataRequest) (*GetDataResponse, error) {
// Generar ID de solicitud si no existe
var requestID string
if id, ok := ctx.Value(soap.RequestIDKey{}).(string); ok {
requestID = id
} else {
requestID = uuid.New().String()
ctx = context.WithValue(ctx, soap.RequestIDKey{}, requestID)
}
// Crear request
soapRequest := GetDataSoapRequest{
Request: request,
}
// Registrar solicitud
c.logger.Debug("Enviando solicitud GetData a API1",
logger.NewField("request_id", requestID),
logger.NewField("parameters", request),
)
// Respuesta
response := &GetDataSoapResponse{}
// Realizar llamada SOAP
startTime := time.Now()
err := c.soapClient.Call(ctx, "GetData", soapRequest, response)
duration := time.Since(startTime)
// Manejar error
if err != nil {
c.logger.Error("Error al realizar solicitud GetData a API1",
logger.NewField("request_id", requestID),
logger.NewField("error", err),
logger.NewField("duration_ms", duration.Milliseconds()),
)
return nil, errors.Wrap(err, "error en solicitud GetData a API1")
}
// Registrar respuesta exitosa
c.logger.Debug("Respuesta recibida de API1 para GetData",
logger.NewField("request_id", requestID),
logger.NewField("duration_ms", duration.Milliseconds()),
logger.NewField("status", response.Response.Status),
)
return &response.Response, nil
}

View File

@ -0,0 +1,44 @@
package api1
import (
"encoding/xml"
)
// GetDataRequest es la solicitud para la operación GetData
type GetDataRequest struct {
XMLName xml.Name `xml:"GetDataRequest"`
Namespace string `xml:"xmlns,attr,omitempty"`
ID string `xml:"Id,omitempty"`
Type string `xml:"Type,omitempty"`
Date string `xml:"Date,omitempty"`
// Otros campos según sea necesario
}
// GetDataResponse es la respuesta para la operación GetData
type GetDataResponse struct {
XMLName xml.Name `xml:"GetDataResponse"`
Status string `xml:"Status,omitempty"`
ResultCode string `xml:"ResultCode,omitempty"`
Description string `xml:"Description,omitempty"`
Data []Data `xml:"Data>Item,omitempty"`
}
// Data representa un elemento de datos en la respuesta
type Data struct {
ID string `xml:"Id,omitempty"`
Name string `xml:"Name,omitempty"`
Value string `xml:"Value,omitempty"`
// Otros campos según sea necesario
}
// GetDataSoapRequest envuelve la solicitud para SOAP
type GetDataSoapRequest struct {
XMLName xml.Name `xml:"api1:GetData"`
Request GetDataRequest `xml:",omitempty"`
}
// GetDataSoapResponse envuelve la respuesta para SOAP
type GetDataSoapResponse struct {
XMLName xml.Name `xml:"GetDataResponse"`
Response GetDataResponse `xml:",omitempty"`
}

View File

@ -0,0 +1,96 @@
package api2
import (
"context"
"time"
"api-soap-facturacion/internal/config"
"api-soap-facturacion/internal/logger"
"api-soap-facturacion/pkg/errors"
"api-soap-facturacion/pkg/soap"
"github.com/google/uuid"
)
// Client es la interfaz para el cliente de API2
type Client interface {
ProcessTransaction(ctx context.Context, request TransactionRequest) (*TransactionResponse, error)
// Agregar más métodos según sea necesario
}
// client implementa la interfaz Client
type client struct {
soapClient soap.Client
logger logger.Logger
}
// crea un nuevo cliente para API2
func NewClient(config config.APIConfig, log logger.Logger) Client {
// Configurar cliente SOAP
options := soap.ClientOptions{
Endpoint: config.Endpoint,
Username: config.Username,
Password: config.Password,
Timeout: config.Timeout * time.Second,
Headers: map[string]string{
"User-Agent": "soap-api-client/1.0",
},
}
return &client{
soapClient: soap.NewClient(options),
logger: log,
}
}
// ProcessTransaction realiza una solicitud de procesamiento de transacción a API2
func (c *client) ProcessTransaction(ctx context.Context, request TransactionRequest) (*TransactionResponse, error) {
// Generar ID de solicitud si no existe
var requestID string
if id, ok := ctx.Value(soap.RequestIDKey{}).(string); ok {
requestID = id
} else {
requestID = uuid.New().String()
ctx = context.WithValue(ctx, soap.RequestIDKey{}, requestID)
}
// Crear request
soapRequest := TransactionSoapRequest{
Request: request,
}
// Registrar solicitud
c.logger.Debug("Enviando solicitud de transacción a API2",
logger.NewField("request_id", requestID),
logger.NewField("transaction_id", request.TransactionID),
logger.NewField("amount", request.Amount),
)
// Respuesta
response := &TransactionSoapResponse{}
// Realizar llamada SOAP
startTime := time.Now()
err := c.soapClient.Call(ctx, "ProcessTransaction", soapRequest, response)
duration := time.Since(startTime)
// Manejar error
if err != nil {
c.logger.Error("Error al realizar solicitud de transacción a API2",
logger.NewField("request_id", requestID),
logger.NewField("transaction_id", request.TransactionID),
logger.NewField("error", err),
logger.NewField("duration_ms", duration.Milliseconds()),
)
return nil, errors.Wrap(err, "error en solicitud de transacción a API2")
}
// Registrar respuesta exitosa
c.logger.Debug("Respuesta recibida de API2 para transacción",
logger.NewField("request_id", requestID),
logger.NewField("transaction_id", request.TransactionID),
logger.NewField("duration_ms", duration.Milliseconds()),
logger.NewField("result_code", response.Response.ResultCode),
)
return &response.Response, nil
}

View File

@ -0,0 +1,40 @@
package api2
import (
"encoding/xml"
)
// TransactionRequest es la solicitud para procesar una transacción
type TransactionRequest struct {
XMLName xml.Name `xml:"TransactionRequest"`
Namespace string `xml:"xmlns,attr,omitempty"`
TransactionID string `xml:"TransactionId,omitempty"`
AccountID string `xml:"AccountId,omitempty"`
Amount float64 `xml:"Amount,omitempty"`
Currency string `xml:"Currency,omitempty"`
Description string `xml:"Description,omitempty"`
// Otros campos según sea necesario
}
// TransactionResponse es la respuesta para una solicitud de transacción
type TransactionResponse struct {
XMLName xml.Name `xml:"TransactionResponse"`
ResultCode string `xml:"ResultCode,omitempty"`
ResultMessage string `xml:"ResultMessage,omitempty"`
TransactionID string `xml:"TransactionId,omitempty"`
ConfirmationCode string `xml:"ConfirmationCode,omitempty"`
Timestamp string `xml:"Timestamp,omitempty"`
// Otros campos según sea necesario
}
// TransactionSoapRequest envuelve la solicitud para SOAP
type TransactionSoapRequest struct {
XMLName xml.Name `xml:"api2:ProcessTransaction"`
Request TransactionRequest `xml:",omitempty"`
}
// TransactionSoapResponse envuelve la respuesta para SOAP
type TransactionSoapResponse struct {
XMLName xml.Name `xml:"ProcessTransactionResponse"`
Response TransactionResponse `xml:",omitempty"`
}

View File

@ -0,0 +1,50 @@
package soap
import (
"api-soap-facturacion/internal/config"
"api-soap-facturacion/internal/logger"
"api-soap-facturacion/internal/soap/api"
"api-soap-facturacion/internal/soap/api2"
factOp "api-soap-facturacion/internal/soap/facturacionOperaciones"
"time"
)
// Clients contiene todos los clientes SOAP disponibles
type Clients struct {
API1 api1.Client
API2 api2.Client
API_FACTURACION_OPERACION factOp.Client
}
// crea nuevos clientes SOAP para todas las APIs configuradas
func NewSoapClients(cfg *config.Config, log logger.Logger) *Clients {
soapLogger := log.With(logger.NewField("component", "soap_client"))
// Crear cliente para API1
api1Client := api1.NewClient(
cfg.Soap.APIs["api1"],
soapLogger.With(logger.NewField("api", "api1")),
)
// Crear cliente para API2
api2Client := api2.NewClient(
cfg.Soap.APIs["api2"],
soapLogger.With(logger.NewField("api", "api2")),
)
// Configurar opciones del cliente
options := &factOp.ClientOptions{
Timeout: 45 * time.Second,
// También puedes configurar un cliente HTTP personalizado si necesitas
// manejar proxies, certificados personalizados, etc.
}
// Registro Codigo Punto de Venta
apifactOperacion := factOp.NewClient(options, cfg.Soap.APIs["facturacionOperaciones"], soapLogger.With(logger.NewField("api", "registroPuntoVenta")))
return &Clients{
API1: api1Client,
API2: api2Client,
API_FACTURACION_OPERACION: *apifactOperacion,
}
}

View File

@ -0,0 +1,23 @@
package facturacionOperaciones
import (
"api-soap-facturacion/internal/logger"
"api-soap-facturacion/pkg/soap"
"net/http"
"time"
)
// ClientOptions contiene opciones para personalizar el cliente SOAP
type ClientOptions struct {
Timeout time.Duration
HTTPClient *http.Client
EndpointURL string
}
// Client es el cliente SOAP para comunicarse con SIAT
type Client struct {
httpClient *http.Client
endpointURL string
soapClient soap.Client
logger logger.Logger
}

View File

@ -0,0 +1,119 @@
package facturacionOperaciones
import (
"api-soap-facturacion/internal/config"
"api-soap-facturacion/internal/logger"
mfo "api-soap-facturacion/internal/models/facturacionOperaciones"
"api-soap-facturacion/pkg/soap"
"bytes"
"context"
"encoding/xml"
"fmt"
"io"
"net/http"
"time"
)
// Constantes de configuración
const (
DefaultTimeout = 30 * time.Second
ContentType = "text/xml;charset=UTF-8"
SoapAction = "https://siat.impuestos.gob.bo/registroPuntoVenta"
SiatEndpoint = "https://pilotosiatservicios.impuestos.gob.bo/v2/FacturacionOperaciones"
)
// NewClient crea una nueva instancia del cliente SOAP
func NewClient(options *ClientOptions, config config.APIConfig, log logger.Logger) *Client {
// Configurar cliente SOAP
factOperaciones := soap.ClientOptions{
Endpoint: config.Endpoint,
Username: config.Username,
Password: config.Password,
Timeout: config.Timeout * time.Second,
Headers: map[string]string{
"User-Agent": "soap-api-client/1.0",
},
}
client := &Client{
endpointURL: SiatEndpoint,
soapClient: soap.NewClient(factOperaciones),
logger: log,
}
if options != nil {
if options.EndpointURL != "" {
client.endpointURL = options.EndpointURL
}
if options.HTTPClient != nil {
client.httpClient = options.HTTPClient
}
}
if client.httpClient == nil {
timeout := DefaultTimeout
if options != nil && options.Timeout > 0 {
timeout = options.Timeout
}
client.httpClient = &http.Client{
Timeout: timeout,
}
}
return client
}
// envía una solicitud para registrar un punto de venta en SIAT
func (c *Client) RegistrarPuntoVenta(ctx context.Context, solicitud mfo.SolicitudRegistroPuntoVenta) (*mfo.RespuestaRegistroPuntoVenta, error) {
// Preparar la petición SOAP
request := mfo.RegistroPuntoVentaRequest{
SoapEnv: "http://schemas.xmlsoap.org/soap/envelope/",
Siat: "https://siat.impuestos.gob.bo/",
}
request.Body.RegistroPuntoVenta.Solicitud = solicitud
// Convertir la petición a XML
requestBody, err := xml.MarshalIndent(request, "", " ")
if err != nil {
return nil, fmt.Errorf("error al convertir la solicitud a XML: %w", err)
}
// Crear la petición HTTP con el contexto
req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.endpointURL, bytes.NewReader(requestBody))
if err != nil {
return nil, fmt.Errorf("error al crear la petición HTTP: %w", err)
}
// Configurar encabezados
req.Header.Set("Content-Type", ContentType)
req.Header.Set("SOAPAction", SoapAction)
// Realizar la petición HTTP
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("error al realizar la petición HTTP: %w", err)
}
defer resp.Body.Close()
// Leer el cuerpo de la respuesta
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("error al leer la respuesta: %w", err)
}
// Verificar el código de estado HTTP
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("error en la respuesta HTTP: %s, cuerpo: %s", resp.Status, respBody)
}
// Parsear la respuesta
var soapResponse mfo.RegistroPuntoVentaResponse
if err := xml.Unmarshal(respBody, &soapResponse); err != nil {
return nil, fmt.Errorf("error al parsear la respuesta XML: %w, cuerpo: %s", err, respBody)
}
return &soapResponse.Body.RegistroPuntoVentaResponse.Respuesta, nil
}

View File

@ -0,0 +1,59 @@
INFO: 2025/05/15 15:07:35 cmd/api/main.go:29: [api-facturacion-myapps] Iniciando aplicación {"app":"api-facturacion-myapps"}
INFO: 2025/05/15 15:07:35 cmd/api/main.go:43: [api-facturacion-myapps] Conexión a la base de datos establecida
INFO: 2025/05/15 15:07:35 cmd/api/main.go:50: [api-facturacion-myapps] Parámetros cargados desde la base de datos {"count":2}
INFO: 2025/05/15 15:07:35 cmd/api/main.go:72: [api-facturacion-myapps] Servidor iniciado {"port":9999}
INFO: 2025/05/15 15:12:58 internal/api/router.go:81: [api-facturacion-myapps] Solicitud iniciada
DEBUG: 2025/05/15 15:12:58 internal/soap/api/client.go:62: [api-facturacion-myapps] Enviando solicitud GetData a API1 {"parameters":{"XMLName":{"Space":"","Local":""},"Namespace":"http://api1.example.com/soap","ID":"12345","Type":"CUSTOMER","Date":"2023-12-01"},"request_id":"req-18515d73-aa22-4f2e-a71e-fe5e4233b340"}
ERROR: 2025/05/15 15:12:58 internal/soap/api/client.go:77: [api-facturacion-myapps] Error al realizar solicitud GetData a API1 {"duration_ms":9,"error":"error sending HTTP request: Post \"https://api1.example.com/soap\": proxyconnect tcp: dial tcp [::1]:80: connectex: No se puede establecer una conexión ya que el equipo de destino denegó expresamente dicha conexión.","errorVerbose":"Post \"https://api1.example.com/soap\": proxyconnect tcp: dial tcp [::1]:80: connectex: No se puede establecer una conexión ya que el equipo de destino denegó expresamente dicha conexión.\nerror sending HTTP request\napi-soap-facturacion/pkg/soap.(*client).Call\n\tC:/Users/USUARIO/Documents/00000-MYAPPS/00-SISTEMA_FACTURACION/000-FACTURACION/api-soap-facturacion/pkg/soap/soap.go:146\napi-soap-facturacion/internal/soap/api.(*client).GetData\n\tC:/Users/USUARIO/Documents/00000-MYAPPS/00-SISTEMA_FACTURACION/000-FACTURACION/api-soap-facturacion/internal/soap/api/client.go:72\napi-soap-facturacion/internal/api.(*Handler).API1GetData\n\tC:/Users/USUARIO/Documents/00000-MYAPPS/00-SISTEMA_FACTURACION/000-FACTURACION/api-soap-facturacion/internal/api/handler.go:97\ngithub.com/gin-gonic/gin.(*Context).Next\n\tC:/Users/USUARIO/go/pkg/mod/github.com/gin-gonic/gin@v1.10.0/context.go:185\napi-soap-facturacion/internal/api.NewRouter.CORSMiddleware.func3\n\tC:/Users/USUARIO/Documents/00000-MYAPPS/00-SISTEMA_FACTURACION/000-FACTURACION/api-soap-facturacion/internal/api/router.go:122\ngithub.com/gin-gonic/gin.(*Context).Next\n\tC:/Users/USUARIO/go/pkg/mod/github.com/gin-gonic/gin@v1.10.0/context.go:185\napi-soap-facturacion/internal/api.NewRouter.RequestIDMiddleware.func2\n\tC:/Users/USUARIO/Documents/00000-MYAPPS/00-SISTEMA_FACTURACION/000-FACTURACION/api-soap-facturacion/internal/api/router.go:106\ngithub.com/gin-gonic/gin.(*Context).Next\n\tC:/Users/USUARIO/go/pkg/mod/github.com/gin-gonic/gin@v1.10.0/context.go:185\napi-soap-facturacion/internal/api.NewRouter.LoggerMiddleware.func1\n\tC:/Users/USUARIO/Documents/00000-MYAPPS/00-SISTEMA_FACTURACION/000-FACTURACION/api-soap-facturacion/internal/api/router.go:84\ngithub.com/gin-gonic/gin.(*Context).Next\n\tC:/Users/USUARIO/go/pkg/mod/github.com/gin-gonic/gin@v1.10.0/context.go:185\ngithub.com/gin-gonic/gin.CustomRecoveryWithWriter.func1\n\tC:/Users/USUARIO/go/pkg/mod/github.com/gin-gonic/gin@v1.10.0/recovery.go:102\ngithub.com/gin-gonic/gin.(*Context).Next\n\tC:/Users/USUARIO/go/pkg/mod/github.com/gin-gonic/gin@v1.10.0/context.go:185\ngithub.com/gin-gonic/gin.(*Engine).handleHTTPRequest\n\tC:/Users/USUARIO/go/pkg/mod/github.com/gin-gonic/gin@v1.10.0/gin.go:633\ngithub.com/gin-gonic/gin.(*Engine).ServeHTTP\n\tC:/Users/USUARIO/go/pkg/mod/github.com/gin-gonic/gin@v1.10.0/gin.go:589\nnet/http.serverHandler.ServeHTTP\n\tC:/Users/USUARIO/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.23.9.windows-amd64/src/net/http/server.go:3210\nnet/http.(*conn).serve\n\tC:/Users/USUARIO/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.23.9.windows-amd64/src/net/http/server.go:2092\nruntime.goexit\n\tC:/Users/USUARIO/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.23.9.windows-amd64/src/runtime/asm_amd64.s:1700","request_id":"req-18515d73-aa22-4f2e-a71e-fe5e4233b340"}
ERROR: 2025/05/15 15:12:58 internal/api/handler.go:99: [api-facturacion-myapps] Error al llamar a API1 {"error":"error en solicitud GetData a API1: error sending HTTP request: Post \"https://api1.example.com/soap\": proxyconnect tcp: dial tcp [::1]:80: connectex: No se puede establecer una conexión ya que el equipo de destino denegó expresamente dicha conexión."}
INFO: 2025/05/15 15:12:58 internal/api/router.go:88: [api-facturacion-myapps] Solicitud completada {"status":500}
INFO: 2025/05/15 15:28:45 internal/api/router.go:81: [api-facturacion-myapps] Solicitud iniciada
INFO: 2025/05/15 15:28:45 internal/api/router.go:88: [api-facturacion-myapps] Solicitud completada {"status":200}
INFO: 2025/05/15 15:33:43 internal/api/router.go:81: [api-facturacion-myapps] Solicitud iniciada
INFO: 2025/05/15 15:33:43 internal/api/router.go:88: [api-facturacion-myapps] Solicitud completada {"status":200}
INFO: 2025/05/15 15:33:49 internal/api/router.go:81: [api-facturacion-myapps] Solicitud iniciada
INFO: 2025/05/15 15:33:49 internal/api/router.go:88: [api-facturacion-myapps] Solicitud completada {"status":200}
INFO: 2025/05/15 15:55:19 cmd/api/main.go:82: [api-facturacion-myapps] Apagando servidor...
INFO: 2025/05/15 15:55:19 cmd/api/main.go:90: [api-facturacion-myapps] Servidor detenido correctamente
INFO: 2025/05/15 16:06:07 cmd/api/main.go:28: [api-facturacion-myapps] Iniciando aplicación {"app":"api-facturacion-myapps"}
INFO: 2025/05/15 16:06:07 cmd/api/main.go:42: [api-facturacion-myapps] Conexión a la base de datos establecida
INFO: 2025/05/15 16:06:07 cmd/api/main.go:49: [api-facturacion-myapps] Parámetros cargados desde la base de datos {"count":2}
INFO: 2025/05/15 16:06:07 cmd/api/main.go:71: [api-facturacion-myapps] Servidor iniciado {"port":9999}
INFO: 2025/05/15 16:09:17 internal/api/router.go:81: [api-facturacion-myapps] Solicitud iniciada
INFO: 2025/05/15 16:09:17 internal/api/router.go:88: [api-facturacion-myapps] Solicitud completada {"status":200}
INFO: 2025/05/15 16:09:21 internal/api/router.go:81: [api-facturacion-myapps] Solicitud iniciada
INFO: 2025/05/15 16:09:21 internal/api/router.go:88: [api-facturacion-myapps] Solicitud completada {"status":200}
INFO: 2025/05/15 16:09:24 internal/api/router.go:81: [api-facturacion-myapps] Solicitud iniciada
INFO: 2025/05/15 16:09:24 internal/api/router.go:88: [api-facturacion-myapps] Solicitud completada {"status":200}
INFO: 2025/05/15 16:09:28 internal/api/router.go:81: [api-facturacion-myapps] Solicitud iniciada
INFO: 2025/05/15 16:09:28 internal/api/router.go:88: [api-facturacion-myapps] Solicitud completada {"status":200}
INFO: 2025/05/15 16:09:33 internal/api/router.go:81: [api-facturacion-myapps] Solicitud iniciada
DEBUG: 2025/05/15 16:09:33 internal/soap/api/client.go:62: [api-facturacion-myapps] Enviando solicitud GetData a API1 {"parameters":{"XMLName":{"Space":"","Local":""},"Namespace":"http://api1.example.com/soap","ID":"12345","Type":"CUSTOMER","Date":"2023-12-01"},"request_id":"req-494f9fbb-01d5-4e9a-b792-79325d7a84c2"}
ERROR: 2025/05/15 16:09:33 internal/soap/api/client.go:77: [api-facturacion-myapps] Error al realizar solicitud GetData a API1 {"duration_ms":10,"error":"error sending HTTP request: Post \"https://new-api1.example.com/soap\": proxyconnect tcp: dial tcp [::1]:80: connectex: No se puede establecer una conexión ya que el equipo de destino denegó expresamente dicha conexión.","errorVerbose":"Post \"https://new-api1.example.com/soap\": proxyconnect tcp: dial tcp [::1]:80: connectex: No se puede establecer una conexión ya que el equipo de destino denegó expresamente dicha conexión.\nerror sending HTTP request\napi-soap-facturacion/pkg/soap.(*client).Call\n\tC:/Users/USUARIO/Documents/00000-MYAPPS/00-SISTEMA_FACTURACION/000-FACTURACION/api-soap-facturacion/pkg/soap/soap.go:160\napi-soap-facturacion/internal/soap/api.(*client).GetData\n\tC:/Users/USUARIO/Documents/00000-MYAPPS/00-SISTEMA_FACTURACION/000-FACTURACION/api-soap-facturacion/internal/soap/api/client.go:72\napi-soap-facturacion/internal/api.(*Handler).API1GetData\n\tC:/Users/USUARIO/Documents/00000-MYAPPS/00-SISTEMA_FACTURACION/000-FACTURACION/api-soap-facturacion/internal/api/handler.go:97\ngithub.com/gin-gonic/gin.(*Context).Next\n\tC:/Users/USUARIO/go/pkg/mod/github.com/gin-gonic/gin@v1.10.0/context.go:185\napi-soap-facturacion/internal/api.CORSMiddleware.func1\n\tC:/Users/USUARIO/Documents/00000-MYAPPS/00-SISTEMA_FACTURACION/000-FACTURACION/api-soap-facturacion/internal/api/router.go:122\ngithub.com/gin-gonic/gin.(*Context).Next\n\tC:/Users/USUARIO/go/pkg/mod/github.com/gin-gonic/gin@v1.10.0/context.go:185\napi-soap-facturacion/internal/api.RequestIDMiddleware.func1\n\tC:/Users/USUARIO/Documents/00000-MYAPPS/00-SISTEMA_FACTURACION/000-FACTURACION/api-soap-facturacion/internal/api/router.go:106\ngithub.com/gin-gonic/gin.(*Context).Next\n\tC:/Users/USUARIO/go/pkg/mod/github.com/gin-gonic/gin@v1.10.0/context.go:185\napi-soap-facturacion/internal/api.LoggerMiddleware.func1\n\tC:/Users/USUARIO/Documents/00000-MYAPPS/00-SISTEMA_FACTURACION/000-FACTURACION/api-soap-facturacion/internal/api/router.go:84\ngithub.com/gin-gonic/gin.(*Context).Next\n\tC:/Users/USUARIO/go/pkg/mod/github.com/gin-gonic/gin@v1.10.0/context.go:185\ngithub.com/gin-gonic/gin.CustomRecoveryWithWriter.func1\n\tC:/Users/USUARIO/go/pkg/mod/github.com/gin-gonic/gin@v1.10.0/recovery.go:102\ngithub.com/gin-gonic/gin.(*Context).Next\n\tC:/Users/USUARIO/go/pkg/mod/github.com/gin-gonic/gin@v1.10.0/context.go:185\ngithub.com/gin-gonic/gin.(*Engine).handleHTTPRequest\n\tC:/Users/USUARIO/go/pkg/mod/github.com/gin-gonic/gin@v1.10.0/gin.go:633\ngithub.com/gin-gonic/gin.(*Engine).ServeHTTP\n\tC:/Users/USUARIO/go/pkg/mod/github.com/gin-gonic/gin@v1.10.0/gin.go:589\nnet/http.serverHandler.ServeHTTP\n\tC:/Users/USUARIO/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.23.9.windows-amd64/src/net/http/server.go:3210\nnet/http.(*conn).serve\n\tC:/Users/USUARIO/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.23.9.windows-amd64/src/net/http/server.go:2092\nruntime.goexit\n\tC:/Users/USUARIO/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.23.9.windows-amd64/src/runtime/asm_amd64.s:1700","request_id":"req-494f9fbb-01d5-4e9a-b792-79325d7a84c2"}
ERROR: 2025/05/15 16:09:33 internal/api/handler.go:99: [api-facturacion-myapps] Error al llamar a API1 {"error":"error en solicitud GetData a API1: error sending HTTP request: Post \"https://new-api1.example.com/soap\": proxyconnect tcp: dial tcp [::1]:80: connectex: No se puede establecer una conexión ya que el equipo de destino denegó expresamente dicha conexión."}
INFO: 2025/05/15 16:09:33 internal/api/router.go:88: [api-facturacion-myapps] Solicitud completada {"status":500}
INFO: 2025/05/15 16:14:58 cmd/api/main.go:81: [api-facturacion-myapps] Apagando servidor...
INFO: 2025/05/15 16:14:58 cmd/api/main.go:89: [api-facturacion-myapps] Servidor detenido correctamente
INFO: 2025/05/15 16:15:50 cmd/api/main.go:28: [api-facturacion-myapps] Iniciando aplicación {"app":"api-facturacion-myapps"}
INFO: 2025/05/15 16:15:50 cmd/api/main.go:42: [api-facturacion-myapps] Conexión a la base de datos establecida
INFO: 2025/05/15 16:15:50 cmd/api/main.go:49: [api-facturacion-myapps] Parámetros cargados desde la base de datos {"count":2}
INFO: 2025/05/15 16:15:50 cmd/api/main.go:71: [api-facturacion-myapps] Servidor iniciado {"port":9999}
INFO: 2025/05/15 16:16:00 internal/api/router.go:81: [api-facturacion-myapps] Solicitud iniciada
DEBUG: 2025/05/15 16:16:10 internal/soap/api/client.go:62: [api-facturacion-myapps] Enviando solicitud GetData a API1 {"parameters":{"XMLName":{"Space":"","Local":""},"Namespace":"http://api1.example.com/soap","ID":"12345","Type":"CUSTOMER","Date":"2023-12-01"},"request_id":"req-88f62aab-f0eb-4e7b-a82c-d1b7aed9c279"}
ERROR: 2025/05/15 16:16:10 internal/soap/api/client.go:77: [api-facturacion-myapps] Error al realizar solicitud GetData a API1 {"duration_ms":16,"error":"error sending HTTP request: Post \"https://new-api1.example.com/soap\": proxyconnect tcp: dial tcp [::1]:80: connectex: No se puede establecer una conexión ya que el equipo de destino denegó expresamente dicha conexión.","errorVerbose":"Post \"https://new-api1.example.com/soap\": proxyconnect tcp: dial tcp [::1]:80: connectex: No se puede establecer una conexión ya que el equipo de destino denegó expresamente dicha conexión.\nerror sending HTTP request\napi-soap-facturacion/pkg/soap.(*client).Call\n\tC:/Users/USUARIO/Documents/00000-MYAPPS/00-SISTEMA_FACTURACION/000-FACTURACION/api-soap-facturacion/pkg/soap/soap.go:160\napi-soap-facturacion/internal/soap/api.(*client).GetData\n\tC:/Users/USUARIO/Documents/00000-MYAPPS/00-SISTEMA_FACTURACION/000-FACTURACION/api-soap-facturacion/internal/soap/api/client.go:72\napi-soap-facturacion/internal/api.(*Handler).API1GetData\n\tC:/Users/USUARIO/Documents/00000-MYAPPS/00-SISTEMA_FACTURACION/000-FACTURACION/api-soap-facturacion/internal/api/getDataHandler.go:50\ngithub.com/gin-gonic/gin.(*Context).Next\n\tC:/Users/USUARIO/go/pkg/mod/github.com/gin-gonic/gin@v1.10.0/context.go:185\napi-soap-facturacion/internal/api.CORSMiddleware.func1\n\tC:/Users/USUARIO/Documents/00000-MYAPPS/00-SISTEMA_FACTURACION/000-FACTURACION/api-soap-facturacion/internal/api/router.go:122\ngithub.com/gin-gonic/gin.(*Context).Next\n\tC:/Users/USUARIO/go/pkg/mod/github.com/gin-gonic/gin@v1.10.0/context.go:185\napi-soap-facturacion/internal/api.RequestIDMiddleware.func1\n\tC:/Users/USUARIO/Documents/00000-MYAPPS/00-SISTEMA_FACTURACION/000-FACTURACION/api-soap-facturacion/internal/api/router.go:106\ngithub.com/gin-gonic/gin.(*Context).Next\n\tC:/Users/USUARIO/go/pkg/mod/github.com/gin-gonic/gin@v1.10.0/context.go:185\napi-soap-facturacion/internal/api.LoggerMiddleware.func1\n\tC:/Users/USUARIO/Documents/00000-MYAPPS/00-SISTEMA_FACTURACION/000-FACTURACION/api-soap-facturacion/internal/api/router.go:84\ngithub.com/gin-gonic/gin.(*Context).Next\n\tC:/Users/USUARIO/go/pkg/mod/github.com/gin-gonic/gin@v1.10.0/context.go:185\ngithub.com/gin-gonic/gin.CustomRecoveryWithWriter.func1\n\tC:/Users/USUARIO/go/pkg/mod/github.com/gin-gonic/gin@v1.10.0/recovery.go:102\ngithub.com/gin-gonic/gin.(*Context).Next\n\tC:/Users/USUARIO/go/pkg/mod/github.com/gin-gonic/gin@v1.10.0/context.go:185\ngithub.com/gin-gonic/gin.(*Engine).handleHTTPRequest\n\tC:/Users/USUARIO/go/pkg/mod/github.com/gin-gonic/gin@v1.10.0/gin.go:633\ngithub.com/gin-gonic/gin.(*Engine).ServeHTTP\n\tC:/Users/USUARIO/go/pkg/mod/github.com/gin-gonic/gin@v1.10.0/gin.go:589\nnet/http.serverHandler.ServeHTTP\n\tC:/Users/USUARIO/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.23.9.windows-amd64/src/net/http/server.go:3210\nnet/http.(*conn).serve\n\tC:/Users/USUARIO/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.23.9.windows-amd64/src/net/http/server.go:2092\nruntime.goexit\n\tC:/Users/USUARIO/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.23.9.windows-amd64/src/runtime/asm_amd64.s:1700","request_id":"req-88f62aab-f0eb-4e7b-a82c-d1b7aed9c279"}
ERROR: 2025/05/15 16:16:10 internal/api/getDataHandler.go:52: [api-facturacion-myapps] Error al llamar a API1 {"error":"error en solicitud GetData a API1: error sending HTTP request: Post \"https://new-api1.example.com/soap\": proxyconnect tcp: dial tcp [::1]:80: connectex: No se puede establecer una conexión ya que el equipo de destino denegó expresamente dicha conexión."}
INFO: 2025/05/15 16:16:14 internal/api/router.go:88: [api-facturacion-myapps] Solicitud completada {"status":500}
INFO: 2025/05/15 16:28:50 cmd/api/main.go:81: [api-facturacion-myapps] Apagando servidor...
INFO: 2025/05/15 16:28:50 cmd/api/main.go:89: [api-facturacion-myapps] Servidor detenido correctamente
INFO: 2025/05/15 16:28:54 cmd/api/main.go:28: [api-facturacion-myapps] Iniciando aplicación {"app":"api-facturacion-myapps"}
INFO: 2025/05/15 16:28:54 cmd/api/main.go:42: [api-facturacion-myapps] Conexión a la base de datos establecida
INFO: 2025/05/15 16:28:54 cmd/api/main.go:49: [api-facturacion-myapps] Parámetros cargados desde la base de datos {"count":2}
INFO: 2025/05/15 16:28:54 cmd/api/main.go:71: [api-facturacion-myapps] Servidor iniciado {"port":9999}
INFO: 2025/05/15 16:28:57 cmd/api/main.go:81: [api-facturacion-myapps] Apagando servidor...
INFO: 2025/05/15 16:28:57 cmd/api/main.go:89: [api-facturacion-myapps] Servidor detenido correctamente
INFO: 2025/05/15 16:31:23 cmd/api/main.go:28: [api-facturacion-myapps] Iniciando aplicación {"app":"api-facturacion-myapps"}
INFO: 2025/05/15 16:31:23 cmd/api/main.go:42: [api-facturacion-myapps] Conexión a la base de datos establecida
INFO: 2025/05/15 16:31:23 cmd/api/main.go:49: [api-facturacion-myapps] Parámetros cargados desde la base de datos {"count":2}
INFO: 2025/05/15 16:31:23 cmd/api/main.go:71: [api-facturacion-myapps] Servidor iniciado {"port":9999}
INFO: 2025/05/15 17:06:47 cmd/api/main.go:81: [api-facturacion-myapps] Apagando servidor...
INFO: 2025/05/15 17:06:47 cmd/api/main.go:89: [api-facturacion-myapps] Servidor detenido correctamente

View File

@ -0,0 +1,129 @@
package errors
import (
"fmt"
"runtime"
"strings"
"github.com/pkg/errors"
)
// Error es una interfaz extendida para errores
type Error interface {
error
Code() string
Unwrap() error
}
// CustomError implementa la interfaz Error
type CustomError struct {
code string
message string
cause error
stack []uintptr
}
// New crea un nuevo error personalizado
func New(code, message string) Error {
return &CustomError{
code: code,
message: message,
stack: callers(),
}
}
// Wrap envuelve un error con información adicional
func Wrap(err error, message string) Error {
if err == nil {
return nil
}
return &CustomError{
code: extractCode(err),
message: message,
cause: err,
stack: callers(),
}
}
// Wrapf envuelve un error con un mensaje formateado
func Wrapf(err error, format string, args ...interface{}) Error {
if err == nil {
return nil
}
return &CustomError{
code: extractCode(err),
message: fmt.Sprintf(format, args...),
cause: err,
stack: callers(),
}
}
// NewWithCode crea un nuevo error con un código específico
func NewWithCode(code, message string) Error {
return &CustomError{
code: code,
message: message,
stack: callers(),
}
}
// Error implementa la interfaz error
func (e *CustomError) Error() string {
if e.cause != nil {
return fmt.Sprintf("%s: %v", e.message, e.cause)
}
return e.message
}
// Code retorna el código de error
func (e *CustomError) Code() string {
return e.code
}
// Unwrap implementa la interfaz de unwrapping
func (e *CustomError) Unwrap() error {
return e.cause
}
// Stack retorna la pila de llamadas
func (e *CustomError) Stack() string {
var sb strings.Builder
frames := runtime.CallersFrames(e.stack)
for {
frame, more := frames.Next()
sb.WriteString(fmt.Sprintf("%s:%d %s\n", frame.File, frame.Line, frame.Function))
if !more {
break
}
}
return sb.String()
}
// extractCode extrae el código de un error
func extractCode(err error) string {
if customErr, ok := err.(Error); ok {
return customErr.Code()
}
return "UNKNOWN"
}
// callers obtiene la pila de llamadas
func callers() []uintptr {
const depth = 32
var pcs [depth]uintptr
n := runtime.Callers(3, pcs[:])
return pcs[0:n]
}
// Errores comunes
var (
ErrNotFound = NewWithCode("NOT_FOUND", "recurso no encontrado")
ErrUnauthorized = NewWithCode("UNAUTHORIZED", "no autorizado")
ErrForbidden = NewWithCode("FORBIDDEN", "acceso prohibido")
ErrBadRequest = NewWithCode("BAD_REQUEST", "solicitud incorrecta")
ErrInternal = NewWithCode("INTERNAL", "error interno del servidor")
ErrTimeout = NewWithCode("TIMEOUT", "tiempo de espera agotado")
ErrNotImplemented = NewWithCode("NOT_IMPLEMENTED", "no implementado")
)

View File

@ -0,0 +1,127 @@
package errors
import (
"fmt"
"runtime"
"strings"
)
// Error es una interfaz extendida para errores
type Error interface {
error
Code() string
Unwrap() error
}
// CustomError implementa la interfaz Error
type CustomError struct {
code string
message string
cause error
stack []uintptr
}
// New crea un nuevo error personalizado
func New(code, message string) Error {
return &CustomError{
code: code,
message: message,
stack: callers(),
}
}
// Wrap envuelve un error con información adicional
func Wrap(err error, message string) Error {
if err == nil {
return nil
}
return &CustomError{
code: extractCode(err),
message: message,
cause: err,
stack: callers(),
}
}
// Wrapf envuelve un error con un mensaje formateado
func Wrapf(err error, format string, args ...interface{}) Error {
if err == nil {
return nil
}
return &CustomError{
code: extractCode(err),
message: fmt.Sprintf(format, args...),
cause: err,
stack: callers(),
}
}
// NewWithCode crea un nuevo error con un código específico
func NewWithCode(code, message string) Error {
return &CustomError{
code: code,
message: message,
stack: callers(),
}
}
// Error implementa la interfaz error
func (e *CustomError) Error() string {
if e.cause != nil {
return fmt.Sprintf("%s: %v", e.message, e.cause)
}
return e.message
}
// Code retorna el código de error
func (e *CustomError) Code() string {
return e.code
}
// Unwrap implementa la interfaz de unwrapping
func (e *CustomError) Unwrap() error {
return e.cause
}
// Stack retorna la pila de llamadas
func (e *CustomError) Stack() string {
var sb strings.Builder
frames := runtime.CallersFrames(e.stack)
for {
frame, more := frames.Next()
sb.WriteString(fmt.Sprintf("%s:%d %s\n", frame.File, frame.Line, frame.Function))
if !more {
break
}
}
return sb.String()
}
// extractCode extrae el código de un error
func extractCode(err error) string {
if customErr, ok := err.(Error); ok {
return customErr.Code()
}
return "UNKNOWN"
}
// callers obtiene la pila de llamadas
func callers() []uintptr {
const depth = 32
var pcs [depth]uintptr
n := runtime.Callers(3, pcs[:])
return pcs[0:n]
}
// Errores comunes
var (
ErrNotFound = NewWithCode("NOT_FOUND", "recurso no encontrado")
ErrUnauthorized = NewWithCode("UNAUTHORIZED", "no autorizado")
ErrForbidden = NewWithCode("FORBIDDEN", "acceso prohibido")
ErrBadRequest = NewWithCode("BAD_REQUEST", "solicitud incorrecta")
ErrInternal = NewWithCode("INTERNAL", "error interno del servidor")
ErrTimeout = NewWithCode("TIMEOUT", "tiempo de espera agotado")
ErrNotImplemented = NewWithCode("NOT_IMPLEMENTED", "no implementado")
)

View File

@ -0,0 +1,200 @@
package soap
import (
"bytes"
"context"
"encoding/xml"
"io"
"net/http"
"time"
"github.com/google/uuid"
"github.com/pkg/errors"
)
// Client es la interfaz para un cliente SOAP
type Client interface {
Call(ctx context.Context, operation string, request interface{}, response interface{}) error
}
// ClientOptions contiene opciones para el cliente SOAP
type ClientOptions struct {
Endpoint string
Username string
Password string
Timeout time.Duration
Headers map[string]string
}
// RequestIDKey es la clave para el ID de solicitud en el contexto
type RequestIDKey struct{}
// client implementa la interfaz Client
//type client struct {
// endpoint string
// httpClient *http.Client
// credentials *credentials
// headers map[string]string
//}
// credentials contiene las credenciales para autenticación
type credentials struct {
username string
password string
}
// Añadir este método al final del archivo pkg/soap/soap.go
// En la definición del cliente:
type client struct {
endpoint string
httpClient *http.Client
credentials *credentials
headers map[string]string
}
// Endpoint retorna el endpoint actual del cliente
func (c *client) Endpoint() string {
return c.endpoint
}
// NewClient crea un nuevo cliente SOAP
func NewClient(options ClientOptions) Client {
httpClient := &http.Client{
Timeout: options.Timeout,
}
var creds *credentials
if options.Username != "" && options.Password != "" {
creds = &credentials{
username: options.Username,
password: options.Password,
}
}
return &client{
endpoint: options.Endpoint,
httpClient: httpClient,
credentials: creds,
headers: options.Headers,
}
}
// Envelope es la estructura para el envelope SOAP
type Envelope struct {
XMLName xml.Name `xml:"Envelope"`
XMLNS string `xml:"xmlns,attr"`
Header interface{} `xml:"Header,omitempty"`
Body Body `xml:"Body"`
}
// Body es la estructura para el cuerpo SOAP
type Body struct {
XMLName xml.Name `xml:"Body"`
Content interface{} `xml:",omitempty"`
Fault *Fault `xml:"Fault,omitempty"`
}
// Fault es la estructura para errores SOAP
type Fault struct {
XMLName xml.Name `xml:"Fault"`
FaultCode string `xml:"faultcode,omitempty"`
FaultString string `xml:"faultstring,omitempty"`
Detail string `xml:"detail,omitempty"`
}
// Call realiza una llamada SOAP
func (c *client) Call(ctx context.Context, operation string, request interface{}, response interface{}) error {
// Obtener ID de solicitud del contexto o generar uno nuevo
var requestID string
if id, ok := ctx.Value(RequestIDKey{}).(string); ok {
requestID = id
} else {
requestID = uuid.New().String()
}
// Crear envelope SOAP
envelope := Envelope{
XMLNS: "http://schemas.xmlsoap.org/soap/envelope/",
Body: Body{
Content: request,
},
}
// Serializar envelope a XML
requestBuffer := &bytes.Buffer{}
enc := xml.NewEncoder(requestBuffer)
enc.Indent("", " ")
if err := enc.Encode(envelope); err != nil {
return errors.Wrap(err, "error encoding SOAP request")
}
// Crear solicitud HTTP
httpReq, err := http.NewRequestWithContext(
ctx,
http.MethodPost,
c.endpoint,
requestBuffer,
)
if err != nil {
return errors.Wrap(err, "error creating HTTP request")
}
// Establecer cabeceras HTTP
httpReq.Header.Set("Content-Type", "text/xml; charset=utf-8")
httpReq.Header.Set("SOAPAction", operation)
httpReq.Header.Set("X-Request-ID", requestID)
// Agregar cabeceras adicionales
for key, value := range c.headers {
httpReq.Header.Set(key, value)
}
// Agregar autenticación si es necesario
if c.credentials != nil {
httpReq.SetBasicAuth(c.credentials.username, c.credentials.password)
}
// Realizar solicitud HTTP
httpResp, err := c.httpClient.Do(httpReq)
if err != nil {
return errors.Wrap(err, "error sending HTTP request")
}
defer httpResp.Body.Close()
// Leer respuesta
respBody, err := io.ReadAll(httpResp.Body)
if err != nil {
return errors.Wrap(err, "error reading HTTP response")
}
// Verificar código de estado HTTP
if httpResp.StatusCode != http.StatusOK {
return errors.Errorf("HTTP error: %d - %s, body: %s",
httpResp.StatusCode,
httpResp.Status,
string(respBody),
)
}
// Deserializar respuesta
respEnvelope := &Envelope{
Body: Body{
Content: response,
},
}
if err := xml.Unmarshal(respBody, respEnvelope); err != nil {
return errors.Wrap(err, "error unmarshalling SOAP response")
}
// Verificar si hay un SOAP Fault
if respEnvelope.Body.Fault != nil {
return errors.Errorf("SOAP Fault: %s - %s, detail: %s",
respEnvelope.Body.Fault.FaultCode,
respEnvelope.Body.Fault.FaultString,
respEnvelope.Body.Fault.Detail,
)
}
return nil
}

View File

@ -0,0 +1,37 @@
CREATE TABLE IF NOT EXISTS parameters (
id SERIAL PRIMARY KEY,
key VARCHAR(100) NOT NULL UNIQUE,
value TEXT NOT NULL,
description TEXT,
active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- Tabla para registro de solicitudes SOAP
CREATE TABLE IF NOT EXISTS soap_requests (
id SERIAL PRIMARY KEY,
request_id VARCHAR(36) NOT NULL,
api_id VARCHAR(50) NOT NULL,
operation_name VARCHAR(100) NOT NULL,
request_body TEXT NOT NULL,
response_body TEXT,
status_code INT,
error TEXT,
request_time TIMESTAMP WITH TIME ZONE NOT NULL,
response_time TIMESTAMP WITH TIME ZONE,
processing_time BIGINT,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- Índices
CREATE INDEX IF NOT EXISTS idx_soap_requests_request_id ON soap_requests(request_id);
CREATE INDEX IF NOT EXISTS idx_soap_requests_api_id ON soap_requests(api_id);
CREATE INDEX IF NOT EXISTS idx_soap_requests_operation_name ON soap_requests(operation_name);
CREATE INDEX IF NOT EXISTS idx_soap_requests_request_time ON soap_requests(request_time);
-- Insertar parámetros iniciales
INSERT INTO parameters (key, value, description)
VALUES('soap.api1.endpoint', 'https://api1.example.com/soap', 'Endpoint for API 1'),
('soap.api2.endpoint', 'https://api2.example.com/soap', 'Endpoint for API 2')
ON CONFLICT (key) DO NOTHING;

8
daemonService/.idea/.gitignore generated vendored Normal file
View File

@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

9
daemonService/.idea/daemonService.iml generated Normal file
View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="Go" enabled="true" />
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

8
daemonService/.idea/modules.xml generated Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/daemonService.iml" filepath="$PROJECT_DIR$/.idea/daemonService.iml" />
</modules>
</component>
</project>

View File

@ -0,0 +1,401 @@
package main
import (
"context"
"daemonService/internal/handlers"
"daemonService/internal/models"
svc "daemonService/internal/models"
"daemonService/internal/models/obtencionCodigos"
"daemonService/internal/repositories"
"daemonService/internal/services/obtencionCodigo"
"daemonService/internal/services/procesar"
"daemonService/internal/services/sincronizacionDatos"
"flag"
"fmt"
"github.com/robfig/cron/v3"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"daemonService/internal/api"
"daemonService/internal/config"
"daemonService/internal/database"
"daemonService/internal/notifications"
"daemonService/internal/services"
)
func main() {
configFile := flag.String("config", "configs/config.yaml", "Ruta al archivo de configuración")
runOnce := flag.Bool("run-once", false, "Ejecutar servicios una vez y salir")
serviceName := flag.String("service", "", "Nombre del servicio específico a ejecutar")
flag.Parse()
// Cargar configuración
cfg, err := config.LoadConfig(*configFile)
if err != nil {
log.Fatalf("Error al cargar configuración: %v", err)
}
// Inicializar la base de datos
db, err := database.InitDB(*cfg)
if err != nil {
log.Fatalf("Error al inicializar la base de datos: %v", err)
}
defer db.Close()
// Crear sistema de notificaciones
notifier := notifications.CreateNotificationSender(cfg.Notifications.Enabled, cfg.Notifications.Method, cfg.Notifications.Config)
if notifier != nil {
log.Printf("Sistema de notificaciones iniciado: %s", cfg.Notifications.Method)
}
// Crear servicios a partir de la configuración
var servicesList []models.SoapService
var servicesList2 []obtencionCodigos.CuisService
for _, svcCfg := range cfg.Services {
if !svcCfg.Enabled {
continue
}
// Crear el logger para el servicio
serviceLogger := log.New(log.Writer(), fmt.Sprintf("[%s] ", svcCfg.Name), log.LstdFlags)
baseSvc := svc.ServiceModel{
Name: svcCfg.Name,
Enabled: svcCfg.Enabled,
Concurrency: svcCfg.Concurrency,
DB: db,
SOAPEndpoint: cfg.SOAP.FACTURA_SINCRONIZACION.Endpoint,
SOAPTimeout: time.Duration(cfg.SOAP.FACTURA_SINCRONIZACION.Timeout) * time.Second,
SOAPRetries: cfg.SOAP.FACTURA_SINCRONIZACION.Retries,
NotificationSender: notifier,
KeyToken: "",
ValueToken: "",
TagNames: "",
QueryInsert: "",
MsgCustomResponse: "",
}
cuisServiceModel := obtencionCodigos.CuisServiceModel{
Name: svcCfg.Name,
Enabled: svcCfg.Enabled,
Concurrency: svcCfg.Concurrency,
DB: db,
SOAPEndpoint: cfg.SOAP.FACTURA_CODIGO.Endpoint,
SOAPTimeout: time.Duration(cfg.SOAP.FACTURA_CODIGO.Timeout) * time.Second,
SOAPRetries: cfg.SOAP.FACTURA_CODIGO.Retries,
NotificationSender: notifier,
TokenKey: "",
TokenValue: "",
TagNames: "",
TagNamesCufd: "",
QueryInsert: "",
MsgCustomResponse: "",
}
//registrar-empresa solo para registrar empresa
cuisServiceModel.TagNames = "cuis"
cuisServiceModel.TagNamesCufd = "cufd"
cuisServiceModel.QueryInsert = "INSERT INTO registroEmpresa (codigo, reqSoap, respSoap, reqJson, respJson, cuis_id, fecha_creacion) VALUES ($1, $2, $3, $4, $5, $6, NOW() AT TIME ZONE 'America/La_Paz') ON CONFLICT (codigo, cuis_id) DO UPDATE SET reqSoap = EXCLUDED.reqSoap, respSoap = EXCLUDED.respSoap, reqJson = EXCLUDED.reqJson, respJson = EXCLUDED.respJson, cuis_id = EXCLUDED.cuis_id, fecha_actualizacion = NOW() AT TIME ZONE 'America/La_Paz';"
cuisServiceModel.MsgCustomResponse = "Registrar Empresa"
procesarRegistro := procesar.NewProcesarRegistro(cuisServiceModel)
empresaRepository := repositories.NewEmpresaRepository(cuisServiceModel)
cuisService := services.NewCuisService(cuisServiceModel, serviceLogger)
cuisRepository := repositories.NewCuisRepository(baseSvc, cuisService)
cronHandler := handlers.NewCronHandler(cuisService, empresaRepository, cuisRepository, procesarRegistro)
apiHandler := handlers.NewApiHandler(cuisService, cuisRepository, procesarRegistro)
// Llamar a NewObtencionCodigoService correctamente
obtencionCodigoSvc := obtencionCodigo.NewObtencionCodigoService(
cuisServiceModel,
cuisService,
cronHandler,
apiHandler,
)
switch svcCfg.Name {
case "registrar-empresa":
servicesList2 = append(servicesList2, obtencionCodigoSvc)
break
case "actividades": //todo revisar que pasa
baseSvc.TagNames = "sincronizarActividades"
baseSvc.QueryInsert = "INSERT INTO sincronizar_actividades (codigo, reqSoap, respSoap, reqJson, respJson, cuis_id, fecha_creacion) VALUES ($1, $2, $3, $4, $5, $6, NOW() AT TIME ZONE 'America/La_Paz') ON CONFLICT (codigo, cuis_id) DO UPDATE SET reqSoap = EXCLUDED.reqSoap, respSoap = EXCLUDED.respSoap, reqJson = EXCLUDED.reqJson, respJson = EXCLUDED.respJson, cuis_id = EXCLUDED.cuis_id, fecha_actualizacion = NOW() AT TIME ZONE 'America/La_Paz';"
baseSvc.MsgCustomResponse = "Listado de Actividades"
servicesList = append(servicesList, &sincronizacionDatos.SincronizacionDatosService{
ServiceModel: baseSvc,
})
break
case "sincronizar_fecha_hora": //todo revisar que pasa
baseSvc.TagNames = "sincronizarFechaHora"
baseSvc.QueryInsert = "INSERT INTO sincronizar_fecha_hora (codigo, reqSoap, respSoap, reqJson, respJson, cuis_id, fecha_creacion) VALUES ($1, $2, $3, $4, $5, $6, NOW() AT TIME ZONE 'America/La_Paz') ON CONFLICT (codigo, cuis_id) DO UPDATE SET reqSoap = EXCLUDED.reqSoap, respSoap = EXCLUDED.respSoap, reqJson = EXCLUDED.reqJson, respJson = EXCLUDED.respJson, cuis_id = EXCLUDED.cuis_id, fecha_actualizacion = NOW() AT TIME ZONE 'America/La_Paz';"
baseSvc.MsgCustomResponse = "Sincronizar la hora"
servicesList = append(servicesList, &sincronizacionDatos.SincronizacionDatosService{
ServiceModel: baseSvc,
})
break
case "sincronizar_mensaje_servicio": //todo revisar que pasa
baseSvc.TagNames = "sincronizarListaMensajesServicios"
baseSvc.QueryInsert = "INSERT INTO sincronizar_mensaje_servicio (codigo, reqSoap, respSoap, reqJson, respJson, cuis_id, fecha_creacion) VALUES ($1, $2, $3, $4, $5, $6, NOW() AT TIME ZONE 'America/La_Paz') ON CONFLICT (codigo, cuis_id) DO UPDATE SET reqSoap = EXCLUDED.reqSoap, respSoap = EXCLUDED.respSoap, reqJson = EXCLUDED.reqJson, respJson = EXCLUDED.respJson, cuis_id = EXCLUDED.cuis_id, fecha_actualizacion = NOW() AT TIME ZONE 'America/La_Paz';"
baseSvc.MsgCustomResponse = "Sincronizar mensaje servicio"
servicesList = append(servicesList, &sincronizacionDatos.SincronizacionDatosService{
ServiceModel: baseSvc,
})
break
case "leyendas_factura":
baseSvc.TagNames = "sincronizarListaLeyendasFactura"
baseSvc.QueryInsert = "INSERT INTO sincronizar_leyendas_factura (codigo, reqSoap, respSoap, reqJson, respJson, cuis_id, fecha_creacion) VALUES ($1, $2, $3, $4, $5, $6, NOW() AT TIME ZONE 'America/La_Paz') ON CONFLICT (codigo, cuis_id) DO UPDATE SET reqSoap = EXCLUDED.reqSoap, respSoap = EXCLUDED.respSoap, reqJson = EXCLUDED.reqJson, respJson = EXCLUDED.respJson, cuis_id = EXCLUDED.cuis_id, fecha_actualizacion = NOW() AT TIME ZONE 'America/La_Paz';"
baseSvc.MsgCustomResponse = "Listado Leyendas de Facturas"
servicesList = append(servicesList, &sincronizacionDatos.SincronizacionDatosService{
ServiceModel: baseSvc,
})
break
case "producto-servicio":
baseSvc.TagNames = "sincronizarListaProductosServicios"
baseSvc.QueryInsert = "INSERT INTO sincronizar_productos_servicios (codigo, reqSoap, respSoap, reqJson, respJson, cuis_id, fecha_creacion) VALUES ($1, $2, $3, $4, $5, $6, NOW() AT TIME ZONE 'America/La_Paz') ON CONFLICT (codigo, cuis_id) DO UPDATE SET reqSoap = EXCLUDED.reqSoap, respSoap = EXCLUDED.respSoap, reqJson = EXCLUDED.reqJson, respJson = EXCLUDED.respJson, cuis_id = EXCLUDED.cuis_id, fecha_actualizacion = NOW() AT TIME ZONE 'America/La_Paz';"
baseSvc.MsgCustomResponse = "Listado Producto Servicio"
servicesList = append(servicesList, &sincronizacionDatos.SincronizacionDatosService{
ServiceModel: baseSvc,
})
break
case "tipo_motivo_anulacion":
baseSvc.TagNames = "sincronizarParametricaMotivoAnulacion"
baseSvc.QueryInsert = "INSERT INTO sincronizar_tipo_motivo_anulacion (codigo, reqSoap, respSoap, reqJson, respJson, cuis_id, fecha_creacion) VALUES ($1, $2, $3, $4, $5, $6, NOW() AT TIME ZONE 'America/La_Paz') ON CONFLICT (codigo, cuis_id) DO UPDATE SET reqSoap = EXCLUDED.reqSoap, respSoap = EXCLUDED.respSoap, reqJson = EXCLUDED.reqJson, respJson = EXCLUDED.respJson, cuis_id = EXCLUDED.cuis_id, fecha_actualizacion = NOW() AT TIME ZONE 'America/La_Paz';"
baseSvc.MsgCustomResponse = "Listado motivo anulacion"
servicesList = append(servicesList, &sincronizacionDatos.SincronizacionDatosService{
ServiceModel: baseSvc,
})
break
case "tipo_documento_identidad":
baseSvc.TagNames = "sincronizarParametricaTipoDocumentoIdentidad"
baseSvc.QueryInsert = "INSERT INTO sincronizar_tipo_documento_identidad (codigo, reqSoap, respSoap, reqJson, respJson, cuis_id, fecha_creacion) VALUES ($1, $2, $3, $4, $5, $6, NOW() AT TIME ZONE 'America/La_Paz') ON CONFLICT (codigo, cuis_id) DO UPDATE SET reqSoap = EXCLUDED.reqSoap, respSoap = EXCLUDED.respSoap, reqJson = EXCLUDED.reqJson, respJson = EXCLUDED.respJson, cuis_id = EXCLUDED.cuis_id, fecha_actualizacion = NOW() AT TIME ZONE 'America/La_Paz';"
baseSvc.MsgCustomResponse = "Listado tipos de documento de identidad"
servicesList = append(servicesList, &sincronizacionDatos.SincronizacionDatosService{
ServiceModel: baseSvc,
})
break
case "tipo_documento_sector":
baseSvc.TagNames = "sincronizarParametricaTipoDocumentoSector"
baseSvc.QueryInsert = "INSERT INTO sincronizar_tipo_documento_sector (codigo, reqSoap, respSoap, reqJson, respJson, cuis_id, fecha_creacion) VALUES ($1, $2, $3, $4, $5, $6, NOW() AT TIME ZONE 'America/La_Paz') ON CONFLICT (codigo, cuis_id) DO UPDATE SET reqSoap = EXCLUDED.reqSoap, respSoap = EXCLUDED.respSoap, reqJson = EXCLUDED.reqJson, respJson = EXCLUDED.respJson, cuis_id = EXCLUDED.cuis_id, fecha_actualizacion = NOW() AT TIME ZONE 'America/La_Paz';"
baseSvc.MsgCustomResponse = "Listado tipos de documento sector"
servicesList = append(servicesList, &sincronizacionDatos.SincronizacionDatosService{
ServiceModel: baseSvc,
})
break
case "tipo_emision": //todo falta este
baseSvc.TagNames = "sincronizarParametricaTipoEmision"
baseSvc.QueryInsert = "INSERT INTO sincronizar_tipo_emision (codigo, reqSoap, respSoap, reqJson, respJson, cuis_id, fecha_creacion) VALUES ($1, $2, $3, $4, $5, $6, NOW() AT TIME ZONE 'America/La_Paz') ON CONFLICT (codigo, cuis_id) DO UPDATE SET reqSoap = EXCLUDED.reqSoap, respSoap = EXCLUDED.respSoap, reqJson = EXCLUDED.reqJson, respJson = EXCLUDED.respJson, cuis_id = EXCLUDED.cuis_id, fecha_actualizacion = NOW() AT TIME ZONE 'America/La_Paz';"
baseSvc.MsgCustomResponse = "Listado tipos de emision"
servicesList = append(servicesList, &sincronizacionDatos.SincronizacionDatosService{
ServiceModel: baseSvc,
})
break
case "tipo_actividades_documento_sector": //todo falta este
baseSvc.TagNames = "sincronizarListaActividadesDocumentoSector"
baseSvc.QueryInsert = "INSERT INTO sincronizar_tipo_actividades_documento_sector (codigo, reqSoap, respSoap, reqJson, respJson, cuis_id, fecha_creacion) VALUES ($1, $2, $3, $4, $5, $6, NOW() AT TIME ZONE 'America/La_Paz') ON CONFLICT (codigo, cuis_id) DO UPDATE SET reqSoap = EXCLUDED.reqSoap, respSoap = EXCLUDED.respSoap, reqJson = EXCLUDED.reqJson, respJson = EXCLUDED.respJson, cuis_id = EXCLUDED.cuis_id, fecha_actualizacion = NOW() AT TIME ZONE 'America/La_Paz';"
baseSvc.MsgCustomResponse = "Listado tipos de emision"
servicesList = append(servicesList, &sincronizacionDatos.SincronizacionDatosService{
ServiceModel: baseSvc,
})
break
case "tipo_metodo_pago":
baseSvc.TagNames = "sincronizarParametricaTipoMetodoPago"
baseSvc.QueryInsert = "INSERT INTO sincronizar_tipo_metodo_pago (codigo, reqSoap, respSoap, reqJson, respJson, cuis_id, fecha_creacion) VALUES ($1, $2, $3, $4, $5, $6, NOW() AT TIME ZONE 'America/La_Paz') ON CONFLICT (codigo, cuis_id) DO UPDATE SET reqSoap = EXCLUDED.reqSoap, respSoap = EXCLUDED.respSoap, reqJson = EXCLUDED.reqJson, respJson = EXCLUDED.respJson, cuis_id = EXCLUDED.cuis_id, fecha_actualizacion = NOW() AT TIME ZONE 'America/La_Paz';"
baseSvc.MsgCustomResponse = "Listado tipos metodo de pago"
servicesList = append(servicesList, &sincronizacionDatos.SincronizacionDatosService{
ServiceModel: baseSvc,
})
break
case "tipo_moneda":
baseSvc.TagNames = "sincronizarParametricaTipoMoneda"
baseSvc.QueryInsert = "INSERT INTO sincronizar_tipo_moneda (codigo, reqSoap, respSoap, reqJson, respJson, cuis_id, fecha_creacion) VALUES ($1, $2, $3, $4, $5, $6, NOW() AT TIME ZONE 'America/La_Paz') ON CONFLICT (codigo, cuis_id) DO UPDATE SET reqSoap = EXCLUDED.reqSoap, respSoap = EXCLUDED.respSoap, reqJson = EXCLUDED.reqJson, respJson = EXCLUDED.respJson, cuis_id = EXCLUDED.cuis_id, fecha_actualizacion = NOW() AT TIME ZONE 'America/La_Paz';"
baseSvc.MsgCustomResponse = "Listado tipos de moneda"
servicesList = append(servicesList, &sincronizacionDatos.SincronizacionDatosService{
ServiceModel: baseSvc,
})
break
case "tipo_punto_venta":
baseSvc.TagNames = "sincronizarParametricaTipoPuntoVenta"
baseSvc.QueryInsert = "INSERT INTO sincronizar_tipo_punto_venta (codigo, reqSoap, respSoap, reqJson, respJson, cuis_id, fecha_creacion) VALUES ($1, $2, $3, $4, $5, $6, NOW() AT TIME ZONE 'America/La_Paz') ON CONFLICT (codigo, cuis_id) DO UPDATE SET reqSoap = EXCLUDED.reqSoap, respSoap = EXCLUDED.respSoap, reqJson = EXCLUDED.reqJson, respJson = EXCLUDED.respJson, cuis_id = EXCLUDED.cuis_id, fecha_actualizacion = NOW() AT TIME ZONE 'America/La_Paz';"
baseSvc.MsgCustomResponse = "Listado tipos de punto de venta"
servicesList = append(servicesList, &sincronizacionDatos.SincronizacionDatosService{
ServiceModel: baseSvc,
})
break
case "tipo_factura": //todo este falta
baseSvc.TagNames = "sincronizarParametricaTiposFactura"
baseSvc.QueryInsert = "INSERT INTO sincronizar_tipo_factura (codigo, reqSoap, respSoap, reqJson, respJson, cuis_id, fecha_creacion) VALUES ($1, $2, $3, $4, $5, $6, NOW() AT TIME ZONE 'America/La_Paz') ON CONFLICT (codigo, cuis_id) DO UPDATE SET reqSoap = EXCLUDED.reqSoap, respSoap = EXCLUDED.respSoap, reqJson = EXCLUDED.reqJson, respJson = EXCLUDED.respJson, cuis_id = EXCLUDED.cuis_id, fecha_actualizacion = NOW() AT TIME ZONE 'America/La_Paz';"
baseSvc.MsgCustomResponse = "Listado tipos de factura"
servicesList = append(servicesList, &sincronizacionDatos.SincronizacionDatosService{
ServiceModel: baseSvc,
})
break
case "tipo_unidad_medida":
baseSvc.TagNames = "sincronizarParametricaUnidadMedida"
baseSvc.QueryInsert = "INSERT INTO sincronizar_tipo_unidad_medida (codigo, reqSoap, respSoap, reqJson, respJson, cuis_id, fecha_creacion) VALUES ($1, $2, $3, $4, $5, $6, NOW() AT TIME ZONE 'America/La_Paz') ON CONFLICT (codigo, cuis_id) DO UPDATE SET reqSoap = EXCLUDED.reqSoap, respSoap = EXCLUDED.respSoap, reqJson = EXCLUDED.reqJson, respJson = EXCLUDED.respJson, cuis_id = EXCLUDED.cuis_id, fecha_actualizacion = NOW() AT TIME ZONE 'America/La_Paz';"
baseSvc.MsgCustomResponse = "Listado tipos de unidad de medida"
servicesList = append(servicesList, &sincronizacionDatos.SincronizacionDatosService{
ServiceModel: baseSvc,
})
break
case "tipo_evento_significativos":
baseSvc.TagNames = "sincronizarParametricaEventosSignificativos"
baseSvc.QueryInsert = "INSERT INTO sincronizar_evento_significativo (codigo, reqSoap, respSoap, reqJson, respJson, cuis_id, fecha_creacion) VALUES ($1, $2, $3, $4, $5, $6, NOW() AT TIME ZONE 'America/La_Paz') ON CONFLICT (codigo, cuis_id) DO UPDATE SET reqSoap = EXCLUDED.reqSoap, respSoap = EXCLUDED.respSoap, reqJson = EXCLUDED.reqJson, respJson = EXCLUDED.respJson, cuis_id = EXCLUDED.cuis_id, fecha_actualizacion = NOW() AT TIME ZONE 'America/La_Paz';"
baseSvc.MsgCustomResponse = "Listado eventos significativos"
servicesList = append(servicesList, &sincronizacionDatos.SincronizacionDatosService{
ServiceModel: baseSvc,
})
break
case "sincronizar_tipo_habitacion":
baseSvc.TagNames = "sincronizarParametricaTipoHabitacion"
baseSvc.QueryInsert = "INSERT INTO sincronizar_Tipo_Habitacion (codigo, reqSoap, respSoap, reqJson, respJson, cuis_id, fecha_creacion) VALUES ($1, $2, $3, $4, $5, $6, NOW() AT TIME ZONE 'America/La_Paz') ON CONFLICT (codigo, cuis_id) DO UPDATE SET reqSoap = EXCLUDED.reqSoap, respSoap = EXCLUDED.respSoap, reqJson = EXCLUDED.reqJson, respJson = EXCLUDED.respJson, cuis_id = EXCLUDED.cuis_id, fecha_actualizacion = NOW() AT TIME ZONE 'America/La_Paz';"
baseSvc.MsgCustomResponse = "Listado eventos significativos"
servicesList = append(servicesList, &sincronizacionDatos.SincronizacionDatosService{
ServiceModel: baseSvc,
})
break
case "sincronizar_pais_origen":
baseSvc.TagNames = "sincronizarParametricaPaisOrigen"
baseSvc.QueryInsert = "INSERT INTO sincronizar_Pais_Origen (codigo, reqSoap, respSoap, reqJson, respJson, cuis_id, fecha_creacion) VALUES ($1, $2, $3, $4, $5, $6, NOW() AT TIME ZONE 'America/La_Paz') ON CONFLICT (codigo, cuis_id) DO UPDATE SET reqSoap = EXCLUDED.reqSoap, respSoap = EXCLUDED.respSoap, reqJson = EXCLUDED.reqJson, respJson = EXCLUDED.respJson, cuis_id = EXCLUDED.cuis_id, fecha_actualizacion = NOW() AT TIME ZONE 'America/La_Paz';"
baseSvc.MsgCustomResponse = "Listado eventos significativos"
servicesList = append(servicesList, &sincronizacionDatos.SincronizacionDatosService{
ServiceModel: baseSvc,
})
break
default:
log.Printf("Servicio desconocido: %s", svcCfg.Name)
}
}
// Si se especifica un servicio específico, filtrar la lista.
if *serviceName != "" {
var filtered []models.SoapService
for _, s := range servicesList {
if s.GetName() == *serviceName {
filtered = append(filtered, s)
break
}
}
if len(filtered) == 0 {
log.Fatalf("No se encontró el servicio: %s", *serviceName)
}
servicesList = filtered
log.Printf("Ejecutando solo el servicio: %s", *serviceName)
}
// Crear contexto con cancelación para manejar señales de terminación.
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// Canal para capturar señales del sistema.
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
// Iniciar servidor HTTP si se configura puerto para API.
var httpServer *http.Server
if cfg.API.Port > 0 {
httpServer = api.StartAPIServer(cfg.API.Port, servicesList, servicesList2, false)
}
// Goroutine para manejar señales.
go func() {
sig := <-sigChan
log.Printf("Recibida señal: %v. Cerrando...", sig)
if notifier != nil {
notifier.SendNotification("Servicio detenido", fmt.Sprintf("El servicio fue detenido por la señal: %v", sig))
}
cancel()
if httpServer != nil {
log.Println("Cerrando servidor HTTP...")
shutdownCtx, cancelServer := context.WithTimeout(context.Background(), 10*time.Second)
defer cancelServer()
if err := httpServer.Shutdown(shutdownCtx); err != nil {
log.Printf("Error al cerrar servidor HTTP: %v", err)
}
}
}()
// Si se debe ejecutar solo una vez.
if *runOnce {
errors := services.RunAllServices(ctx, servicesList, false)
if len(errors) > 0 {
for _, e := range errors {
log.Printf("Error: %v", e)
}
os.Exit(1)
}
return
}
// Configurar primer cron
c := cron.New(cron.WithSeconds())
_, err = c.AddFunc(cfg.Schedule.FACTURA_SINCRONIZACION.Cron, func() {
log.Println("Iniciando ejecución programada de servicios")
execCtx, execCancel := context.WithCancel(ctx)
defer execCancel()
errors := services.RunAllServices(execCtx, servicesList, true)
if len(errors) > 0 {
errMsg := "Errores durante la ejecución programada:"
for _, err := range errors {
errMsg += "\n- " + err.Error()
}
if notifier != nil {
notifier.SendNotification("Errores en ejecución programada", errMsg)
}
log.Println(errMsg)
} else {
log.Println("Ejecución programada completada exitosamente")
}
})
if err != nil {
log.Fatalf("Error al programar tareas: %v", err)
}
// Configurar segundo cron
cronGetCode := cron.New(cron.WithSeconds())
_, err = cronGetCode.AddFunc(cfg.Schedule.FACTURA_CODIGO.Cron, func() {
log.Println("Iniciando ejecución programada para Obtener Codigos")
execCtx, cancel := context.WithCancel(context.Background())
defer cancel()
//execCtx, execCancel := context.WithCancel(ctx)
//defer execCancel()
errors := services.RunAllGetCodeServices(execCtx, servicesList2, true)
if len(errors) > 0 {
errMsg := "Errores durante la ejecución programada Obtener Codigos:"
for _, err := range errors {
errMsg += "\n- " + err.Error()
}
if notifier != nil {
notifier.SendNotification("Errores en ejecución programada Obtener Codigos", errMsg)
}
log.Println(errMsg)
} else {
log.Println("Ejecución programada completada exitosamente Obtener Codigos")
}
})
if err != nil {
log.Fatalf("Error al programar tareas Obtener Codigos: %v", err)
}
// Iniciar ambos cron
c.Start()
cronGetCode.Start()
log.Printf("Demonio de servicios iniciado. Programación: %s", cfg.Schedule.FACTURA_SINCRONIZACION.Cron)
log.Printf("Demonio de obtención de códigos iniciado. Programación: %s", cfg.Schedule.FACTURA_CODIGO.Cron)
if notifier != nil {
notifier.SendNotification("Servicios iniciados",
fmt.Sprintf("Los servicios SOAP han sido iniciados con programación: %s", cfg.Schedule.FACTURA_SINCRONIZACION.Cron))
}
// Esperar a que el contexto termine y luego detener ambos cron
<-ctx.Done()
c.Stop()
cronGetCode.Stop()
log.Println("Demonios detenidos correctamente")
}

View File

@ -0,0 +1,104 @@
# Configuración del demonio SOAP
database:
host: localhost
port: 5555
user: facturacion_user
password: facturacion_pass
dbname: facturacion_bd
sslmode: disable
soap:
factura_sincronizacion:
endpoint: https://pilotosiatservicios.impuestos.gob.bo/v2/FacturacionSincronizacion
# endpoint: http://localhost:8088/mockServicioFacturacionSincronizacionSoapBinding
timeout: 30 # segundos
retries: 3
factura_codigo:
endpoint: https://pilotosiatservicios.impuestos.gob.bo/v2/FacturacionCodigos
# endpoint: http://localhost:8088/mockServicioFacturacionCodigosSoapBinding
timeout: 30 # segundos
retries: 3
schedule:
factura_sincronizacion:
# cron: "0 0 */6 * * *" # Cada 6 horas
cron: "0 */1 * * * *" # Cada 1 minutos
factura_codigo:
# cron: "0 0 */7 * * *" # Cada 7 horas
cron: "0 */1 * * * *" # Cada 1 minutos
# Configuración de la API HTTP
api:
port: 9999 # Puerto para el servidor HTTP
# Configuración de notificaciones
notifications:
enabled: true
method: "email" # Opciones: "webhook", "email", "slack"
config:
# email - correo
smtp_server: "mail.myapps.bo"
smtp_port: "465"
username: "keycloaktest@myapps.bo"
password: "%l12k*89f1w:"
from_email: "keycloaktest@myapps.bo"
to_email: "ariel.anivarro@myapps.com.bo"
services:
- name: registrar-empresa
enabled: true
concurrency: 1
- name: actividades
enabled: true
concurrency: 1
- name: sincronizar_fecha_hora
enabled: true
concurrency: 1
- name: sincronizar_mensaje_servicio
enabled: true
concurrency: 1
- name: leyendas_factura
enabled: true
concurrency: 1
- name: producto-servicio
enabled: true
concurrency: 1
- name: tipo_motivo_anulacion
enabled: true
concurrency: 1
- name: tipo_actividades_documento_sector
enabled: true
concurrency: 1
- name: tipo_documento_identidad
enabled: true
concurrency: 1
- name: tipo_documento_sector
enabled: true
concurrency: 1
- name: tipo_emision
enabled: true
concurrency: 1
- name: tipo_metodo_pago
enabled: true
concurrency: 1
- name: tipo_moneda
enabled: true
concurrency: 1
- name: tipo_punto_venta
enabled: true
concurrency: 1
- name: tipo_factura
enabled: true
concurrency: 1
- name: tipo_unidad_medida
enabled: true
concurrency: 1
- name: tipo_evento_significativos
enabled: true
concurrency: 1
- name: sincronizar_tipo_habitacion
enabled: true
concurrency: 1
- name: sincronizar_pais_origen
enabled: true
concurrency: 1

Binary file not shown.

10
daemonService/go.mod Normal file
View File

@ -0,0 +1,10 @@
module daemonService
go 1.21.6
require (
github.com/gorilla/mux v1.8.1
github.com/lib/pq v1.10.9
github.com/robfig/cron/v3 v3.0.1
gopkg.in/yaml.v2 v2.4.0
)

10
daemonService/go.sum Normal file
View File

@ -0,0 +1,10 @@
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=

View File

@ -0,0 +1,196 @@
package api
//import (
// "context"
// "daemonService/internal/models"
// "encoding/json"
// "fmt"
// "io"
// "log"
// "net/http"
// "time"
//
// "daemonService/internal/services"
//
// "github.com/gorilla/mux"
//)
//
//// APIResponse estructura la respuesta común del API.
//type APIResponse struct {
// Success bool `json:"success"`
// Message string `json:"message"`
// Data interface{} `json:"data,omitempty"`
// Error string `json:"error,omitempty"`
//}
//
//// setupAPIHandlers configura los endpoints del API HTTP.
//func setupAPIHandlers(router *mux.Router, service []models.SoapService, isCron bool) {
// // Lista de servicios disponibles
// router.HandleFunc("/api/services", func(w http.ResponseWriter, r *http.Request) {
// serviceNames := make([]string, len(service))
// for i, svc := range service {
// serviceNames[i] = svc.GetName()
// }
//
// response := APIResponse{
// Success: true,
// Message: "Lista de servicios disponibles",
// Data: serviceNames,
// }
// w.Header().Set("Content-Type", "application/json")
// json.NewEncoder(w).Encode(response)
// }).Methods("GET")
//
// // Ejecutar todos los servicios
// router.HandleFunc("/api/services/run-all", func(w http.ResponseWriter, r *http.Request) {
// ctx, cancel := context.WithTimeout(r.Context(), 5*time.Minute)
// defer cancel()
//
// go func() {
// errors := services.RunAllServices(ctx, service, isCron) //ariel agrego:request
// log.Printf("Ejecución de todos los servicios completada con %d errores", len(errors))
// }()
//
// response := APIResponse{
// Success: true,
// Message: "Ejecución de todos los servicios iniciada",
// }
// w.Header().Set("Content-Type", "application/json")
// json.NewEncoder(w).Encode(response)
// }).Methods("POST")
//
// // Ejecutar un servicio específico
// router.HandleFunc("/api/services/{name}", func(w http.ResponseWriter, r *http.Request) {
// vars := mux.Vars(r)
//
// //////////////////////////////////////////////////////////////////////////////////////////////
// // Es importante cerrar el body después de leerlo para liberar recursos.
// defer r.Body.Close()
// // Leemos el contenido del body.
// body, err := io.ReadAll(r.Body)
// if err != nil {
// http.Error(w, "Error leyendo el body", http.StatusInternalServerError)
// return
// }
// // Si quieres convertirlo a string:
// bodyStr := string(body)
// fmt.Println("Body de la petición:", bodyStr)
// //////////////////////////////////////////////////////////////////////////////////////////////
//
// serviceName := vars["name"]
//
// ctx, cancel := context.WithTimeout(r.Context(), 5*time.Minute)
// defer cancel()
//
// var encontrado bool
// for _, svc := range service {
// if svc.GetName() == serviceName {
// encontrado = true
// go func() {
// // if err := svc.Execute(ctx); err != nil {
// // log.Printf("Error al ejecutar servicio %s: %v", serviceName, err)
// // } else {
// // log.Printf("Servicio %s ejecutado correctamente", serviceName)
// // }
// if err := svc.Execute(ctx, r, isCron); err != nil { //ariel agrego:request
// log.Printf("Error al ejecutar servicio %s: %v", serviceName, err) ///modificado
// } else {
// log.Printf("Servicio %s ejecutado correctamente", serviceName)
// }
// }()
// break
// }
// }
//
// if !encontrado {
// w.WriteHeader(http.StatusNotFound)
// response := APIResponse{
// Success: false,
// Error: fmt.Sprintf("Servicio no encontrado: %s", serviceName),
// }
// w.Header().Set("Content-Type", "application/json")
// json.NewEncoder(w).Encode(response)
// return
// }
//
// response := APIResponse{
// Success: true,
// Message: fmt.Sprintf("Ejecución del servicio %s iniciada", serviceName),
// }
// w.Header().Set("Content-Type", "application/json")
// json.NewEncoder(w).Encode(response)
// }).Methods("POST")
//
// // Health check
// router.HandleFunc("/api/health", func(w http.ResponseWriter, r *http.Request) {
// response := APIResponse{
// Success: true,
// Message: "Servicio activo",
// Data: map[string]interface{}{
// "status": "healthy",
// "timestamp": time.Now().Format(time.RFC3339),
// },
// }
// w.Header().Set("Content-Type", "application/json")
// json.NewEncoder(w).Encode(response)
// }).Methods("GET")
//}
//
//// StartAPIServer inicia el servidor HTTP con la configuración del API.
//func StartAPIServer(port int, services []models.SoapService, isCron bool) *http.Server {
// router := mux.NewRouter()
// setupAPIHandlers(router, services, isCron)
//
// // Middleware para logging
// router.Use(func(next http.Handler) http.Handler {
// return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// start := time.Now()
// log.Printf("API Request: %s %s", r.Method, r.URL.Path)
// next.ServeHTTP(w, r)
// log.Printf("API Request: %s %s completada en %v", r.Method, r.URL.Path, time.Since(start))
// })
// })
//
// srv := &http.Server{
// Addr: fmt.Sprintf(":%d", port),
// Handler: router,
// ReadTimeout: 15 * time.Second,
// WriteTimeout: 15 * time.Second,
// IdleTimeout: 60 * time.Second,
// }
//
// go func() {
// log.Printf("API HTTP iniciada en puerto %d", port)
// if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
// log.Fatalf("Error en servidor HTTP: %v", err)
// }
// }()
//
// return srv
//}
// // runAllServices ejecuta todos los servicios de manera concurrente.
// func runAllServices(ctx context.Context, services []services.SoapService) []error {
// var wg sync.WaitGroup
// var mu sync.Mutex
// var errors []error
// for _, svc := range services {
// s := svc
// wg.Add(1)
// go func() {
// defer wg.Done()
// serviceCtx, cancel := context.WithCancel(ctx)
// defer cancel()
// // if err := s.Execute(serviceCtx); err != nil {
// if err := s.Execute(serviceCtx, nil); err != nil { //ariel agrego:request
// mu.Lock()
// errors = append(errors, err)
// mu.Unlock()
// }
// }()
// }
// wg.Wait()
// log.Println("Todos los servicios han sido ejecutados")
// return errors
// }

View File

@ -0,0 +1,87 @@
package api
import (
"daemonService/internal/handlers"
"daemonService/internal/models"
"daemonService/internal/models/obtencionCodigos"
"fmt"
"github.com/gorilla/mux"
"log"
"net/http"
"time"
)
type Server struct {
router *mux.Router
services []models.SoapService
servicesCuis []obtencionCodigos.CuisService
isCron bool
port int
}
func NewServer(port int, services []models.SoapService, servicesCuis []obtencionCodigos.CuisService, isCron bool) *Server {
s := &Server{
router: mux.NewRouter(),
services: services,
servicesCuis: servicesCuis,
isCron: isCron,
port: port,
}
s.setupRoutes()
s.setupMiddleware()
return s
}
func (s *Server) setupRoutes() {
// Register service-related routes
serviceHandler := handlers.NewServiceHandler(s.services, s.servicesCuis, s.isCron)
s.router.HandleFunc("/api/services", serviceHandler.ListServices).Methods("GET")
s.router.HandleFunc("/api/services/run-all", serviceHandler.RunAllServices).Methods("GET")
s.router.HandleFunc("/api/services/{name}", serviceHandler.RunService).Methods("GET")
//s.router.HandleFunc("/api/services/run-all", serviceHandler.RunAllServices).Methods("POST")
//s.router.HandleFunc("/api/services/{name}", serviceHandler.RunService).Methods("POST")
//serviceCuisHandler := obtencionCodigo.HandlerServiceCuis(s.services, s.isCron)
s.router.HandleFunc("/api/services/get-code/{obtenerCodigo}", serviceHandler.RunGetCodeService).Methods("POST")
healthHandler := handlers.NewHealthHandler()
s.router.HandleFunc("/api/health", healthHandler.Check).Methods("GET")
}
func (s *Server) setupMiddleware() {
s.router.Use(LoggingMiddleware)
}
func (s *Server) Start() *http.Server {
srv := &http.Server{
Addr: fmt.Sprintf(":%d", s.port),
Handler: s.router,
ReadTimeout: 15 * time.Second,
WriteTimeout: 15 * time.Second,
IdleTimeout: 60 * time.Second,
}
go func() {
log.Printf("API HTTP started on port %d", s.port)
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("HTTP server error: %v", err)
}
}()
return srv
}
func LoggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
log.Printf("API Request: %s %s", r.Method, r.URL.Path)
next.ServeHTTP(w, r)
log.Printf("API Request: %s %s completed in %v", r.Method, r.URL.Path, time.Since(start))
})
}
func StartAPIServer(port int, services []models.SoapService, serviceCuis []obtencionCodigos.CuisService, isCron bool) *http.Server {
server := NewServer(port, services, serviceCuis, isCron)
return server.Start()
}

View File

@ -0,0 +1,127 @@
package config
import (
"os"
"gopkg.in/yaml.v2"
)
// Config estructura la configuración completa del demonio.
type Config struct {
Database struct {
Host string `yaml:"host"`
Port int `yaml:"port"`
User string `yaml:"user"`
Password string `yaml:"password"`
DBName string `yaml:"dbname"`
SSLMode string `yaml:"sslmode"`
} `yaml:"database"`
SOAP struct {
APIKey string `yaml:"apikey"`
FACTURA_SINCRONIZACION struct {
Endpoint string `yaml:"endpoint"`
Timeout int `yaml:"timeout"`
Retries int `yaml:"retries"`
}
FACTURA_CODIGO struct {
Endpoint string `yaml:"endpoint"`
Timeout int `yaml:"timeout"`
Retries int `yaml:"retries"`
}
} `yaml:"soap"`
Schedule struct {
FACTURA_SINCRONIZACION struct {
Cron string `yaml:"cron"`
}
FACTURA_CODIGO struct {
Cron string `yaml:"cron"`
}
} `yaml:"schedule"`
API struct {
Port int `yaml:"port"`
} `yaml:"api"`
Notifications struct {
Enabled bool `yaml:"enabled"`
Method string `yaml:"method"` // "email", "webhook", "slack", etc.
Config map[string]string `yaml:"config"`
} `yaml:"notifications"`
Services []struct {
Name string `yaml:"name"`
Enabled bool `yaml:"enabled"`
Concurrency int `yaml:"concurrency"`
} `yaml:"services"`
}
// LoadConfig carga la configuración desde el archivo YAML.
func LoadConfig(path string) (*Config, error) {
f, err := os.Open(path)
if err != nil {
return nil, err
}
defer f.Close()
var cfg Config
decoder := yaml.NewDecoder(f)
if err := decoder.Decode(&cfg); err != nil {
return nil, err
}
return &cfg, nil
}
// package config
// import (
// "os"
// "gopkg.in/yaml.v2"
// )
// // Config estructura global de configuración.
// type Config struct {
// Database struct {
// Host string `yaml:"host"`
// Port int `yaml:"port"`
// User string `yaml:"user"`
// Password string `yaml:"password"`
// DBName string `yaml:"dbname"`
// SSLMode string `yaml:"sslmode"`
// } `yaml:"database"`
// SOAP struct {
// Endpoint string `yaml:"endpoint"`
// Timeout int `yaml:"timeout"`
// Retries int `yaml:"retries"`
// } `yaml:"soap"`
// Schedule struct {
// Cron string `yaml:"cron"`
// } `yaml:"schedule"`
// API struct {
// Port int `yaml:"port"`
// } `yaml:"api"`
// Notifications struct {
// Enabled bool `yaml:"enabled"`
// Method string `yaml:"method"` // "email", "webhook", "slack", etc.
// Config map[string]string `yaml:"config"`
// } `yaml:"notifications"`
// Services []struct {
// Name string `yaml:"name"`
// Enabled bool `yaml:"enabled"`
// Concurrency int `yaml:"concurrency"`
// } `yaml:"services"`
// }
// // Load carga la configuración a partir de un archivo YAML.
// func Load(configFile string) (*Config, error) {
// f, err := os.Open(configFile)
// if err != nil {
// return nil, err
// }
// defer f.Close()
// var cfg Config
// decoder := yaml.NewDecoder(f)
// if err := decoder.Decode(&cfg); err != nil {
// return nil, err
// }
// return &cfg, nil
// }

View File

@ -0,0 +1,124 @@
package controllers
import (
"daemonService/internal/models"
"daemonService/internal/models/obtencionCodigos/response"
"daemonService/internal/models/sincronizacionDatos/bodyGeneric"
"encoding/json"
"errors"
"net/http"
)
func RespondWithJSON[T any](w http.ResponseWriter, statusCode int, payload bodyGeneric.CustomResponse[T]) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(statusCode)
if err := json.NewEncoder(w).Encode(payload); err != nil {
http.Error(w, "Error encoding JSON response", http.StatusInternalServerError)
}
}
func RespondWithJSONDEMO(w http.ResponseWriter, statusCode int, payload interface{}) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(statusCode)
if err := json.NewEncoder(w).Encode(payload); err != nil {
http.Error(w, "Error encoding JSON response", http.StatusInternalServerError)
}
}
func RespondWithError(w http.ResponseWriter, statusCode int, errMsg string) {
RespondWithJSON(w, statusCode, bodyGeneric.CustomResponse[any]{
Success: false,
Status: statusCode,
Message: errMsg,
Data: nil,
})
}
func RespondWithSuccess(w http.ResponseWriter, statusCode int, message string, data interface{}) {
////df := data.(models.APIResponse)
////
////println(datas)
//
//// Ejemplo: si data viene como []Moneda
//if monedas, ok := data.(models.APIResponse); ok {
// //primera := monedas[0]
// fmt.Printf("Primera moneda: %+v\n", monedas)
//} else {
// // Quizá data es []interface{} (lo típico tras un Unmarshal genérico)
// if arr, ok2 := data.([]interface{}); ok2 {
// // arr[0] es un interface{} que suele ser un map[string]interface{}
// if m0, ok3 := arr[0].(map[string]interface{}); ok3 {
// fmt.Println("Código:", m0["codigo"])
// fmt.Println("Descripción:", m0["descripcion"])
// }
// } else {
// // Tipo inesperado
// http.Error(w, "tipo de dato no soportado en RespondWithSuccess", 500)
// return
// }
//}
//
//RespondWithJSONDEMO(w, statusCode, data)
RespondWithJSON(w, statusCode, bodyGeneric.CustomResponse[any]{
Success: true,
Status: statusCode,
Message: message,
Data: data,
})
}
// nuevo repsonse --------------------------------------------------------------------------------------------
func RespondWithJSONDemo(w http.ResponseWriter, statusCode int, payload models.APIResponseCuis) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(statusCode)
if err := json.NewEncoder(w).Encode(payload); err != nil {
http.Error(w, "Error encoding JSON response", http.StatusInternalServerError)
}
}
// ConvertToCuisMinimal convierte diferentes tipos de datos a CuisMinimal
func ConvertToCuisStruct(data interface{}) (models.CuisMinimal, error) {
var cuisMinimal models.CuisMinimal
switch typedData := data.(type) {
case response.SoapBodyCuis:
cuisMinimal = models.CuisMinimal{
Codigo: typedData.Response.Respuesta.Codigo,
FechaVigencia: typedData.Response.Respuesta.FechaVigencia,
Transaccion: typedData.Response.Respuesta.Transaccion,
}
case models.CuisMinimal:
cuisMinimal = typedData
default:
return models.CuisMinimal{}, data.(error)
//return CuisMinimal{}, fmt.Errorf("tipo de datos incompatible")
}
return cuisMinimal, nil
}
// responde con éxito usando datos de tipo CuisMinimal
func RespondCuisWithSuccess(w http.ResponseWriter, statusCode int, message string, data interface{}) {
cuisMinimal, err := ConvertToCuisStruct(data)
if err != nil {
var apiErr models.ApiErrorCustom
if errors.As(err.(error), &apiErr) {
RespondWithError(w, apiErr.Status, apiErr.Message)
return
} else {
RespondWithError(w, http.StatusInternalServerError, err.Error())
return
}
}
RespondWithJSONDemo(w, statusCode, models.APIResponseCuis{
Success: true,
Message: message,
Data: cuisMinimal,
})
}

View File

@ -0,0 +1,127 @@
package database
import (
"database/sql"
"fmt"
"daemonService/internal/config"
_ "github.com/lib/pq"
)
// InitDB inicializa la conexión a la base de datos y crea las tablas si no existen.
func InitDB(cfg config.Config) (*sql.DB, error) {
connStr := fmt.Sprintf(
"host=%s port=%d user=%s password=%s dbname=%s sslmode=%s",
cfg.Database.Host,
cfg.Database.Port,
cfg.Database.User,
cfg.Database.Password,
cfg.Database.DBName,
cfg.Database.SSLMode,
)
db, err := sql.Open("postgres", connStr)
if err != nil {
return nil, err
}
// Verificar la conexión
if err = db.Ping(); err != nil {
return nil, err
}
// Crear tabla para tipos de documento de identidad si no existe
//_, err = db.Exec(`
// CREATE TABLE IF NOT EXISTS tipo_documento_identidad (
// codigo VARCHAR(10) PRIMARY KEY,
// descripcion TEXT NOT NULL,
// fecha_actualizacion TIMESTAMP NOT NULL
// )
//`)
//if err != nil {
// return nil, err
//}
// Crear tabla para el log de sincronización
//_, err = db.Exec(`
// CREATE TABLE IF NOT EXISTS log_sincronizacion (
// id SERIAL PRIMARY KEY,
// servicio VARCHAR(50) NOT NULL,
// req_soap TEXT,
// resp_soap TEXT,
// req_json TEXT,
// resp_json TEXT,
// fecha TIMESTAMP NOT NULL
// )
//`)
//if err != nil {
// return nil, err
//}
return db, nil
}
// package db
// import (
// "database/sql"
// "fmt"
// "daemonService/internal/config"
// _ "github.com/lib/pq"
// )
// // Init inicializa la conexión a la base de datos y crea las tablas necesarias.
// func Init(cfg *config.Config) (*sql.DB, error) {
// connStr := fmt.Sprintf(
// "host=%s port=%d user=%s password=%s dbname=%s sslmode=%s",
// cfg.Database.Host,
// cfg.Database.Port,
// cfg.Database.User,
// cfg.Database.Password,
// cfg.Database.DBName,
// cfg.Database.SSLMode,
// )
// db, err := sql.Open("postgres", connStr)
// if err != nil {
// return nil, err
// }
// // Verificar la conexión
// if err = db.Ping(); err != nil {
// return nil, err
// }
// // Crear tabla para tipo de documento de identidad
// _, err = db.Exec(`
// CREATE TABLE IF NOT EXISTS tipo_documento_identidad (
// codigo VARCHAR(10) PRIMARY KEY,
// descripcion TEXT NOT NULL,
// fecha_actualizacion TIMESTAMP NOT NULL
// )
// `)
// if err != nil {
// return nil, err
// }
// // Crear tabla de log para auditoría
// _, err = db.Exec(`
// CREATE TABLE IF NOT EXISTS log_sincronizacion (
// id SERIAL PRIMARY KEY,
// servicio VARCHAR(50) NOT NULL,
// req_soap TEXT,
// resp_soap TEXT,
// req_json TEXT,
// resp_json TEXT,
// fecha TIMESTAMP NOT NULL
// )
// `)
// if err != nil {
// return nil, err
// }
// return db, nil
// }

View File

@ -0,0 +1,101 @@
package handlers
import (
"context"
"daemonService/internal/models/obtencionCodigos/request"
"daemonService/internal/repositories"
serv "daemonService/internal/services"
"daemonService/internal/services/procesar"
"daemonService/internal/utils"
"encoding/json"
"fmt"
"net/http"
)
type APIHandler struct {
CuisService *serv.CuisService
CuisRepository *repositories.CuisRepository
ProcesarRegistro *procesar.ProcesarRegistro
}
func NewApiHandler(cuisService *serv.CuisService, cuisRepo *repositories.CuisRepository, procesarRegistro *procesar.ProcesarRegistro) *APIHandler {
return &APIHandler{CuisService: cuisService, CuisRepository: cuisRepo, ProcesarRegistro: procesarRegistro}
}
// ParseCuisRequest parsea la solicitud de CUIS desde el body del request
func (h *APIHandler) ParseCuisRequest(w http.ResponseWriter, req *http.Request) (request.SolicitudCuisCufd, error) {
var cuisRequest request.SolicitudCuisCufd
// Limitar el tamaño del body para prevenir ataques DoS
req.Body = http.MaxBytesReader(w, req.Body, utils.MaxBodySize)
decoder := json.NewDecoder(req.Body)
decoder.DisallowUnknownFields() // Rechazar campos desconocidos
if err := decoder.Decode(&cuisRequest); err != nil {
return cuisRequest, fmt.Errorf("error al decodificar JSON: %v", err)
}
return cuisRequest, nil
}
// maneja las solicitudes API
func (h *APIHandler) HandleAPIRequest(ctx context.Context, w http.ResponseWriter, req *http.Request) (interface{}, error) {
// Parsear la solicitud
cuisRequest, err := h.ParseCuisRequest(w, req)
if err != nil {
return nil, err
}
// Validar la solicitud
if err := h.ValidateCuisRequest(cuisRequest); err != nil {
return nil, fmt.Errorf("datos inválidos: %v", err)
}
// Procesar la solicitud Cuis
respCuis, _, err := h.ProcesarRegistro.CodigoProcesarRegistro(cuisRequest)
if err != nil {
return nil, fmt.Errorf(utils.LogErrorProcessing, cuisRequest.CodigoSistema, err)
}
// Parsear respuesta SOAP
codCuis, fechaVigencia, transaccion, err := utils.ParseSoapCuisResponse(respCuis)
if err != nil {
return nil, fmt.Errorf("error al parsear respuesta CUIS SOAP: %v", err)
}
// Procesamos la solicitud Cufd
cuisRequest.Cuis = codCuis
_, codCufd, err := h.ProcesarRegistro.CodigoProcesarRegistro(cuisRequest)
if err != nil {
return nil, fmt.Errorf(utils.LogErrorProcessing, cuisRequest.CodigoSistema, err)
}
fmt.Println("codigo CUFD: %v", codCufd)
cufdData, err := utils.ParseSoapCufdResponse(codCufd)
if err != nil {
return nil, fmt.Errorf("error al parsear respuesta CUFD SOAP: %v", err)
}
// Crear nuevo registro
if err := h.CuisRepository.CrearNuevoRegistro(ctx, cuisRequest, codCuis, fechaVigencia, transaccion, cufdData); err != nil {
//return nil, fmt.Errorf("error creando nuevo registro: %v", err)
return nil, err
}
return respCuis, nil
}
// valida los datos de la solicitud CUIS
func (s *APIHandler) ValidateCuisRequest(request request.SolicitudCuisCufd) error {
if request.CodigoAmbiente == "" ||
request.CodigoModalidad == "" ||
request.CodigoPuntoVenta == "" ||
request.CodigoSistema == "" ||
request.CodigoSucursal == "" ||
request.Nit == "" {
return utils.ErrAllFieldsRequired
}
return nil
}

View File

@ -0,0 +1,165 @@
// internal/handlers/cron_handler.go
package handlers
import (
"context"
"daemonService/internal/repositories"
service "daemonService/internal/services"
"daemonService/internal/services/procesar"
"daemonService/internal/utils"
"fmt"
)
type CronHandler struct {
CuisService *service.CuisService
EmpresaRepo *repositories.EmpresaRepository
CuisRespository *repositories.CuisRepository
ProcesarRegistro *procesar.ProcesarRegistro
}
// Ahora recibimos también la instancia de CuisService
func NewCronHandler(
cuisService *service.CuisService,
empresaRepo *repositories.EmpresaRepository,
cuisRespository *repositories.CuisRepository,
procesarRegistro *procesar.ProcesarRegistro,
) *CronHandler {
return &CronHandler{
CuisService: cuisService,
EmpresaRepo: empresaRepo,
CuisRespository: cuisRespository,
ProcesarRegistro: procesarRegistro,
}
}
func (s *CronHandler) ProcessCronMode(dbCtx context.Context) (interface{}, error) {
empresasConCuis, err := s.EmpresaRepo.GetEmpresasConCuisMinimal(dbCtx)
if err != nil {
return nil, s.CuisService.LogError("Error al obtener empresas con CUIS: %v", err)
}
var lastResponse interface{}
for _, ec := range empresasConCuis {
s.CuisService.LogMessage("Procesando empresa ID: %d", ec.ID)
if len(ec.Cuis) == 0 {
s.CuisService.LogMessage("Empresa ID %d sin CUIS registrados", ec.ID)
continue
}
for _, c := range ec.Cuis {
s.CuisService.LogMessage("Procesando CUIS ID: %d", c.Cuis_id)
cuisRequest := s.CuisService.BuildCuisRequestFromEmpresa(ec)
respCuis, _, err := s.ProcesarRegistro.CodigoProcesarRegistro(cuisRequest)
if err != nil {
s.CuisService.LogError(utils.LogErrorProcessing, err, cuisRequest.CodigoSistema)
continue
}
// Parsear respuesta SOAP
codCuis, fechaVigencia, transaccion, err := utils.ParseSoapCuisResponse(respCuis)
if err != nil {
return nil, fmt.Errorf("error al parsear respuesta CUIS SOAP: %v", err)
}
// Procesamos la solicitud Cufd
cuisRequest.Cuis = codCuis
_, codCufd, err := s.ProcesarRegistro.CodigoProcesarRegistro(cuisRequest)
if err != nil {
return nil, fmt.Errorf(utils.LogErrorProcessing, cuisRequest.CodigoSistema, err)
}
fmt.Println("codigo CUFD: %v", codCufd)
cufdData, err := utils.ParseSoapCufdResponse(codCufd)
if err != nil {
return nil, fmt.Errorf("error al parsear respuesta CUFD SOAP: %v", err)
}
fmt.Println("codigo CUFD: %v", cufdData)
//if err := s.CuisRespository.ActualizarRegistro(dbCtx, ec.ID, c.Cuis_id, cuisRequest, respCuis); err != nil {
// s.CuisService.LogError("Error actualizando registro: %v", err)
// continue
//}
// Crear nuevo registro
if err := s.CuisRespository.CrearNuevoRegistro(dbCtx, cuisRequest, codCuis, fechaVigencia, transaccion, cufdData); err != nil {
//return nil, fmt.Errorf("error creando nuevo registro: %v", err)
return nil, err
}
lastResponse = respCuis
}
}
return lastResponse, nil
}
//package handlers
//
//import (
// "context"
// "daemonService/internal/repositories"
// service "daemonService/internal/services"
// "daemonService/internal/services/procesar"
// "daemonService/internal/utils"
//)
//
//type CronHandler struct {
// CuisService *service.CuisService
// EmpresaRepo *repositories.EmpresaRepository
// CuisRespository *repositories.CuisRepository
// ProcesarRegistro *procesar.ProcesarRegistro
//}
//
//// creamos una instancia
//func NewCronHandler(
// empresaRepo *repositories.EmpresaRepository, cuisRespository *repositories.CuisRepository,
// procesarRegistro *procesar.ProcesarRegistro,
//) *CronHandler {
// return &CronHandler{
// EmpresaRepo: empresaRepo, CuisRespository: cuisRespository,
// ProcesarRegistro: procesarRegistro,
// }
//}
//
//// procesa la ejecución en modo cron
//func (s *CronHandler) ProcessCronMode(dbCtx context.Context) (interface{}, error) {
// empresasConCuis, err := s.EmpresaRepo.GetEmpresasConCuisMinimal(dbCtx)
// if err != nil {
// return nil, s.CuisService.LogError("Error al obtener empresas con CUIS: %v", err)
// }
//
// var lastResponse interface{}
// for _, ec := range empresasConCuis {
// s.CuisService.LogMessage("Procesando empresa ID: %d", ec.ID)
//
// if len(ec.Cuis) == 0 {
// s.CuisService.LogMessage("Empresa ID %d sin CUIS registrados", ec.ID)
// continue
// }
//
// for _, c := range ec.Cuis {
// s.CuisService.LogMessage("Procesando CUIS ID: %d", c.Cuis_id)
//
// cuisRequest := s.CuisService.BuildCuisRequestFromEmpresa(ec)
// respCuis, err := s.ProcesarRegistro.CodigoProcesarRegistro(cuisRequest, 0)
//
// if err != nil {
// s.CuisService.LogError(utils.LogErrorProcessing, err, cuisRequest.CodigoSistema)
// continue
// }
//
// if err := s.CuisRespository.ActualizarRegistro(dbCtx, ec.ID, c.Cuis_id, cuisRequest, respCuis); err != nil {
// s.CuisService.LogError("Error actualizando registro: %v", err)
// continue
// }
//
// lastResponse = respCuis
// }
// }
//
// return lastResponse, nil
//}

View File

@ -0,0 +1,29 @@
package handlers
import (
"daemonService/internal/models"
"daemonService/internal/models/obtencionCodigos"
"fmt"
)
func HandleError(s *models.ServiceModel, format string, args ...interface{}) error {
errMsg := fmt.Sprintf(format, args...)
if s.NotificationSender != nil {
s.NotificationSender.SendNotification(
fmt.Sprintf("Error en servicio %s", s.Name),
errMsg,
)
}
return fmt.Errorf(errMsg)
}
func HandleGetCodeError(s *obtencionCodigos.CuisServiceModel, format string, args ...interface{}) error {
errMsg := fmt.Sprintf(format, args...)
if s.NotificationSender != nil {
s.NotificationSender.SendNotification(
fmt.Sprintf("Error en servicio %s", s.Name),
errMsg,
)
}
return fmt.Errorf(errMsg)
}

View File

@ -0,0 +1,22 @@
package handlers
import (
"daemonService/internal/controllers"
"net/http"
"time"
)
type HealthHandler struct{}
func NewHealthHandler() *HealthHandler {
return &HealthHandler{}
}
func (h *HealthHandler) Check(w http.ResponseWriter, r *http.Request) {
healthData := map[string]interface{}{
"status": "healthy",
"timestamp": time.Now().Format(time.RFC3339),
}
controllers.RespondWithSuccess(w, http.StatusOK, "El servicio está activo.", healthData)
}

View File

@ -0,0 +1,157 @@
package handlers
import (
"bytes"
"context"
"daemonService/internal/controllers"
"daemonService/internal/models"
"daemonService/internal/models/obtencionCodigos"
"daemonService/internal/services"
"fmt"
"io"
"log"
"net/http"
"time"
"github.com/gorilla/mux"
)
type ServiceHandler struct {
services []models.SoapService
serviceCuis []obtencionCodigos.CuisService
isCron bool
}
func NewServiceHandler(services []models.SoapService, serviceCuis []obtencionCodigos.CuisService, isCron bool) *ServiceHandler {
return &ServiceHandler{
services: services,
serviceCuis: serviceCuis,
isCron: isCron,
}
}
func (h *ServiceHandler) ListServices(w http.ResponseWriter, r *http.Request) {
serviceNames := make([]string, len(h.services))
for i, svc := range h.services {
serviceNames[i] = svc.GetName()
}
controllers.RespondWithSuccess(w, http.StatusOK, "Lista de servicios disponibles", serviceNames)
}
func (h *ServiceHandler) RunAllServices(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Minute)
defer cancel()
go func() {
errors := services.RunAllServices(ctx, h.services, h.isCron)
log.Printf("All services execution completed with %d errors", len(errors))
}()
controllers.RespondWithSuccess(w, http.StatusAccepted, "Se inició la ejecución de todos los servicios.", nil)
}
func (h *ServiceHandler) RunService(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
serviceName := vars["name"]
bodyStr, err := h.readRequestBody(r)
if err != nil {
controllers.RespondWithError(w, http.StatusInternalServerError, fmt.Sprintf("Error al leer el request body: %v", err))
return
}
fmt.Println("Request body:", bodyStr)
service, found := h.findServiceByName(serviceName)
if !found {
controllers.RespondWithError(w, http.StatusNotFound, fmt.Sprintf("Servicio no encontrado: %s", serviceName))
return
}
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Minute)
defer cancel()
respChannel := make(chan interface{}, 1)
go func() {
resp := h.executeService(ctx, service, r, serviceName)
respChannel <- resp
}()
resp := <-respChannel
//controllers.RespondWithSuccess(w, http.StatusAccepted, fmt.Sprintf("Service %s execution initiated", serviceName), resp)
controllers.RespondWithSuccess(w, 200, fmt.Sprintf("Listado del servicio %s", serviceName), resp)
}
func (h *ServiceHandler) readRequestBody(r *http.Request) (string, error) {
body, err := io.ReadAll(r.Body)
if err != nil {
return "", err
}
r.Body.Close()
r.Body = io.NopCloser(bytes.NewBuffer(body))
return string(body), nil
}
func (h *ServiceHandler) findServiceByName(name string) (models.SoapService, bool) {
for _, svc := range h.services {
if svc.GetName() == name {
return svc, true
}
}
return nil, false
}
func (h *ServiceHandler) executeService(ctx context.Context, service models.SoapService, r *http.Request, serviceName string) interface{} {
resp, err := service.Execute(ctx, r, h.isCron)
if err != nil {
log.Printf("Error al ejecutar el servicio %s: %v", serviceName, err)
return nil
} else {
log.Printf("Servicio %s ejecutado satisfactoriamente", serviceName)
return resp
}
}
// registrar empresa -------------------------------------------------------------------------------------------------------
func (h *ServiceHandler) RunGetCodeService(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
serviceName := vars["obtenerCodigo"]
service, found := h.findGetCodeByName(serviceName)
if !found {
controllers.RespondWithError(w, http.StatusNotFound, fmt.Sprintf("Servicio no encontrado: %s", serviceName))
return
}
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Minute)
defer cancel()
respChannel := make(chan interface{}, 1)
go func() {
respCuis := h.executeGetCodeService(ctx, service, w, r, serviceName)
respChannel <- respCuis
}()
respCuis := <-respChannel
controllers.RespondCuisWithSuccess(w, http.StatusAccepted, fmt.Sprintf("Servicio %s ejecucion iniciada", respCuis), respCuis)
}
func (h *ServiceHandler) executeGetCodeService(ctx context.Context, service obtencionCodigos.CuisService, w http.ResponseWriter, r *http.Request, serviceName string) interface{} {
//var result interface{}
respCuis, err := service.ExecuteGetCode(ctx, w, r, h.isCron)
if err != nil {
log.Printf("Error ejecutando servicio %s: %v", serviceName, err)
return err // o podrías retornar el error
} else {
log.Printf("Servicio %s ejecutado satisfactoriamente", serviceName)
return respCuis // Aquí retornarías el resultado del servicio
}
}
func (h *ServiceHandler) findGetCodeByName(name string) (obtencionCodigos.CuisService, bool) {
for _, svc := range h.serviceCuis {
if svc.GetName() == name {
return svc, true
}
}
return nil, false
}

View File

@ -0,0 +1,12 @@
package models
type ApiErrorCustom struct {
Success bool `json:"success"`
Status int `json:"status"`
Message string `json:"message"`
Data interface{} `json:"data,omitempty"`
}
func (e ApiErrorCustom) Error() string {
return e.Message
}

View File

@ -0,0 +1,10 @@
package obtencionCodigos
//type CuisRequestModel struct {
// CodigoAmbiente string `json:"CodigoAmbiente"`
// CodigoModalidad string `json:"codigoModalidad"`
// CodigoPuntoVenta string `json:"CodigoPuntoVenta"`
// CodigoSistema string `json:"CodigoSistema"`
// CodigoSucursal string `json:"CodigoSucursal"`
// Nit string `json:"Nit"`
//}

View File

@ -0,0 +1,34 @@
package obtencionCodigos
import (
"context"
"daemonService/internal/notifications"
"database/sql"
"net/http"
"time"
)
// interface
type CuisService interface {
ExecuteGetCode(ctx context.Context, w http.ResponseWriter, req *http.Request, isCron bool) (interface{}, error)
GetName() string
}
// configuración y dependencias para cada servicio
type CuisServiceModel struct {
Name string
Enabled bool
Concurrency int
DB *sql.DB
SOAPEndpoint string
SOAPTimeout time.Duration
SOAPRetries int
NotificationSender notifications.NotificationSender
//APIKey string
TokenKey string
TokenValue string
TagNames string
TagNamesCufd string
QueryInsert string
MsgCustomResponse string
}

View File

@ -0,0 +1,63 @@
package obtencionCodigos
import "time"
// mapea toda la tabla registroEmpresa
type RegistroEmpresa struct {
ID int `sql:"id"`
CodigoAmbiente int `sql:"codigo_ambiente"`
CodigoModalidad int `sql:"codigo_modalidad"`
CodigoPuntoVenta int `sql:"codigo_punto_venta"`
CodigoSistema string `sql:"codigo_sistema"`
CodigoSucursal int `sql:"codigo_sucursal"`
Nit string `sql:"nit"`
FechaCreacion time.Time `sql:"fecha_creacion"`
FechaActualizacion time.Time `sql:"fecha_actualizacion"`
TokenKey string `sql:"token_key"`
TokenValue string `sql:"token_value"`
NombreArchivoCertificado string `sql:"nombre_archivo_certificado"`
NombreArchivoClavePrivada string `sql:"nombre_archivo_clave_privada"`
}
// solo tiene los dos campos que quieres de la tabla cuis
type CuisMinimal struct {
Cuis_id int64 `sql:"id"`
Cuis string `sql:"cuis"`
FechaVigencia time.Time `sql:"fecha_vigencia"`
}
// agrupa una empresa con sus cuis mínimos
type EmpresaConCuis struct {
RegistroEmpresa
Cuis []CuisMinimal
}
//import (
//"time"
//)
//
//// RegistroEmpresa mapea toda la tabla registroEmpresa
//type RegistroEmpresa struct {
// ID int `sql:"id"`
// CodigoAmbiente int `sql:"codigo_ambiente"`
// CodigoModalidad int `sql:"codigo_modalidad"`
// CodigoPuntoVenta int `sql:"codigo_punto_venta"`
// CodigoSistema string `sql:"codigo_sistema"`
// CodigoSucursal int `sql:"codigo_sucursal"`
// Nit string `sql:"nit"`
// FechaCreacion time.Time `sql:"fecha_creacion"`
// FechaActualizacion time.Time `sql:"fecha_actualizacion"`
//}
//
//// CuisMinimal solo tiene los dos campos que quieres de la tabla cuis
//type CuisMinimal struct {
// Cuis_id int64 `sql:"id"`
// Cuis string `sql:"cuis"`
// FechaVigencia time.Time `sql:"fecha_vigencia"`
//}
//
//// EmpresaConCuis agrupa una empresa con sus cuis mínimos
//type EmpresaConCuis struct {
// RegistroEmpresa
// Cuis []CuisMinimal
//}

View File

@ -0,0 +1,32 @@
package request
import "encoding/xml"
// SOAPRequestEnvelope define la estructura general del mensaje SOAP.
type SOAPRequestEnvelopeCufd struct {
XMLName xml.Name `xml:"soapenv:Envelope"`
XmlnsSoapenv string `xml:"xmlns:soapenv,attr"` // URL del namespace SOAP
XmlnsSiat string `xml:"xmlns:siat,attr"` // URL del namespace siat
Header *SOAPRequestHeaderCufd `xml:"soapenv:Header,omitempty"`
Body SOAPRequestBodyCufd `xml:"soapenv:Body"`
}
type SOAPRequestHeaderCufd struct{}
type SOAPRequestBodyCufd struct {
Operacion OperacionXMLCufd `xml:",any,omitempty"`
}
type OperacionXMLCufd struct {
XMLName xml.Name
Solicitud SolicitudCuisCufd `xml:"SolicitudCufd"`
}
// permite controlar la serialización de OperacionXML para usar el nombre de etiqueta deseado.
func (o OperacionXMLCufd) MarshalXML2(e *xml.Encoder, start xml.StartElement) error {
start.Name = o.XMLName
// Encapsulamos la solicitud en una estructura anónima para que se serialice correctamente.
return e.EncodeElement(struct {
Solicitud SolicitudCuisCufd `xml:"SolicitudCufd"`
}{o.Solicitud}, start)
}

View File

@ -0,0 +1,49 @@
package request
import "encoding/xml"
// SOAPRequestEnvelope define la estructura general del mensaje SOAP.
type SOAPRequestEnvelope struct {
XMLName xml.Name `xml:"soapenv:Envelope"`
XmlnsSoapenv string `xml:"xmlns:soapenv,attr"` // URL del namespace SOAP
XmlnsSiat string `xml:"xmlns:siat,attr"` // URL del namespace siat
Header *SOAPRequestHeader `xml:"soapenv:Header,omitempty"`
Body SOAPRequestBody `xml:"soapenv:Body"`
}
type SOAPRequestHeader struct{}
type SOAPRequestBody struct {
Operacion OperacionXML `xml:",any,omitempty"`
}
// /////////////////////////////// CUIS //////////////////////////////////////
type OperacionXML struct {
XMLName xml.Name
Solicitud SolicitudCuisCufd `xml:"SolicitudCuis"`
}
// permite controlar la serialización de OperacionXML para usar el nombre de etiqueta deseado.
func (o OperacionXML) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
start.Name = o.XMLName
// Encapsulamos la solicitud en una estructura anónima para que se serialice correctamente.
return e.EncodeElement(struct {
Solicitud SolicitudCuisCufd `xml:"SolicitudCuis"`
}{o.Solicitud}, start)
}
// /////////////////////////////// CUIS //////////////////////////////////////
type SolicitudCuisCufd struct {
CodigoAmbiente string `xml:"codigoAmbiente,omitempty" json:"codigoAmbiente"`
CodigoModalidad string `xml:"codigoModalidad,omitempty" json:"codigoModalidad"`
CodigoPuntoVenta string `xml:"codigoPuntoVenta,omitempty" json:"codigoPuntoVenta"`
CodigoSistema string `xml:"codigoSistema,omitempty" json:"codigoSistema"`
CodigoSucursal string `xml:"codigoSucursal,omitempty" json:"codigoSucursal"`
Nit string `xml:"nit,omitempty" json:"nit"`
Cuis string `xml:"cuis,omitempty" json:"cuis,omitempty"`
KeyToken string `xml:"keyToken,omitempty" json:"keyToken,omitempty"`
ValueToken string `xml:"valueToken,omitempty" json:"valueToken,omitempty"`
NombreArchivoCertificado string `xml:"nombreArchivoCertificado,omitempty" json:"nombreArchivoCertificado,omitempty"`
NombreArchivoClavePrivada string `xml:"nombreArchivoClavePrivada,omitempty" json:"nombreArchivoClavePrivada,omitempty"`
}

View File

@ -0,0 +1,7 @@
package response
// Fault estándar
type SoapFault struct {
FaultCode string `xml:"faultcode"`
FaultString string `xml:"faultstring"`
}

View File

@ -0,0 +1,33 @@
package response
import "encoding/xml"
// Estructuras para respuesta SOAP
type SoapEnvelopeCufd struct {
XMLName xml.Name `xml:"Envelope"`
Body SoapBodyCufd `xml:"Body"`
}
type SoapBodyCufd struct {
Response *CufdResponse `xml:"cufdResponse,omitempty" json:"response,omitempty"`
ResponseError *SoapFault `xml:"Fault,omitempty" json:"responseError,omitempty"`
}
type CufdResponse struct {
Respuesta RespuestaCufd `xml:"RespuestaCufd"`
}
type RespuestaCufd struct {
Codigo string `xml:"codigo,omitempty" json:"codigo,omitempty"`
CodigoControl string `xml:"codigoControl,omitempty" json:"codigoControl,omitempty"`
Direccion string `xml:"direccion,omitempty" json:"direccion,omitempty"`
FechaVigencia string `xml:"fechaVigencia,omitempty" json:"fechaVigencia,omitempty"`
Transaccion bool `xml:"transaccion,omitempty" json:"transaccion,omitempty"`
MensajesList []MsgDetalleCufd `xml:"mensajesList,omitempty" json:"mensajesList,omitempty"`
}
// CUSTOMER RESPONSE: parte del Body se envia al CustomerResponseModel.go
type MsgDetalleCufd struct {
Codigo string `xml:"codigo,omitempty" json:"codigo,omitempty"`
Descripcion string `xml:"descripcion,omitempty" json:"descripcion,omitempty"`
}

View File

@ -0,0 +1,31 @@
package response
import "encoding/xml"
// Estructuras para respuesta SOAP
type SoapEnvelopeCuis struct {
XMLName xml.Name `xml:"Envelope"`
Body SoapBodyCuis `xml:"Body"`
}
type SoapBodyCuis struct {
Response *CuisResponse `xml:"cuisResponse,omitempty" json:"response,omitempty"`
ResponseError *SoapFault `xml:"Fault,omitempty" json:"responseError,omitempty"`
}
type CuisResponse struct {
Respuesta RespuestaCuis `xml:"RespuestaCuis"`
}
type RespuestaCuis struct {
Codigo string `xml:"codigo,omitempty" json:"codigo,omitempty"`
FechaVigencia string `xml:"fechaVigencia,omitempty" json:"fechaVigencia,omitempty"`
Transaccion bool `xml:"transaccion,omitempty" json:"transaccion,omitempty"`
MensajesList []MsgDetalle `xml:"mensajesList,omitempty" json:"mensajesList,omitempty"`
}
// CUSTOMER RESPONSE: parte del Body se envia al CustomerResponseModel.go
type MsgDetalle struct {
Codigo string `xml:"codigo,omitempty" json:"codigo,omitempty"`
Mescripcion string `xml:"descripcion,omitempty" json:"descripcion,omitempty"`
}

View File

@ -0,0 +1,21 @@
package models
type APIResponse struct {
Success bool `json:"success"`
Message string `json:"message"`
Data interface{} `json:"data,omitempty"`
Error string `json:"error,omitempty"`
}
type APIResponseCuis struct {
Success bool `json:"success"`
Message string `json:"message"`
Data CuisMinimal `json:"data,omitempty"`
Error string `json:"error,omitempty"`
}
type CuisMinimal struct {
Codigo string `json:"codigo"`
FechaVigencia string `json:"fechaVigencia"`
Transaccion bool `json:"transaccion"`
}

View File

@ -0,0 +1,8 @@
package bodyGeneric
type CustomResponse[T any] struct {
Success bool `json:"success"`
Status int `json:"status"`
Message string `json:"message"`
Data T `json:"data"`
}

View File

@ -0,0 +1,18 @@
package entities
import (
"database/sql"
"time"
)
type TipoMoneda struct {
ID int
Codigo string
ReqSoap sql.NullString
RespSoap sql.NullString
ReqJson sql.NullString
RespJson sql.NullString
FechaCreacion time.Time
FechaActualizacion time.Time
CUISID int
}

View File

@ -0,0 +1,43 @@
package request
import "encoding/xml"
// SOAPRequestEnvelope define la estructura general del mensaje SOAP.
type SOAPRequestEnvelope struct {
XMLName xml.Name `xml:"soapenv:Envelope"`
XmlnsSoapenv string `xml:"xmlns:soapenv,attr"` // URL del namespace SOAP
XmlnsSiat string `xml:"xmlns:siat,attr"` // URL del namespace siat
Header *SOAPRequestHeader `xml:"soapenv:Header,omitempty"`
//Body SOAPRequestBody `xml:"soapenv:Body"`
Body SOAPRequestBody `xml:"soapenv:Body"`
}
type SOAPRequestHeader struct{}
type SOAPRequestBody struct {
Operacion OperacionXML `xml:",any"`
}
type OperacionXML struct {
XMLName xml.Name
Solicitud SolicitudSincronizacion `xml:"SolicitudSincronizacion"`
}
// MarshalXML permite controlar la serialización de OperacionXML para usar el nombre de etiqueta deseado.
func (o OperacionXML) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
start.Name = o.XMLName
// Encapsulamos la solicitud en una estructura anónima para que se serialice correctamente.
return e.EncodeElement(struct {
Solicitud SolicitudSincronizacion `xml:"SolicitudSincronizacion"`
}{o.Solicitud}, start)
}
// SolicitudSincronizacion representa la información de la sincronización que se debe enviar.
type SolicitudSincronizacion struct {
CodigoAmbiente string `xml:"codigoAmbiente,omitempty"`
CodigoPuntoVenta string `xml:"codigoPuntoVenta,omitempty"`
CodigoSistema string `xml:"codigoSistema,omitempty"`
CodigoSucursal string `xml:"codigoSucursal,omitempty"`
Cuis string `xml:"cuis,omitempty"`
Nit string `xml:"nit,omitempty"`
}

View File

@ -0,0 +1,50 @@
package response
import (
"encoding/xml"
"fmt"
)
// Estructuras para respuesta SOAP
type SoapEnvelopeLeyendas struct {
XMLName xml.Name `xml:"Envelope"`
Body SoapBodyLeyendas `xml:"Body"`
}
type SoapBodyLeyendas struct {
Response SincronizarLeyendasResponse `xml:"sincronizarListaLeyendasFacturaResponse"`
}
type SincronizarLeyendasResponse struct {
Respuesta RespuestaListaLeyendas `xml:"RespuestaListaParametricasLeyendas"`
}
type RespuestaListaLeyendas struct {
Transaccion bool `xml:"transaccion" json:"transaccion"`
ListaLeyendas []LeyendaDetalle `xml:"listaLeyendas" json:"listaLeyendas"`
}
// CUSTOMER RESPONSE: parte del Body se envia al CustomerResponseModel.go
type LeyendaDetalle struct {
CodigoProducto string `xml:"codigoProducto,omitempty" json:"codigo,omitempty"`
CodigoActividad string `xml:"codigoActividad" json:"codigoActividad"`
DescripcionLeyenda string `xml:"descripcionLeyenda" json:"descripcion"`
}
// Implementaciones para cada tipo de envelope
func (e SoapEnvelopeLeyendas) IsTransactionSuccessful() bool {
return e.Body.Response.Respuesta.Transaccion
}
func (e SoapEnvelopeLeyendas) GetData() []LeyendaDetalle {
return e.Body.Response.Respuesta.ListaLeyendas
}
// Método modificado para usar SoapEnvelopeLeyendas como receptor
func (r SoapEnvelopeLeyendas) AsignarCodigosSecuenciales() {
for i := range r.Body.Response.Respuesta.ListaLeyendas {
// Asignar un código secuencial comenzando desde 1
//r.Body.Response.Respuesta.ListaLeyendas[i].CodigoProducto = fmt.Sprintf("PROD%04d", i+1)
r.Body.Response.Respuesta.ListaLeyendas[i].CodigoProducto = fmt.Sprintf("%d", i+1)
}
}

View File

@ -0,0 +1,39 @@
package response
import (
"encoding/xml"
)
type SoapEnvelopeProductoServicio struct {
XMLName xml.Name `xml:"Envelope"`
Body SoapBodyProductoServicio `xml:"Body"`
}
type SoapBodyProductoServicio struct {
Response SincronizarResponseProductoServicio `xml:"sincronizarListaProductosServiciosResponse"`
}
type SincronizarResponseProductoServicio struct {
RespuestaLista RespuestaListaParametricasProductoServicio `xml:"RespuestaListaProductos"`
}
type RespuestaListaParametricasProductoServicio struct {
Transaccion bool `xml:"transaccion"`
ListaCodigos []ProductoServicio `xml:"listaCodigos"`
}
type ProductoServicio struct {
CodigoActividad string `xml:"codigoActividad,omitempty" json:"codigoActividad,omitempty"`
CodigoProducto string `xml:"codigoProducto,omitempty" json:"codigo,omitempty"`
DescripcionProducto string `xml:"descripcionProducto,omitempty" json:"descripcion,omitempty"`
Nandina []string `xml:"nandina,omitempty" json:"nandina,omitempty"`
}
// Implementaciones para cada tipo de envelope
func (e SoapEnvelopeProductoServicio) IsTransactionSuccessful() bool {
return e.Body.Response.RespuestaLista.Transaccion
}
func (e SoapEnvelopeProductoServicio) GetData() []ProductoServicio {
return e.Body.Response.RespuestaLista.ListaCodigos
}

View File

@ -0,0 +1,39 @@
package response
import (
"encoding/xml"
)
// Estructuras para la respuesta SOAP.
type SoapEnvelopeActividad struct {
XMLName xml.Name `xml:"Envelope"`
Body SoapBodyActividad `xml:"Body"`
}
type SoapBodyActividad struct {
Response SincronizarResponseActividad `xml:"sincronizarActividadesResponse"`
}
type SincronizarResponseActividad struct {
RespuestaLista RespuestaListaActividades `xml:"RespuestaListaActividades"`
}
type RespuestaListaActividades struct {
Transaccion bool `xml:"transaccion"`
ListaCodigos []ListaActividades `xml:"listaActividades"`
}
type ListaActividades struct {
CodigoCaeb string `xml:"codigoCaeb,omitempty" json:"codigo,omitempty"`
Descripcion string `xml:"descripcion,omitempty" json:"descripcion,omitempty"`
TipoActividad string `xml:"tipoActividad,omitempty" json:"tipoActividad,omitempty"`
}
// Implementaciones para cada tipo de envelope
func (e SoapEnvelopeActividad) IsTransactionSuccessful() bool {
return e.Body.Response.RespuestaLista.Transaccion
}
func (e SoapEnvelopeActividad) GetData() []ListaActividades {
return e.Body.Response.RespuestaLista.ListaCodigos
}

View File

@ -0,0 +1,37 @@
package response
import (
"encoding/xml"
)
type SoapEnvelopeEventoSignificativo struct {
XMLName xml.Name `xml:"Envelope"`
Body SoapBodyEventoSignificativo `xml:"Body"`
}
type SoapBodyEventoSignificativo struct {
Response SincronizarResponseEventoSignificativo `xml:"sincronizarParametricaEventosSignificativosResponse"`
}
type SincronizarResponseEventoSignificativo struct {
RespuestaLista RespuestaListaParametricasEventoSignificativo `xml:"RespuestaListaParametricas"`
}
type RespuestaListaParametricasEventoSignificativo struct {
Transaccion bool `xml:"transaccion"`
ListaCodigos []EventoSignificativo `xml:"listaCodigos"`
}
type EventoSignificativo struct {
CodigoClasificador string `xml:"codigoClasificador" json:"codigo"`
Descripcion string `xml:"descripcion" json:"descripcion"`
}
// Implementaciones para cada tipo de envelope
func (e SoapEnvelopeEventoSignificativo) IsTransactionSuccessful() bool {
return e.Body.Response.RespuestaLista.Transaccion
}
func (e SoapEnvelopeEventoSignificativo) GetData() []EventoSignificativo {
return e.Body.Response.RespuestaLista.ListaCodigos
}

View File

@ -0,0 +1,28 @@
package response
import (
"encoding/xml"
)
// Estructuras para la respuesta SOAP.
type SoapEnvelopeFechaHora struct {
XMLName xml.Name `xml:"Envelope"`
Body SoapBodyFechaHora `xml:"Body"`
}
type SoapBodyFechaHora struct {
Response SincronizarResponseFechaHora `xml:"sincronizarFechaHoraResponse"`
}
type SincronizarResponseFechaHora struct {
RespuestaFechaHora RespuestaFechaHora `xml:"RespuestaFechaHora"`
}
type RespuestaFechaHora struct {
//Transaccion bool `xml:"transaccion"`
FechaHora string `xml:"fechaHora"`
}
func (e SoapEnvelopeFechaHora) GetData() RespuestaFechaHora {
return e.Body.Response.RespuestaFechaHora
}

View File

@ -0,0 +1,37 @@
package response
import (
"encoding/xml"
)
type SoapEnvelopeMensajeServicio struct {
XMLName xml.Name `xml:"Envelope"`
Body SoapBodyMensajeServicio `xml:"Body"`
}
type SoapBodyMensajeServicio struct {
Response SincronizarResponseMensajeServicio `xml:"sincronizarListaMensajesServiciosResponse"`
}
type SincronizarResponseMensajeServicio struct {
RespuestaLista RespuestaListaParametricasMensajeServicio `xml:"RespuestaListaParametricas"`
}
type RespuestaListaParametricasMensajeServicio struct {
Transaccion bool `xml:"transaccion"`
ListaCodigos []MensajeServicio `xml:"listaCodigos"`
}
type MensajeServicio struct {
CodigoClasificador string `xml:"codigoClasificador" json:"codigo"`
Descripcion string `xml:"descripcion" json:"descripcion"`
}
// Implementaciones para cada tipo de envelope
func (e SoapEnvelopeMensajeServicio) IsTransactionSuccessful() bool {
return e.Body.Response.RespuestaLista.Transaccion
}
func (e SoapEnvelopeMensajeServicio) GetData() []MensajeServicio {
return e.Body.Response.RespuestaLista.ListaCodigos
}

View File

@ -0,0 +1,37 @@
package response
import (
"encoding/xml"
)
type SoapEnvelopePaisOrigen struct {
XMLName xml.Name `xml:"Envelope"`
Body SoapBodyPaisOrigen `xml:"Body"`
}
type SoapBodyPaisOrigen struct {
Response SincronizarResponsePaisOrigen `xml:"sincronizarParametricaPaisOrigenResponse"`
}
type SincronizarResponsePaisOrigen struct {
RespuestaLista RespuestaListaParametricasPaisOrigen `xml:"RespuestaListaParametricas"`
}
type RespuestaListaParametricasPaisOrigen struct {
Transaccion bool `xml:"transaccion"`
ListaCodigos []PaisOrigen `xml:"listaCodigos"`
}
type PaisOrigen struct {
CodigoClasificador string `xml:"codigoClasificador" json:"codigo"`
Descripcion string `xml:"descripcion" json:"descripcion"`
}
// Implementaciones para cada tipo de envelope
func (e SoapEnvelopePaisOrigen) IsTransactionSuccessful() bool {
return e.Body.Response.RespuestaLista.Transaccion
}
func (e SoapEnvelopePaisOrigen) GetData() []PaisOrigen {
return e.Body.Response.RespuestaLista.ListaCodigos
}

View File

@ -0,0 +1,37 @@
package response
import (
"encoding/xml"
)
type SoapEnvelopeTipoHabitacion struct {
XMLName xml.Name `xml:"Envelope"`
Body SoapBodyTipoHabitacion `xml:"Body"`
}
type SoapBodyTipoHabitacion struct {
Response SincronizarResponseTipoHabitacion `xml:"sincronizarParametricaTipoHabitacionResponse"`
}
type SincronizarResponseTipoHabitacion struct {
RespuestaLista RespuestaListaParametricasTipoHabitacion `xml:"RespuestaListaParametricas"`
}
type RespuestaListaParametricasTipoHabitacion struct {
Transaccion bool `xml:"transaccion"`
ListaCodigos []TipoHabitacion `xml:"listaCodigos"`
}
type TipoHabitacion struct {
CodigoClasificador string `xml:"codigoClasificador" json:"codigo"`
Descripcion string `xml:"descripcion" json:"descripcion"`
}
// Implementaciones para cada tipo de envelope
func (e SoapEnvelopeTipoHabitacion) IsTransactionSuccessful() bool {
return e.Body.Response.RespuestaLista.Transaccion
}
func (e SoapEnvelopeTipoHabitacion) GetData() []TipoHabitacion {
return e.Body.Response.RespuestaLista.ListaCodigos
}

View File

@ -0,0 +1,17 @@
package response
// Interfaz para respuestas SOAP que tienen transacción y datos
type SoapResponse[T any] interface {
IsTransactionSuccessful() bool
GetData() T
}
type SoapResponseLeyenda[T any] interface {
IsTransactionSuccessful() bool
AsignarCodigosSecuenciales()
GetData() T
}
type SoapResponseFechaHora[T any] interface {
GetData() T
}

View File

@ -0,0 +1,38 @@
package response
import (
"encoding/xml"
)
type SoapEnvelopeTipoActividadesSector struct {
XMLName xml.Name `xml:"Envelope"`
Body SoapBodyTipoActividadesSector `xml:"Body"`
}
type SoapBodyTipoActividadesSector struct {
Response SincronizarResponseTipoActividadesSector `xml:"sincronizarListaActividadesDocumentoSectorResponse"`
}
type SincronizarResponseTipoActividadesSector struct {
RespuestaLista RespuestaListaParametricasTipoActividadesSector `xml:"RespuestaListaActividadesDocumentoSector"`
}
type RespuestaListaParametricasTipoActividadesSector struct {
Transaccion bool `xml:"transaccion"`
ListaCodigos []TipoActividadesSector `xml:"listaActividadesDocumentoSector"`
}
type TipoActividadesSector struct {
CodigoActividad string `xml:"codigoActividad,omitempty" json:"codigo,omitempty"`
CodigoDocumentoSector string `xml:"codigoDocumentoSector,omitempty" json:"codigoDocumentoSector,omitempty"`
TipoDocumentoSector string `xml:"tipoDocumentoSector,omitempty" json:"descripcion,omitempty"`
}
// Implementaciones para cada tipo de envelope
func (e SoapEnvelopeTipoActividadesSector) IsTransactionSuccessful() bool {
return e.Body.Response.RespuestaLista.Transaccion
}
func (e SoapEnvelopeTipoActividadesSector) GetData() []TipoActividadesSector {
return e.Body.Response.RespuestaLista.ListaCodigos
}

View File

@ -0,0 +1,37 @@
package response
import (
"encoding/xml"
)
type SoapEnvelopeTipoDocumento struct {
XMLName xml.Name `xml:"Envelope"`
Body SoapBodyTipoDocumento `xml:"Body"`
}
type SoapBodyTipoDocumento struct {
Response SincronizarResponseTipoDocumento `xml:"sincronizarParametricaTipoDocumentoIdentidadResponse"`
}
type SincronizarResponseTipoDocumento struct {
RespuestaLista RespuestaListaParametricasTipoDocumento `xml:"RespuestaListaParametricas"`
}
type RespuestaListaParametricasTipoDocumento struct {
Transaccion bool `xml:"transaccion"`
ListaCodigos []TipoDocumento `xml:"listaCodigos"`
}
type TipoDocumento struct {
CodigoClasificador string `xml:"codigoClasificador" json:"codigo"`
Descripcion string `xml:"descripcion" json:"descripcion"`
}
// Implementaciones para cada tipo de envelope
func (e SoapEnvelopeTipoDocumento) IsTransactionSuccessful() bool {
return e.Body.Response.RespuestaLista.Transaccion
}
func (e SoapEnvelopeTipoDocumento) GetData() []TipoDocumento {
return e.Body.Response.RespuestaLista.ListaCodigos
}

View File

@ -0,0 +1,37 @@
package response
import (
"encoding/xml"
)
type SoapEnvelopeTipoSector struct {
XMLName xml.Name `xml:"Envelope"`
Body SoapBodyTipoSector `xml:"Body"`
}
type SoapBodyTipoSector struct {
Response SincronizarResponseTipoSector `xml:"sincronizarParametricaTipoDocumentoSectorResponse"`
}
type SincronizarResponseTipoSector struct {
RespuestaLista RespuestaListaParametricasTipoSector `xml:"RespuestaListaParametricas"`
}
type RespuestaListaParametricasTipoSector struct {
Transaccion bool `xml:"transaccion"`
ListaCodigos []TipoSector `xml:"listaCodigos"`
}
type TipoSector struct {
CodigoClasificador string `xml:"codigoClasificador" json:"codigo"`
Descripcion string `xml:"descripcion" json:"descripcion"`
}
// Implementaciones para cada tipo de envelope
func (e SoapEnvelopeTipoSector) IsTransactionSuccessful() bool {
return e.Body.Response.RespuestaLista.Transaccion
}
func (e SoapEnvelopeTipoSector) GetData() []TipoSector {
return e.Body.Response.RespuestaLista.ListaCodigos
}

View File

@ -0,0 +1,38 @@
package response
import (
"encoding/xml"
)
// Estructuras para la respuesta SOAP.
type SoapEnvelopeTipoEmision struct {
XMLName xml.Name `xml:"Envelope"`
Body SoapBodyTipoEmision `xml:"Body"`
}
type SoapBodyTipoEmision struct {
Response SincronizarResponseTipoEmision `xml:"sincronizarParametricaTipoEmisionResponse"`
}
type SincronizarResponseTipoEmision struct {
RespuestaLista RespuestaListaParametricasTipoEmision `xml:"RespuestaListaParametricas"`
}
type RespuestaListaParametricasTipoEmision struct {
Transaccion bool `xml:"transaccion"`
ListaCodigos []TipoEmision `xml:"listaCodigos"`
}
type TipoEmision struct {
CodigoClasificador string `xml:"codigoClasificador" json:"codigo"`
Descripcion string `xml:"descripcion" json:"descripcion"`
}
// Implementaciones para cada tipo de envelope
func (e SoapEnvelopeTipoEmision) IsTransactionSuccessful() bool {
return e.Body.Response.RespuestaLista.Transaccion
}
func (e SoapEnvelopeTipoEmision) GetData() []TipoEmision {
return e.Body.Response.RespuestaLista.ListaCodigos
}

View File

@ -0,0 +1,38 @@
package response
import (
"encoding/xml"
)
// Estructuras para la respuesta SOAP.
type SoapEnvelopeTipoFactura struct {
XMLName xml.Name `xml:"Envelope"`
Body SoapBodyTipoFactura `xml:"Body"`
}
type SoapBodyTipoFactura struct {
Response SincronizarResponseTipoFactura `xml:"sincronizarParametricaTiposFacturaResponse"`
}
type SincronizarResponseTipoFactura struct {
RespuestaLista RespuestaListaParametricasTipoFactura `xml:"RespuestaListaParametricas"`
}
type RespuestaListaParametricasTipoFactura struct {
Transaccion bool `xml:"transaccion"`
ListaCodigos []TipoFactura `xml:"listaCodigos"`
}
type TipoFactura struct {
CodigoClasificador string `xml:"codigoClasificador" json:"codigo"`
Descripcion string `xml:"descripcion" json:"descripcion"`
}
// Implementaciones para cada tipo de envelope
func (e SoapEnvelopeTipoFactura) IsTransactionSuccessful() bool {
return e.Body.Response.RespuestaLista.Transaccion
}
func (e SoapEnvelopeTipoFactura) GetData() []TipoFactura {
return e.Body.Response.RespuestaLista.ListaCodigos
}

View File

@ -0,0 +1,38 @@
package response
import (
"encoding/xml"
)
// Estructuras para la respuesta SOAP.
type SoapEnvelopeMetodoPago struct {
XMLName xml.Name `xml:"Envelope"`
Body SoapBodyMetodoPago `xml:"Body"`
}
type SoapBodyMetodoPago struct {
Response SincronizarResponseMetodoPago `xml:"sincronizarParametricaTipoMetodoPagoResponse"`
}
type SincronizarResponseMetodoPago struct {
RespuestaLista RespuestaListaParametricasMetodoPago `xml:"RespuestaListaParametricas"`
}
type RespuestaListaParametricasMetodoPago struct {
Transaccion bool `xml:"transaccion"`
ListaCodigos []MetodoPago `xml:"listaCodigos"`
}
type MetodoPago struct {
CodigoClasificador string `xml:"codigoClasificador" json:"codigo"`
Descripcion string `xml:"descripcion" json:"descripcion"`
}
// Implementaciones para cada tipo de envelope
func (e SoapEnvelopeMetodoPago) IsTransactionSuccessful() bool {
return e.Body.Response.RespuestaLista.Transaccion
}
func (e SoapEnvelopeMetodoPago) GetData() []MetodoPago {
return e.Body.Response.RespuestaLista.ListaCodigos
}

View File

@ -0,0 +1,38 @@
package response
import (
"encoding/xml"
)
// Estructuras para la respuesta SOAP.
type SoapEnvelopeMoneda struct {
XMLName xml.Name `xml:"Envelope"`
Body SoapBodyMoneda `xml:"Body"`
}
type SoapBodyMoneda struct {
Response SincronizarResponseMoneda `xml:"sincronizarParametricaTipoMonedaResponse"`
}
type SincronizarResponseMoneda struct {
RespuestaLista RespuestaListaParametricasMoneda `xml:"RespuestaListaParametricas"`
}
type RespuestaListaParametricasMoneda struct {
Transaccion bool `xml:"transaccion"`
ListaCodigos []Moneda `xml:"listaCodigos"`
}
type Moneda struct {
CodigoClasificador string `xml:"codigoClasificador" json:"codigo"`
Descripcion string `xml:"descripcion" json:"descripcion"`
}
// Implementaciones para cada tipo de envelope
func (e SoapEnvelopeMoneda) IsTransactionSuccessful() bool {
return e.Body.Response.RespuestaLista.Transaccion
}
func (e SoapEnvelopeMoneda) GetData() []Moneda {
return e.Body.Response.RespuestaLista.ListaCodigos
}

View File

@ -0,0 +1,37 @@
package response
import (
"encoding/xml"
)
type SoapEnvelopeMotivoAnulacion struct {
XMLName xml.Name `xml:"Envelope"`
Body SoapBodyMotivoAnulacion `xml:"Body"`
}
type SoapBodyMotivoAnulacion struct {
Response SincronizarResponseMotivoAnulacion `xml:"sincronizarParametricaMotivoAnulacionResponse"`
}
type SincronizarResponseMotivoAnulacion struct {
RespuestaLista RespuestaListaParametricasMotivoAnulacion `xml:"RespuestaListaParametricas"`
}
type RespuestaListaParametricasMotivoAnulacion struct {
Transaccion bool `xml:"transaccion"`
ListaCodigos []MotivoAnulacion `xml:"listaCodigos"`
}
type MotivoAnulacion struct {
CodigoClasificador string `xml:"codigoClasificador" json:"codigo"`
Descripcion string `xml:"descripcion" json:"descripcion"`
}
// Implementaciones para cada tipo de envelope
func (e SoapEnvelopeMotivoAnulacion) IsTransactionSuccessful() bool {
return e.Body.Response.RespuestaLista.Transaccion
}
func (e SoapEnvelopeMotivoAnulacion) GetData() []MotivoAnulacion {
return e.Body.Response.RespuestaLista.ListaCodigos
}

View File

@ -0,0 +1,38 @@
package response
import (
"encoding/xml"
)
// Estructuras para la respuesta SOAP.
type SoapEnvelopePuntoVenta struct {
XMLName xml.Name `xml:"Envelope"`
Body SoapBodyPuntoVenta `xml:"Body"`
}
type SoapBodyPuntoVenta struct {
Response SincronizarResponsePuntoVenta `xml:"sincronizarParametricaTipoPuntoVentaResponse"`
}
type SincronizarResponsePuntoVenta struct {
RespuestaLista RespuestaListaParametricasPuntoVenta `xml:"RespuestaListaParametricas"`
}
type RespuestaListaParametricasPuntoVenta struct {
Transaccion bool `xml:"transaccion"`
ListaCodigos []PuntoVenta `xml:"listaCodigos"`
}
type PuntoVenta struct {
CodigoClasificador string `xml:"codigoClasificador" json:"codigo"`
Descripcion string `xml:"descripcion" json:"descripcion"`
}
// Implementaciones para cada tipo de envelope
func (e SoapEnvelopePuntoVenta) IsTransactionSuccessful() bool {
return e.Body.Response.RespuestaLista.Transaccion
}
func (e SoapEnvelopePuntoVenta) GetData() []PuntoVenta {
return e.Body.Response.RespuestaLista.ListaCodigos
}

View File

@ -0,0 +1,37 @@
package response
import (
"encoding/xml"
)
type SoapEnvelopeUnidadMedida struct {
XMLName xml.Name `xml:"Envelope"`
Body SoapBodyUnidadMedida `xml:"Body"`
}
type SoapBodyUnidadMedida struct {
Response SincronizarResponseUnidadMedida `xml:"sincronizarParametricaUnidadMedidaResponse"`
}
type SincronizarResponseUnidadMedida struct {
RespuestaLista RespuestaListaParametricasUnidadMedida `xml:"RespuestaListaParametricas"`
}
type RespuestaListaParametricasUnidadMedida struct {
Transaccion bool `xml:"transaccion"`
ListaCodigos []UnidadMedida `xml:"listaCodigos"`
}
type UnidadMedida struct {
CodigoClasificador string `xml:"codigoClasificador" json:"codigo"`
Descripcion string `xml:"descripcion" json:"descripcion"`
}
// Implementaciones para cada tipo de envelope
func (e SoapEnvelopeUnidadMedida) IsTransactionSuccessful() bool {
return e.Body.Response.RespuestaLista.Transaccion
}
func (e SoapEnvelopeUnidadMedida) GetData() []UnidadMedida {
return e.Body.Response.RespuestaLista.ListaCodigos
}

View File

@ -0,0 +1,33 @@
package models
import (
"context"
"daemonService/internal/notifications"
"database/sql"
"net/http"
"time"
)
// Interfaz base que todos implementan
type SoapService interface {
Execute(ctx context.Context, req *http.Request, isCron bool) (interface{}, error)
GetName() string
}
// configuración y dependencias para cada servicio
type ServiceModel struct {
Name string
Enabled bool
Concurrency int
DB *sql.DB
SOAPEndpoint string
SOAPTimeout time.Duration
SOAPRetries int
NotificationSender notifications.NotificationSender
//APIKey string
KeyToken string
ValueToken string
TagNames string
QueryInsert string
MsgCustomResponse string
}

View File

@ -0,0 +1,271 @@
package notifications
import (
"bytes"
"encoding/json"
"fmt"
"log"
"net/http"
"net/smtp"
"time"
)
// NotificationSender es la interfaz que deben cumplir los sistemas de notificación.
type NotificationSender interface {
SendNotification(subject, message string) error
}
// WebhookNotifier implementa la notificación vía webhook.
type WebhookNotifier struct {
WebhookURL string
}
// SendNotification envía notificación a través de webhook.
func (w *WebhookNotifier) SendNotification(subject, message string) error {
if w.WebhookURL == "" {
return nil
}
payload, err := json.Marshal(map[string]string{
"subject": subject,
"message": message,
"time": time.Now().Format(time.RFC3339),
})
if err != nil {
return err
}
resp, err := http.Post(w.WebhookURL, "application/json", bytes.NewBuffer(payload))
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
return fmt.Errorf("webhook error: %s", resp.Status)
}
return nil
}
// EmailNotifier simula el envío de notificaciones por correo electrónico.
type EmailNotifier struct {
SMTPServer string
SMTPPort string
Username string
Password string
FromEmail string
ToEmail string
}
// SendNotification envía un email a través de un servidor SMTP.
func (e *EmailNotifier) SendNotification(subject, message string) error {
// Construir los encabezados del mensaje
header := make(map[string]string)
header["From"] = e.FromEmail
header["To"] = e.ToEmail
header["Subject"] = subject
header["MIME-Version"] = "1.0"
header["Content-Type"] = "text/plain; charset=\"utf-8\""
var msg string
for k, v := range header {
msg += fmt.Sprintf("%s: %s\r\n", k, v)
}
msg += "\r\n" + message
// Configurar autenticación SMTP
auth := smtp.PlainAuth("", e.Username, e.Password, e.SMTPServer)
addr := fmt.Sprintf("%s:%s", e.SMTPServer, e.SMTPPort)
// Enviar el email
err := smtp.SendMail(addr, auth, e.FromEmail, []string{e.ToEmail}, []byte(msg))
if err != nil {
return err
}
log.Printf("Email enviado correctamente a %s", e.ToEmail)
return nil
}
// SlackNotifier implementa las notificaciones vía Slack.
type SlackNotifier struct {
WebhookURL string
}
// SendNotification envía un mensaje a Slack.
func (s *SlackNotifier) SendNotification(subject, message string) error {
if s.WebhookURL == "" {
return nil
}
payload, err := json.Marshal(map[string]interface{}{
"text": fmt.Sprintf("*%s*\n%s", subject, message),
})
if err != nil {
return err
}
resp, err := http.Post(s.WebhookURL, "application/json", bytes.NewBuffer(payload))
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
return fmt.Errorf("slack webhook error: %s", resp.Status)
}
return nil
}
// CreateNotificationSender crea el notificador según la configuración.
func CreateNotificationSender(enabled bool, method string, cfg map[string]string) NotificationSender {
if !enabled {
return nil
}
switch method {
case "webhook":
return &WebhookNotifier{
WebhookURL: cfg["url"],
}
case "email":
return &EmailNotifier{
SMTPServer: cfg["smtp_server"],
SMTPPort: cfg["smtp_port"],
Username: cfg["username"],
Password: cfg["password"],
FromEmail: cfg["from_email"],
ToEmail: cfg["to_email"],
}
case "slack":
return &SlackNotifier{
WebhookURL: cfg["webhook_url"],
}
default:
log.Printf("Método de notificación desconocido: %s", method)
return nil
}
}
// package notifications
// import (
// "bytes"
// "encoding/json"
// "fmt"
// "log"
// "net/http"
// "time"
// )
// // NotificationSender define la interfaz para enviar notificaciones.
// type NotificationSender interface {
// SendNotification(subject, message string) error
// }
// // WebhookNotifier envía notificaciones mediante webhook.
// type WebhookNotifier struct {
// WebhookURL string
// }
// func (w *WebhookNotifier) SendNotification(subject, message string) error {
// if w.WebhookURL == "" {
// return nil
// }
// payload, err := json.Marshal(map[string]string{
// "subject": subject,
// "message": message,
// "time": time.Now().Format(time.RFC3339),
// })
// if err != nil {
// return err
// }
// resp, err := http.Post(w.WebhookURL, "application/json", bytes.NewBuffer(payload))
// if err != nil {
// return err
// }
// defer resp.Body.Close()
// if resp.StatusCode >= 400 {
// return fmt.Errorf("webhook error: %s", resp.Status)
// }
// return nil
// }
// // EmailNotifier simula el envío de notificaciones por email.
// type EmailNotifier struct {
// SMTPServer string
// SMTPPort string
// Username string
// Password string
// FromEmail string
// ToEmail string
// }
// func (e *EmailNotifier) SendNotification(subject, message string) error {
// log.Printf("Simulando envío de email: Asunto: %s, Mensaje: %s", subject, message)
// log.Printf("Destinatario: %s, Remitente: %s", e.ToEmail, e.FromEmail)
// // Aquí se integraría el cliente SMTP real si fuera necesario
// return nil
// }
// // SlackNotifier envía notificaciones a Slack.
// type SlackNotifier struct {
// WebhookURL string
// }
// func (s *SlackNotifier) SendNotification(subject, message string) error {
// if s.WebhookURL == "" {
// return nil
// }
// payload, err := json.Marshal(map[string]interface{}{
// "text": fmt.Sprintf("*%s*\n%s", subject, message),
// })
// if err != nil {
// return err
// }
// resp, err := http.Post(s.WebhookURL, "application/json", bytes.NewBuffer(payload))
// if err != nil {
// return err
// }
// defer resp.Body.Close()
// if resp.StatusCode >= 400 {
// return fmt.Errorf("slack webhook error: %s", resp.Status)
// }
// return nil
// }
// // NewNotificationSender crea la instancia del notificador según la configuración.
// func NewNotificationSender(enabled bool, method string, cfg map[string]string) NotificationSender {
// if !enabled {
// return nil
// }
// switch method {
// case "webhook":
// return &WebhookNotifier{WebhookURL: cfg["url"]}
// case "email":
// return &EmailNotifier{
// SMTPServer: cfg["smtp_server"],
// SMTPPort: cfg["smtp_port"],
// Username: cfg["username"],
// Password: cfg["password"],
// FromEmail: cfg["from_email"],
// ToEmail: cfg["to_email"],
// }
// case "slack":
// return &SlackNotifier{WebhookURL: cfg["webhook_url"]}
// default:
// log.Printf("Método de notificación desconocido: %s", method)
// return nil
// }
// }

View File

@ -0,0 +1,292 @@
// repository/cuis_repo.go
package repositories
import (
"context"
"daemonService/internal/models"
"daemonService/internal/models/obtencionCodigos/request"
"daemonService/internal/models/obtencionCodigos/response"
srv "daemonService/internal/services"
"daemonService/internal/utils"
"database/sql"
"fmt"
"net/http"
)
type CuisRepository struct {
CuisService *srv.CuisService
ModelService models.ServiceModel
}
// instanciamos cuisRepository
func NewCuisRepository(model models.ServiceModel, cuisService *srv.CuisService) *CuisRepository {
return &CuisRepository{ModelService: model, CuisService: cuisService}
}
func (cr *CuisRepository) CrearNuevoRegistro(ctx context.Context, request request.SolicitudCuisCufd, codigoCuis string, fechaVigencia string, transaccion bool, cufdData response.SoapBodyCufd) error {
// Obtener la hora actual de Bolivia
currentBolivia, err := utils.GetCurrentBoliviaTime()
if err != nil {
return fmt.Errorf("error al obtener hora de Bolivia: %w", err)
}
// Iniciar transacción
tx, err := cr.ModelService.DB.BeginTx(ctx, nil)
if err != nil {
cr.CuisService.LogMessage("Error al iniciar transacción: %v", err)
return utils.ErrorCustom(http.StatusBadRequest, "Error al iniciar transacción")
}
defer func() {
if err != nil {
tx.Rollback()
}
}()
// Consulta corregida para manejar actualizaciones de CUIS y CUFD
query := `
WITH empresa AS (
-- Inserta o actualiza empresa usando la clave natural
INSERT INTO registroEmpresa (
codigo_ambiente,
codigo_modalidad,
codigo_punto_venta,
codigo_sistema,
codigo_sucursal,
nit,
token_key,
token_value,
nombre_archivo_certificado,
nombre_archivo_clave_privada
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
ON CONFLICT (nit, codigo_sucursal, codigo_punto_venta)
DO UPDATE SET
codigo_ambiente = EXCLUDED.codigo_ambiente,
codigo_modalidad = EXCLUDED.codigo_modalidad,
codigo_sistema = EXCLUDED.codigo_sistema,
token_key = EXCLUDED.token_key,
token_value = EXCLUDED.token_value,
nombre_archivo_certificado = EXCLUDED.nombre_archivo_certificado,
nombre_archivo_clave_privada = EXCLUDED.nombre_archivo_clave_privada,
fecha_actualizacion = CURRENT_TIMESTAMP
RETURNING id
),
cuis_existente AS (
-- Busca si existe un CUIS para la empresa
SELECT id, cuis, registro_empresa_id
FROM cuis
WHERE registro_empresa_id = (SELECT id FROM empresa)
LIMIT 1
),
cuis_actualizado AS (
-- Si existe CUIS pero es diferente, actualízalo
UPDATE cuis
SET
cuis = $11,
fecha_vigencia = $12,
transaccion = $13,
fecha_actualizacion = CURRENT_TIMESTAMP
FROM cuis_existente
WHERE cuis.id = cuis_existente.id
AND (cuis_existente.cuis <> $11 OR cuis_existente.cuis IS NULL)
RETURNING cuis.id, cuis.registro_empresa_id
),
cuis_insertado AS (
-- Si no existe CUIS, inserta uno nuevo
INSERT INTO cuis (
cuis,
fecha_vigencia,
transaccion,
fecha_creacion,
registro_empresa_id
)
SELECT
$11, $12, $13, $14, id
FROM empresa
WHERE NOT EXISTS (SELECT 1 FROM cuis_existente)
RETURNING id, registro_empresa_id
),
cuis_final AS (
-- Usa el CUIS actualizado si existe
SELECT id, registro_empresa_id FROM cuis_actualizado
UNION ALL
-- O el insertado si no había uno previo
SELECT id, registro_empresa_id FROM cuis_insertado
UNION ALL
-- O el existente si no se actualizó (porque era igual)
SELECT id, registro_empresa_id FROM cuis_existente
WHERE NOT EXISTS (SELECT 1 FROM cuis_actualizado)
AND EXISTS (SELECT 1 FROM cuis_existente)
),
cufd_existente AS (
-- Busca si existe un CUFD para el CUIS actual
SELECT c.id, c.codigo, c.codigo_control, c.direccion, c.cuis_id
FROM cufd c
JOIN cuis_final cf ON c.cuis_id = cf.id
LIMIT 1
),
cufd_actualizado AS (
-- Si existe CUFD pero algún valor es diferente, actualízalo
UPDATE cufd
SET
codigo = $15,
codigo_control = $16,
direccion = $17,
fecha_vigencia = $18,
transaccion = $19,
fecha_actualizacion = CURRENT_TIMESTAMP
FROM cufd_existente, cuis_final
WHERE cufd.id = cufd_existente.id
AND cufd.cuis_id = cuis_final.id
AND (
cufd_existente.codigo <> $15 OR
cufd_existente.codigo_control <> $16 OR
cufd_existente.direccion <> $17 OR
cufd_existente.codigo IS NULL
)
RETURNING (SELECT registro_empresa_id FROM cuis_final LIMIT 1) as registro_id
),
cufd_insertado AS (
-- Si no existe CUFD, inserta uno nuevo
INSERT INTO cufd (
codigo,
codigo_control,
direccion,
fecha_vigencia,
transaccion,
fecha_creacion,
cuis_id
)
SELECT
$15, $16, $17, $18, $19, $20, id
FROM cuis_final
WHERE NOT EXISTS (SELECT 1 FROM cufd_existente)
RETURNING (SELECT registro_empresa_id FROM cuis_final LIMIT 1) as registro_id
),
resultado_final AS (
-- Usa el resultado de cualquiera de las operaciones
SELECT registro_id FROM cufd_actualizado
UNION ALL
SELECT registro_id FROM cufd_insertado
UNION ALL
-- O usa el ID de empresa si el CUFD ya existía y no cambió
SELECT registro_empresa_id as registro_id FROM cuis_final
WHERE NOT EXISTS (SELECT 1 FROM cufd_actualizado)
AND NOT EXISTS (SELECT 1 FROM cufd_insertado)
AND EXISTS (SELECT 1 FROM cufd_existente)
LIMIT 1
)
SELECT registro_id FROM resultado_final LIMIT 1;
`
var registroID int
err = tx.QueryRowContext(
ctx,
query,
// Parámetros para registroEmpresa
request.CodigoAmbiente,
request.CodigoModalidad,
request.CodigoPuntoVenta,
request.CodigoSistema,
request.CodigoSucursal,
request.Nit,
request.KeyToken,
request.ValueToken,
request.NombreArchivoCertificado, // Nuevo parámetro
request.NombreArchivoClavePrivada, // Nuevo parámetro
// Parámetros para cuis
codigoCuis,
fechaVigencia,
transaccion,
currentBolivia,
// Parámetros para cufd
cufdData.Response.Respuesta.Codigo,
cufdData.Response.Respuesta.CodigoControl,
cufdData.Response.Respuesta.Direccion,
cufdData.Response.Respuesta.FechaVigencia,
cufdData.Response.Respuesta.Transaccion,
currentBolivia,
).Scan(&registroID)
if err != nil {
cr.CuisService.LogMessage("Error al insertar/actualizar registros: %v", err)
return utils.ErrorCustom(http.StatusInternalServerError, "Error al procesar los registros en la base de datos")
}
// Confirmar la transacción
if err = tx.Commit(); err != nil {
cr.CuisService.LogMessage("Error al confirmar transacción: %v", err)
return utils.ErrorCustom(http.StatusInternalServerError, "Error al confirmar transacción")
}
return nil
}
// actualiza un registro existente de empresa y CUIS
func (cr *CuisRepository) ActualizarRegistro(ctx context.Context, empresaID int, cuisID int64, req request.SolicitudCuisCufd, respRaw interface{}) error {
currentBolivia, err := utils.GetCurrentBoliviaTime()
if err != nil {
return err
}
// 1. Iniciar tx
tx, err := cr.ModelService.DB.BeginTx(ctx, nil)
if err != nil {
cr.CuisService.LogMessage("Error iniciar transacción: %d", err)
return fmt.Errorf("%w", utils.ErrorCustom(http.StatusBadRequest, "Error iniciar transacción"))
}
defer tx.Rollback()
// 2. Parsear respuesta
nuevoCuis, fechaVigencia, _, err := utils.ParseSoapCuisResponse(respRaw)
if err != nil {
cr.CuisService.LogMessage("Error al parsear la respuesta: %d", err)
return fmt.Errorf("%w", utils.ErrorCustom(http.StatusBadRequest, "Error al parsear la respuesta"))
}
// 3. Actualizar empresa
_, err = tx.ExecContext(ctx, `
UPDATE registroEmpresa
SET codigo_ambiente=$1, codigo_modalidad=$2, codigo_punto_venta=$3,
codigo_sistema=$4, codigo_sucursal=$5, nit=$6, fecha_actualizacion=$7
WHERE id=$8
`,
req.CodigoAmbiente, req.CodigoModalidad, req.CodigoPuntoVenta,
req.CodigoSistema, req.CodigoSucursal, req.Nit, currentBolivia, empresaID,
)
if err != nil {
cr.CuisService.LogMessage("Error al actualizar registroEmpresa: %d", err)
return fmt.Errorf("%w", utils.ErrorCustom(http.StatusBadRequest, "Error al actualizar la tabla registroEmpresa"))
}
// 4. Comprobar si hay un CUIS existente
var existingCuis string
err = tx.QueryRowContext(ctx, `SELECT cuis FROM cuis WHERE id=$1 AND registro_empresa_id=$2`, cuisID, empresaID).Scan(&existingCuis)
if err != nil && err != sql.ErrNoRows {
cr.CuisService.LogMessage("Error al consultar cuis existente: %d", err)
return fmt.Errorf("%w", utils.ErrorCustom(http.StatusBadRequest, "Error al consultar cuis existente"))
}
// 5. Insertar o actualizar
if err == sql.ErrNoRows {
_, err = tx.ExecContext(ctx, `INSERT INTO cuis (registro_empresa_id, cuis) VALUES ($1, $2)`, empresaID, nuevoCuis)
if err != nil {
cr.CuisService.LogMessage("Error al insertar nuevo cuis: %d", err)
return fmt.Errorf("%w", utils.ErrorCustom(http.StatusBadRequest, "Error al insertar nuevo cuis"))
}
} else if existingCuis != nuevoCuis {
_, err = tx.ExecContext(ctx, `UPDATE cuis SET cuis=$1, fecha_vigencia=$2, fecha_actualizacion=$3 WHERE id=$4`,
nuevoCuis, fechaVigencia, currentBolivia, cuisID)
if err != nil {
cr.CuisService.LogMessage("Error al actualizar cuis existente: %d", err)
return fmt.Errorf("%w", utils.ErrorCustom(http.StatusBadRequest, "Error al actualizar cuis existente"))
}
}
// 6. Confirmar tx
if err := tx.Commit(); err != nil {
cr.CuisService.LogMessage("Error al confirmar transacción: %d", err)
return fmt.Errorf("%w", utils.ErrorCustom(http.StatusBadRequest, "Error al confirmar transacción"))
}
return nil
}

View File

@ -0,0 +1,124 @@
package repositories
import (
"context"
"daemonService/internal/models/obtencionCodigos"
"database/sql"
"fmt"
"time"
)
type EmpresaRepository struct {
Model obtencionCodigos.CuisServiceModel
}
// instanciamos empresaRepository
func NewEmpresaRepository(
model obtencionCodigos.CuisServiceModel,
) *EmpresaRepository {
return &EmpresaRepository{Model: model}
}
// obtiene todas las empresas con sus CUIS
func (s *EmpresaRepository) GetEmpresasConCuisMinimal(ctx context.Context) ([]obtencionCodigos.EmpresaConCuis, error) {
const query = `
SELECT
-- columnas de registroEmpresa
re.id, re.codigo_ambiente, re.codigo_modalidad, re.codigo_punto_venta,
re.codigo_sistema, re.codigo_sucursal, re.nit,
re.fecha_creacion, re.fecha_actualizacion, re.token_key, re.token_value,
re.nombre_archivo_clave_privada, re.nombre_archivo_certificado,
c.id, c.cuis, c.fecha_vigencia
FROM registroEmpresa re
LEFT JOIN cuis c ON c.registro_empresa_id = re.id
ORDER BY re.id;
`
rows, err := s.Model.DB.QueryContext(ctx, query)
if err != nil {
return nil, fmt.Errorf("consulta empresas con cuis minimal: %w", err)
}
defer rows.Close()
// Mapa temporal para agrupar por empresa.ID
empresasMap := make(map[int]*obtencionCodigos.EmpresaConCuis)
for rows.Next() {
// Variables temporales de escaneo
var (
id int
codigoAmbiente int
codigoModalidad int
codigoPuntoVenta int
codigoSistema string
codigoSucursal int
nit string
fechaCreacion time.Time
fechaActualizacion time.Time
tokenKey string
tokenValue string
nombreArchivoClavePrivada string
nombreArchivoCertificado string
idCuis sql.NullInt64
cuis sql.NullString
fechaVigencia sql.NullTime
)
// Leer la fila
if err := rows.Scan(
&id, &codigoAmbiente, &codigoModalidad, &codigoPuntoVenta,
&codigoSistema, &codigoSucursal, &nit,
&fechaCreacion, &fechaActualizacion, &tokenKey, &tokenValue,
&nombreArchivoClavePrivada, &nombreArchivoCertificado,
&idCuis, &cuis, &fechaVigencia,
); err != nil {
return nil, fmt.Errorf("scan fila: %w", err)
}
// ¿Ya existe la empresa en el mapa?
ent, ok := empresasMap[id]
if !ok {
// Si no existe, la creamos y rellenamos datos de registroEmpresa
ent = &obtencionCodigos.EmpresaConCuis{
RegistroEmpresa: obtencionCodigos.RegistroEmpresa{
ID: id,
CodigoAmbiente: codigoAmbiente,
CodigoModalidad: codigoModalidad,
CodigoPuntoVenta: codigoPuntoVenta,
CodigoSistema: codigoSistema,
CodigoSucursal: codigoSucursal,
Nit: nit,
FechaCreacion: fechaCreacion,
FechaActualizacion: fechaActualizacion,
TokenKey: tokenKey,
TokenValue: tokenValue,
NombreArchivoClavePrivada: nombreArchivoClavePrivada,
NombreArchivoCertificado: nombreArchivoCertificado,
},
Cuis: make([]obtencionCodigos.CuisMinimal, 0, 1),
}
empresasMap[id] = ent
}
// Si hay un CUIS válido, lo añadimos al slice
if idCuis.Valid && cuis.Valid && fechaVigencia.Valid {
ent.Cuis = append(ent.Cuis, obtencionCodigos.CuisMinimal{
Cuis_id: idCuis.Int64,
Cuis: cuis.String,
FechaVigencia: fechaVigencia.Time,
})
}
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("iteración filas: %w", err)
}
// Convertimos el mapa a slice ordenado por aparición
resp := make([]obtencionCodigos.EmpresaConCuis, 0, len(empresasMap))
for _, ent := range empresasMap {
resp = append(resp, *ent)
}
return resp, nil
}

View File

@ -0,0 +1,85 @@
package repositories
import (
"context"
"daemonService/internal/models"
bg "daemonService/internal/models/sincronizacionDatos/bodyGeneric"
"daemonService/internal/models/sincronizacionDatos/request"
"encoding/json"
"fmt"
"time"
)
// func (s *ListaLeyendasFacturaService) guardarRegistro(solicitud models.SolicitudSincronizacionLeyendas, cuisID int, soapReq string, bodyBytes []byte, leyendas []models.LeyendaDetalle) error {
func GuardarRegistro[T any](s *models.ServiceModel, solicitud request.SolicitudSincronizacion, cuisID int, soapReq string, bodyBytes []byte, bodyGeneric T) error {
dbCtx, dbCancel := context.WithTimeout(context.Background(), 30*time.Minute)
defer dbCancel()
tx, err := s.DB.BeginTx(dbCtx, nil)
if err != nil {
return fmt.Errorf("error iniciando transacción: %v", err)
}
defer func() {
if err != nil {
tx.Rollback()
}
}()
solicitudJSON, _ := json.Marshal(solicitud)
// Creación de la respuesta utilizando la estructura genérica
respJson := bg.CustomResponse[T]{
Success: true,
Status: 200,
Message: s.MsgCustomResponse,
Data: bodyGeneric,
}
respuestaJSON, _ := json.Marshal(respJson)
_, err = tx.ExecContext(dbCtx, s.QueryInsert,
solicitud.CodigoSistema,
soapReq,
string(bodyBytes),
string(solicitudJSON),
string(respuestaJSON),
cuisID,
)
//_, err = tx.ExecContext(dbCtx, `
// INSERT INTO leyendas_factura (
// codigo,
// reqSoap,
// respSoap,
// reqJson,
// respJson,
// cuis_id,
// fecha_creacion
// )
// VALUES ($1, $2, $3, $4, $5, $6, NOW() AT TIME ZONE 'America/La_Paz')
// ON CONFLICT (codigo, cuis_id)
// DO UPDATE SET
// reqSoap = EXCLUDED.reqSoap,
// respSoap = EXCLUDED.respSoap,
// reqJson = EXCLUDED.reqJson,
// respJson = EXCLUDED.respJson,
// cuis_id = EXCLUDED.cuis_id,
// fecha_actualizacion = NOW() AT TIME ZONE 'America/La_Paz'`,
// solicitud.CodigoSistema,
// soapReq,
// string(bodyBytes),
// string(solicitudJSON),
// string(respuestaJSON),
// cuisID,
//)
if err != nil {
return fmt.Errorf("error ejecutando UPSERT: %v", err)
}
if err := tx.Commit(); err != nil {
return fmt.Errorf("error confirmando transacción: %v", err)
}
return nil
}

View File

@ -0,0 +1,124 @@
// service/cuis_service.go
package services
import (
"context"
"daemonService/internal/models/obtencionCodigos"
"daemonService/internal/models/obtencionCodigos/request"
"daemonService/internal/utils"
"database/sql"
"fmt"
"log"
"strconv"
)
type CuisService struct {
Model obtencionCodigos.CuisServiceModel
Logger *log.Logger
}
// En services/cuis_service.go
func NewCuisService(
model obtencionCodigos.CuisServiceModel, logger *log.Logger,
) *CuisService {
return &CuisService{Model: model, Logger: logger}
}
// GetName retorna el nombre del servicio
func (s *CuisService) GetName() string {
return s.Model.Name
}
// logMessage registra un mensaje con el prefijo del servicio
func (s *CuisService) LogMessage(format string, v ...interface{}) {
if s.Logger != nil {
s.Logger.Printf(format, v...)
}
}
// registra un error y devuelve el mismo error
func (s *CuisService) LogError(format string, err error, v ...interface{}) error {
args := append([]interface{}{err}, v...)
if s.Logger != nil {
s.Logger.Printf(format, args...)
}
return fmt.Errorf(format, args...)
}
// crea una solicitud CUIS a partir de datos de empresa
func (s *CuisService) BuildCuisRequestFromEmpresa(ec obtencionCodigos.EmpresaConCuis) request.SolicitudCuisCufd {
return request.SolicitudCuisCufd{
CodigoAmbiente: strconv.Itoa(ec.CodigoAmbiente),
CodigoModalidad: strconv.Itoa(ec.CodigoModalidad),
CodigoPuntoVenta: strconv.Itoa(ec.CodigoPuntoVenta),
CodigoSistema: ec.CodigoSistema,
CodigoSucursal: strconv.Itoa(ec.CodigoSucursal),
Nit: ec.Nit,
KeyToken: ec.TokenKey,
ValueToken: ec.TokenValue,
NombreArchivoClavePrivada: ec.NombreArchivoClavePrivada,
NombreArchivoCertificado: ec.NombreArchivoCertificado,
}
}
// actualiza un registro existente de empresa y CUIS
func (s *CuisService) actualizarRegistro(ctx context.Context, empresaID int, cuisID int64, req request.SolicitudCuisCufd, respRaw interface{}) error {
currentBolivia, err := utils.GetCurrentBoliviaTime()
if err != nil {
return err
}
// 1. Iniciar tx
tx, err := s.Model.DB.BeginTx(ctx, nil)
if err != nil {
return fmt.Errorf("iniciar transacción: %w", err)
}
defer tx.Rollback()
// 2. Parsear respuesta
nuevoCuis, fechaVigencia, _, err := utils.ParseSoapCuisResponse(respRaw)
if err != nil {
return err
}
// 3. Actualizar empresa
_, err = tx.ExecContext(ctx, `
UPDATE registroEmpresa
SET codigo_ambiente=$1, codigo_modalidad=$2, codigo_punto_venta=$3,
codigo_sistema=$4, codigo_sucursal=$5, nit=$6, fecha_actualizacion=$7
WHERE id=$8
`,
req.CodigoAmbiente, req.CodigoModalidad, req.CodigoPuntoVenta,
req.CodigoSistema, req.CodigoSucursal, req.Nit, currentBolivia, empresaID,
)
if err != nil {
return fmt.Errorf("actualizar registroEmpresa: %w", err)
}
// 4. Comprobar si hay un CUIS existente
var existingCuis string
err = tx.QueryRowContext(ctx, `SELECT cuis FROM cuis WHERE id=$1 AND registro_empresa_id=$2`, cuisID, empresaID).Scan(&existingCuis)
if err != nil && err != sql.ErrNoRows {
return fmt.Errorf("consultar cuis existente: %w", err)
}
// 5. Insertar o actualizar
if err == sql.ErrNoRows {
_, err = tx.ExecContext(ctx, `INSERT INTO cuis (registro_empresa_id, cuis) VALUES ($1, $2)`, empresaID, nuevoCuis)
if err != nil {
return fmt.Errorf("insertar nuevo cuis: %w", err)
}
} else if existingCuis != nuevoCuis {
_, err = tx.ExecContext(ctx, `UPDATE cuis SET cuis=$1, fecha_vigencia=$2, fecha_actualizacion=$3 WHERE id=$4`,
nuevoCuis, fechaVigencia, currentBolivia, cuisID)
if err != nil {
return fmt.Errorf("actualizar cuis existente: %w", err)
}
}
// 6. Confirmar tx
if err := tx.Commit(); err != nil {
return fmt.Errorf("confirmar transacción: %w", err)
}
return nil
}

View File

@ -0,0 +1,100 @@
package obtencionCodigo
//import (
// "context"
// "daemonService/internal/models/obtencionCodigos/request"
// "encoding/json"
// "net/http"
//)
//
//// procesa la ejecución en modo cron
//func (s *ObtencionCodigoService) processCronMode(dbCtx context.Context) (interface{}, error) {
// empresasConCuis, err := s.GetEmpresasConCuisMinimal(dbCtx)
// if err != nil {
// return nil, s.LogError("Error al obtener empresas con CUIS: %v", err)
// }
//
// var lastResponse interface{}
// for _, ec := range empresasConCuis {
// s.LogMessage("Procesando empresa ID: %d", ec.ID)
//
// if len(ec.Cuis) == 0 {
// s.LogMessage("Empresa ID %d sin CUIS registrados", ec.ID)
// continue
// }
//
// for _, c := range ec.Cuis {
// s.LogMessage("Procesando CUIS ID: %d", c.Cuis_id)
//
// cuisRequest := s.buildCuisRequestFromEmpresa(ec)
// respCuis, err := codigoProcesarRegistro(&s.CuisServiceModel, cuisRequest, 0)
//
// if err != nil {
// s.LogError(logErrorProcessing, err, cuisRequest.CodigoSistema)
// continue
// }
//
// if err := s.actualizarRegistro(dbCtx, ec.ID, c.Cuis_id, cuisRequest, respCuis); err != nil {
// s.LogError("Error actualizando registro: %v", err)
// continue
// }
//
// lastResponse = respCuis
// }
// }
//
// return lastResponse, nil
//}
//
//// procesa la ejecución en modo API
//func (s *ObtencionCodigoService) processAPIMode(dbCtx context.Context, w http.ResponseWriter, req *http.Request) (interface{}, error) {
// cuisRequest, err := s.parseCuisRequest(w, req)
// if err != nil {
// return nil, err
// }
//
// if err := s.validateCuisRequest(cuisRequest); err != nil {
// return nil, s.LogError("Datos inválidos: %v", err)
// }
//
// respCuis, err := codigoProcesarRegistro(&s.CuisServiceModel, cuisRequest, 0)
// if err != nil {
// return nil, s.LogError(logErrorProcessing, err, cuisRequest.CodigoSistema)
// }
//
// if err := s.crearNuevoRegistro(dbCtx, cuisRequest, respCuis); err != nil {
// return nil, s.LogError("Error creando nuevo registro: %v", err)
// }
//
// return respCuis, nil
//}
//
//// parsea la solicitud de CUIS desde el body del request
//func (s *ObtencionCodigoService) parseCuisRequest(w http.ResponseWriter, req *http.Request) (request.SolicitudCuis, error) {
// var cuisRequest request.SolicitudCuis
//
// // Limitar el tamaño del body para prevenir ataques DoS
// req.Body = http.MaxBytesReader(w, req.Body, maxBodySize)
//
// decoder := json.NewDecoder(req.Body)
// decoder.DisallowUnknownFields() // Rechazar campos desconocidos
//
// if err := decoder.Decode(&cuisRequest); err != nil {
// return cuisRequest, s.LogError("Error al decodificar JSON: %v", err)
// }
//
// return cuisRequest, nil
//}
//
//// valida los datos de la solicitud CUIS
//func (s *ObtencionCodigoService) validateCuisRequest(request request.SolicitudCuis) error {
// if request.CodigoAmbiente == "" ||
// request.CodigoModalidad == "" ||
// request.CodigoPuntoVenta == "" ||
// request.CodigoSistema == "" ||
// request.CodigoSucursal == "" ||
// request.Nit == "" {
// return ErrAllFieldsRequired
// }
// return nil
//}

View File

@ -0,0 +1,715 @@
// // domain/models.go
package obtencionCodigo
//
//import (
// "time"
//)
//
//// RegistroEmpresa mapea toda la tabla registroEmpresa
//type RegistroEmpresa struct {
// ID int `sql:"id"`
// CodigoAmbiente int `sql:"codigo_ambiente"`
// CodigoModalidad int `sql:"codigo_modalidad"`
// CodigoPuntoVenta int `sql:"codigo_punto_venta"`
// CodigoSistema string `sql:"codigo_sistema"`
// CodigoSucursal int `sql:"codigo_sucursal"`
// Nit string `sql:"nit"`
// FechaCreacion time.Time `sql:"fecha_creacion"`
// FechaActualizacion time.Time `sql:"fecha_actualizacion"`
//}
//
//// CuisMinimal solo tiene los dos campos que quieres de la tabla cuis
//type CuisMinimal struct {
// Cuis_id int64 `sql:"id"`
// Cuis string `sql:"cuis"`
// FechaVigencia time.Time `sql:"fecha_vigencia"`
//}
//
//// EmpresaConCuis agrupa una empresa con sus cuis mínimos
//type EmpresaConCuis struct {
// RegistroEmpresa
// Cuis []CuisMinimal
//}
//// domain/errors.go
//package domain
//
//import "errors"
//
//// Errores comunes
//var (
// ErrAllFieldsRequired = errors.New("todos los campos son requeridos")
// ErrUnexpectedRespType = errors.New("tipo de respuesta inesperado")
// ErrDuplicateRecord = errors.New("ya existe un registro con el mismo NIT, código sucursal y código punto de venta")
//)
//// utils/constants.go
//package utils
//
//// Constantes para la configuración general
//const (
// MaxBodySize = 1 * 1024 * 1024 // 1MB
// DefaultTimeZone = "America/La_Paz"
// LogServiceStart = "Iniciando servicio %s"
// LogServiceEnd = "Finalizado servicio %s"
// LogErrorProcessing = "Error procesando registro %s: %v"
//)
//// utils/time_utils.go
//package utils
//
//import (
//"time"
//)
//
//// GetCurrentBoliviaTime obtiene la hora actual en la zona horaria de Bolivia
//func GetCurrentBoliviaTime() (time.Time, error) {
// loc, err := time.LoadLocation(DefaultTimeZone)
// if err != nil {
// return time.Time{}, err
// }
// return time.Now().In(loc), nil
//}
//// utils/soap_utils.go
//package utils
//
//import (
//"fmt"
//
//"daemonService/internal/models/obtencionCodigos/response"
//"obtencionCodigo/domain"
//)
//
//// ParseSoapCuisResponse parsea la respuesta SOAP para CUIS
//func ParseSoapCuisResponse(respSoap interface{}) (string, string, bool, error) {
// resp, ok := respSoap.(response.SoapBodyCuis)
// if !ok {
// return "", "", false, fmt.Errorf("%w: %T", domain.ErrUnexpectedRespType, respSoap)
// }
//
// return resp.Response.Respuesta.Codigo,
// resp.Response.Respuesta.FechaVigencia,
// resp.Response.Respuesta.Transaccion,
// nil
//}
// repository/empresa_repo.go
//package repository
//
//import (
//"context"
//"database/sql"
//"fmt"
//"time"
//
//"obtencionCodigo/domain"
//"daemonService/internal/models/obtencionCodigos/request"
//)
//
//type EmpresaRepository struct {
// DB *sql.DB
//}
//
//// GetEmpresasConCuisMinimal obtiene todas las empresas con sus CUIS
//func (r *EmpresaRepository) GetEmpresasConCuisMinimal(ctx context.Context) ([]domain.EmpresaConCuis, error) {
// const query = `
// SELECT
// -- columnas de registroEmpresa
// re.id, re.codigo_ambiente, re.codigo_modalidad, re.codigo_punto_venta,
// re.codigo_sistema, re.codigo_sucursal, re.nit,
// re.fecha_creacion, re.fecha_actualizacion,
// -- columnas específicas de cuis
// c.id, c.cuis, c.fecha_vigencia
// FROM registroEmpresa re
// LEFT JOIN cuis c ON c.registro_empresa_id = re.id
// ORDER BY re.id;
// `
//
// rows, err := r.DB.QueryContext(ctx, query)
// if err != nil {
// return nil, fmt.Errorf("consulta empresas con cuis minimal: %w", err)
// }
// defer rows.Close()
//
// // Mapa temporal para agrupar por empresa.ID
// empresasMap := make(map[int]*domain.EmpresaConCuis)
//
// for rows.Next() {
// // Variables temporales de escaneo
// var (
// id int
// codigoAmbiente int
// codigoModalidad int
// codigoPuntoVenta int
// codigoSistema string
// codigoSucursal int
// nit string
// fechaCreacion time.Time
// fechaActualizacion time.Time
//
// idCuis sql.NullInt64
// cuis sql.NullString
// fechaVigencia sql.NullTime
// )
//
// // Leer la fila
// if err := rows.Scan(
// &id, &codigoAmbiente, &codigoModalidad, &codigoPuntoVenta,
// &codigoSistema, &codigoSucursal, &nit,
// &fechaCreacion, &fechaActualizacion,
// &idCuis, &cuis, &fechaVigencia,
// ); err != nil {
// return nil, fmt.Errorf("scan fila: %w", err)
// }
//
// // ¿Ya existe la empresa en el mapa?
// ent, ok := empresasMap[id]
// if !ok {
// // Si no existe, la creamos y rellenamos datos de registroEmpresa
// ent = &domain.EmpresaConCuis{
// RegistroEmpresa: domain.RegistroEmpresa{
// ID: id,
// CodigoAmbiente: codigoAmbiente,
// CodigoModalidad: codigoModalidad,
// CodigoPuntoVenta: codigoPuntoVenta,
// CodigoSistema: codigoSistema,
// CodigoSucursal: codigoSucursal,
// Nit: nit,
// FechaCreacion: fechaCreacion,
// FechaActualizacion: fechaActualizacion,
// },
// Cuis: make([]domain.CuisMinimal, 0, 1),
// }
// empresasMap[id] = ent
// }
//
// // Si hay un CUIS válido, lo añadimos al slice
// if idCuis.Valid && cuis.Valid && fechaVigencia.Valid {
// ent.Cuis = append(ent.Cuis, domain.CuisMinimal{
// Cuis_id: idCuis.Int64,
// Cuis: cuis.String,
// FechaVigencia: fechaVigencia.Time,
// })
// }
// }
//
// if err := rows.Err(); err != nil {
// return nil, fmt.Errorf("iteración filas: %w", err)
// }
//
// // Convertimos el mapa a slice ordenado por aparición
// resp := make([]domain.EmpresaConCuis, 0, len(empresasMap))
// for _, ent := range empresasMap {
// resp = append(resp, *ent)
// }
// return resp, nil
//}
//
//// BuscarRegistroEmpresa busca un registro de empresa por sus criterios
//func (r *EmpresaRepository) BuscarRegistroEmpresa(ctx context.Context, request request.SolicitudCuis) (int, error) {
// query := `
// SELECT id
// FROM registroEmpresa
// WHERE codigo_sistema = $1
// AND nit = $2
// AND codigo_ambiente = $3
// AND codigo_punto_venta = $4
// AND codigo_sucursal = $5
// LIMIT 1
// `
//
// var id int
// err := r.DB.QueryRowContext(
// ctx,
// query,
// request.CodigoSistema,
// request.Nit,
// request.CodigoAmbiente,
// request.CodigoPuntoVenta,
// request.CodigoSucursal,
// ).Scan(&id)
//
// if err == sql.ErrNoRows {
// return 0, nil // No existe el registro
// }
//
// if err != nil {
// return 0, err
// }
//
// return id, nil
//}
//// repository/cuis_repo.go
//package repository
//
//import (
//"context"
//"database/sql"
//"fmt"
//"time"
//
//"obtencionCodigo/domain"
//"obtencionCodigo/utils"
//"daemonService/internal/models/obtencionCodigos/request"
//)
//
//type CuisRepository struct {
// DB *sql.DB
//}
//
//// CrearNuevoRegistro crea un nuevo registro de empresa y CUIS
//func (r *CuisRepository) CrearNuevoRegistro(ctx context.Context, request request.SolicitudCuis, codigoCuis string, fechaVigencia string, transaccion bool) error {
// currentBolivia, err := utils.GetCurrentBoliviaTime()
// if err != nil {
// return err
// }
//
// // Iniciar transacción
// tx, err := r.DB.BeginTx(ctx, nil)
// if err != nil {
// return fmt.Errorf("error al iniciar transacción: %w", err)
// }
// defer tx.Rollback()
//
// // Verificar si ya existe el NIT en la base de datos
// var existeNIT bool
// queryVerificacionNIT := `
// SELECT EXISTS (
// SELECT 1 FROM registroEmpresa
// WHERE nit = $1
// )
// `
// err = tx.QueryRowContext(
// ctx,
// queryVerificacionNIT,
// request.Nit,
// ).Scan(&existeNIT)
//
// if err != nil {
// return fmt.Errorf("error al verificar existencia del NIT: %w", err)
// }
//
// // Si el NIT ya existe, verificar si la combinación de sucursal y punto de venta existe
// if existeNIT {
// var existeCombinacion bool
// queryVerificacionCombinacion := `
// SELECT EXISTS (
// SELECT 1 FROM registroEmpresa
// WHERE nit = $1
// AND codigo_sucursal = $2
// AND codigo_punto_venta = $3
// )
// `
// err = tx.QueryRowContext(
// ctx,
// queryVerificacionCombinacion,
// request.Nit,
// request.CodigoSucursal,
// request.CodigoPuntoVenta,
// ).Scan(&existeCombinacion)
//
// if err != nil {
// return fmt.Errorf("error al verificar combinación de sucursal y punto de venta: %w", err)
// }
//
// if existeCombinacion {
// return domain.ErrDuplicateRecord
// }
// }
//
// // Insertar en registroEmpresa
// query := `
// INSERT INTO registroEmpresa (
// codigo_ambiente,
// codigo_modalidad,
// codigo_punto_venta,
// codigo_sistema,
// codigo_sucursal,
// nit
// ) VALUES ($1, $2, $3, $4, $5, $6)
// RETURNING id
// `
// var registroID int
// err = tx.QueryRowContext(
// ctx,
// query,
// request.CodigoAmbiente,
// request.CodigoModalidad,
// request.CodigoPuntoVenta,
// request.CodigoSistema,
// request.CodigoSucursal,
// request.Nit,
// ).Scan(&registroID)
// if err != nil {
// return fmt.Errorf("error al insertar registro: %w", err)
// }
//
// // Insertar en cuis
// queryInsertCuis := `
// INSERT INTO cuis (cuis, fecha_vigencia, transaccion, fecha_creacion, registro_empresa_id)
// VALUES ($1, $2, $3, $4, $5)
// `
// _, err = tx.ExecContext(ctx, queryInsertCuis, codigoCuis, fechaVigencia, transaccion, currentBolivia, registroID)
// if err != nil {
// return fmt.Errorf("error al insertar CUIS: %w", err)
// }
//
// if err = tx.Commit(); err != nil {
// return fmt.Errorf("error al confirmar transacción: %w", err)
// }
//
// return nil
//}
//
//// ActualizarRegistro actualiza un registro existente de empresa y CUIS
//func (r *CuisRepository) ActualizarRegistro(ctx context.Context, empresaID int, cuisID int64, req request.SolicitudCuis, nuevoCuis string, fechaVigencia string) error {
// currentBolivia, err := utils.GetCurrentBoliviaTime()
// if err != nil {
// return err
// }
//
// // Iniciar tx
// tx, err := r.DB.BeginTx(ctx, nil)
// if err != nil {
// return fmt.Errorf("iniciar transacción: %w", err)
// }
// defer tx.Rollback()
//
// // Actualizar empresa
// _, err = tx.ExecContext(ctx, `
// UPDATE registroEmpresa
// SET codigo_ambiente=$1, codigo_modalidad=$2, codigo_punto_venta=$3,
// codigo_sistema=$4, codigo_sucursal=$5, nit=$6, fecha_actualizacion=$7
// WHERE id=$8
// `,
// req.CodigoAmbiente, req.CodigoModalidad, req.CodigoPuntoVenta,
// req.CodigoSistema, req.CodigoSucursal, req.Nit, currentBolivia, empresaID,
// )
// if err != nil {
// return fmt.Errorf("actualizar registroEmpresa: %w", err)
// }
//
// // Comprobar si hay un CUIS existente
// var existingCuis string
// err = tx.QueryRowContext(ctx, `SELECT cuis FROM cuis WHERE id=$1 AND registro_empresa_id=$2`, cuisID, empresaID).Scan(&existingCuis)
// if err != nil && err != sql.ErrNoRows {
// return fmt.Errorf("consultar cuis existente: %w", err)
// }
//
// // Insertar o actualizar
// if err == sql.ErrNoRows {
// _, err = tx.ExecContext(ctx, `INSERT INTO cuis (registro_empresa_id, cuis) VALUES ($1, $2)`, empresaID, nuevoCuis)
// if err != nil {
// return fmt.Errorf("insertar nuevo cuis: %w", err)
// }
// } else if existingCuis != nuevoCuis {
// _, err = tx.ExecContext(ctx, `UPDATE cuis SET cuis=$1, fecha_vigencia=$2, fecha_actualizacion=$3 WHERE id=$4`,
// nuevoCuis, fechaVigencia, currentBolivia, cuisID)
// if err != nil {
// return fmt.Errorf("actualizar cuis existente: %w", err)
// }
// }
//
// // Confirmar tx
// if err := tx.Commit(); err != nil {
// return fmt.Errorf("confirmar transacción: %w", err)
// }
// return nil
//}
//// service/cuis_service.go
//package service
//
//import (
//"context"
//"fmt"
//"log"
//"strconv"
//
//"obtencionCodigo/domain"
//"obtencionCodigo/repository"
//"obtencionCodigo/utils"
//"daemonService/internal/models/obtencionCodigos"
//"daemonService/internal/models/obtencionCodigos/request"
//)
//
//// Función externa que se supone que existe en el proyecto original
//// Esta es una aproximación, ya que no tenemos el código fuente
//var codigoProcesarRegistro = func(model *obtencionCodigos.CuisServiceModel, request request.SolicitudCuis, intentos int) (interface{}, error) {
// // Esta función debe implementarse según el código original
// return nil, nil
//}
//
//type CuisService struct {
// Model *obtencionCodigos.CuisServiceModel
// EmpresaRepo *repository.EmpresaRepository
// CuisRepo *repository.CuisRepository
// Logger *log.Logger
//}
//
//// GetName retorna el nombre del servicio
//func (s *CuisService) GetName() string {
// return s.Model.Name
//}
//
//// logMessage registra un mensaje con el prefijo del servicio
//func (s *CuisService) logMessage(format string, v ...interface{}) {
// if s.Logger != nil {
// s.Logger.Printf(format, v...)
// }
//}
//
//// logError registra un error y devuelve el mismo error
//func (s *CuisService) logError(format string, err error, v ...interface{}) error {
// args := append([]interface{}{err}, v...)
// if s.Logger != nil {
// s.Logger.Printf(format, args...)
// }
// return fmt.Errorf(format, args...)
//}
//
//// ValidateCuisRequest valida los datos de la solicitud CUIS
//func (s *CuisService) ValidateCuisRequest(request request.SolicitudCuis) error {
// if request.CodigoAmbiente == "" ||
// request.CodigoModalidad == "" ||
// request.CodigoPuntoVenta == "" ||
// request.CodigoSistema == "" ||
// request.CodigoSucursal == "" ||
// request.Nit == "" {
// return domain.ErrAllFieldsRequired
// }
// return nil
//}
//
//// BuildCuisRequestFromEmpresa crea una solicitud CUIS a partir de datos de empresa
//func (s *CuisService) BuildCuisRequestFromEmpresa(ec domain.EmpresaConCuis) request.SolicitudCuis {
// return request.SolicitudCuis{
// CodigoAmbiente: strconv.Itoa(ec.CodigoAmbiente),
// CodigoModalidad: strconv.Itoa(ec.CodigoModalidad),
// CodigoPuntoVenta: strconv.Itoa(ec.CodigoPuntoVenta),
// CodigoSistema: ec.CodigoSistema,
// CodigoSucursal: strconv.Itoa(ec.CodigoSucursal),
// Nit: ec.Nit,
// }
//}
//
//// ProcessEmpresa procesa una empresa y actualiza su CUIS si es necesario
//func (s *CuisService) ProcessEmpresa(ctx context.Context, ec domain.EmpresaConCuis) (interface{}, error) {
// s.logMessage("Procesando empresa ID: %d", ec.ID)
//
// if len(ec.Cuis) == 0 {
// s.logMessage("Empresa ID %d sin CUIS registrados", ec.ID)
// return nil, nil
// }
//
// var lastResponse interface{}
// for _, c := range ec.Cuis {
// s.logMessage("Procesando CUIS ID: %d", c.Cuis_id)
//
// cuisRequest := s.BuildCuisRequestFromEmpresa(ec)
// respCuis, err := codigoProcesarRegistro(s.Model, cuisRequest, 0)
//
// if err != nil {
// s.logError(utils.LogErrorProcessing, err, cuisRequest.CodigoSistema)
// continue
// }
//
// // Parsear respuesta SOAP
// nuevoCuis, fechaVigencia, transaccion, err := utils.ParseSoapCuisResponse(respCuis)
// if err != nil {
// s.logError("Error al parsear respuesta SOAP: %v", err)
// continue
// }
//
// if err := s.CuisRepo.ActualizarRegistro(ctx, ec.ID, c.Cuis_id, cuisRequest, nuevoCuis, fechaVigencia); err != nil {
// s.logError("Error actualizando registro: %v", err)
// continue
// }
//
// lastResponse = respCuis
// }
//
// return lastResponse, nil
//}
//
//// handler/api_handler.go
//package handler
//
//import (
//"context"
//"encoding/json"
//"fmt"
//"net/http"
//
//"obtencionCodigo/service"
//"obtencionCodigo/utils"
//"daemonService/internal/models/obtencionCodigos/request"
//)
//
//type APIHandler struct {
// CuisService *service.CuisService
//}
//
//// ParseCuisRequest parsea la solicitud de CUIS desde el body del request
//func (h *APIHandler) ParseCuisRequest(w http.ResponseWriter, req *http.Request) (request.SolicitudCuis, error) {
// var cuisRequest request.SolicitudCuis
//
// // Limitar el tamaño del body para prevenir ataques DoS
// req.Body = http.MaxBytesReader(w, req.Body, utils.MaxBodySize)
//
// decoder := json.NewDecoder(req.Body)
// decoder.DisallowUnknownFields() // Rechazar campos desconocidos
//
// if err := decoder.Decode(&cuisRequest); err != nil {
// return cuisRequest, fmt.Errorf("error al decodificar JSON: %v", err)
// }
//
// return cuisRequest, nil
//}
//
//// HandleAPIRequest maneja las solicitudes API
//func (h *APIHandler) HandleAPIRequest(ctx context.Context, w http.ResponseWriter, req *http.Request) (interface{}, error) {
// // Parsear la solicitud
// cuisRequest, err := h.ParseCuisRequest(w, req)
// if err != nil {
// return nil, err
// }
//
// // Validar la solicitud
// if err := h.CuisService.ValidateCuisRequest(cuisRequest); err != nil {
// return nil, fmt.Errorf("datos inválidos: %v", err)
// }
//
// // Procesar la solicitud
// respCuis, err := codigoProcesarRegistro(h.CuisService.Model, cuisRequest, 0)
// if err != nil {
// return nil, fmt.Errorf(utils.LogErrorProcessing, cuisRequest.CodigoSistema, err)
// }
//
// // Parsear respuesta SOAP
// codigoCuis, fechaVigencia, transaccion, err := utils.ParseSoapCuisResponse(respCuis)
// if err != nil {
// return nil, fmt.Errorf("error al parsear respuesta SOAP: %v", err)
// }
//
// // Crear nuevo registro
// if err := h.CuisService.CuisRepo.CrearNuevoRegistro(ctx, cuisRequest, codigoCuis, fechaVigencia, transaccion); err != nil {
// return nil, fmt.Errorf("error creando nuevo registro: %v", err)
// }
//
// return respCuis, nil
//}
//// handler/cron_handler.go
//package handler
//
//import (
//"context"
//
//"obtencionCodigo/service"
//)
//
//type CronHandler struct {
// CuisService *service.CuisService
//}
//
//// HandleCronRequest maneja las ejecuciones programadas
//func (h *CronHandler) HandleCronRequest(ctx context.Context) (interface{}, error) {
// // Obtener todas las empresas con sus CUIS
// empresasConCuis, err := h.CuisService.EmpresaRepo.GetEmpresasConCuisMinimal(ctx)
// if err != nil {
// return nil, fmt.Errorf("error al obtener empresas con CUIS: %v", err)
// }
//
// var lastResponse interface{}
// // Procesar cada empresa
// for _, ec := range empresasConCuis {
// resp, err := h.CuisService.ProcessEmpresa(ctx, ec)
// if err != nil {
// h.CuisService.logError("Error procesando empresa %d: %v", err, ec.ID)
// continue
// }
// if resp != nil {
// lastResponse = resp
// }
// }
//
// return lastResponse, nil
//}
//// service.go (punto de entrada principal)
//package obtencionCodigo
//
//import (
//"context"
//"log"
//"net/http"
//
//"obtencionCodigo/domain"
//"obtencionCodigo/repository"
//"obtencionCodigo/service"
//"obtencionCodigo/handler"
//"obtencionCodigo/utils"
//"daemonService/internal/models/obtencionCodigos"
//)
//
//type ObtencionCodigoService struct {
// Model obtencionCodigos.CuisServiceModel
// Logger *log.Logger
// EmpresaRepo *repository.EmpresaRepository
// CuisRepo *repository.CuisRepository
// CuisService *service.CuisService
// APIHandler *handler.APIHandler
// CronHandler *handler.CronHandler
//}
//
//// NewObtencionCodigoService crea una nueva instancia del servicio
//func NewObtencionCodigoService(model obtencionCodigos.CuisServiceModel, logger *log.Logger) *ObtencionCodigoService {
// empresaRepo := &repository.EmpresaRepository{DB: model.DB}
// cuisRepo := &repository.CuisRepository{DB: model.DB}
//
// cuisService := &service.CuisService{
// Model: &model,
// EmpresaRepo: empresaRepo,
// CuisRepo: cuisRepo,
// Logger: logger,
// }
//
// return &ObtencionCodigoService{
// Model: model,
// Logger: logger,
// EmpresaRepo: empresaRepo,
// CuisRepo: cuisRepo,
// CuisService: cuisService,
// APIHandler: &handler.APIHandler{CuisService: cuisService},
// CronHandler: &handler.CronHandler{CuisService: cuisService},
// }
//}
//
//// GetName retorna el nombre del servicio
//func (s *ObtencionCodigoService) GetName() string {
// return s.Model.Name
//}
//
//// ExecuteGetCode maneja la obtención de códigos CUIS
//func (s *ObtencionCodigoService) ExecuteGetCode(dbCtx context.Context, w http.ResponseWriter, req *http.Request, isCron bool) (interface{}, error) {
// s.Logger.Printf(utils.LogServiceStart, s.GetName())
// defer s.Logger.Printf(utils.LogServiceEnd, s.GetName())
//
// var respCuis interface{}
// var err error
//
// if isCron {
// respCuis, err = s.CronHandler.HandleCronRequest(dbCtx)
// } else {
// respCuis, err = s.APIHandler.HandleAPIRequest(dbCtx, w, req)
// }
//
// if err != nil {
// return nil, err
// }
// return respCuis, nil
//}

View File

@ -0,0 +1,54 @@
package obtencionCodigo
import (
"context"
"daemonService/internal/handlers"
"daemonService/internal/models/obtencionCodigos"
service "daemonService/internal/services"
"daemonService/internal/utils"
"net/http"
)
// maneja la obtención de CUIS
type ObtencionCodigoService struct {
Model obtencionCodigos.CuisServiceModel
CuisService *service.CuisService
CronHandler *handlers.CronHandler
ApiHandler *handlers.APIHandler
}
// crea una nueva instancia del servicio
func NewObtencionCodigoService(
model obtencionCodigos.CuisServiceModel, cuisService *service.CuisService,
cronHandler *handlers.CronHandler, apiHandler *handlers.APIHandler,
) *ObtencionCodigoService {
return &ObtencionCodigoService{
Model: model, CuisService: cuisService,
CronHandler: cronHandler, ApiHandler: apiHandler,
}
}
// maneja la obtención de códigos CUIS
func (s *ObtencionCodigoService) ExecuteGetCode(dbCtx context.Context, w http.ResponseWriter, req *http.Request, isCron bool) (interface{}, error) {
s.CuisService.LogMessage(utils.LogServiceStart, s.GetName())
defer s.CuisService.LogMessage(utils.LogServiceEnd, s.GetName())
var respCuis interface{}
var err error
if isCron {
respCuis, err = s.CronHandler.ProcessCronMode(dbCtx)
} else {
respCuis, err = s.ApiHandler.HandleAPIRequest(dbCtx, w, req)
}
if err != nil {
return nil, err
}
return respCuis, nil
}
// GetName retorna el nombre del servicio
func (s *ObtencionCodigoService) GetName() string {
return s.Model.Name
}

View File

@ -0,0 +1,851 @@
package obtencionCodigo
//
//import (
// "context"
// "daemonService/internal/handlers"
// "daemonService/internal/models"
// svc "daemonService/internal/models"
// "daemonService/internal/models/obtencionCodigos"
// "daemonService/internal/repositories"
// "daemonService/internal/services/obtencionCodigo"
// "daemonService/internal/services/procesar"
// "daemonService/internal/services/sincronizacionDatos"
// "flag"
// "fmt"
// "github.com/robfig/cron/v3"
// "log"
// "net/http"
// "os"
// "os/signal"
// "syscall"
// "time"
//
// "daemonService/internal/api"
// "daemonService/internal/config"
// "daemonService/internal/database"
// "daemonService/internal/notifications"
// "daemonService/internal/services"
//)
//
//func main() {
// configFile := flag.String("config", "configs/config.yaml", "Ruta al archivo de configuración")
// runOnce := flag.Bool("run-once", false, "Ejecutar servicios una vez y salir")
// serviceName := flag.String("service", "", "Nombre del servicio específico a ejecutar")
// flag.Parse()
//
// // Cargar configuración
// cfg, err := config.LoadConfig(*configFile)
// if err != nil {
// log.Fatalf("Error al cargar configuración: %v", err)
// }
//
// // Inicializar la base de datos
// db, err := database.InitDB(*cfg)
// if err != nil {
// log.Fatalf("Error al inicializar la base de datos: %v", err)
// }
// defer db.Close()
//
// // Crear sistema de notificaciones
// notifier := notifications.CreateNotificationSender(cfg.Notifications.Enabled, cfg.Notifications.Method, cfg.Notifications.Config)
// if notifier != nil {
// log.Printf("Sistema de notificaciones iniciado: %s", cfg.Notifications.Method)
// }
//
// // Crear servicios a partir de la configuración
// //var cuisServices []models.CuisService
// var servicesList []models.SoapService
// var servicesList2 []obtencionCodigos.CuisService
// for _, svcCfg := range cfg.Services {
// if !svcCfg.Enabled {
// continue
// }
//
// // Crear el logger para el servicio
// serviceLogger := log.New(log.Writer(), fmt.Sprintf("[%s] ", svcCfg.Name), log.LstdFlags)
//
// log.Println(cfg.SOAP.FACTURA_SINCRONIZACION.Retries)
// baseSvc := svc.ServiceModel{
// Name: svcCfg.Name,
// Enabled: svcCfg.Enabled,
// Concurrency: svcCfg.Concurrency,
// DB: db,
// SOAPEndpoint: cfg.SOAP.FACTURA_SINCRONIZACION.Endpoint,
// SOAPTimeout: time.Duration(cfg.SOAP.FACTURA_SINCRONIZACION.Timeout) * time.Second,
// SOAPRetries: cfg.SOAP.FACTURA_SINCRONIZACION.Retries,
// NotificationSender: notifier,
// APIKey: cfg.SOAP.APIKey,
// TagNames: "",
// QueryInsert: "",
// MsgCustomResponse: "",
// }
//
// cuisServiceModel := obtencionCodigos.CuisServiceModel{
// Name: svcCfg.Name,
// Enabled: svcCfg.Enabled,
// Concurrency: svcCfg.Concurrency,
// DB: db,
// SOAPEndpoint: cfg.SOAP.FACTURA_CODIGO.Endpoint,
// SOAPTimeout: time.Duration(cfg.SOAP.FACTURA_CODIGO.Timeout) * time.Second,
// SOAPRetries: cfg.SOAP.FACTURA_CODIGO.Retries,
// NotificationSender: notifier,
// APIKey: cfg.SOAP.APIKey,
// TagNames: "",
// QueryInsert: "",
// MsgCustomResponse: "",
// }
//
// //registrar-empresa solo para registrar empresa
// cuisServiceModel.TagNames = "cuis"
// cuisServiceModel.QueryInsert = "INSERT INTO registroEmpresa (codigo, reqSoap, respSoap, reqJson, respJson, cuis_id, fecha_creacion) VALUES ($1, $2, $3, $4, $5, $6, NOW() AT TIME ZONE 'America/La_Paz') ON CONFLICT (codigo, cuis_id) DO UPDATE SET reqSoap = EXCLUDED.reqSoap, respSoap = EXCLUDED.respSoap, reqJson = EXCLUDED.reqJson, respJson = EXCLUDED.respJson, cuis_id = EXCLUDED.cuis_id, fecha_actualizacion = NOW() AT TIME ZONE 'America/La_Paz';"
// cuisServiceModel.MsgCustomResponse = "Registrar Empresa"
//
// procesarRegistro := procesar.NewProcesarRegistro(cuisServiceModel)
// empresaRepository := repositories.NewEmpresaRepository(cuisServiceModel)
// cuisService := services.NewCuisService(cuisServiceModel, serviceLogger)
// cuisRepository := repositories.NewCuisRepository(baseSvc, cuisService)
// cronHandler := handlers.NewCronHandler(cuisService, empresaRepository, cuisRepository, procesarRegistro)
// apiHandler := handlers.NewApiHandler(cuisService, cuisRepository, procesarRegistro)
//
// // Llamar a NewObtencionCodigoService correctamente
// obtencionCodigoSvc := obtencionCodigo.NewObtencionCodigoService(
// cuisServiceModel,
// cuisService,
// cronHandler,
// apiHandler,
// )
//
// switch svcCfg.Name {
// case "registrar-empresa":
// servicesList2 = append(servicesList2, obtencionCodigoSvc)
// break
// case "leyendas_factura":
// baseSvc.TagNames = "sincronizarListaLeyendasFactura"
// baseSvc.QueryInsert = "INSERT INTO leyendas_factura (codigo, reqSoap, respSoap, reqJson, respJson, cuis_id, fecha_creacion) VALUES ($1, $2, $3, $4, $5, $6, NOW() AT TIME ZONE 'America/La_Paz') ON CONFLICT (codigo, cuis_id) DO UPDATE SET reqSoap = EXCLUDED.reqSoap, respSoap = EXCLUDED.respSoap, reqJson = EXCLUDED.reqJson, respJson = EXCLUDED.respJson, cuis_id = EXCLUDED.cuis_id, fecha_actualizacion = NOW() AT TIME ZONE 'America/La_Paz';"
// baseSvc.MsgCustomResponse = "Listado Leyendas de Facturas"
// servicesList = append(servicesList, &sincronizacionDatos.SincronizacionDatosService{
// ServiceModel: baseSvc,
// })
// case "producto-servicio":
// baseSvc.TagNames = "sincronizarListaProductosServicios"
// baseSvc.QueryInsert = "INSERT INTO productos_servicios (codigo, reqSoap, respSoap, reqJson, respJson, cuis_id, fecha_creacion) VALUES ($1, $2, $3, $4, $5, $6, NOW() AT TIME ZONE 'America/La_Paz') ON CONFLICT (codigo, cuis_id) DO UPDATE SET reqSoap = EXCLUDED.reqSoap, respSoap = EXCLUDED.respSoap, reqJson = EXCLUDED.reqJson, respJson = EXCLUDED.respJson, cuis_id = EXCLUDED.cuis_id, fecha_actualizacion = NOW() AT TIME ZONE 'America/La_Paz';"
// baseSvc.MsgCustomResponse = "Listado Producto Servicio"
// servicesList = append(servicesList, &sincronizacionDatos.SincronizacionDatosService{
// ServiceModel: baseSvc,
// })
// case "tipo_documento_identidad":
// baseSvc.TagNames = "sincronizarParametricaTipoDocumentoIdentidad"
// baseSvc.QueryInsert = "INSERT INTO tipo_documento_identidad (codigo, reqSoap, respSoap, reqJson, respJson, cuis_id, fecha_creacion) VALUES ($1, $2, $3, $4, $5, $6, NOW() AT TIME ZONE 'America/La_Paz') ON CONFLICT (codigo, cuis_id) DO UPDATE SET reqSoap = EXCLUDED.reqSoap, respSoap = EXCLUDED.respSoap, reqJson = EXCLUDED.reqJson, respJson = EXCLUDED.respJson, cuis_id = EXCLUDED.cuis_id, fecha_actualizacion = NOW() AT TIME ZONE 'America/La_Paz';"
// baseSvc.MsgCustomResponse = "Listado tipos de documento de identidad"
// servicesList = append(servicesList, &sincronizacionDatos.SincronizacionDatosService{
// ServiceModel: baseSvc,
// })
// case "tipo_documento_sector":
// baseSvc.TagNames = "sincronizarParametricaTipoDocumentoSector"
// baseSvc.QueryInsert = "INSERT INTO tipo_documento_sector (codigo, reqSoap, respSoap, reqJson, respJson, cuis_id, fecha_creacion) VALUES ($1, $2, $3, $4, $5, $6, NOW() AT TIME ZONE 'America/La_Paz') ON CONFLICT (codigo, cuis_id) DO UPDATE SET reqSoap = EXCLUDED.reqSoap, respSoap = EXCLUDED.respSoap, reqJson = EXCLUDED.reqJson, respJson = EXCLUDED.respJson, cuis_id = EXCLUDED.cuis_id, fecha_actualizacion = NOW() AT TIME ZONE 'America/La_Paz';"
// baseSvc.MsgCustomResponse = "Listado tipos de documento de identidad"
// servicesList = append(servicesList, &sincronizacionDatos.SincronizacionDatosService{
// ServiceModel: baseSvc,
// })
// case "tipo_metodo_pago":
// baseSvc.TagNames = "sincronizarParametricaTipoMetodoPago"
// baseSvc.QueryInsert = "INSERT INTO tipo_metodo_pago (codigo, reqSoap, respSoap, reqJson, respJson, cuis_id, fecha_creacion) VALUES ($1, $2, $3, $4, $5, $6, NOW() AT TIME ZONE 'America/La_Paz') ON CONFLICT (codigo, cuis_id) DO UPDATE SET reqSoap = EXCLUDED.reqSoap, respSoap = EXCLUDED.respSoap, reqJson = EXCLUDED.reqJson, respJson = EXCLUDED.respJson, cuis_id = EXCLUDED.cuis_id, fecha_actualizacion = NOW() AT TIME ZONE 'America/La_Paz';"
// baseSvc.MsgCustomResponse = "Listado tipos metodo de pago"
// servicesList = append(servicesList, &sincronizacionDatos.SincronizacionDatosService{
// ServiceModel: baseSvc,
// })
// case "tipo_moneda":
// baseSvc.TagNames = "sincronizarParametricaTipoMoneda"
// baseSvc.QueryInsert = "INSERT INTO tipo_moneda (codigo, reqSoap, respSoap, reqJson, respJson, cuis_id, fecha_creacion) VALUES ($1, $2, $3, $4, $5, $6, NOW() AT TIME ZONE 'America/La_Paz') ON CONFLICT (codigo, cuis_id) DO UPDATE SET reqSoap = EXCLUDED.reqSoap, respSoap = EXCLUDED.respSoap, reqJson = EXCLUDED.reqJson, respJson = EXCLUDED.respJson, cuis_id = EXCLUDED.cuis_id, fecha_actualizacion = NOW() AT TIME ZONE 'America/La_Paz';"
// baseSvc.MsgCustomResponse = "Listado tipos de moneda"
// servicesList = append(servicesList, &sincronizacionDatos.SincronizacionDatosService{
// ServiceModel: baseSvc,
// })
// case "tipo_punto_venta":
// baseSvc.TagNames = "sincronizarParametricaTipoPuntoVenta"
// baseSvc.QueryInsert = "INSERT INTO tipo_punto_venta (codigo, reqSoap, respSoap, reqJson, respJson, cuis_id, fecha_creacion) VALUES ($1, $2, $3, $4, $5, $6, NOW() AT TIME ZONE 'America/La_Paz') ON CONFLICT (codigo, cuis_id) DO UPDATE SET reqSoap = EXCLUDED.reqSoap, respSoap = EXCLUDED.respSoap, reqJson = EXCLUDED.reqJson, respJson = EXCLUDED.respJson, cuis_id = EXCLUDED.cuis_id, fecha_actualizacion = NOW() AT TIME ZONE 'America/La_Paz';"
// baseSvc.MsgCustomResponse = "Listado tipos de punto de venta"
// servicesList = append(servicesList, &sincronizacionDatos.SincronizacionDatosService{
// ServiceModel: baseSvc,
// })
// case "tipo_unidad_medida":
// baseSvc.TagNames = "sincronizarParametricaUnidadMedida"
// baseSvc.QueryInsert = "INSERT INTO tipo_unidad_medida (codigo, reqSoap, respSoap, reqJson, respJson, cuis_id, fecha_creacion) VALUES ($1, $2, $3, $4, $5, $6, NOW() AT TIME ZONE 'America/La_Paz') ON CONFLICT (codigo, cuis_id) DO UPDATE SET reqSoap = EXCLUDED.reqSoap, respSoap = EXCLUDED.respSoap, reqJson = EXCLUDED.reqJson, respJson = EXCLUDED.respJson, cuis_id = EXCLUDED.cuis_id, fecha_actualizacion = NOW() AT TIME ZONE 'America/La_Paz';"
// baseSvc.MsgCustomResponse = "Listado tipos de unidad de medida"
// servicesList = append(servicesList, &sincronizacionDatos.SincronizacionDatosService{
// ServiceModel: baseSvc,
// })
// default:
// log.Printf("Servicio desconocido: %s", svcCfg.Name)
// }
// }
//
// // Si se especifica un servicio específico, filtrar la lista.
// if *serviceName != "" {
// var filtered []models.SoapService
// for _, s := range servicesList {
// if s.GetName() == *serviceName {
// filtered = append(filtered, s)
// break
// }
// }
// if len(filtered) == 0 {
// log.Fatalf("No se encontró el servicio: %s", *serviceName)
// }
// servicesList = filtered
// log.Printf("Ejecutando solo el servicio: %s", *serviceName)
// }
//
// // Crear contexto con cancelación para manejar señales de terminación.
// ctx, cancel := context.WithCancel(context.Background())
// defer cancel()
//
// // Canal para capturar señales del sistema.
// sigChan := make(chan os.Signal, 1)
// signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
//
// // Iniciar servidor HTTP si se configura puerto para API.
// var httpServer *http.Server
// if cfg.API.Port > 0 {
// httpServer = api.StartAPIServer(cfg.API.Port, servicesList, servicesList2, false)
// }
//
// // Goroutine para manejar señales.
// go func() {
// sig := <-sigChan
// log.Printf("Recibida señal: %v. Cerrando...", sig)
// if notifier != nil {
// notifier.SendNotification("Servicio detenido", fmt.Sprintf("El servicio fue detenido por la señal: %v", sig))
// }
// cancel()
// if httpServer != nil {
// log.Println("Cerrando servidor HTTP...")
// shutdownCtx, cancelServer := context.WithTimeout(context.Background(), 10*time.Second)
// defer cancelServer()
// if err := httpServer.Shutdown(shutdownCtx); err != nil {
// log.Printf("Error al cerrar servidor HTTP: %v", err)
// }
// }
// }()
//
// // Si se debe ejecutar solo una vez.
// if *runOnce {
// errors := services.RunAllServices(ctx, servicesList, false)
// if len(errors) > 0 {
// for _, e := range errors {
// log.Printf("Error: %v", e)
// }
// os.Exit(1)
// }
// return
// }
//
// // Configurar primer cron
// c := cron.New(cron.WithSeconds())
// _, err = c.AddFunc(cfg.Schedule.FACTURA_SINCRONIZACION.Cron, func() {
// log.Println("Iniciando ejecución programada de servicios")
// execCtx, execCancel := context.WithCancel(ctx)
// defer execCancel()
//
// errors := services.RunAllServices(execCtx, servicesList, true)
// if len(errors) > 0 {
// errMsg := "Errores durante la ejecución programada:"
// for _, err := range errors {
// errMsg += "\n- " + err.Error()
// }
// if notifier != nil {
// notifier.SendNotification("Errores en ejecución programada", errMsg)
// }
// log.Println(errMsg)
// } else {
// log.Println("Ejecución programada completada exitosamente")
// }
// })
//
// if err != nil {
// log.Fatalf("Error al programar tareas: %v", err)
// }
//
// // Configurar segundo cron
// cronGetCode := cron.New(cron.WithSeconds())
// _, err = cronGetCode.AddFunc(cfg.Schedule.FACTURA_CODIGO.Cron, func() {
// log.Println("Iniciando ejecución programada para Obtener Codigos")
// execCtx, cancel := context.WithCancel(context.Background())
// defer cancel()
// //execCtx, execCancel := context.WithCancel(ctx)
// //defer execCancel()
//
// errors := services.RunAllGetCodeServices(execCtx, servicesList2, true)
// if len(errors) > 0 {
// errMsg := "Errores durante la ejecución programada Obtener Codigos:"
// for _, err := range errors {
// errMsg += "\n- " + err.Error()
// }
// if notifier != nil {
// notifier.SendNotification("Errores en ejecución programada Obtener Codigos", errMsg)
// }
// log.Println(errMsg)
// } else {
// log.Println("Ejecución programada completada exitosamente Obtener Codigos")
// }
// })
//
// if err != nil {
// log.Fatalf("Error al programar tareas Obtener Codigos: %v", err)
// }
//
// // Iniciar ambos cron
// c.Start()
// cronGetCode.Start()
//
// log.Printf("Demonio de servicios iniciado. Programación: %s", cfg.Schedule.FACTURA_SINCRONIZACION.Cron)
// log.Printf("Demonio de obtención de códigos iniciado. Programación: %s", cfg.Schedule.FACTURA_CODIGO.Cron)
//
// if notifier != nil {
// notifier.SendNotification("Servicios iniciados",
// fmt.Sprintf("Los servicios SOAP han sido iniciados con programación: %s", cfg.Schedule.FACTURA_SINCRONIZACION.Cron))
// }
//
// // Esperar a que el contexto termine y luego detener ambos cron
// <-ctx.Done()
// c.Stop()
// cronGetCode.Stop()
// log.Println("Demonios detenidos correctamente")
//}
//package obtencionCodigo
//
//import (
// "context"
// "daemonService/internal/services/procesar"
// "database/sql"
// "encoding/json"
// "errors"
// "fmt"
// "log"
// "net/http"
// "strconv"
// "time"
//
// "daemonService/internal/models/obtencionCodigos"
// "daemonService/internal/models/obtencionCodigos/request"
// "daemonService/internal/models/obtencionCodigos/response"
//)
//
//// Constantes para la configuración general
//const (
// maxBodySize = 1 * 1024 * 1024 // 1MB
// defaultTimeZone = "America/La_Paz"
// logServiceStart = "Iniciando servicio %s"
// logServiceEnd = "Finalizado servicio %s"
// logErrorProcessing = "Error procesando registro %s: %v"
//)
//
//// Errores comunes
//var (
// ErrAllFieldsRequired = errors.New("todos los campos son requeridos")
// ErrUnexpectedRespType = errors.New("tipo de respuesta inesperado")
//)
//
//// mapea toda la tabla registroEmpresa
//type RegistroEmpresa struct {
// ID int `sql:"id"`
// CodigoAmbiente int `sql:"codigo_ambiente"`
// CodigoModalidad int `sql:"codigo_modalidad"`
// CodigoPuntoVenta int `sql:"codigo_punto_venta"`
// CodigoSistema string `sql:"codigo_sistema"`
// CodigoSucursal int `sql:"codigo_sucursal"`
// Nit string `sql:"nit"`
// FechaCreacion time.Time `sql:"fecha_creacion"`
// FechaActualizacion time.Time `sql:"fecha_actualizacion"`
//}
//
//// solo tiene los dos campos que quieres de la tabla cuis
//type CuisMinimal struct {
// Cuis_id int64 `sql:"id"`
// Cuis string `sql:"cuis"`
// FechaVigencia time.Time `sql:"fecha_vigencia"`
//}
//
//// EmpresaConCuis agrupa una empresa con sus cuis mínimos
//type EmpresaConCuis struct {
// RegistroEmpresa
// Cuis []CuisMinimal
//}
//
//type ObtencionCodigoService struct {
// obtencionCodigos.CuisServiceModel
// procesar.Procesar
// Logger *log.Logger
//}
//
//// retorna el nombre del servicio
//func (s *ObtencionCodigoService) GetName() string {
// return s.Name
//}
//
//// registra un mensaje con el prefijo del servicio
//func (s *ObtencionCodigoService) logMessage(format string, v ...interface{}) {
// if s.Logger != nil {
// s.Logger.Printf(format, v...)
// }
//}
//
//// registra un error y devuelve el mismo error
//func (s *ObtencionCodigoService) logError(format string, err error, v ...interface{}) error {
// args := append([]interface{}{err}, v...)
// if s.Logger != nil {
// s.Logger.Printf(format, args...)
// }
// return fmt.Errorf(format, args...)
//}
//
//// maneja la obtención de códigos CUIS
//func (s *ObtencionCodigoService) ExecuteGetCode(dbCtx context.Context, w http.ResponseWriter, req *http.Request, isCron bool) (interface{}, error) {
// s.logMessage(logServiceStart, s.Name)
// defer s.logMessage(logServiceEnd, s.Name)
//
// var respCuis interface{}
// var err error
//
// if isCron {
// respCuis, err = s.processCronMode(dbCtx)
// } else {
// respCuis, err = s.processAPIMode(dbCtx, w, req)
// }
//
// if err != nil {
// return nil, err
// }
// return respCuis, nil
//}
//
//// procesa la ejecución en modo cron
//func (s *ObtencionCodigoService) processCronMode(dbCtx context.Context) (interface{}, error) {
// empresasConCuis, err := s.GetEmpresasConCuisMinimal(dbCtx)
// if err != nil {
// return nil, s.logError("Error al obtener empresas con CUIS: %v", err)
// }
//
// var lastResponse interface{}
// for _, ec := range empresasConCuis {
// s.logMessage("Procesando empresa ID: %d", ec.ID)
//
// if len(ec.Cuis) == 0 {
// s.logMessage("Empresa ID %d sin CUIS registrados", ec.ID)
// continue
// }
//
// for _, c := range ec.Cuis {
// s.logMessage("Procesando CUIS ID: %d", c.Cuis_id)
//
// cuisRequest := s.buildCuisRequestFromEmpresa(ec)
// respCuis, err := s.CodigoProcesarRegistro(&s.CuisServiceModel, cuisRequest, 0)
//
// if err != nil {
// s.logError(logErrorProcessing, err, cuisRequest.CodigoSistema)
// continue
// }
//
// if err := s.actualizarRegistro(dbCtx, ec.ID, c.Cuis_id, cuisRequest, respCuis); err != nil {
// s.logError("Error actualizando registro: %v", err)
// continue
// }
//
// lastResponse = respCuis
// }
// }
//
// return lastResponse, nil
//}
//
//// procesa la ejecución en modo API
//func (s *ObtencionCodigoService) processAPIMode(dbCtx context.Context, w http.ResponseWriter, req *http.Request) (interface{}, error) {
// cuisRequest, err := s.parseCuisRequest(w, req)
// if err != nil {
// return nil, err
// }
//
// if err := s.validateCuisRequest(cuisRequest); err != nil {
// return nil, s.logError("Datos inválidos: %v", err)
// }
//
// respCuis, err := s.CodigoProcesarRegistro(&s.CuisServiceModel, cuisRequest, 0)
// if err != nil {
// return nil, s.logError(logErrorProcessing, err, cuisRequest.CodigoSistema)
// }
//
// if err := s.crearNuevoRegistro(dbCtx, cuisRequest, respCuis); err != nil {
// return nil, s.logError("Error creando nuevo registro: %v", err)
// }
//
// return respCuis, nil
//}
//
//// parsea la solicitud de CUIS desde el body del request
//func (s *ObtencionCodigoService) parseCuisRequest(w http.ResponseWriter, req *http.Request) (request.SolicitudCuis, error) {
// var cuisRequest request.SolicitudCuis
//
// // Limitar el tamaño del body para prevenir ataques DoS
// req.Body = http.MaxBytesReader(w, req.Body, maxBodySize)
//
// decoder := json.NewDecoder(req.Body)
// decoder.DisallowUnknownFields() // Rechazar campos desconocidos
//
// if err := decoder.Decode(&cuisRequest); err != nil {
// return cuisRequest, s.logError("Error al decodificar JSON: %v", err)
// }
//
// return cuisRequest, nil
//}
//
//// valida los datos de la solicitud CUIS
//func (s *ObtencionCodigoService) validateCuisRequest(request request.SolicitudCuis) error {
// if request.CodigoAmbiente == "" ||
// request.CodigoModalidad == "" ||
// request.CodigoPuntoVenta == "" ||
// request.CodigoSistema == "" ||
// request.CodigoSucursal == "" ||
// request.Nit == "" {
// return ErrAllFieldsRequired
// }
// return nil
//}
//
//// crea una solicitud CUIS a partir de datos de empresa
//func (s *ObtencionCodigoService) buildCuisRequestFromEmpresa(ec EmpresaConCuis) request.SolicitudCuis {
// return request.SolicitudCuis{
// CodigoAmbiente: strconv.Itoa(ec.CodigoAmbiente),
// CodigoModalidad: strconv.Itoa(ec.CodigoModalidad),
// CodigoPuntoVenta: strconv.Itoa(ec.CodigoPuntoVenta),
// CodigoSistema: ec.CodigoSistema,
// CodigoSucursal: strconv.Itoa(ec.CodigoSucursal),
// Nit: ec.Nit,
// }
//}
//
//// obtiene todas las empresas con sus CUIS
//func (s *ObtencionCodigoService) GetEmpresasConCuisMinimal(ctx context.Context) ([]EmpresaConCuis, error) {
// const query = `
// SELECT
// -- columnas de registroEmpresa
// re.id, re.codigo_ambiente, re.codigo_modalidad, re.codigo_punto_venta,
// re.codigo_sistema, re.codigo_sucursal, re.nit,
// re.fecha_creacion, re.fecha_actualizacion,
// -- columnas específicas de cuis
// c.id, c.cuis, c.fecha_vigencia
// FROM registroEmpresa re
// LEFT JOIN cuis c ON c.registro_empresa_id = re.id
// ORDER BY re.id;
// `
//
// rows, err := s.DB.QueryContext(ctx, query)
// if err != nil {
// return nil, fmt.Errorf("consulta empresas con cuis minimal: %w", err)
// }
// defer rows.Close()
//
// // Mapa temporal para agrupar por empresa.ID
// empresasMap := make(map[int]*EmpresaConCuis)
//
// for rows.Next() {
// // Variables temporales de escaneo
// var (
// id int
// codigoAmbiente int
// codigoModalidad int
// codigoPuntoVenta int
// codigoSistema string
// codigoSucursal int
// nit string
// fechaCreacion time.Time
// fechaActualizacion time.Time
//
// idCuis sql.NullInt64
// cuis sql.NullString
// fechaVigencia sql.NullTime
// )
//
// // Leer la fila
// if err := rows.Scan(
// &id, &codigoAmbiente, &codigoModalidad, &codigoPuntoVenta,
// &codigoSistema, &codigoSucursal, &nit,
// &fechaCreacion, &fechaActualizacion,
// &idCuis, &cuis, &fechaVigencia,
// ); err != nil {
// return nil, fmt.Errorf("scan fila: %w", err)
// }
//
// // ¿Ya existe la empresa en el mapa?
// ent, ok := empresasMap[id]
// if !ok {
// // Si no existe, la creamos y rellenamos datos de registroEmpresa
// ent = &EmpresaConCuis{
// RegistroEmpresa: RegistroEmpresa{
// ID: id,
// CodigoAmbiente: codigoAmbiente,
// CodigoModalidad: codigoModalidad,
// CodigoPuntoVenta: codigoPuntoVenta,
// CodigoSistema: codigoSistema,
// CodigoSucursal: codigoSucursal,
// Nit: nit,
// FechaCreacion: fechaCreacion,
// FechaActualizacion: fechaActualizacion,
// },
// Cuis: make([]CuisMinimal, 0, 1),
// }
// empresasMap[id] = ent
// }
//
// // Si hay un CUIS válido, lo añadimos al slice
// if idCuis.Valid && cuis.Valid && fechaVigencia.Valid {
// ent.Cuis = append(ent.Cuis, CuisMinimal{
// Cuis_id: idCuis.Int64,
// Cuis: cuis.String,
// FechaVigencia: fechaVigencia.Time,
// })
// }
// }
//
// if err := rows.Err(); err != nil {
// return nil, fmt.Errorf("iteración filas: %w", err)
// }
//
// // Convertimos el mapa a slice ordenado por aparición
// resp := make([]EmpresaConCuis, 0, len(empresasMap))
// for _, ent := range empresasMap {
// resp = append(resp, *ent)
// }
// return resp, nil
//}
//
//// busca un registro de empresa por sus criterios
//func (s *ObtencionCodigoService) buscarRegistroEmpresa(ctx context.Context, request request.SolicitudCuis) (int, error) {
// query := `
// SELECT id
// FROM registroEmpresa
// WHERE codigo_sistema = $1
// AND nit = $2
// AND codigo_ambiente = $3
// AND codigo_punto_venta = $4
// AND codigo_sucursal = $5
// LIMIT 1
// `
//
// var id int
// err := s.DB.QueryRowContext(
// ctx,
// query,
// request.CodigoSistema,
// request.Nit,
// request.CodigoAmbiente,
// request.CodigoPuntoVenta,
// request.CodigoSucursal,
// ).Scan(&id)
//
// if err == sql.ErrNoRows {
// return 0, nil // No existe el registro
// }
//
// if err != nil {
// return 0, err
// }
//
// return id, nil
//}
//
//// obtiene la hora actual en la zona horaria de Bolivia
//func (s *ObtencionCodigoService) getCurrentBoliviaTime() (time.Time, error) {
// loc, err := time.LoadLocation(defaultTimeZone)
// if err != nil {
// return time.Time{}, err
// }
// return time.Now().In(loc), nil
//}
//
//// parsea la respuesta SOAP para CUIS
//func (s *ObtencionCodigoService) parseSoapCuisResponse(respSoap interface{}) (string, string, bool, error) {
// resp, ok := respSoap.(response.SoapBodyCuis)
// if !ok {
// return "", "", false, fmt.Errorf("%w: %T", ErrUnexpectedRespType, respSoap)
// }
//
// return resp.Response.Respuesta.Codigo,
// resp.Response.Respuesta.FechaVigencia,
// resp.Response.Respuesta.Transaccion,
// nil
//}
//
//// crea un nuevo registro de empresa y CUIS
//func (s *ObtencionCodigoService) crearNuevoRegistro(ctx context.Context, request request.SolicitudCuis, respSoap interface{}) error {
// currentBolivia, err := s.getCurrentBoliviaTime()
// if err != nil {
// return err
// }
//
// // Parsear respuesta
// codigoCuis, fechaVigencia, transaccion, err := s.parseSoapCuisResponse(respSoap)
// if err != nil {
// return err
// }
//
// // Iniciar transacción
// tx, err := s.DB.BeginTx(ctx, nil)
// if err != nil {
// return fmt.Errorf("error al iniciar transacción: %w", err)
// }
// defer tx.Rollback()
//
// // Verificar si ya existe el NIT en la base de datos
// var existeNIT bool
// queryVerificacionNIT := `
// SELECT EXISTS (
// SELECT 1 FROM registroEmpresa
// WHERE nit = $1
// )
// `
// err = tx.QueryRowContext(
// ctx,
// queryVerificacionNIT,
// request.Nit,
// ).Scan(&existeNIT)
//
// if err != nil {
// return fmt.Errorf("error al verificar existencia del NIT: %w", err)
// }
//
// // Si el NIT ya existe, verificar si la combinación de sucursal y punto de venta existe
// if existeNIT {
// var existeCombinacion bool
// queryVerificacionCombinacion := `
// SELECT EXISTS (
// SELECT 1 FROM registroEmpresa
// WHERE nit = $1
// AND codigo_sucursal = $2
// AND codigo_punto_venta = $3
// )
// `
// err = tx.QueryRowContext(
// ctx,
// queryVerificacionCombinacion,
// request.Nit,
// request.CodigoSucursal,
// request.CodigoPuntoVenta,
// ).Scan(&existeCombinacion)
//
// if err != nil {
// return fmt.Errorf("error al verificar combinación de sucursal y punto de venta: %w", err)
// }
//
// if existeCombinacion {
// return fmt.Errorf("ya existe un registro con el mismo NIT, código sucursal y código punto de venta")
// }
// }
//
// // Insertar en registroEmpresa
// query := `
// INSERT INTO registroEmpresa (
// codigo_ambiente,
// codigo_modalidad,
// codigo_punto_venta,
// codigo_sistema,
// codigo_sucursal,
// nit
// ) VALUES ($1, $2, $3, $4, $5, $6)
// RETURNING id
// `
// var registroID int
// err = tx.QueryRowContext(
// ctx,
// query,
// request.CodigoAmbiente,
// request.CodigoModalidad,
// request.CodigoPuntoVenta,
// request.CodigoSistema,
// request.CodigoSucursal,
// request.Nit,
// ).Scan(&registroID)
// if err != nil {
// return fmt.Errorf("error al insertar registro: %w", err)
// }
//
// // Insertar en cuis
// queryInsertCuis := `
// INSERT INTO cuis (cuis, fecha_vigencia, transaccion, fecha_creacion, registro_empresa_id)
// VALUES ($1, $2, $3, $4, $5)
// `
// _, err = tx.ExecContext(ctx, queryInsertCuis, codigoCuis, fechaVigencia, transaccion, currentBolivia, registroID)
// if err != nil {
// return fmt.Errorf("error al insertar CUIS: %w", err)
// }
//
// if err = tx.Commit(); err != nil {
// return fmt.Errorf("error al confirmar transacción: %w", err)
// }
//
// return nil
//}
//
//// actualiza un registro existente de empresa y CUIS
//func (s *ObtencionCodigoService) actualizarRegistro(ctx context.Context, empresaID int, cuisID int64, req request.SolicitudCuis, respRaw interface{}) error {
// currentBolivia, err := s.getCurrentBoliviaTime()
// if err != nil {
// return err
// }
//
// // 1. Iniciar tx
// tx, err := s.DB.BeginTx(ctx, nil)
// if err != nil {
// return fmt.Errorf("iniciar transacción: %w", err)
// }
// defer tx.Rollback()
//
// // 2. Parsear respuesta
// nuevoCuis, fechaVigencia, _, err := s.parseSoapCuisResponse(respRaw)
// if err != nil {
// return err
// }
//
// // 3. Actualizar empresa
// _, err = tx.ExecContext(ctx, `
// UPDATE registroEmpresa
// SET codigo_ambiente=$1, codigo_modalidad=$2, codigo_punto_venta=$3,
// codigo_sistema=$4, codigo_sucursal=$5, nit=$6, fecha_actualizacion=$7
// WHERE id=$8
// `,
// req.CodigoAmbiente, req.CodigoModalidad, req.CodigoPuntoVenta,
// req.CodigoSistema, req.CodigoSucursal, req.Nit, currentBolivia, empresaID,
// )
// if err != nil {
// return fmt.Errorf("actualizar registroEmpresa: %w", err)
// }
//
// // 4. Comprobar si hay un CUIS existente
// var existingCuis string
// err = tx.QueryRowContext(ctx, `SELECT cuis FROM cuis WHERE id=$1 AND registro_empresa_id=$2`, cuisID, empresaID).Scan(&existingCuis)
// if err != nil && err != sql.ErrNoRows {
// return fmt.Errorf("consultar cuis existente: %w", err)
// }
//
// // 5. Insertar o actualizar
// if err == sql.ErrNoRows {
// _, err = tx.ExecContext(ctx, `INSERT INTO cuis (registro_empresa_id, cuis) VALUES ($1, $2)`, empresaID, nuevoCuis)
// if err != nil {
// return fmt.Errorf("insertar nuevo cuis: %w", err)
// }
// } else if existingCuis != nuevoCuis {
// _, err = tx.ExecContext(ctx, `UPDATE cuis SET cuis=$1, fecha_vigencia=$2, fecha_actualizacion=$3 WHERE id=$4`,
// nuevoCuis, fechaVigencia, currentBolivia, cuisID)
// if err != nil {
// return fmt.Errorf("actualizar cuis existente: %w", err)
// }
// }
//
// // 6. Confirmar tx
// if err := tx.Commit(); err != nil {
// return fmt.Errorf("confirmar transacción: %w", err)
// }
// return nil
//}

View File

@ -0,0 +1,107 @@
package procesar
import (
"daemonService/internal/models/obtencionCodigos"
oc "daemonService/internal/models/obtencionCodigos/request"
"encoding/xml"
"fmt"
)
func codigoGenerarSOAPRequest(s obtencionCodigos.CuisServiceModel, solicitud oc.SolicitudCuisCufd) (string, error) {
var xmlBytes []byte
var err error
var envelope oc.SOAPRequestEnvelope
var envelopeCufd oc.SOAPRequestEnvelopeCufd
switch solicitud.Cuis {
case "":
envelope = oc.SOAPRequestEnvelope{
XmlnsSoapenv: "http://schemas.xmlsoap.org/soap/envelope/",
XmlnsSiat: "https://siat.impuestos.gob.bo/",
Header: &oc.SOAPRequestHeader{},
Body: oc.SOAPRequestBody{
Operacion: oc.OperacionXML{
XMLName: xml.Name{Local: "siat:" + s.TagNames},
Solicitud: oc.SolicitudCuisCufd{
CodigoAmbiente: solicitud.CodigoAmbiente,
CodigoModalidad: solicitud.CodigoModalidad,
CodigoPuntoVenta: solicitud.CodigoPuntoVenta,
CodigoSistema: solicitud.CodigoSistema,
CodigoSucursal: solicitud.CodigoSucursal,
Nit: solicitud.Nit,
},
},
},
}
// Se genera el XML con indentación para mayor legibilidad
xmlBytes, err = xml.MarshalIndent(envelope, "", " ")
if err != nil {
return "", fmt.Errorf("error al generar XML: %w", err)
}
break
default:
envelopeCufd = oc.SOAPRequestEnvelopeCufd{
XmlnsSoapenv: "http://schemas.xmlsoap.org/soap/envelope/",
XmlnsSiat: "https://siat.impuestos.gob.bo/",
Header: &oc.SOAPRequestHeaderCufd{},
Body: oc.SOAPRequestBodyCufd{
Operacion: oc.OperacionXMLCufd{
XMLName: xml.Name{Local: "siat:" + s.TagNamesCufd},
Solicitud: oc.SolicitudCuisCufd{
CodigoAmbiente: solicitud.CodigoAmbiente,
CodigoModalidad: solicitud.CodigoModalidad,
CodigoPuntoVenta: solicitud.CodigoPuntoVenta,
CodigoSistema: solicitud.CodigoSistema,
CodigoSucursal: solicitud.CodigoSucursal,
Cuis: solicitud.Cuis,
Nit: solicitud.Nit,
},
},
},
}
// Se genera el XML con indentación para mayor legibilidad
xmlBytes, err = xml.MarshalIndent(envelopeCufd, "", " ")
if err != nil {
return "", fmt.Errorf("error al generar XML: %w", err)
}
break
}
// Se concatena la cabecera XML
soapRequest := xml.Header + string(xmlBytes)
return soapRequest, nil
}
//func codigoGenerarSOAPRequest(s obtencionCodigos.CuisServiceModel, solicitud oc.SolicitudCuisCufd) (string, error) {
//
// envelope := oc.SOAPRequestEnvelope{
// XmlnsSoapenv: "http://schemas.xmlsoap.org/soap/envelope/",
// XmlnsSiat: "https://siat.impuestos.gob.bo/",
// Header: &oc.SOAPRequestHeader{},
// Body: oc.SOAPRequestBody{
// Operacion: oc.OperacionXML{
// XMLName: xml.Name{Local: "siat:" + s.TagNames},
// Solicitud: oc.SolicitudCuisCufd{
// CodigoAmbiente: solicitud.CodigoAmbiente,
// CodigoModalidad: solicitud.CodigoModalidad,
// CodigoPuntoVenta: solicitud.CodigoPuntoVenta,
// CodigoSistema: solicitud.CodigoSistema,
// CodigoSucursal: solicitud.CodigoSucursal,
// Nit: solicitud.Nit,
// },
// },
// },
// }
//
// // Se genera el XML con indentación para mayor legibilidad
// xmlBytes, err := xml.MarshalIndent(envelope, "", " ")
// if err != nil {
// return "", fmt.Errorf("error al generar XML: %w", err)
// }
//
// // Se concatena la cabecera XML
// soapRequest := xml.Header + string(xmlBytes)
//
// return soapRequest, nil
//}

View File

@ -0,0 +1,60 @@
package procesar
//
//import (
// "daemonService/internal/models/obtencionCodigos"
// "daemonService/internal/models/obtencionCodigos/request"
// "daemonService/internal/models/obtencionCodigos/response"
// oc "daemonService/internal/soap/obtencionCodigo"
// "fmt"
// "log"
// "net/http"
//)
//
//type ProcesarRegistroCufd struct {
// CuisServiceModel obtencionCodigos.CuisServiceModel
//}
//
//// instaciamos procesarRegistro
//func NewProcesarRegistroCufd(
// cuisServiceModel obtencionCodigos.CuisServiceModel,
//) *ProcesarRegistroCufd {
// return &ProcesarRegistroCufd{CuisServiceModel: cuisServiceModel}
//}
//
//func (s *ProcesarRegistroCufd) CodigoProcesarRegistroCufd(solicitud request.SolicitudCuisCufd, cuisID int) (interface{}, error) {
// //func codigoProcesarRegistro(s *obtencionCodigos.CuisServiceModel, solicitud request.SolicitudCuis, cuisID int) (interface{}, error) {
//
// log.Printf("Procesando código sistema: %s", solicitud.CodigoSistema)
//
// // 1. Generar solicitud SOAP
// soapReq, err := codigoGenerarSOAPRequest(s.CuisServiceModel, solicitud)
// if err != nil {
// //return fmt.Errorf("error generando SOAP: %v", err)
// fmt.Printf("error generando SOAP: %v", err)
// }
//
// // 2. Enviar solicitud con reintentos CUIS
// //resp, bodyBytes, err := s.enviarSOAPRequest(soapReq)
// resp, bodyBytes, err := oc.CodigoEnviarSOAPRequest(s.CuisServiceModel, soapReq)
// if err != nil {
// fmt.Printf("error enviando SOAP: %v", err)
// return nil, fmt.Errorf("error enviando SOAP: %v", err)
// }
// defer resp.Body.Close()
//
// // 3. Validar respuesta HTTP
// if resp.StatusCode != http.StatusOK {
// //return fmt.Errorf("código de estado HTTP inválido: %d", resp.StatusCode)
// fmt.Printf("código de estado HTTP inválido: %d", resp.StatusCode)
// }
//
// // 4. Parsear respuesta
// //println("respuestaCUIS:", string(bodyBytes))
// respCuis, err := oc.CuisParsearRespSOAP[[]response.CuisResponse](&s.CuisServiceModel, bodyBytes)
// if err != nil {
// //return fmt.Errorf("error parseando respuesta: %v", err)
// fmt.Printf("error parseando respuesta: %v", err)
// }
// return respCuis, nil
//}

View File

@ -0,0 +1,102 @@
package procesar
import (
"daemonService/internal/models/obtencionCodigos"
"daemonService/internal/models/obtencionCodigos/request"
"daemonService/internal/models/obtencionCodigos/response"
oc "daemonService/internal/soap/obtencionCodigo"
"fmt"
"log"
"net/http"
)
type ProcesarRegistro struct {
CuisServiceModel obtencionCodigos.CuisServiceModel
}
// instaciamos procesarRegistro
func NewProcesarRegistro(
cuisServiceModel obtencionCodigos.CuisServiceModel,
) *ProcesarRegistro {
return &ProcesarRegistro{CuisServiceModel: cuisServiceModel}
}
func (s *ProcesarRegistro) CodigoProcesarRegistro(solicitud request.SolicitudCuisCufd) (interface{}, interface{}, error) {
log.Printf("Procesando código sistema: %s", solicitud.CodigoSistema)
// cargamos las variables apikey y su token
s.CuisServiceModel.TokenKey = solicitud.KeyToken
s.CuisServiceModel.TokenValue = solicitud.ValueToken
// 1. Generar solicitud SOAP
soapReq, err := codigoGenerarSOAPRequest(s.CuisServiceModel, solicitud)
if err != nil {
fmt.Printf("error generando SOAP: %v", err)
return nil, nil, fmt.Errorf("error generando SOAP: %v", err)
}
// 2. Enviar solicitud con reintentos CUIS
resp, bodyBytes, err := oc.CodigoEnviarSOAPRequest(s.CuisServiceModel, soapReq)
if err != nil {
fmt.Printf("error enviando SOAP: %v", err)
return nil, nil, fmt.Errorf("error enviando SOAP: %v", err)
}
defer resp.Body.Close()
// 3. Validar respuesta HTTP
if resp.StatusCode != http.StatusOK {
fmt.Printf("código de estado HTTP inválido: %d", resp.StatusCode)
return nil, nil, fmt.Errorf("código de estado HTTP inválido: %d", resp.StatusCode)
}
// 4. Parsear respuesta
var respCuis response.SoapBodyCuis
var respCufd response.SoapBodyCufd
switch solicitud.Cuis {
case "":
respCuis, err = oc.CuisParsearRespSOAP[[]response.CuisResponse](&s.CuisServiceModel, bodyBytes)
if err != nil {
fmt.Printf("error parseando respuesta: %v", err)
return nil, nil, fmt.Errorf("error parseando respuesta: %v", err)
}
break
default:
respCufd, err = oc.CufdParsearRespSOAP[[]response.CufdResponse](&s.CuisServiceModel, bodyBytes)
if err != nil {
fmt.Printf("error parseando respuesta: %v", err)
return nil, nil, fmt.Errorf("error parseando respuesta: %v", err)
}
break
}
// 4. Parsear respuesta
//var leyendas any
//switch s.TagNames {
//case "sincronizarListaLeyendasFactura":
// leyendas, err = soap.ParsearRespLeyendaFacturaSOAP[[]response.LeyendaDetalle](s, bodyBytes)
// //leyendas, err := soap.ParsearRespLeyendaFacturaSOAP(s, bodyBytes)
// if err != nil {
// return fmt.Errorf("error parseando respuesta: %v", err)
// }
// break
//case "sincronizarParametricaTipoDocumentoIdentidad":
// leyendas, err = soap.ParsearRespTipoDocumentoSOAP[[]response.CodigoDetalle](s, bodyBytes)
// if err != nil {
// return fmt.Errorf("error parseando respuesta: %v", err)
// }
// break
//default:
// log.Println("no ingreso por ninguna de las opciones")
//}
// 5. Guardar en base de datos
//if err := s.guardarRegistro(solicitud, cuisID, soapReq, bodyBytes, leyendas); err != nil {
//if err := repositories.GuardarRegistro(s, solicitud, cuisID, soapReq, bodyBytes, leyendas); err != nil {
// return fmt.Errorf("error guardando registro: %v", err)
//}
//return nil
return respCuis, respCufd, nil
}

View File

@ -0,0 +1,59 @@
package services
import (
"context"
"daemonService/internal/models"
"daemonService/internal/models/obtencionCodigos"
"log"
"sync"
)
func RunAllServices(ctx context.Context, services []models.SoapService, isCron bool) []error {
var errors []error
var wg sync.WaitGroup
var mu sync.Mutex
for _, service := range services {
s := service
wg.Add(1)
go func() {
defer wg.Done()
serviceCtx, cancel := context.WithCancel(ctx)
defer cancel()
_, err := s.Execute(serviceCtx, nil, isCron)
if err != nil {
mu.Lock()
errors = append(errors, err)
mu.Unlock()
}
}()
}
wg.Wait()
log.Println("Todos los servicios han sido ejecutados")
return errors
}
func RunAllGetCodeServices(ctx context.Context, services []obtencionCodigos.CuisService, isCron bool) []error {
var errors []error
var wg sync.WaitGroup
var mu sync.Mutex
for _, service := range services {
s := service
wg.Add(1)
go func() {
defer wg.Done()
serviceCtx, cancel := context.WithCancel(ctx)
defer cancel()
_, err := s.ExecuteGetCode(serviceCtx, nil, nil, isCron)
if err != nil {
mu.Lock()
errors = append(errors, err)
mu.Unlock()
}
}()
}
wg.Wait()
log.Println("Todos los servicios han sido ejecutados Obtener Codigos")
return errors
}

View File

@ -0,0 +1,62 @@
package sincronizacionDatos
import (
"daemonService/internal/models"
sd "daemonService/internal/models/sincronizacionDatos/request"
"encoding/xml"
"fmt"
)
func GenerarSOAPRequest(s *models.ServiceModel, solicitud sd.SolicitudSincronizacion) (string, error) {
// Validar datos de entrada
//if err := validarSolicitud(solicitud); err != nil {
// return "", err
//}
envelope := sd.SOAPRequestEnvelope{
XmlnsSoapenv: "http://schemas.xmlsoap.org/soap/envelope/",
XmlnsSiat: "https://siat.impuestos.gob.bo/",
Header: &sd.SOAPRequestHeader{},
Body: sd.SOAPRequestBody{
Operacion: sd.OperacionXML{
XMLName: xml.Name{Local: "siat:" + s.TagNames},
Solicitud: sd.SolicitudSincronizacion{
CodigoAmbiente: solicitud.CodigoAmbiente,
CodigoPuntoVenta: solicitud.CodigoPuntoVenta,
CodigoSistema: solicitud.CodigoSistema,
CodigoSucursal: solicitud.CodigoSucursal,
Cuis: solicitud.Cuis,
Nit: solicitud.Nit,
},
},
},
}
// Se genera el XML con indentación para mayor legibilidad
xmlBytes, err := xml.MarshalIndent(envelope, "", " ")
if err != nil {
return "", fmt.Errorf("error al generar XML: %w", err)
}
// Se concatena la cabecera XML
soapRequest := xml.Header + string(xmlBytes)
return soapRequest, nil
}
// validarSolicitud verifica que los campos requeridos estén presentes en la solicitud
//func validarSolicitud(solicitud sd.SolicitudSincronizacionLeyendas) error {
// if solicitud.CodigoSistema == "" {
// return fmt.Errorf("código de sistema no puede estar vacío")
// }
// if solicitud.Nit == "0" {
// return fmt.Errorf("NIT no puede ser cero")
// }
// if solicitud.Cuis == "" {
// return fmt.Errorf("CUIS no puede estar vacío")
// }
// if solicitud.CodigoAmbiente == "0" {
// return fmt.Errorf("código de ambiente no puede ser cero")
// }
// return nil
//}

View File

@ -0,0 +1,48 @@
package sincronizacionDatos
import (
"daemonService/internal/models"
"daemonService/internal/models/sincronizacionDatos/request"
"daemonService/internal/repositories"
"daemonService/internal/services/utils"
"daemonService/internal/soap"
"fmt"
"log"
"net/http"
)
func ProcesarRegistro(s *models.ServiceModel, solicitud request.SolicitudSincronizacion, cuisID int) (interface{}, error) {
log.Printf("Procesando código sistema: %s", solicitud.CodigoSistema)
// 1. Generar solicitud SOAP
soapReq, err := GenerarSOAPRequest(s, solicitud)
if err != nil {
return nil, fmt.Errorf("error generando SOAP: %v", err)
}
// 2. Enviar solicitud con reintentos
resp, bodyBytes, err := soap.EnviarSOAPRequest(s, soapReq)
if err != nil {
return nil, fmt.Errorf("error enviando SOAP: %v", err)
}
defer resp.Body.Close()
// 3. Validar respuesta HTTP
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("código de estado HTTP inválido: %d", resp.StatusCode)
}
// 4. Parsear respuesta
respSincronizacion, err := utils.ProcesarRespuestaSincronizacion(s.TagNames, bodyBytes)
if err != nil {
return nil, err
}
// 5. Guardar en base de datos
if err := repositories.GuardarRegistro(s, solicitud, cuisID, soapReq, bodyBytes, respSincronizacion); err != nil {
return nil, fmt.Errorf("error guardando registro: %v", err)
}
return respSincronizacion, nil
}

View File

@ -0,0 +1,347 @@
package sincronizacionDatos
import (
"context"
"daemonService/internal/handlers"
"daemonService/internal/models"
"daemonService/internal/models/sincronizacionDatos/request"
"database/sql"
"errors"
"fmt"
"log"
"net/http"
"time"
)
type SincronizacionDatosService struct {
models.ServiceModel
}
func (s *SincronizacionDatosService) Execute(ctx context.Context, req *http.Request, isCron bool) (interface{}, error) {
var nit, codPuntoVenta, codSucursal, codigoSistema string
if req != nil {
//2) Recuperar los query params
nit = req.URL.Query().Get("nit")
codPuntoVenta = req.URL.Query().Get("code_point_sale")
codSucursal = req.URL.Query().Get("code_branch")
codigoSistema = req.URL.Query().Get("code_system")
log.Printf("nit: %v", nit)
log.Printf("codPuntoVenta: %v", codPuntoVenta)
log.Printf("codSucursal: %v", codSucursal)
log.Printf("codigoSistema: %v", codigoSistema)
}
// 1. Obtener datos de la base de datos
dbCtx, dbCancel := context.WithTimeout(context.Background(), 10*time.Minute)
defer dbCancel()
var rows *sql.Rows
var err error
// Verificar si los parámetros nit, codPuntoVenta y codSucursal tienen valores
if nit != "" && codPuntoVenta != "" && codSucursal != "" {
// Consulta específica con los parámetros proporcionados
query := `
SELECT
r.codigo_ambiente::text,
r.codigo_punto_venta::text,
r.codigo_sistema,
r.codigo_sucursal::text,
c.cuis,
r.nit,
r.token_key,
r.token_value,
c.id
FROM registroEmpresa r
JOIN cuis c ON r.id = c.registro_empresa_id
WHERE r.nit = $1 AND r.codigo_punto_venta = $2 AND r.codigo_sucursal = $3
ORDER BY r.id`
rows, err = s.DB.QueryContext(dbCtx, query, nit, codPuntoVenta, codSucursal)
} else if isCron || req == nil {
// Consulta para el caso de CRON (asumiendo que necesitas todos los registros)
query := `
SELECT
r.codigo_ambiente::text,
r.codigo_punto_venta::text,
r.codigo_sistema,
r.codigo_sucursal::text,
c.cuis,
r.nit,
r.token_key,
r.token_value,
c.id
FROM registroEmpresa r
JOIN cuis c ON r.id = c.registro_empresa_id
ORDER BY r.id`
rows, err = s.DB.QueryContext(dbCtx, query)
} else {
// Si no es CRON y faltan parámetros, retornar error
return nil, fmt.Errorf("parámetros incompletos: se requiere nit, code_point_sale y code_branch")
}
if err != nil {
return nil, handlers.HandleError(&s.ServiceModel, "Error en consulta SQL: %v", err)
}
defer rows.Close()
var respService interface{}
// 2. Procesar registros
success, errors := 0, 0
for rows.Next() {
var solicitud request.SolicitudSincronizacion
var cuisID int
var tokenKey string
var tokenValue string
if err := rows.Scan(
&solicitud.CodigoAmbiente,
&solicitud.CodigoPuntoVenta,
&solicitud.CodigoSistema,
&solicitud.CodigoSucursal,
&solicitud.Cuis,
&solicitud.Nit,
&tokenKey,
&tokenValue,
&cuisID,
); err != nil {
log.Printf("Error escaneando fila: %v", err)
errors++
continue
}
s.ServiceModel.KeyToken = tokenKey
s.ServiceModel.ValueToken = tokenValue
respService, err = ProcesarRegistro(&s.ServiceModel, solicitud, cuisID)
if err != nil {
log.Printf("Error procesando registro: %v", err)
errors++
} else {
success++
}
fmt.Printf("respuesta del servicio: %v", respService)
}
if err := rows.Err(); err != nil {
return nil, handlers.HandleError(&s.ServiceModel, "Error en resultados: %v", err)
}
log.Printf("Proceso completado. Éxitos: %d, Errores: %d", success, errors)
if errors > 0 {
return nil, fmt.Errorf("proceso completado con %d errores", errors)
}
return respService, nil
}
//func (s *SincronizacionDatosService) Execute(ctx context.Context, req *http.Request, isCron bool) (interface{}, error) {
//
// //2) Recuperar los query params
// nit := req.URL.Query().Get("nit")
// codPuntoVenta := req.URL.Query().Get("code_point_sale")
// codSucursal := req.URL.Query().Get("code_branch")
// codigoSistema := req.URL.Query().Get("code_system")
// log.Printf("ni: %v", nit)
// log.Printf("codPuntoVenta: %v", codPuntoVenta)
// log.Printf("codSucursal: %v", codSucursal)
// log.Printf("codigoSistema: %v", codigoSistema)
//
// // 1. Obtener datos de la base de datos
// dbCtx, dbCancel := context.WithTimeout(context.Background(), 10*time.Minute)
// defer dbCancel()
//
// ///////// cuando es CRON = true
// query := `
// SELECT
// r.codigo_ambiente::text,
// r.codigo_punto_venta::text,
// r.codigo_sistema,
// r.codigo_sucursal::text,
// c.cuis,
// r.nit,
// r.token_key,
// r.token_value,
// c.id
// FROM registroEmpresa r
// JOIN cuis c ON r.id = c.registro_empresa_id
// WHERE r.nit = $1 and r.codigo_punto_venta = $2 and r.codigo_sucursal = $3
// ORDER BY r.id`
//
// rows, err := s.DB.QueryContext(dbCtx, query, nit, codPuntoVenta, codSucursal)
// if err != nil {
// return nil, handlers.HandleError(&s.ServiceModel, "Error en consulta SQL: %v", err)
// }
// defer rows.Close()
//
// var respService interface{}
//
// // 2. Procesar registros
// success, errors := 0, 0
// for rows.Next() {
// var solicitud request.SolicitudSincronizacion
// var cuisID int
// var tokenKey string
// var tokenValue string
//
// if err := rows.Scan(
// &solicitud.CodigoAmbiente,
// &solicitud.CodigoPuntoVenta,
// &solicitud.CodigoSistema,
// &solicitud.CodigoSucursal,
// &solicitud.Cuis,
// &solicitud.Nit,
// &tokenKey,
// &tokenValue,
// &cuisID,
// ); err != nil {
// log.Printf("Error escaneando fila: %v", err)
// errors++
// continue
// }
//
// s.ServiceModel.KeyToken = tokenKey
// s.ServiceModel.ValueToken = tokenValue
// respService, err = ProcesarRegistro(&s.ServiceModel, solicitud, cuisID)
// if err != nil {
// log.Printf("Error escaneando fila: %v", err)
// }
//
// fmt.Printf("respuesta del servicio: %v", respService)
// }
//
// if err := rows.Err(); err != nil {
// return nil, handlers.HandleError(&s.ServiceModel, "Error en resultados: %v", err)
// }
//
// log.Printf("Proceso completado. Éxitos: %d, Errores: %d", success, errors)
// if errors > 0 {
// return nil, fmt.Errorf("proceso completado con %d errores", errors)
// }
// return respService, nil
//}
func (s *SincronizacionDatosService) GetName() string {
//func GetName(s models.Service) string {
return s.Name
}
// getEmpresaYCuisID busca registroEmpresa primero por NIT y luego filtra
// por puntoVenta y/o sucursal si vienen > 0.
func getEmpresaYCuisID(db *sql.DB, nit string, puntoVenta, sucursal int) (empresaID, cuisID int, err error) {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
// Construyo dinámicamente el WHERE
where := "nit = $1"
args := []interface{}{nit}
idx := 2
if puntoVenta > 0 {
where += fmt.Sprintf(" AND codigo_punto_venta = $%d", idx)
args = append(args, puntoVenta)
idx++
}
if sucursal > 0 {
where += fmt.Sprintf(" AND codigo_sucursal = $%d", idx)
args = append(args, sucursal)
idx++
}
// Si solo vino nit, dejamos where = "nit = $1"
query := fmt.Sprintf("SELECT id FROM registroEmpresa WHERE %s", where)
row := db.QueryRowContext(ctx, query, args...)
if err = row.Scan(&empresaID); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return 0, 0, fmt.Errorf("no existe registroEmpresa para %q (pv=%d, suc=%d)", nit, puntoVenta, sucursal)
}
return 0, 0, err
}
// Ahora buscamos en cuis
row2 := db.QueryRowContext(ctx,
`SELECT id FROM cuis WHERE registro_empresa_id = $1 ORDER BY fecha_vigencia DESC LIMIT 1`,
empresaID,
)
if err = row2.Scan(&cuisID); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return empresaID, 0, fmt.Errorf("no hay CUIS para registro_empresa_id=%d", empresaID)
}
return empresaID, 0, err
}
return empresaID, cuisID, nil
}
// de sincronizar_tipo_moneda para un cuis dado.
func getRespJSONTipoMoneda(db *sql.DB, cuisID int) ([]string, error) {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
rows, err := db.QueryContext(ctx,
`SELECT respJson
FROM sincronizar_tipo_moneda
WHERE cuis_id = $1`,
cuisID,
)
if err != nil {
return nil, err
}
defer rows.Close()
var respList []string
for rows.Next() {
var resp sql.NullString
if err := rows.Scan(&resp); err != nil {
return nil, err
}
if resp.Valid {
respList = append(respList, resp.String)
} else {
// si fuera NULL, podrías elegir omitirlo o añadir una cadena vacía
respList = append(respList, "")
}
}
if err := rows.Err(); err != nil {
return nil, err
}
return respList, nil
}
//// getTipoMoneda trae todas las filas de sincronizar_tipo_moneda para un cuis dado
//func getTipoMoneda(db *sql.DB, cuisID int) ([]entities.TipoMoneda, error) {
// ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
// defer cancel()
//
// rows, err := db.QueryContext(ctx,
// `SELECT id, codigo, reqSoap, respSoap, reqJson, respJson, fecha_creacion, fecha_actualizacion, cuis_id
// FROM sincronizar_tipo_moneda
// WHERE cuis_id = $1`,
// cuisID,
// )
// if err != nil {
// return nil, err
// }
// defer rows.Close()
//
// var resultados []entities.TipoMoneda
// for rows.Next() {
// var tm entities.TipoMoneda
// if err := rows.Scan(
// &tm.ID, &tm.Codigo, &tm.ReqSoap, &tm.RespSoap,
// &tm.ReqJson, &tm.RespJson, &tm.FechaCreacion,
// &tm.FechaActualizacion, &tm.CUISID,
// ); err != nil {
// return nil, err
// }
// resultados = append(resultados, tm)
// }
// if err := rows.Err(); err != nil {
// return nil, err
// }
// return resultados, nil
//}

View File

@ -0,0 +1,125 @@
package utils
import (
"daemonService/internal/models/sincronizacionDatos/response"
"daemonService/internal/soap"
"fmt"
)
func ProcesarRespuestaSincronizacion(tagName string, bodyBytes []byte) (interface{}, error) {
var err error
var respSincronizacion interface{}
switch tagName {
case "sincronizarActividades":
respSincronizacion, err = soap.ParsearRespuestaSOAP[[]response.ListaActividades, response.SoapEnvelopeActividad](bodyBytes)
if err != nil {
return nil, fmt.Errorf("error parseando respuesta ListaActividades: %v", err)
}
case "sincronizarFechaHora":
respSincronizacion, err = soap.ParsearRespuestaSOAPfechaHora[response.RespuestaFechaHora, response.SoapEnvelopeFechaHora](bodyBytes)
if err != nil {
return nil, fmt.Errorf("error parseando respuesta FechaHora: %v", err)
}
case "sincronizarListaMensajesServicios":
respSincronizacion, err = soap.ParsearRespuestaSOAP[[]response.MensajeServicio, response.SoapEnvelopeMensajeServicio](bodyBytes)
if err != nil {
return nil, fmt.Errorf("error parseando respuesta MensajeServicio: %v", err)
}
case "sincronizarListaLeyendasFactura":
respSincronizacion, err = soap.ParsearRespuestaSOAPIdentificador[[]response.LeyendaDetalle, response.SoapEnvelopeLeyendas](bodyBytes)
if err != nil {
return nil, fmt.Errorf("error parseando respuesta LeyendaDetalle: %v", err)
}
case "sincronizarListaProductosServicios":
respSincronizacion, err = soap.ParsearRespuestaSOAP[[]response.ProductoServicio, response.SoapEnvelopeProductoServicio](bodyBytes)
if err != nil {
return nil, fmt.Errorf("error parseando respuesta ProductoServicio: %v", err)
}
case "sincronizarParametricaMotivoAnulacion":
respSincronizacion, err = soap.ParsearRespuestaSOAP[[]response.MotivoAnulacion, response.SoapEnvelopeMotivoAnulacion](bodyBytes)
if err != nil {
return nil, fmt.Errorf("error parseando respuesta MotivoAnulacion: %v", err)
}
case "sincronizarParametricaTipoDocumentoIdentidad":
respSincronizacion, err = soap.ParsearRespuestaSOAP[[]response.TipoDocumento, response.SoapEnvelopeTipoDocumento](bodyBytes)
if err != nil {
return nil, fmt.Errorf("error parseando respuesta TipoDocumento: %v", err)
}
case "sincronizarParametricaTipoDocumentoSector":
respSincronizacion, err = soap.ParsearRespuestaSOAP[[]response.TipoSector, response.SoapEnvelopeTipoSector](bodyBytes)
if err != nil {
return nil, fmt.Errorf("error parseando respuesta TipoSector: %v", err)
}
case "sincronizarListaActividadesDocumentoSector":
respSincronizacion, err = soap.ParsearRespuestaSOAP[[]response.TipoActividadesSector, response.SoapEnvelopeTipoActividadesSector](bodyBytes)
if err != nil {
return nil, fmt.Errorf("error parseando respuesta TipoSctividadSector: %v", err)
}
case "sincronizarParametricaTipoEmision":
respSincronizacion, err = soap.ParsearRespuestaSOAP[[]response.TipoEmision, response.SoapEnvelopeTipoEmision](bodyBytes)
if err != nil {
return nil, fmt.Errorf("error parseando respuesta TipoEmision: %v", err)
}
case "sincronizarParametricaTiposFactura":
respSincronizacion, err = soap.ParsearRespuestaSOAP[[]response.TipoFactura, response.SoapEnvelopeTipoFactura](bodyBytes)
if err != nil {
return nil, fmt.Errorf("error parseando respuesta TiposFactura: %v", err)
}
case "sincronizarParametricaTipoMoneda":
respSincronizacion, err = soap.ParsearRespuestaSOAP[[]response.Moneda, response.SoapEnvelopeMoneda](bodyBytes)
if err != nil {
return nil, fmt.Errorf("error parseando respuesta Moneda: %v", err)
}
case "sincronizarParametricaTipoMetodoPago":
respSincronizacion, err = soap.ParsearRespuestaSOAP[[]response.MetodoPago, response.SoapEnvelopeMetodoPago](bodyBytes)
if err != nil {
return nil, fmt.Errorf("error parseando respuesta MetodoPago: %v", err)
}
case "sincronizarParametricaTipoPuntoVenta":
respSincronizacion, err = soap.ParsearRespuestaSOAP[[]response.PuntoVenta, response.SoapEnvelopePuntoVenta](bodyBytes)
if err != nil {
return nil, fmt.Errorf("error parseando respuesta PuntoVenta: %v", err)
}
case "sincronizarParametricaUnidadMedida":
respSincronizacion, err = soap.ParsearRespuestaSOAP[[]response.UnidadMedida, response.SoapEnvelopeUnidadMedida](bodyBytes)
if err != nil {
return nil, fmt.Errorf("error parseando respuesta UnidadMedida: %v", err)
}
case "sincronizarParametricaEventosSignificativos":
respSincronizacion, err = soap.ParsearRespuestaSOAP[[]response.EventoSignificativo, response.SoapEnvelopeEventoSignificativo](bodyBytes)
if err != nil {
return nil, fmt.Errorf("error parseando respuesta EventosSignificativos: %v", err)
}
case "sincronizarParametricaTipoHabitacion":
respSincronizacion, err = soap.ParsearRespuestaSOAP[[]response.TipoHabitacion, response.SoapEnvelopeTipoHabitacion](bodyBytes)
if err != nil {
return nil, fmt.Errorf("error parseando respuesta TipoHabitacion: %v", err)
}
case "sincronizarParametricaPaisOrigen":
respSincronizacion, err = soap.ParsearRespuestaSOAP[[]response.PaisOrigen, response.SoapEnvelopePaisOrigen](bodyBytes)
if err != nil {
return nil, fmt.Errorf("error parseando respuesta PaisOrigen: %v", err)
}
default:
return nil, fmt.Errorf("tag no reconocido: %s", tagName)
}
return respSincronizacion, nil
}

View File

@ -0,0 +1,59 @@
package soap
import (
"bytes"
"daemonService/internal/models"
"fmt"
"io"
"net/http"
"time"
)
// func (s *ListaLeyendasFacturaService) enviarSOAPRequest(soapReq string) (*http.Response, []byte, error) {
func EnviarSOAPRequest(s *models.ServiceModel, soapReq string) (*http.Response, []byte, error) {
var resp *http.Response
var bodyBytes []byte
var err error
backoff := time.Second
for i := 0; i < s.SOAPRetries; i++ {
if i > 0 {
time.Sleep(backoff)
backoff *= 2
}
req, err := http.NewRequest("POST", s.SOAPEndpoint, bytes.NewBufferString(soapReq))
if err != nil {
continue
}
req.Header.Set("Content-Type", "text/xml; charset=utf-8")
//req.Header.Set("apikey", s.APIKey)
req.Header.Set(s.KeyToken, s.ValueToken)
// Crea un transporte que ignore la configuración de proxy
transport := &http.Transport{
Proxy: nil, // Esto desactiva el uso de proxy
}
client := &http.Client{
Transport: transport,
Timeout: 30 * time.Second,
}
resp, err = client.Do(req)
if err != nil || resp.StatusCode != http.StatusOK {
continue
}
bodyBytes, err = io.ReadAll(resp.Body)
if err != nil {
resp.Body.Close()
continue
}
return resp, bodyBytes, nil
}
return nil, nil, fmt.Errorf("fallo después de %d reintentos: %v", s.SOAPRetries, err)
}

Some files were not shown because too many files have changed in this diff Show More