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, }) }