package handlers import ( "strings" "time" "github.com/gofiber/fiber/v2" flightinspection "wucher/internal/domain/flight_inspection" "wucher/internal/shared/pkg/uuidv7" "wucher/internal/transport/http/dto" "wucher/internal/transport/http/jsonapi" "wucher/internal/transport/http/response" "wucher/internal/transport/http/validators" ) type FlightInspectionHandler struct { svc flightinspection.FlightInspectionService validate *validators.Validator } func NewFlightInspectionHandler(svc flightinspection.FlightInspectionService) *FlightInspectionHandler { return &FlightInspectionHandler{ svc: svc, validate: validators.New(), } } // CreateFlightInspection godoc // @Summary Create flight inspection draft // @Tags Flight Inspection // @Accept json // @Produce json // @Param request body dto.FlightInspectionCreateRequest true "JSON:API flight inspection create request" // @Success 201 {object} dto.FlightInspectionResponse // @Failure 422 {object} dto.ErrorResponse // @Failure 400 {object} dto.ErrorResponse // Public route disabled (managed via reserve AC flow). func (h *FlightInspectionHandler) Create(c *fiber.Ctx) error { var req dto.FlightInspectionCreateRequest if err := c.BodyParser(&req); err != nil { return response.WriteErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{ Status: "400", Title: "Invalid JSON", Detail: safeInternalDetail(), }}) } if errs := h.validate.ValidateStruct(req); errs != nil { return response.WriteErrors(c, fiber.StatusUnprocessableEntity, errs) } inspectionDate, err := time.Parse("2006-01-02", strings.TrimSpace(req.Data.Attributes.InspectionDate)) if err != nil { return response.WriteErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{ Status: "422", Title: "Validation error", Detail: "inspection_date must be YYYY-MM-DD", Source: &jsonapi.ErrorSource{Pointer: "/data/attributes/inspection_date"}, }}) } model := &flightinspection.FlightInspection{ InspectionDate: inspectionDate.UTC(), CreatedBy: actorUserID(c), UpdatedBy: actorUserID(c), } if err := h.svc.CreateDraft(c.UserContext(), model); err != nil { return response.WriteErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{ Status: "400", Title: "Create failed", Detail: safeInternalDetail(), }}) } return response.Write(c, fiber.StatusCreated, flightInspectionResource(model)) } // GetFlightInspection godoc // @Summary Get flight inspection by ID // @Tags Flight Inspection // @Produce json // @Param id path string true "Flight inspection UUID (UUIDv7)" // @Success 200 {object} dto.FlightInspectionResponse // @Failure 404 {object} dto.ErrorResponse // @Failure 422 {object} dto.ErrorResponse // Public route disabled (managed via reserve AC flow). func (h *FlightInspectionHandler) GetByID(c *fiber.Ctx) error { id, err := uuidv7.ParseString(strings.TrimSpace(c.Params("id"))) if err != nil { return response.WriteErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{ Status: "422", Title: "Validation error", Detail: "id is invalid UUID", Source: &jsonapi.ErrorSource{Pointer: "/path/id"}, }}) } row, err := h.svc.GetByID(c.UserContext(), id) if err != nil { return response.WriteErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{ Status: "400", Title: "Get failed", Detail: safeInternalDetail(), }}) } if row == nil { return response.WriteErrors(c, fiber.StatusNotFound, []jsonapi.ErrorObject{{ Status: "404", Title: "Not found", Detail: "flight inspection not found", }}) } return response.Write(c, fiber.StatusOK, flightInspectionResource(row)) } // UpdateFlightInspection godoc // @Summary Update flight inspection // @Tags Flight Inspection // @Accept json // @Produce json // @Param id path string true "Flight inspection UUID (UUIDv7)" // @Param request body dto.FlightInspectionUpdateRequest true "JSON:API flight inspection update request" // @Success 200 {object} dto.FlightInspectionResponse // @Failure 400 {object} dto.ErrorResponse // @Failure 404 {object} dto.ErrorResponse // @Failure 422 {object} dto.ErrorResponse // Public route disabled (managed via reserve AC flow). func (h *FlightInspectionHandler) Update(c *fiber.Ctx) error { idStr := strings.TrimSpace(c.Params("id")) id, err := uuidv7.ParseString(idStr) if err != nil { return response.WriteErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{ Status: "422", Title: "Validation error", Detail: "id is invalid UUID", Source: &jsonapi.ErrorSource{Pointer: "/path/id"}, }}) } var req dto.FlightInspectionUpdateRequest if err := c.BodyParser(&req); err != nil { return response.WriteErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{ Status: "400", Title: "Invalid JSON", Detail: safeInternalDetail(), }}) } if errs := h.validate.ValidateStruct(req); errs != nil { return response.WriteErrors(c, fiber.StatusUnprocessableEntity, errs) } if strings.TrimSpace(req.Data.ID) == "" || req.Data.ID != idStr { return response.WriteErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{ Status: "422", Title: "Validation error", Detail: "id is required and must match path id", Source: &jsonapi.ErrorSource{Pointer: "/data/id"}, }}) } if req.Data.Attributes.InspectionDate == nil && req.Data.Attributes.Status == nil { return response.WriteErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{ Status: "422", Title: "Validation error", Detail: "at least one attribute must be provided", Source: &jsonapi.ErrorSource{Pointer: "/data/attributes"}, }}) } row, err := h.svc.GetByID(c.UserContext(), id) if err != nil { return response.WriteErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{ Status: "400", Title: "Update failed", Detail: safeInternalDetail(), }}) } if row == nil { return response.WriteErrors(c, fiber.StatusNotFound, []jsonapi.ErrorObject{{ Status: "404", Title: "Not found", Detail: "flight inspection not found", }}) } if req.Data.Attributes.InspectionDate != nil { inspectionDate, parseErr := time.Parse("2006-01-02", strings.TrimSpace(*req.Data.Attributes.InspectionDate)) if parseErr != nil { return response.WriteErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{ Status: "422", Title: "Validation error", Detail: "inspection_date must be YYYY-MM-DD", Source: &jsonapi.ErrorSource{Pointer: "/data/attributes/inspection_date"}, }}) } row.InspectionDate = inspectionDate.UTC() } if req.Data.Attributes.Status != nil { nextStatus := strings.TrimSpace(*req.Data.Attributes.Status) switch nextStatus { case flightinspection.StatusDraft, flightinspection.StatusInProgress, flightinspection.StatusCompleted: row.Status = nextStatus default: return response.WriteErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{ Status: "422", Title: "Validation error", Detail: "status must be one of: Draft, InProgress, Completed", Source: &jsonapi.ErrorSource{Pointer: "/data/attributes/status"}, }}) } } row.UpdatedBy = actorUserID(c) if err := h.svc.Update(c.UserContext(), row); err != nil { return response.WriteErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{ Status: "400", Title: "Update failed", Detail: safeInternalDetail(), }}) } updated, err := h.svc.GetByID(c.UserContext(), id) if err == nil && updated != nil { return response.Write(c, fiber.StatusOK, flightInspectionResource(updated)) } return response.Write(c, fiber.StatusOK, flightInspectionResource(row)) } // ListFlightInspections godoc // @Summary List flight inspections // @Tags Flight Inspection // @Produce json // @Param page[number] query int false "Page number (default 1)" // @Param page[size] query int false "Page size (default 20, max 100)" // @Param sort query string false "Sort order" // @Success 200 {object} dto.FlightInspectionListResponse // @Failure 422 {object} dto.ErrorResponse // Public route disabled (managed via reserve AC flow). func (h *FlightInspectionHandler) List(c *fiber.Ctx) error { pageNumber := parseIntDefault(c.Query("page[number]"), 1) if pageNumber < 1 { pageNumber = 1 } pageSize := parseIntDefault(c.Query("page[size]"), 20) if pageSize < 1 { pageSize = 20 } if pageSize > 100 { pageSize = 100 } offset := (pageNumber - 1) * pageSize _ = offset rows, total, err := h.svc.List(c.UserContext(), c.Query("sort"), 0, 0) if err != nil { return response.WriteErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{ Status: "400", Title: "List failed", Detail: safeInternalDetail(), }}) } data := make([]dto.FlightInspectionResource, 0, len(rows)) for i := range rows { data = append(data, flightInspectionResource(&rows[i])) } return response.WriteWithMeta(c, fiber.StatusOK, data, map[string]any{ "page_number": pageNumber, "page_size": pageSize, "total": total, }) } func flightInspectionResource(row *flightinspection.FlightInspection) dto.FlightInspectionResource { id, _ := uuidv7.BytesToString(row.ID) return dto.FlightInspectionResource{ Type: "flight_inspection", ID: id, Attributes: dto.FlightInspectionAttributes{ InspectionDate: row.InspectionDate.UTC().Format("2006-01-02"), Status: row.Status, CreatedAt: row.CreatedAt.UTC().Format(time.RFC3339), CreatedBy: uuidStringOrEmpty(row.CreatedBy), UpdatedAt: row.UpdatedAt.UTC().Format(time.RFC3339), UpdatedBy: uuidStringOrEmpty(row.UpdatedBy), }, } }