-
-
Notifications
You must be signed in to change notification settings - Fork 10
Expand file tree
/
Copy pathsiat.go
More file actions
334 lines (301 loc) · 10.8 KB
/
siat.go
File metadata and controls
334 lines (301 loc) · 10.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
package siat
import (
"encoding/json"
"fmt"
"net/http"
"reflect"
"strings"
"github.com/ron86i/go-siat/internal/adapter/services"
"github.com/ron86i/go-siat/internal/core/domain/siat/common"
"github.com/ron86i/go-siat/internal/core/ports"
)
// Map es un alias para map[string]interface{} que proporciona métodos de utilidad
// para trabajar con datos JSON de forma más cómda.
// Es especialmente útil al trabajar con respuestas heterogéneas del SIAT.
type Map map[string]interface{}
// ToJSON convierte el Map a su representación en string JSON.
// Retorna un error si la codificación falla.
func (m Map) ToJSON() (string, error) {
bytes, err := json.Marshal(m)
if err != nil {
return "", err
}
return string(bytes), nil
}
// Sum retorna la suma de todos los valores numéricos en el Map.
// Soporta tipos float64, float32, int, int64 e int32.
// Los valores no numéricos se ignoran.
func (m Map) Sum() float64 {
var total float64
for _, v := range m {
switch val := v.(type) {
case float64:
total += val
case float32:
total += float64(val)
case int:
total += float64(val)
case int64:
total += float64(val)
case int32:
total += float64(val)
}
}
return total
}
// ToStruct convierte el Map en la estructura Go especificada.
// Utiliza encoding/json internamente, por lo que se requiere que v sea un puntero
// a una estructura con etiquetas json apropiadas.
func (m Map) ToStruct(v interface{}) error {
bytes, err := m.ToJSON()
if err != nil {
return err
}
return json.Unmarshal([]byte(bytes), v)
}
// SiatServices es el punto de entrada principal del SDK.
// Agrupa todas las implementaciones de los servicios del SIAT
// (Códigos, Sincronización, Operaciones, Compra-Venta, Computarizada, Electrónica)
// y proporciona acceso a ellos a través de métodos orientados a objetivos.
// Los usuarios deben crear una instancia usando New().
type SiatServices struct {
operaciones ports.SiatOperacionesPort
sincronizacion ports.SiatSincronizacionService
codigos ports.SiatCodigosService
compraVenta ports.SiatCompraVentaService
computarizada ports.SiatComputarizadaService
electronica ports.SiatElectronicaService
traceID string // Opcional, para correlacionar solicitudes en sistemas distribuidos
}
// Operaciones retorna el servicio para la gestión de puntos de venta (PV),
// cierre de períodos de facturación y eventos significativos (cambios de modalidad, etc.).
func (s *SiatServices) Operaciones() ports.SiatOperacionesPort {
return s.operaciones
}
// Sincronizacion retorna el servicio que proporciona acceso a catálogos maestros:
// actividades económicas, documentos fiscales, monedas, tipos de cambio, etc.
// Estos catálogos son esenciales para validar datos antes de emitir invoices.
func (s *SiatServices) Sincronizacion() ports.SiatSincronizacionService {
return s.sincronizacion
}
// Codigos retorna el servicio para:
// - Solicitud de códigos CUIS (Código Único de Identificación de Sistemas)
// - Solicitud de códigos CUFD (Código Único de Facturación por Dirección)
// - Validación de números NIT (Rol Tributario)
// Los códigos CUIS y CUFD son obligatorios para emitir invoices.
func (s *SiatServices) Codigos() ports.SiatCodigosService {
return s.codigos
}
// CompraVenta retorna el servicio para el sector de compra-venta (Sector 1).
// Permite enviar, recibir y anular facturas comerciales estándar.
// Este es el sector más común para comercios generales.
func (s *SiatServices) CompraVenta() ports.SiatCompraVentaService {
return s.compraVenta
}
// Computarizada retorna el servicio para facturación computarizada
// (sin firma digital, basada en máquinas registradoras fiscales).
// Permite enviar, recibir y anular facturas de este tipo.
func (s *SiatServices) Computarizada() ports.SiatComputarizadaService {
return s.computarizada
}
// Electronica retorna el servicio para facturación electrónica (con firma digital).
// Permite enviar, recibir y anular facturas electrónicas de todos los sectores.
// Este es el tipo de facturación más moderno y flexible del SIAT.
func (s *SiatServices) Electronica() ports.SiatElectronicaService {
return s.electronica
}
// WithConfig retorna una nueva instancia de ports.Config con el traceID actual.
// Esto permite que el usuario establezca el traceID una sola vez con WithTraceID()
// y que automáticamente se inyecte en todas las solicitudes posteriores.
//
// El usuario debe proporcionar el Token de autenticación del SIAT.
// El UserAgent se establece en una cadena vacía y puede ser personalizado.
//
// Parámetros:
// - token: El token de autenticación del SIAT (obligatorio)
//
// Retorna:
// - ports.Config con TraceID pre-establecido
//
// Ejemplo:
//
// s.WithTraceID("trace-12345")
// config := s.WithConfig("myToken123")
// // config.TraceID es "trace-12345" automáticamente
func (s *SiatServices) WithConfig(token string) ports.Config {
return ports.Config{
Token: token,
TraceId: s.traceID,
}
}
// New crea e inicializa una nueva instancia de SiatServices.
//
// Parámetros:
// - baseUrl: URL base de los servicios SIAT (ej: https://pilotosiatservicios.impuestos.gob.bo/v2)
// - httpClient: Cliente HTTP personalizado (opcional). Si es nil, se crea uno con configuración segura.
//
// La función configura automáticamente:
// - Timeouts apropiados (15s handshake, 45s total)
// - TLS 1.2+ para seguridad
// - Pools de conexión para alto rendimiento
// - Proxy desde variables de entorno si están configuradas
//
// Retorna un error si baseUrl está vacía o si alguno de los servicios falla al inicializarse.
//
// Ejemplo:
//
// s, err := siat.New("https://pilotosiatservicios.impuestos.gob.bo/v2", nil)
// if err != nil {
// log.Fatal(err)
// }
func New(baseUrl string, httpClient *http.Client) (*SiatServices, error) {
if httpClient != nil {
clonedClient := *httpClient
httpClient = &clonedClient
} else {
// Usar HTTPConfig para crear cliente optimizado por defecto
httpClient = services.NewHTTPClient(services.DefaultHTTPConfig())
}
baseUrl = strings.TrimSpace(baseUrl)
if baseUrl == "" {
return nil, fmt.Errorf("baseUrl is empty")
}
operaciones, err := services.NewSiatOperacionesService(baseUrl, httpClient)
if err != nil {
return nil, err
}
sincronizacion, err := services.NewSiatSincronizacionService(baseUrl, httpClient)
if err != nil {
return nil, err
}
codigos, err := services.NewSiatCodigosService(baseUrl, httpClient)
if err != nil {
return nil, err
}
compraVenta, err := services.NewSiatCompraVentaService(baseUrl, httpClient)
if err != nil {
return nil, err
}
computarizada, err := services.NewSiatComputarizadaService(baseUrl, httpClient)
if err != nil {
return nil, err
}
electronica, err := services.NewSiatElectronicaService(baseUrl, httpClient)
if err != nil {
return nil, err
}
return &SiatServices{
operaciones: operaciones,
sincronizacion: sincronizacion,
codigos: codigos,
compraVenta: compraVenta,
computarizada: computarizada,
electronica: electronica,
traceID: "", // Inicialmente vacío
}, nil
}
// WithTraceID establece un ID de seguimiento (trace ID) para correlacionar solicitudes
// en sistemas distribuidos. El trace ID se inyecta en el encabezado HTTP "X-Trace-ID"
// en todas las solicitudes posteriores.
//
// El trace ID es completamente opcional y no afecta la funcionalidad del SDK si no se proporciona.
// Es útil para:
// - Correlacionar logs entre múltiples servicios
// - Rastrear solicitudes a través de sistemas distribuidos
// - Debugging de problemas en producción
//
// Parámetros:
// - id: El ID de seguimiento (puede estar vacío para limpiar el trace ID anterior)
//
// Retorna el receptor (SiatServices) para permitir encadenamiento de métodos.
//
// Ejemplo:
//
// s.WithTraceID("trace-12345-dev").Operaciones()...
// // El trace-12345-dev se incluirá en el encabezado X-Trace-ID de todas las solicitudes
func (s *SiatServices) WithTraceID(id string) *SiatServices {
s.traceID = id
return s
}
// Verify analiza una respuesta del SIAT y determina si la operación fue exitosa.
// Si la respuesta contiene errores del SIAT (Transaccion=false o mensajes de error),
// construye y retorna un *SiatError detallado.
//
// Es compatible con cualquier objeto de respuesta del SDK (RespuestaCuis, RespuestaRecepcion, etc.)
// utilizando reflexión para extraer los campos 'Transaccion' y 'MensajesList'.
//
// Ejemplo:
//
// resp, err := s.Codigos().VerificarNit(ctx, cfg, req)
// if err != nil { return err } // Error de red
// if err := siat.Verify(resp.Body.Content.RespuestaCuis); err != nil {
// return err // Error del SIAT (ej: NIT inválido)
// }
func Verify(resp interface{}) error {
if resp == nil {
return nil
}
// Intentar usar la interfaz común primero (más eficiente y seguro)
if res, ok := resp.(common.Result); ok {
return checkResult(res.IsSuccess(), res.GetMessages())
}
// Fallback por reflexión para objetos que aún no implementan la interfaz
val := reflect.ValueOf(resp)
if val.Kind() == reflect.Ptr {
val = val.Elem()
}
if val.Kind() != reflect.Struct {
return nil
}
// Extraer campo Transaccion (bool)
transaccionField := val.FieldByName("Transaccion")
success := true
if transaccionField.IsValid() && transaccionField.Kind() == reflect.Bool {
success = transaccionField.Bool()
}
// Extraer campo MensajesList (slice)
mensajesField := val.FieldByName("MensajesList")
var messages []common.MensajeServicio
if mensajesField.IsValid() && mensajesField.Kind() == reflect.Slice {
for i := 0; i < mensajesField.Len(); i++ {
msgVal := mensajesField.Index(i)
codeField := msgVal.FieldByName("Codigo")
descField := msgVal.FieldByName("Descripcion")
if codeField.IsValid() && descField.IsValid() {
messages = append(messages, common.MensajeServicio{
Codigo: int(codeField.Int()),
Descripcion: descField.String(),
})
}
}
}
return checkResult(success, messages)
}
// checkResult es un helper interno para validar el éxito y categorizar mensajes.
func checkResult(success bool, mensajes []common.MensajeServicio) error {
var messagesStr []string
var firstErrorCode int
hasErrors := false
for _, m := range mensajes {
// Categorizar: solo fallar si no es un warning
if !IsWarningCode(m.Codigo) {
hasErrors = true
if firstErrorCode == 0 {
firstErrorCode = m.Codigo
}
}
messagesStr = append(messagesStr, fmt.Sprintf("[%d] %s", m.Codigo, m.Descripcion))
}
// Si Transaccion es false o hay mensajes que no son warnings, es un error
if !success || hasErrors {
fullMsg := strings.Join(messagesStr, "; ")
if fullMsg == "" {
fullMsg = "Operación rechazada por el SIAT sin mensajes específicos"
}
err := NewSiatError(firstErrorCode, fullMsg)
// Enriquecer con metadatos de categoría
err.IsRetryable = IsRetryableCode(firstErrorCode)
return err
}
return nil
}