Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion backend/internal/events/dto/requests.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,19 @@
/*
File: requests.go

Contains:
Request DTO definitions for the Evento module.
It centralizes HTTP payload structures used to create and
update events.

Course: CI-4712 Ingeniería de Software II
Term: Enero - Marzo 2026
Designed by: Equipo 2 - Arcadian
*/

package dto

// CreateEventoRequest represents the payload to create a new event.
// CreateEventoRequest represents the payload used to create a new event.
type CreateEventoRequest struct {
Nombre string `json:"nombre"`
FechaInicio string `json:"fecha_inicio"`
Expand Down
15 changes: 15 additions & 0 deletions backend/internal/events/dto/responses.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,16 @@
/*
File: responses.go

Contains:
Response DTO definitions for the Evento module.
It centralizes API payload structures returned for events,
sessions, speakers, and date ranges.

Course: CI-4712 Ingeniería de Software II
Term: Enero - Marzo 2026
Designed by: Equipo 2 - Arcadian
*/

package dto

// EventoResponse represents the response payload for an event.
Expand All @@ -12,6 +25,7 @@ type EventoResponse struct {
Sesiones []SesionResponse `json:"sesiones"`
}

// SesionResponse represents the response payload for a session.
type SesionResponse struct {
IDSesion int `json:"id_sesion"`
Titulo string `json:"titulo"`
Expand All @@ -22,6 +36,7 @@ type SesionResponse struct {
Ponentes []PonenteResponse `json:"ponentes"`
}

// PonenteResponse represents the response payload for a session speaker.
type PonenteResponse struct {
IDUsuario int `json:"id_usuario"`
Nombre string `json:"nombre"`
Expand Down
86 changes: 84 additions & 2 deletions backend/internal/events/handler/handler.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,16 @@
/*
File: handler.go

Contains:
HTTP handler implementation for the Evento module.
It centralizes request routing, input validation, service calls,
response shaping, and error mapping for event endpoints.

Course: CI-4712 Ingeniería de Software II
Term: Enero - Marzo 2026
Designed by: Equipo 2 - Arcadian
*/

package handler

import (
Expand All @@ -15,27 +28,33 @@ import (
"project/backend/prisma/db"
)

// Internal constants used by this HTTP handler.
const (
dateLayout = "02/01/2006"
contentTypeKey = "Content-Type"
contentTypeJSON = "application/json"
dbErrorMessage = "db error"
)

// venezuelaLocation defines the fixed UTC-4 timezone used for event date display.
var venezuelaLocation = time.FixedZone("VET", -4*60*60)

// formatDateVE formats a time value as dd/mm/yyyy in Venezuela local time.
func formatDateVE(t time.Time) string {
return t.In(venezuelaLocation).Format(dateLayout)
}

// formatDateTimeVE formats a time value as dd/mm/yyyy HH:MM in Venezuela local time.
func formatDateTimeVE(t time.Time) string {
return t.In(venezuelaLocation).Format("02/01/2006 15:04")
}

// Handler serves HTTP requests for the Evento module.
type Handler struct {
svc EventService
}

// EventService defines the business operations required by the HTTP handler.
type EventService interface {
EnsureNombreUnico(ctx context.Context, nombre string) error
EnsureNoSolapamiento(ctx context.Context, start, end time.Time) error
Expand All @@ -49,14 +68,18 @@ type EventService interface {
GetFechasOcupadas(ctx context.Context) ([]dto.RangoFechas, error)
}

// New creates an HTTP handler wired with the concrete events service.
func New(client *db.PrismaClient) http.Handler {
return &Handler{svc: service.New(client)}
}

// NewWithService creates a handler with a custom service implementation.
// This constructor is mainly useful for tests and dependency injection.
func NewWithService(svc EventService) *Handler {
return &Handler{svc: svc}
}

// ServeHTTP routes incoming requests by HTTP method.
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodPost:
Expand All @@ -74,6 +97,14 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}
}

/*
Endpoint: POST /api/eventos
Creates a new event using the request JSON payload.

Usage:
- Body: CreateEventoRequest
- Response: EventoResponse
*/
func (h *Handler) createEvento(w http.ResponseWriter, r *http.Request) {
var req dto.CreateEventoRequest

Expand Down Expand Up @@ -140,8 +171,18 @@ func (h *Handler) createEvento(w http.ResponseWriter, r *http.Request) {
_ = json.NewEncoder(w).Encode(res)
}

/*
Endpoint: GET /api/eventos
Returns either the event list or one detailed event.

Usage:
- GET /api/eventos
Returns a list of events.
- GET /api/eventos?evento_id=<id>
Returns one event with sessions and speakers.
*/
func (h *Handler) listEventos(w http.ResponseWriter, r *http.Request) {
// Si viene el parámetro evento_id, retornar el detalle con sesiones y ponentes
// If evento_id is present, return detailed event data with sessions and speakers.
eventoIDStr := r.URL.Query().Get("evento_id")
if eventoIDStr != "" {
eventoID, err := strconv.Atoi(eventoIDStr)
Expand All @@ -156,7 +197,7 @@ func (h *Handler) listEventos(w http.ResponseWriter, r *http.Request) {
httperror.WriteJSON(w, http.StatusNotFound, "Evento no encontrado")
return
}
// Obtener sesiones usando el tipo concreto
// Retrieve sessions through the concrete service type.
svc, ok := h.svc.(*service.Service)
if !ok {
httperror.WriteJSON(w, http.StatusInternalServerError, "Error interno de servicio")
Expand Down Expand Up @@ -226,6 +267,14 @@ func (h *Handler) listEventos(w http.ResponseWriter, r *http.Request) {
_ = json.NewEncoder(w).Encode(res)
}

/*
Endpoint: PUT /api/eventos
Updates an existing event using request JSON.

Usage:
- Body: UpdateEventoRequest (id_evento is required)
- Response: EventoResponse
*/
func (h *Handler) updateEvento(w http.ResponseWriter, r *http.Request) {
var req dto.UpdateEventoRequest

Expand Down Expand Up @@ -294,6 +343,16 @@ func (h *Handler) updateEvento(w http.ResponseWriter, r *http.Request) {
_ = json.NewEncoder(w).Encode(res)
}

/*
Endpoint: PATCH /api/eventos
Dispatches registration state changes for an event.

Usage:
- PATCH /api/eventos?action=cerrar&id=<id>
Closes registrations for the event.
- PATCH /api/eventos?action=abrir&id=<id>
Opens registrations for the event.
*/
func (h *Handler) patchEvento(w http.ResponseWriter, r *http.Request) {
action := r.URL.Query().Get("action")
idStr := r.URL.Query().Get("id")
Expand All @@ -319,6 +378,13 @@ func (h *Handler) patchEvento(w http.ResponseWriter, r *http.Request) {
}
}

/*
Endpoint: DELETE /api/eventos
Performs logical deletion (cancellation) for an event.

Usage:
- DELETE /api/eventos?id=<id>
*/
func (h *Handler) deleteEvento(w http.ResponseWriter, r *http.Request) {
idStr := r.URL.Query().Get("id")
if idStr == "" {
Expand All @@ -344,6 +410,7 @@ func (h *Handler) deleteEvento(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNoContent)
}

// cerrarInscripciones closes event registrations and returns the updated event.
func (h *Handler) cerrarInscripciones(w http.ResponseWriter, r *http.Request, eventoID int) {
ctx, cancel := context.WithTimeout(r.Context(), 3*time.Second)
defer cancel()
Expand Down Expand Up @@ -376,6 +443,7 @@ func (h *Handler) cerrarInscripciones(w http.ResponseWriter, r *http.Request, ev
_ = json.NewEncoder(w).Encode(res)
}

// abrirInscripciones opens event registrations and returns the updated event.
func (h *Handler) abrirInscripciones(w http.ResponseWriter, r *http.Request, eventoID int) {
ctx, cancel := context.WithTimeout(r.Context(), 3*time.Second)
defer cancel()
Expand Down Expand Up @@ -413,6 +481,8 @@ func (h *Handler) abrirInscripciones(w http.ResponseWriter, r *http.Request, eve
_ = json.NewEncoder(w).Encode(res)
}

// isInscripcionesAbiertas computes whether registrations are open at the
// current time based on the manual flag and event date constraints.
func isInscripcionesAbiertas(evento *db.EventoModel, now time.Time) bool {
if !evento.InscripcionesAbiertasManual {
return false
Expand All @@ -423,10 +493,20 @@ func isInscripcionesAbiertas(evento *db.EventoModel, now time.Time) bool {
return now.Before(evento.FechaCierreInscripcion)
}

// Svc exposes the underlying service used by the handler.
func (h *Handler) Svc() EventService {
return h.svc
}

/*
Endpoint: GET <configured-route-for-fechas-ocupadas>
Returns occupied date ranges for existing events.

Usage:
- Route is configured by the application router using
GetFechasOcupadasHandler(svc).
- Response: []RangoFechas
*/
func GetFechasOcupadasHandler(svc EventService) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
Expand All @@ -441,6 +521,8 @@ func GetFechasOcupadasHandler(svc EventService) http.HandlerFunc {
}
}

// handleUpdateEventoError maps update errors to HTTP responses.
// It returns true when an error was handled and a response was written.
func handleUpdateEventoError(w http.ResponseWriter, err error) bool {
if err == nil {
return false
Expand Down
23 changes: 23 additions & 0 deletions backend/internal/events/handler/handler_test.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,16 @@
/*
File: handler_test.go

Contains:
Unit tests for the Evento HTTP handler.
It validates method routing, request handling, status codes,
and service interaction behavior using mocks.

Course: CI-4712 Ingeniería de Software II
Term: Enero - Marzo 2026
Designed by: Equipo 2 - Arcadian
*/

package handler

import (
Expand Down Expand Up @@ -29,69 +42,79 @@ type mockEventService struct {
getFechasOcupadas func(ctx context.Context) ([]dto.RangoFechas, error)
}

// EnsureNombreUnico mocks unique-name validation.
func (m mockEventService) EnsureNombreUnico(ctx context.Context, nombre string) error {
if m.ensureNombreUnico == nil {
return nil
}
return m.ensureNombreUnico(ctx, nombre)
}

// EnsureNoSolapamiento mocks overlap validation.
func (m mockEventService) EnsureNoSolapamiento(ctx context.Context, start, end time.Time) error {
if m.ensureNoSolapamiento == nil {
return nil
}
return m.ensureNoSolapamiento(ctx, start, end)
}

// CreateEvento mocks event creation.
func (m mockEventService) CreateEvento(ctx context.Context, req dto.CreateEventoRequest, start, end, cierre time.Time) (*db.EventoModel, error) {
if m.createEvento == nil {
return nil, errors.New("not implemented")
}
return m.createEvento(ctx, req, start, end, cierre)
}

// ListEventos mocks event listing.
func (m mockEventService) ListEventos(ctx context.Context) ([]db.EventoModel, error) {
if m.listEventos == nil {
return nil, errors.New("not implemented")
}
return m.listEventos(ctx)
}

// GetEventoByID mocks event lookup by identifier.
func (m mockEventService) GetEventoByID(ctx context.Context, id int) (*db.EventoModel, error) {
if m.getEventoByID == nil {
return nil, errors.New("not implemented")
}
return m.getEventoByID(ctx, id)
}

// UpdateEvento mocks event update.
func (m mockEventService) UpdateEvento(ctx context.Context, req dto.UpdateEventoRequest, start, end, cierre time.Time) (*db.EventoModel, error) {
if m.updateEvento == nil {
return nil, errors.New("not implemented")
}
return m.updateEvento(ctx, req, start, end, cierre)
}

// DeleteEvento mocks event deletion.
func (m mockEventService) DeleteEvento(ctx context.Context, id int) error {
if m.deleteEvento == nil {
return errors.New("not implemented")
}
return m.deleteEvento(ctx, id)
}

// CerrarInscripciones mocks registration closing.
func (m mockEventService) CerrarInscripciones(ctx context.Context, eventoID int) (*db.EventoModel, error) {
if m.cerrarInscripciones == nil {
return nil, errors.New("not implemented")
}
return m.cerrarInscripciones(ctx, eventoID)
}

// AbrirInscripciones mocks registration opening.
func (m mockEventService) AbrirInscripciones(ctx context.Context, eventoID int) (*db.EventoModel, error) {
if m.abrirInscripciones == nil {
return nil, errors.New("not implemented")
}
return m.abrirInscripciones(ctx, eventoID)
}

// GetFechasOcupadas mocks occupied date range retrieval.
func (m mockEventService) GetFechasOcupadas(ctx context.Context) ([]dto.RangoFechas, error) {
if m.getFechasOcupadas == nil {
return nil, errors.New("not implemented")
Expand Down
Loading
Loading