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

434 lines
12 KiB
Go

package api
import (
"database/sql"
"encoding/json"
"errors"
"io"
"net/http"
"producer/internal/kafka"
"producer/internal/logging"
"producer/internal/models"
"producer/pkg/utils"
"github.com/google/uuid"
"github.com/gorilla/mux"
)
// APIServer contiene los componentes necesarios para el API
type APIServer struct {
kc *kafka.KafkaClient
db *sql.DB
}
// NewAPIServer crea una nueva instancia de APIServer
func NewAPIServer(kc *kafka.KafkaClient, db *sql.DB) *APIServer {
return &APIServer{kc: kc, db: db}
}
// EnviarFacturaHandler procesa las solicitudes para enviar facturas
func (s *APIServer) EnviarFacturaHandler(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
if ct := r.Header.Get("Content-Type"); ct != "application/json" {
logging.ErrorLogger.Printf("El Content-Type debe ser application/json")
writeError(w, ErrInvalidContentType)
return
}
// Get max body size from env var with fallback to 1MB
maxBodySize := int64(utils.GetEnvInt("API_MAX_BODY_SIZE", 1<<20))
lr := io.LimitReader(r.Body, maxBodySize)
dec := json.NewDecoder(lr)
dec.DisallowUnknownFields()
var req models.RequestFacturacionModel
if err := dec.Decode(&req); err != nil {
logging.ErrorLogger.Printf("El formato JSON enviado es inválido")
var apiErr = ErrInvalidJSON
apiErr.Data = err.Error()
writeError(w, apiErr)
return
}
if err := dec.Decode(&struct{}{}); err != io.EOF {
logging.ErrorLogger.Printf("El JSON contiene contenido extra después del objeto principal: %v", err)
writeError(w, ErrJSONExtraContent)
return
}
transID := uuid.New().String()
msg := models.FacturaEventModel{
TransaccionID: transID,
Estado: "PENDIENTE",
Payload: req,
}
payload, err := json.Marshal(msg)
if err != nil {
logging.ErrorLogger.Printf("Error al serializar mensaje: %v", err)
writeError(w, ErrInternal)
return
}
facturacionTopic := utils.GetEnv("KAFKA_TOPIC_FACTURACION", "FACTURACION")
if err := s.kc.Publish(r.Context(), facturacionTopic, transID, payload); err != nil {
logging.ErrorLogger.Printf("Error al publicar en %s: %v", facturacionTopic, err)
writeError(w, ErrKafkaPublish)
return
}
logging.InfoLogger.Printf("Factura enviada: %s", transID)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(models.ApiResponse{
Success: true,
Status: 200,
TransaccionID: transID,
Message: "processing",
})
}
// RevertirFacturaHandler procesa las solicitudes para revertir facturas
func (s *APIServer) RevertirFacturaHandler(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
var req models.FacturaEventModel
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
logging.ErrorLogger.Printf("Error al decodificar payload: %v", err)
var apiErr = ErrInvalidJSON
apiErr.Data = err.Error()
writeError(w, apiErr)
return
}
if req.TransaccionID == "" {
logging.ErrorLogger.Printf("El transaccion_id es requerido")
writeError(w, ErrMissingTransactionID)
return
}
// Validar que el ID sea un UUID válido
if _, err := uuid.Parse(req.TransaccionID); err != nil {
logging.ErrorLogger.Printf("El ID proporcionado no es un UUID válido")
writeError(w, ErrInvalidUUID)
return
}
req.Estado = "REPROCESAR"
payload, err := json.Marshal(req)
if err != nil {
logging.ErrorLogger.Printf("Error al serializar mensaje: %v", err)
writeError(w, ErrInternal)
return
}
facturacionTopic := utils.GetEnv("KAFKA_TOPIC_FACTURACION", "FACTURACION")
if err := s.kc.Publish(r.Context(), facturacionTopic, req.TransaccionID, payload); err != nil {
logging.ErrorLogger.Printf("Error al publicar en mensaje en Kafka %s: %v", facturacionTopic, err)
writeError(w, ErrKafkaPublish)
return
}
logging.InfoLogger.Printf("Solicitud de reversión enviada: %s", req.TransaccionID)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(models.ApiResponse{
Success: true,
Status: 200,
TransaccionID: req.TransaccionID,
Message: "error",
Data: []models.DataItem{{Msg: "Solicitud de reversión enviada"}},
})
}
// ConsultarFacturaHandler procesa las solicitudes para consultar facturas
func (s *APIServer) ConsultarFacturaHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id := vars["id"]
if id == "" {
logging.ErrorLogger.Printf("El transaccion_id es requerido")
writeError(w, ErrMissingTransactionID)
return
}
if _, err := uuid.Parse(id); err != nil {
logging.ErrorLogger.Printf("El ID proporcionado no es un UUID válido")
writeError(w, ErrInvalidUUID)
return
}
var f models.Factura
var codeAuth sql.NullString
var fechaCreacion sql.NullTime
var msgRespSoap sql.NullString
row := s.db.QueryRowContext(r.Context(), `
SELECT id, fecha_emision, estado, cuf, url, codigo_documento_sector, fecha_creacion, mensaje_respuesta_soap
FROM facturacion_facturas
WHERE id = $1
`, id)
err := row.Scan(&f.ID, &f.FechaEmision, &f.Estado, &f.Cuf, &f.UrlFactura, &codeAuth, &fechaCreacion, &msgRespSoap)
if errors.Is(err, sql.ErrNoRows) {
logging.ErrorLogger.Printf("La factura solicitada no existe")
writeError(w, ErrFacturaNotFound)
return
} else if err != nil {
logging.ErrorLogger.Printf("Error al consultar BD: %v", err)
writeError(w, ErrDatabaseQuery)
return
}
if codeAuth.Valid {
f.CodigoAutorizacion = codeAuth.String
}
if fechaCreacion.Valid {
f.FechaCreacion = fechaCreacion.Time
}
var (
success bool
status int
message string
data []models.DataItem
)
// parsear el JSON de entrada
var mensajes []models.MensajeSOAP
if err := json.Unmarshal([]byte(msgRespSoap.String), &mensajes); err != nil {
logging.ErrorLogger.Printf("Error parsing JSON de entrada", err)
}
// extraer sólo las descripciones
msgData := make([]models.DataItem, len(mensajes))
for i, m := range mensajes {
msgData[i] = models.DataItem{Msg: m.Descripcion}
}
switch f.Estado {
case "FINALIZADA":
success = true
status = http.StatusOK
message = "done"
data = []models.DataItem{{CodigoAutorizacion: f.Cuf, URL: f.UrlFactura}}
case "ANULADA":
success = true
status = http.StatusOK
message = "canceled"
//data = []models.DataItem{}
data = msgData
default:
success = false
status = http.StatusBadRequest
message = "error"
data = msgData
}
estadoEvt := models.ApiResponse{
Success: success,
Status: status,
Message: message,
Data: data,
}
// Si no está finalizado, publicar el estado en Kafka
if f.Estado != "FINALIZADA" && f.Estado != "ANULADA" {
if payload, err := json.Marshal(estadoEvt); err != nil {
logging.ErrorLogger.Printf("Error al serializar estado: %v", err)
} else {
factEstadoTopic := utils.GetEnv("KAFKA_TOPIC_FACTURACION_ESTADO", "FACTURACION_ESTADO")
if err := s.kc.Publish(r.Context(), factEstadoTopic, f.ID, payload); err != nil {
logging.ErrorLogger.Printf("Error al publicar en %s: %v", factEstadoTopic, err)
} else {
logging.InfoLogger.Printf("Estado enviado: %s (%s)", f.ID, f.Estado)
}
}
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(estadoEvt)
}
// AnularFacturaHandler procesa las solicitudes para anular facturas
func (s *APIServer) AnularFacturaHandler(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
var req struct {
TransaccionID string `json:"transaccion_id"`
CodigoMotivo int `json:"codigo_motivo"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
logging.ErrorLogger.Printf("Error decoding payload: %v", err)
var apiErr = ErrInvalidJSON
apiErr.Data = err.Error()
writeError(w, apiErr)
return
}
if req.TransaccionID == "" {
logging.ErrorLogger.Printf("transaccion_id is required")
writeError(w, ErrMissingTransactionID)
return
}
// Validate that ID is a valid UUID
if _, err := uuid.Parse(req.TransaccionID); err != nil {
logging.ErrorLogger.Printf("ID provided is not a valid UUID")
writeError(w, ErrInvalidUUID)
return
}
// If no reason code is provided, use default (1)
if req.CodigoMotivo <= 0 {
req.CodigoMotivo = 1
}
// Create message for Kafka
payload, err := json.Marshal(req)
if err != nil {
logging.ErrorLogger.Printf("Error serializing message: %v", err)
writeError(w, ErrInternal)
return
}
// Publish to Kafka FACTURACION_ANULAR topic
factAnularTopic := utils.GetEnv("KAFKA_TOPIC_FACTURACION_ANULAR", "FACTURACION_ANULAR")
if err := s.kc.Publish(r.Context(), factAnularTopic, req.TransaccionID, payload); err != nil {
logging.ErrorLogger.Printf("Error publishing message to Kafka %s: %v", factAnularTopic, err)
writeError(w, ErrKafkaPublish)
return
}
logging.InfoLogger.Printf("Cancellation request sent: %s", req.TransaccionID)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(models.ApiResponse{
Success: true,
Status: 200,
TransaccionID: req.TransaccionID,
Message: "Done",
})
}
// ReprocesarDLQHandler procesa las solicitudes para reprocesar mensajes fallidos
func (s *APIServer) ReprocesarDLQHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
// Use environment variable for DLQ processing limit
dlqLimit := utils.GetEnvInt("DLQ_MAX_PROCESS_LIMIT", 100)
// Consultar mensajes pendientes de reprocesamiento
rows, err := s.db.QueryContext(ctx, `
SELECT id, transaccion_id, mensaje, fecha_creacion, reprocesado
FROM facturacion_dlq
WHERE reprocesado = false
LIMIT $1
`, dlqLimit)
if err != nil {
logging.ErrorLogger.Printf("Error al consultar DLQ: %v", err)
writeError(w, ErrDatabaseQuery)
return
}
defer rows.Close()
type ReprocesadoResult struct {
ID int `json:"id"`
TransID string `json:"transaccion_id"`
Error string `json:"error,omitempty"`
}
var processed []ReprocesadoResult
var failed []ReprocesadoResult
facturacionTopic := utils.GetEnv("KAFKA_TOPIC_FACTURACION", "FACTURACION")
for rows.Next() {
var m models.DLQMessage
if err := rows.Scan(&m.ID, &m.TransaccionID, &m.Mensaje, &m.FechaCreacion, &m.Reprocesado); err != nil {
logging.ErrorLogger.Printf("Error al escanear registro DLQ: %v", err)
continue
}
var req models.FacturaEventModel
if err := json.Unmarshal([]byte(m.Mensaje), &req); err != nil {
logging.ErrorLogger.Printf("Error al parsear mensaje DLQ: %v", err)
failed = append(failed, ReprocesadoResult{
ID: m.ID,
TransID: m.TransaccionID,
Error: "Error al parsear mensaje: " + err.Error(),
})
continue
}
payload, err := json.Marshal(req)
if err != nil {
logging.ErrorLogger.Printf("Error al serializar mensaje: %v", err)
failed = append(failed, ReprocesadoResult{
ID: m.ID,
TransID: m.TransaccionID,
Error: "Error al serializar mensaje: " + err.Error(),
})
continue
}
if err := s.kc.Publish(ctx, facturacionTopic, req.TransaccionID, payload); err != nil {
logging.ErrorLogger.Printf("Error al publicar en %s: %v", facturacionTopic, err)
failed = append(failed, ReprocesadoResult{
ID: m.ID,
TransID: m.TransaccionID,
Error: "Error al publicar en Kafka: " + err.Error(),
})
continue
}
if _, err := s.db.ExecContext(ctx,
`UPDATE facturacion_dlq SET reprocesado = true WHERE id = $1`, m.ID,
); err != nil {
logging.ErrorLogger.Printf("Error al marcar reprocesado: %v", err)
failed = append(failed, ReprocesadoResult{
ID: m.ID,
TransID: m.TransaccionID,
Error: "Error al marcar como reprocesado: " + err.Error(),
})
continue
}
processed = append(processed, ReprocesadoResult{
ID: m.ID,
TransID: m.TransaccionID,
})
logging.InfoLogger.Printf("Reprocesado DLQ: %s", req.TransaccionID)
}
// Verificar si hubo error al leer los registros
if err := rows.Err(); err != nil {
logging.ErrorLogger.Printf("Error al iterar registros DLQ: %v", err)
}
w.Header().Set("Content-Type", "application/json")
respStatus := "success"
if len(processed) == 0 && len(failed) > 0 {
respStatus = "error"
w.WriteHeader(http.StatusPartialContent)
} else if len(processed) == 0 {
respStatus = "empty"
}
json.NewEncoder(w).Encode(map[string]interface{}{
"status": respStatus,
"message": "Reprocesado DLQ",
"processed": len(processed),
"success": processed,
"failed": failed,
})
}