434 lines
12 KiB
Go
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,
|
|
})
|
|
}
|