2025-05-17 11:36:26 -04:00

671 lines
23 KiB
Go

// internal/soap/client.go
package soap
import (
"bytes"
"context"
"encoding/xml"
"fmt"
"io"
"net/http"
"time"
"consumer/internal/logging"
"consumer/internal/models/entidad_db"
soapModels "consumer/internal/models/soap"
"consumer/internal/utils"
)
// SOAPClient maneja las interacciones con el servicio SOAP
type SOAPClient struct {
client *http.Client
logger *logging.LoggerSystem
SoapURL string // Cambiado a público para acceder desde otros paquetes
}
// NewSOAPClient crea un nuevo cliente SOAP
func NewSOAPClient(soapURL string, logger *logging.LoggerSystem) *SOAPClient {
transport := &http.Transport{
TLSHandshakeTimeout: 10 * time.Second,
ResponseHeaderTimeout: 20 * time.Second,
ExpectContinueTimeout: 5 * time.Second,
DisableKeepAlives: false,
IdleConnTimeout: 90 * time.Second,
MaxIdleConns: 100,
MaxIdleConnsPerHost: 10,
}
return &SOAPClient{
client: &http.Client{
Timeout: 60 * time.Second, // Reducir de 120s a 60s
Transport: transport,
},
logger: logger,
SoapURL: soapURL, // Ahora usando campo público
}
}
// parseSOAPResponse extrae la respuesta de la estructura SOAP
func (sc *SOAPClient) parseSOAPResponse(respBytes []byte) (soapModels.RespuestaServicioFacturacion, error) {
var env struct {
Body struct {
Content []byte `xml:",innerxml"`
} `xml:"Body"`
}
if err := xml.Unmarshal(respBytes, &env); err != nil {
return soapModels.RespuestaServicioFacturacion{}, fmt.Errorf("error parseando envelope: %w", err)
}
var wrap soapModels.RecepcionFacturaResponse
if err := xml.Unmarshal(env.Body.Content, &wrap); err != nil {
return soapModels.RespuestaServicioFacturacion{}, fmt.Errorf("error parseando respuesta: %w", err)
}
return wrap.RespuestaServicioFacturacion, nil
}
// callService envía el XML firmado al servicio SOAP de SIAT
func (sc *SOAPClient) CallService(
ctx context.Context,
registro entidad_db.DatosFactura,
firmaXML, hashArchivo string,
) (soapModels.RespuestaServicioFacturacion, int, string, string, int64, error) {
// Construir la solicitud
inner := soapModels.SolicitudServicioRecepcionFactura{
CodigoAmbiente: registro.CodigoAmbiente,
CodigoDocumentoSector: 1,
CodigoEmision: 1,
CodigoModalidad: registro.CodigoModalidad,
CodigoPuntoVenta: registro.CodigoPuntoVenta,
CodigoSistema: registro.CodigoSistema,
CodigoSucursal: registro.CodigoSucursal,
CUFD: registro.CUFD,
CUIS: registro.CUIS,
NIT: registro.NIT,
TipoFacturaDocumento: 1,
Archivo: firmaXML,
FechaEnvio: utils.NowInBolivia().Format("2006-01-02T15:04:05.000"),
HashArchivo: hashArchivo,
}
env := soapModels.RecepcionFacturaEnvelope{
SoapEnv: "http://schemas.xmlsoap.org/soap/envelope/",
Siat: "https://siat.impuestos.gob.bo/",
}
env.Body.Recepcion.Solicitud = inner
payload, err := xml.MarshalIndent(env, "", " ")
if err != nil {
return soapModels.RespuestaServicioFacturacion{}, 0, "", "", 0, fmt.Errorf("error marshalling XML: %w", err)
}
fullReq := xml.Header + string(payload)
// Crear la solicitud HTTP con el contexto
httpReq, err := http.NewRequestWithContext(ctx, "POST", sc.SoapURL, bytes.NewReader([]byte(fullReq)))
if err != nil {
return soapModels.RespuestaServicioFacturacion{}, 0, fullReq, "", 0, fmt.Errorf("error creando request: %w", err)
}
// Configurar exactamente los mismos encabezados que en el curl exitoso
httpReq.Header.Set("Content-Type", "text/xml; charset=utf-8")
httpReq.Header.Set("Accept", "text/xml")
httpReq.Header.Set(registro.TokenKey, registro.TokenValue)
// Realizar la solicitud con timeout aumentado
start := utils.NowInBolivia()
client := &http.Client{
Timeout: 120 * time.Second, // Aumentar a 2 minutos
Transport: &http.Transport{
TLSHandshakeTimeout: 30 * time.Second,
ResponseHeaderTimeout: 30 * time.Second,
ExpectContinueTimeout: 10 * time.Second,
DisableKeepAlives: false,
IdleConnTimeout: 90 * time.Second,
},
}
resp, err := client.Do(httpReq)
latency := time.Since(start).Milliseconds()
if err != nil {
return soapModels.RespuestaServicioFacturacion{}, 0, fullReq, "", latency, fmt.Errorf("error llamando al servicio: %w", err)
}
defer resp.Body.Close()
respBytes, err := io.ReadAll(resp.Body)
if err != nil {
return soapModels.RespuestaServicioFacturacion{}, resp.StatusCode, fullReq, "", latency, fmt.Errorf("error leyendo respuesta: %w", err)
}
respStr := string(respBytes)
result, err := sc.parseSOAPResponse(respBytes)
if err != nil {
return soapModels.RespuestaServicioFacturacion{}, resp.StatusCode, fullReq, respStr, latency, fmt.Errorf("error parseando SOAP: %w", err)
}
return result, resp.StatusCode, fullReq, respStr, latency, nil
}
// CallVerificacionEstadoFactura sends a request to verify invoice status
func (sc *SOAPClient) CallVerificacionEstadoFactura(
ctx context.Context,
registro entidad_db.DatosFactura,
cuf string,
) (soapModels.RespuestaServicioFacturacion, int, string, string, int64, error) {
// Construir la solicitud
inner := soapModels.SolicitudServicioVerificacionEstadoFactura{
CodigoAmbiente: registro.CodigoAmbiente,
CodigoDocumentoSector: 1,
CodigoEmision: 1,
CodigoModalidad: registro.CodigoModalidad,
CodigoPuntoVenta: registro.CodigoPuntoVenta,
CodigoSistema: registro.CodigoSistema,
CodigoSucursal: registro.CodigoSucursal,
CUFD: registro.CUFD,
CUIS: registro.CUIS,
NIT: registro.NIT,
TipoFacturaDocumento: 1,
CUF: cuf,
}
env := soapModels.VerificacionEstadoFacturaEnvelope{
SoapEnv: "http://schemas.xmlsoap.org/soap/envelope/",
Siat: "https://siat.impuestos.gob.bo/",
}
env.Body.Verificacion.Solicitud = inner
payload, err := xml.MarshalIndent(env, "", " ")
if err != nil {
return soapModels.RespuestaServicioFacturacion{}, 0, "", "", 0, fmt.Errorf("error marshalling XML: %w", err)
}
fullReq := xml.Header + string(payload)
// Crear la solicitud HTTP con el contexto
httpReq, err := http.NewRequestWithContext(ctx, "POST", sc.SoapURL, bytes.NewReader([]byte(fullReq)))
if err != nil {
return soapModels.RespuestaServicioFacturacion{}, 0, fullReq, "", 0, fmt.Errorf("error creando request: %w", err)
}
// Configurar encabezados
httpReq.Header.Set("Content-Type", "text/xml; charset=utf-8")
httpReq.Header.Set("Accept", "text/xml")
httpReq.Header.Set(registro.TokenKey, registro.TokenValue)
// Realizar la solicitud con timeout aumentado
start := utils.NowInBolivia()
client := &http.Client{
Timeout: 120 * time.Second,
Transport: &http.Transport{
TLSHandshakeTimeout: 30 * time.Second,
ResponseHeaderTimeout: 30 * time.Second,
ExpectContinueTimeout: 10 * time.Second,
DisableKeepAlives: false,
IdleConnTimeout: 90 * time.Second,
},
}
resp, err := client.Do(httpReq)
latency := time.Since(start).Milliseconds()
if err != nil {
return soapModels.RespuestaServicioFacturacion{}, 0, fullReq, "", latency, fmt.Errorf("error llamando al servicio: %w", err)
}
defer resp.Body.Close()
respBytes, err := io.ReadAll(resp.Body)
if err != nil {
return soapModels.RespuestaServicioFacturacion{}, resp.StatusCode, fullReq, "", latency, fmt.Errorf("error leyendo respuesta: %w", err)
}
respStr := string(respBytes)
// Parsear la respuesta
var soapEnv struct {
Body struct {
Content []byte `xml:",innerxml"`
} `xml:"Body"`
}
if err := xml.Unmarshal(respBytes, &soapEnv); err != nil {
return soapModels.RespuestaServicioFacturacion{}, resp.StatusCode, fullReq, respStr, latency, fmt.Errorf("error parseando envelope: %w", err)
}
var wrap soapModels.VerificacionEstadoFacturaResponse
if err := xml.Unmarshal(soapEnv.Body.Content, &wrap); err != nil {
return soapModels.RespuestaServicioFacturacion{}, resp.StatusCode, fullReq, respStr, latency, fmt.Errorf("error parseando respuesta: %w", err)
}
return wrap.RespuestaServicioFacturacion, resp.StatusCode, fullReq, respStr, latency, nil
}
// CallAnulacionFactura sends a cancellation request to the SOAP service
func (sc *SOAPClient) CallAnulacionFactura(
ctx context.Context,
registro entidad_db.DatosFactura,
cuf string,
codigoMotivo int,
) (soapModels.RespuestaServicioFacturacion, int, string, string, int64, error) {
// Build the request
inner := soapModels.SolicitudServicioAnulacionFactura{
CodigoAmbiente: registro.CodigoAmbiente,
CodigoDocumentoSector: 1,
CodigoEmision: 1,
CodigoModalidad: registro.CodigoModalidad,
CodigoPuntoVenta: registro.CodigoPuntoVenta,
CodigoSistema: registro.CodigoSistema,
CodigoSucursal: registro.CodigoSucursal,
CUFD: registro.CUFD,
CUIS: registro.CUIS,
NIT: registro.NIT,
TipoFacturaDocumento: 1,
CodigoMotivo: codigoMotivo, // Default is 1, can be made configurable
CUF: cuf,
}
env := soapModels.AnulacionFacturaEnvelope{
SoapEnv: "http://schemas.xmlsoap.org/soap/envelope/",
Siat: "https://siat.impuestos.gob.bo/",
}
env.Body.Anulacion.Solicitud = inner
payload, err := xml.MarshalIndent(env, "", " ")
if err != nil {
return soapModels.RespuestaServicioFacturacion{}, 0, "", "", 0, fmt.Errorf("error marshalling XML: %w", err)
}
fullReq := xml.Header + string(payload)
// Create HTTP request with context
httpReq, err := http.NewRequestWithContext(ctx, "POST", sc.SoapURL, bytes.NewReader([]byte(fullReq)))
if err != nil {
return soapModels.RespuestaServicioFacturacion{}, 0, fullReq, "", 0, fmt.Errorf("error creating request: %w", err)
}
// Set headers
httpReq.Header.Set("Content-Type", "text/xml; charset=utf-8")
httpReq.Header.Set("Accept", "text/xml")
httpReq.Header.Set(registro.TokenKey, registro.TokenValue)
// Send request with increased timeout
start := utils.NowInBolivia()
client := &http.Client{
Timeout: 120 * time.Second,
Transport: &http.Transport{
TLSHandshakeTimeout: 30 * time.Second,
ResponseHeaderTimeout: 30 * time.Second,
ExpectContinueTimeout: 10 * time.Second,
DisableKeepAlives: false,
IdleConnTimeout: 90 * time.Second,
},
}
resp, err := client.Do(httpReq)
latency := time.Since(start).Milliseconds()
if err != nil {
return soapModels.RespuestaServicioFacturacion{}, 0, fullReq, "", latency, fmt.Errorf("error calling service: %w", err)
}
defer resp.Body.Close()
respBytes, err := io.ReadAll(resp.Body)
if err != nil {
return soapModels.RespuestaServicioFacturacion{}, resp.StatusCode, fullReq, "", latency, fmt.Errorf("error reading response: %w", err)
}
respStr := string(respBytes)
// Parse response
var soapEnv struct {
Body struct {
Content []byte `xml:",innerxml"`
} `xml:"Body"`
}
if err := xml.Unmarshal(respBytes, &soapEnv); err != nil {
return soapModels.RespuestaServicioFacturacion{}, resp.StatusCode, fullReq, respStr, latency, fmt.Errorf("error parsing envelope: %w", err)
}
var wrap soapModels.AnulacionFacturaResponse
if err := xml.Unmarshal(soapEnv.Body.Content, &wrap); err != nil {
return soapModels.RespuestaServicioFacturacion{}, resp.StatusCode, fullReq, respStr, latency, fmt.Errorf("error parsing response: %w", err)
}
return wrap.RespuestaServicioFacturacion, resp.StatusCode, fullReq, respStr, latency, nil
}
//package soap
//
//import (
// "bytes"
// "context"
// "encoding/xml"
// "fmt"
// "io"
// "net/http"
// "time"
//
// "consumer/internal/logging"
// "consumer/internal/models/entidad_db"
// soapModels "consumer/internal/models/soap"
// "consumer/internal/utils"
//)
//
//// SOAPClient maneja las interacciones con el servicio SOAP
//type SOAPClient struct {
// client *http.Client
// logger *logging.LoggerSystem
// soapURL string
//}
//
//// NewSOAPClient crea un nuevo cliente SOAP
//func NewSOAPClient(soapURL string, logger *logging.LoggerSystem) *SOAPClient {
// transport := &http.Transport{
// TLSHandshakeTimeout: 10 * time.Second,
// ResponseHeaderTimeout: 20 * time.Second,
// ExpectContinueTimeout: 5 * time.Second,
// DisableKeepAlives: false,
// IdleConnTimeout: 90 * time.Second,
// MaxIdleConns: 100,
// MaxIdleConnsPerHost: 10,
// }
//
// return &SOAPClient{
// client: &http.Client{
// Timeout: 60 * time.Second, // Reducir de 120s a 60s
// Transport: transport,
// },
// logger: logger,
// soapURL: soapURL,
// }
//}
//
//// parseSOAPResponse extrae la respuesta de la estructura SOAP
//func (sc *SOAPClient) parseSOAPResponse(respBytes []byte) (soapModels.RespuestaServicioFacturacion, error) {
// var env struct {
// Body struct {
// Content []byte `xml:",innerxml"`
// } `xml:"Body"`
// }
//
// if err := xml.Unmarshal(respBytes, &env); err != nil {
// return soapModels.RespuestaServicioFacturacion{}, fmt.Errorf("error parseando envelope: %w", err)
// }
//
// var wrap soapModels.RecepcionFacturaResponse
// if err := xml.Unmarshal(env.Body.Content, &wrap); err != nil {
// return soapModels.RespuestaServicioFacturacion{}, fmt.Errorf("error parseando respuesta: %w", err)
// }
//
// return wrap.RespuestaServicioFacturacion, nil
//}
//
//// callService envía el XML firmado al servicio SOAP de SIAT
//func (sc *SOAPClient) CallService(
// ctx context.Context,
// registro entidad_db.DatosFactura,
// firmaXML, hashArchivo string,
//) (soapModels.RespuestaServicioFacturacion, int, string, string, int64, error) {
// // Construir la solicitud
// inner := soapModels.SolicitudServicioRecepcionFactura{
// CodigoAmbiente: registro.CodigoAmbiente,
// CodigoDocumentoSector: 1,
// CodigoEmision: 1,
// CodigoModalidad: registro.CodigoModalidad,
// CodigoPuntoVenta: registro.CodigoPuntoVenta,
// CodigoSistema: registro.CodigoSistema,
// CodigoSucursal: registro.CodigoSucursal,
// CUFD: registro.CUFD,
// CUIS: registro.CUIS,
// NIT: registro.NIT,
// TipoFacturaDocumento: 1,
// Archivo: firmaXML,
// FechaEnvio: utils.NowInBolivia().Format("2006-01-02T15:04:05.000"),
// HashArchivo: hashArchivo,
// }
//
// env := soapModels.RecepcionFacturaEnvelope{
// SoapEnv: "http://schemas.xmlsoap.org/soap/envelope/",
// Siat: "https://siat.impuestos.gob.bo/",
// }
// env.Body.Recepcion.Solicitud = inner
//
// payload, err := xml.MarshalIndent(env, "", " ")
// if err != nil {
// return soapModels.RespuestaServicioFacturacion{}, 0, "", "", 0, fmt.Errorf("error marshalling XML: %w", err)
// }
//
// fullReq := xml.Header + string(payload)
//
// // Crear la solicitud HTTP con el contexto
// httpReq, err := http.NewRequestWithContext(ctx, "POST", sc.soapURL, bytes.NewReader([]byte(fullReq)))
// if err != nil {
// return soapModels.RespuestaServicioFacturacion{}, 0, fullReq, "", 0, fmt.Errorf("error creando request: %w", err)
// }
//
// // Configurar exactamente los mismos encabezados que en el curl exitoso
// httpReq.Header.Set("Content-Type", "text/xml; charset=utf-8")
// httpReq.Header.Set("Accept", "text/xml")
// httpReq.Header.Set(registro.TokenKey, registro.TokenValue)
//
// // Realizar la solicitud con timeout aumentado
// start := utils.NowInBolivia()
// client := &http.Client{
// Timeout: 120 * time.Second, // Aumentar a 2 minutos
// Transport: &http.Transport{
// TLSHandshakeTimeout: 30 * time.Second,
// ResponseHeaderTimeout: 30 * time.Second,
// ExpectContinueTimeout: 10 * time.Second,
// DisableKeepAlives: false,
// IdleConnTimeout: 90 * time.Second,
// },
// }
//
// resp, err := client.Do(httpReq)
// latency := time.Since(start).Milliseconds()
//
// if err != nil {
// return soapModels.RespuestaServicioFacturacion{}, 0, fullReq, "", latency, fmt.Errorf("error llamando al servicio: %w", err)
// }
// defer resp.Body.Close()
//
// respBytes, err := io.ReadAll(resp.Body)
// if err != nil {
// return soapModels.RespuestaServicioFacturacion{}, resp.StatusCode, fullReq, "", latency, fmt.Errorf("error leyendo respuesta: %w", err)
// }
// respStr := string(respBytes)
//
// result, err := sc.parseSOAPResponse(respBytes)
// if err != nil {
// return soapModels.RespuestaServicioFacturacion{}, resp.StatusCode, fullReq, respStr, latency, fmt.Errorf("error parseando SOAP: %w", err)
// }
//
// return result, resp.StatusCode, fullReq, respStr, latency, nil
//}
//
//// CallVerificacionEstadoFactura sends a request to verify invoice status
//func (sc *SOAPClient) CallVerificacionEstadoFactura(
// ctx context.Context,
// registro entidad_db.DatosFactura,
// cuf string,
//) (soapModels.RespuestaServicioFacturacion, int, string, string, int64, error) {
// // Construir la solicitud
// inner := soapModels.SolicitudServicioVerificacionEstadoFactura{
// CodigoAmbiente: registro.CodigoAmbiente,
// CodigoDocumentoSector: 1,
// CodigoEmision: 1,
// CodigoModalidad: registro.CodigoModalidad,
// CodigoPuntoVenta: registro.CodigoPuntoVenta,
// CodigoSistema: registro.CodigoSistema,
// CodigoSucursal: registro.CodigoSucursal,
// CUFD: registro.CUFD,
// CUIS: registro.CUIS,
// NIT: registro.NIT,
// TipoFacturaDocumento: 1,
// CUF: cuf,
// }
//
// env := soapModels.VerificacionEstadoFacturaEnvelope{
// SoapEnv: "http://schemas.xmlsoap.org/soap/envelope/",
// Siat: "https://siat.impuestos.gob.bo/",
// }
// env.Body.Verificacion.Solicitud = inner
//
// payload, err := xml.MarshalIndent(env, "", " ")
// if err != nil {
// return soapModels.RespuestaServicioFacturacion{}, 0, "", "", 0, fmt.Errorf("error marshalling XML: %w", err)
// }
//
// fullReq := xml.Header + string(payload)
//
// // Crear la solicitud HTTP con el contexto
// httpReq, err := http.NewRequestWithContext(ctx, "POST", sc.soapURL, bytes.NewReader([]byte(fullReq)))
// if err != nil {
// return soapModels.RespuestaServicioFacturacion{}, 0, fullReq, "", 0, fmt.Errorf("error creando request: %w", err)
// }
//
// // Configurar encabezados
// httpReq.Header.Set("Content-Type", "text/xml; charset=utf-8")
// httpReq.Header.Set("Accept", "text/xml")
// httpReq.Header.Set(registro.TokenKey, registro.TokenValue)
//
// // Realizar la solicitud con timeout aumentado
// start := utils.NowInBolivia()
// client := &http.Client{
// Timeout: 120 * time.Second,
// Transport: &http.Transport{
// TLSHandshakeTimeout: 30 * time.Second,
// ResponseHeaderTimeout: 30 * time.Second,
// ExpectContinueTimeout: 10 * time.Second,
// DisableKeepAlives: false,
// IdleConnTimeout: 90 * time.Second,
// },
// }
//
// resp, err := client.Do(httpReq)
// latency := time.Since(start).Milliseconds()
//
// if err != nil {
// return soapModels.RespuestaServicioFacturacion{}, 0, fullReq, "", latency, fmt.Errorf("error llamando al servicio: %w", err)
// }
// defer resp.Body.Close()
//
// respBytes, err := io.ReadAll(resp.Body)
// if err != nil {
// return soapModels.RespuestaServicioFacturacion{}, resp.StatusCode, fullReq, "", latency, fmt.Errorf("error leyendo respuesta: %w", err)
// }
// respStr := string(respBytes)
//
// // Parsear la respuesta
// var soapEnv struct {
// Body struct {
// Content []byte `xml:",innerxml"`
// } `xml:"Body"`
// }
//
// if err := xml.Unmarshal(respBytes, &soapEnv); err != nil {
// return soapModels.RespuestaServicioFacturacion{}, resp.StatusCode, fullReq, respStr, latency, fmt.Errorf("error parseando envelope: %w", err)
// }
//
// var wrap soapModels.VerificacionEstadoFacturaResponse
// if err := xml.Unmarshal(soapEnv.Body.Content, &wrap); err != nil {
// return soapModels.RespuestaServicioFacturacion{}, resp.StatusCode, fullReq, respStr, latency, fmt.Errorf("error parseando respuesta: %w", err)
// }
//
// return wrap.RespuestaServicioFacturacion, resp.StatusCode, fullReq, respStr, latency, nil
//}
//
//// CallAnulacionFactura sends a cancellation request to the SOAP service
//func (sc *SOAPClient) CallAnulacionFactura(
// ctx context.Context,
// registro entidad_db.DatosFactura,
// cuf string,
// codigoMotivo int,
//) (soapModels.RespuestaServicioFacturacion, int, string, string, int64, error) {
// // Build the request
// inner := soapModels.SolicitudServicioAnulacionFactura{
// CodigoAmbiente: registro.CodigoAmbiente,
// CodigoDocumentoSector: 1,
// CodigoEmision: 1,
// CodigoModalidad: registro.CodigoModalidad,
// CodigoPuntoVenta: registro.CodigoPuntoVenta,
// CodigoSistema: registro.CodigoSistema,
// CodigoSucursal: registro.CodigoSucursal,
// CUFD: registro.CUFD,
// CUIS: registro.CUIS,
// NIT: registro.NIT,
// TipoFacturaDocumento: 1,
// CodigoMotivo: codigoMotivo, // Default is 1, can be made configurable
// CUF: cuf,
// }
//
// env := soapModels.AnulacionFacturaEnvelope{
// SoapEnv: "http://schemas.xmlsoap.org/soap/envelope/",
// Siat: "https://siat.impuestos.gob.bo/",
// }
// env.Body.Anulacion.Solicitud = inner
//
// payload, err := xml.MarshalIndent(env, "", " ")
// if err != nil {
// return soapModels.RespuestaServicioFacturacion{}, 0, "", "", 0, fmt.Errorf("error marshalling XML: %w", err)
// }
//
// fullReq := xml.Header + string(payload)
//
// // Create HTTP request with context
// httpReq, err := http.NewRequestWithContext(ctx, "POST", sc.soapURL, bytes.NewReader([]byte(fullReq)))
// if err != nil {
// return soapModels.RespuestaServicioFacturacion{}, 0, fullReq, "", 0, fmt.Errorf("error creating request: %w", err)
// }
//
// // Set headers
// httpReq.Header.Set("Content-Type", "text/xml; charset=utf-8")
// httpReq.Header.Set("Accept", "text/xml")
// httpReq.Header.Set(registro.TokenKey, registro.TokenValue)
//
// // Send request with increased timeout
// start := utils.NowInBolivia()
// client := &http.Client{
// Timeout: 120 * time.Second,
// Transport: &http.Transport{
// TLSHandshakeTimeout: 30 * time.Second,
// ResponseHeaderTimeout: 30 * time.Second,
// ExpectContinueTimeout: 10 * time.Second,
// DisableKeepAlives: false,
// IdleConnTimeout: 90 * time.Second,
// },
// }
//
// resp, err := client.Do(httpReq)
// latency := time.Since(start).Milliseconds()
//
// if err != nil {
// return soapModels.RespuestaServicioFacturacion{}, 0, fullReq, "", latency, fmt.Errorf("error calling service: %w", err)
// }
// defer resp.Body.Close()
//
// respBytes, err := io.ReadAll(resp.Body)
// if err != nil {
// return soapModels.RespuestaServicioFacturacion{}, resp.StatusCode, fullReq, "", latency, fmt.Errorf("error reading response: %w", err)
// }
// respStr := string(respBytes)
//
// // Parse response
// var soapEnv struct {
// Body struct {
// Content []byte `xml:",innerxml"`
// } `xml:"Body"`
// }
//
// if err := xml.Unmarshal(respBytes, &soapEnv); err != nil {
// return soapModels.RespuestaServicioFacturacion{}, resp.StatusCode, fullReq, respStr, latency, fmt.Errorf("error parsing envelope: %w", err)
// }
//
// var wrap soapModels.AnulacionFacturaResponse
// if err := xml.Unmarshal(soapEnv.Body.Content, &wrap); err != nil {
// return soapModels.RespuestaServicioFacturacion{}, resp.StatusCode, fullReq, respStr, latency, fmt.Errorf("error parsing response: %w", err)
// }
//
// return wrap.RespuestaServicioFacturacion, resp.StatusCode, fullReq, respStr, latency, nil
//}