package handlers import ( "errors" "strconv" "strings" "time" "github.com/gofiber/fiber/v2" "gorm.io/gorm" helicopterusage "wucher/internal/domain/helicopter_usage" "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 HelicopterUsageHandler struct { svc helicopterusage.Service validate *validators.Validator } func NewHelicopterUsageHandler(svc helicopterusage.Service) *HelicopterUsageHandler { return &HelicopterUsageHandler{ svc: svc, validate: validators.New(), } } // CreateHelicopterUsage godoc // @Summary Create helicopter usage // @Description Create helicopter usage counters for a helicopter. // @Tags Helicopter - Helicopter Usage // @Accept json // @Produce json // @Param request body dto.HelicopterUsageCreateRequest true "JSON:API helicopter usage create request" // @Success 201 {object} dto.HelicopterUsageResponse // @Failure 422 {object} dto.ErrorResponse // @Failure 400 {object} dto.ErrorResponse // @Router /api/v1/helicopter-usage/create [post] func (h *HelicopterUsageHandler) Create(c *fiber.Ctx) error { var req dto.HelicopterUsageCreateRequest if err := c.BodyParser(&req); err != nil { return writeHelicopterUsageErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{ Status: "400", Title: "Invalid JSON", Detail: safeInternalDetail(), }}) } if errs := h.validate.ValidateStruct(req); errs != nil { return writeHelicopterUsageErrors(c, fiber.StatusUnprocessableEntity, errs) } helicopterID, err := uuidv7.ParseString(strings.TrimSpace(req.Data.Attributes.HelicopterID)) if err != nil { return writeHelicopterUsageErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{ Status: "422", Title: "Validation error", Detail: "helicopter_id is invalid UUID", Source: &jsonapi.ErrorSource{Pointer: "/data/attributes/helicopter_id"}, }}) } row := helicopterUsageFromCreate(req.Data.Attributes) row.HelicopterID = helicopterID row.CreatedBy = actorUserID(c) row.UpdatedBy = actorUserID(c) if err := h.svc.Create(c.UserContext(), row); err != nil { return writeHelicopterUsageErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{ Status: "400", Title: "Create failed", Detail: safeInternalDetail(), }}) } created, err := h.svc.GetByID(c.UserContext(), row.ID) if err == nil && created != nil { return response.Write(c, fiber.StatusCreated, helicopterUsageResource(created)) } return response.Write(c, fiber.StatusCreated, helicopterUsageResource(row)) } // GetHelicopterUsage godoc // @Summary Get helicopter usage // @Tags Helicopter - Helicopter Usage // @Produce json // @Param uuid path string true "Helicopter Usage UUID (UUIDv7)" // @Success 200 {object} dto.HelicopterUsageResponse // @Failure 404 {object} dto.ErrorResponse // @Router /api/v1/helicopter-usage/get/{uuid} [get] func (h *HelicopterUsageHandler) Get(c *fiber.Ctx) error { id, err := uuidv7.ParseString(c.Params("uuid")) if err != nil { return writeHelicopterUsageErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{ Status: "422", Title: "Validation error", Detail: "uuid is invalid UUID", Source: &jsonapi.ErrorSource{Pointer: "/path/uuid"}, }}) } row, err := h.svc.GetByID(c.UserContext(), id) if err != nil { return writeHelicopterUsageErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{ Status: "400", Title: "Get failed", Detail: safeInternalDetail(), }}) } if row == nil { return writeHelicopterUsageErrors(c, fiber.StatusNotFound, []jsonapi.ErrorObject{{ Status: "404", Title: "Not found", Detail: "helicopter usage not found", }}) } return response.Write(c, fiber.StatusOK, helicopterUsageResource(row)) } // UpdateHelicopterUsage godoc // @Summary Update helicopter usage (partial) // @Tags Helicopter - Helicopter Usage // @Accept json // @Produce json // @Param uuid path string true "Helicopter Usage UUID (UUIDv7)" // @Param request body dto.HelicopterUsageUpdateRequest true "JSON:API helicopter usage update request" // @Success 200 {object} dto.HelicopterUsageResponse // @Failure 422 {object} dto.ErrorResponse // @Failure 404 {object} dto.ErrorResponse // @Router /api/v1/helicopter-usage/update/{uuid} [patch] func (h *HelicopterUsageHandler) Update(c *fiber.Ctx) error { uuidStr := c.Params("uuid") id, err := uuidv7.ParseString(uuidStr) if err != nil { return writeHelicopterUsageErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{ Status: "422", Title: "Validation error", Detail: "uuid is invalid UUID", Source: &jsonapi.ErrorSource{Pointer: "/path/uuid"}, }}) } var req dto.HelicopterUsageUpdateRequest if err := c.BodyParser(&req); err != nil { return writeHelicopterUsageErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{ Status: "400", Title: "Invalid JSON", Detail: safeInternalDetail(), }}) } if errs := h.validate.ValidateStruct(req); errs != nil { return writeHelicopterUsageErrors(c, fiber.StatusUnprocessableEntity, errs) } if strings.TrimSpace(req.Data.ID) == "" { return writeHelicopterUsageErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{ Status: "422", Title: "Validation error", Detail: "id is required", Source: &jsonapi.ErrorSource{Pointer: "/data/id"}, }}) } if req.Data.ID != uuidStr { return writeHelicopterUsageErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{ Status: "422", Title: "Validation error", Detail: "id does not match path uuid", Source: &jsonapi.ErrorSource{Pointer: "/data/id"}, }}) } attrs := req.Data.Attributes if attrs.HelicopterID == nil && attrs.TotalLanding == nil && attrs.TotalAirframeHours == nil && attrs.TotalAirframeCycles == nil && attrs.TotalEngine1Hours == nil && attrs.TotalEngine1GpcNgN1 == nil && attrs.TotalEngine1PtcNfN2 == nil && attrs.TotalEngine1Ccc == nil && attrs.TotalEngine2Hours == nil && attrs.TotalEngine2GpcNgN1 == nil && attrs.TotalEngine2PtcNfN2 == nil && attrs.TotalEngine2Ccc == nil && attrs.TotalFlightReport == nil && attrs.TotalHookRelease == nil && attrs.TotalRotorBrakeCycle == nil { return writeHelicopterUsageErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{ Status: "422", Title: "Validation error", Detail: "at least one attribute must be provided", Source: &jsonapi.ErrorSource{Pointer: "/data/attributes"}, }}) } existing, err := h.svc.GetByID(c.UserContext(), id) if err != nil || existing == nil { return writeHelicopterUsageErrors(c, fiber.StatusNotFound, []jsonapi.ErrorObject{{ Status: "404", Title: "Not found", Detail: "helicopter usage not found", }}) } if attrs.HelicopterID != nil { helicopterID, parseErr := uuidv7.ParseString(strings.TrimSpace(*attrs.HelicopterID)) if parseErr != nil { return writeHelicopterUsageErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{ Status: "422", Title: "Validation error", Detail: "helicopter_id is invalid UUID", Source: &jsonapi.ErrorSource{Pointer: "/data/attributes/helicopter_id"}, }}) } existing.HelicopterID = helicopterID } existing.ApplyManualSummary(helicopterUsageManualInput(attrs)) existing.UpdatedBy = actorUserID(c) if err := h.svc.Update(c.UserContext(), existing); err != nil { return writeHelicopterUsageErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{ Status: "400", Title: "Update failed", Detail: safeInternalDetail(), }}) } updated, err := h.svc.GetByID(c.UserContext(), existing.ID) if err == nil && updated != nil { return response.Write(c, fiber.StatusOK, helicopterUsageResource(updated)) } return response.Write(c, fiber.StatusOK, helicopterUsageResource(existing)) } // DeleteHelicopterUsage godoc // @Summary Delete helicopter usage // @Tags Helicopter - Helicopter Usage // @Produce json // @Param uuid path string true "Helicopter Usage UUID (UUIDv7)" // @Success 200 {object} dto.GenericDeleteResponse // @Failure 404 {object} dto.ErrorResponse // @Router /api/v1/helicopter-usage/delete/{uuid} [delete] func (h *HelicopterUsageHandler) Delete(c *fiber.Ctx) error { id, err := uuidv7.ParseString(c.Params("uuid")) if err != nil { return writeHelicopterUsageErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{ Status: "422", Title: "Validation error", Detail: "uuid is invalid UUID", Source: &jsonapi.ErrorSource{Pointer: "/path/uuid"}, }}) } if err := h.svc.Delete(c.UserContext(), id, actorUserID(c)); err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return writeHelicopterUsageErrors(c, fiber.StatusNotFound, []jsonapi.ErrorObject{{ Status: "404", Title: "Not found", Detail: "helicopter usage not found", }}) } return writeHelicopterUsageErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{ Status: "400", Title: "Delete failed", Detail: safeInternalDetail(), }}) } return response.Write(c, fiber.StatusOK, jsonapi.Resource{ Type: "helicopter_usage_delete", Attributes: map[string]any{ "deleted": true, }, }) } // ListHelicopterUsage godoc // @Summary List helicopter usage // @Description JSON:API list without pagination. Sort values: helicopter_id, -helicopter_id, total_landing, -total_landing, total_airframe_hours, -total_airframe_hours, total_airframe_cycles, -total_airframe_cycles, total_engine_1_hours, -total_engine_1_hours, total_engine_1_gpc_ng_n1, -total_engine_1_gpc_ng_n1, total_engine_1_ptc_nf_n2, -total_engine_1_ptc_nf_n2, total_engine_1_ccc, -total_engine_1_ccc, total_engine_2_hours, -total_engine_2_hours, total_engine_2_gpc_ng_n1, -total_engine_2_gpc_ng_n1, total_engine_2_ptc_nf_n2, -total_engine_2_ptc_nf_n2, total_engine_2_ccc, -total_engine_2_ccc, total_flight_report, -total_flight_report, total_hook_release, -total_hook_release, total_rotor_brake_cycle, -total_rotor_brake_cycle, created_at, -created_at, updated_at, -updated_at. // @Tags Helicopter - Helicopter Usage // @Produce json // @Param search query string false "Search by helicopter or usage ID" // @Param sort query string false "Sort order" // @Success 200 {object} dto.HelicopterUsageListResponse // @Router /api/v1/helicopter-usage/get-all [get] func (h *HelicopterUsageHandler) List(c *fiber.Ctx) error { filter := strings.TrimSpace(c.Query("search")) sort := strings.TrimSpace(c.Query("sort")) rows, total, err := h.svc.List(c.UserContext(), filter, sort, 0, 0) if err != nil { return writeHelicopterUsageErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{ Status: "400", Title: "List failed", Detail: safeInternalDetail(), }}) } data := make([]dto.HelicopterUsageResource, 0, len(rows)) for i := range rows { data = append(data, helicopterUsageResource(&rows[i])) } meta := map[string]any{ "total": total, } return response.WriteWithMeta(c, fiber.StatusOK, data, meta) } // ListHelicopterUsageDatatable godoc // @Summary List helicopter usage (datatable) // @Description Datatable response with simple pagination params. // @Tags Helicopter - Helicopter Usage // @Produce json // @Param page query int false "Page number (default 1)" // @Param size query int false "Page size (default 20, max 100)" // @Param limit query int false "Page size override (same as size)" // @Param draw query int false "Draw counter (optional)" // @Param search query string false "Search term" // @Success 200 {object} dto.HelicopterUsageListResponse // @Router /api/v1/helicopter-usage/get-all/dt [get] func (h *HelicopterUsageHandler) ListDatatable(c *fiber.Ctx) error { pageNumber := parseIntDefault(c.Query("page"), 1) if pageNumber < 1 { pageNumber = 1 } length := parseIntDefault(c.Query("limit"), 0) if length <= 0 { length = parseIntDefault(c.Query("size"), 20) } if length > 100 { length = 100 } if length < 1 { length = 20 } start := (pageNumber - 1) * length draw := parseIntDefault(c.Query("draw"), pageNumber) search := strings.TrimSpace(c.Query("search")) sort := strings.TrimSpace(c.Query("sort")) rows, total, err := h.svc.List(c.UserContext(), search, sort, length, start) if err != nil { return writeHelicopterUsageErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{ Status: "400", Title: "List failed", Detail: safeInternalDetail(), }}) } data := make([]dto.HelicopterUsageResource, 0, len(rows)) for i := range rows { data = append(data, helicopterUsageResource(&rows[i])) } meta := map[string]any{ "draw": draw, "records_total": total, "records_filtered": total, } return response.WriteWithMeta(c, fiber.StatusOK, data, meta) } func helicopterUsageFromCreate(attrs dto.HelicopterUsageCreateAttributes) *helicopterusage.HelicopterUsage { row := &helicopterusage.HelicopterUsage{} row.ApplyManualSummary(helicopterusage.ManualSummaryInput{ AirframeHours: attrs.TotalAirframeHours, AirframeCycles: attrs.TotalAirframeCycles, Engine1Hours: attrs.TotalEngine1Hours, Engine1GpcNgN1: attrs.TotalEngine1GpcNgN1, Engine1PtcNfN2: attrs.TotalEngine1PtcNfN2, Engine1Ccc: attrs.TotalEngine1Ccc, Engine2Hours: attrs.TotalEngine2Hours, Engine2GpcNgN1: attrs.TotalEngine2GpcNgN1, Engine2PtcNfN2: attrs.TotalEngine2PtcNfN2, Engine2Ccc: attrs.TotalEngine2Ccc, Landing: attrs.TotalLanding, FlightReport: attrs.TotalFlightReport, HookRelease: attrs.TotalHookRelease, RotorBrakeCycle: attrs.TotalRotorBrakeCycle, }) return row } func helicopterUsageManualInput(attrs dto.HelicopterUsageUpdateAttributes) helicopterusage.ManualSummaryInput { return helicopterusage.ManualSummaryInput{ AirframeHours: attrs.TotalAirframeHours, AirframeCycles: attrs.TotalAirframeCycles, Engine1Hours: attrs.TotalEngine1Hours, Engine1GpcNgN1: attrs.TotalEngine1GpcNgN1, Engine1PtcNfN2: attrs.TotalEngine1PtcNfN2, Engine1Ccc: attrs.TotalEngine1Ccc, Engine2Hours: attrs.TotalEngine2Hours, Engine2GpcNgN1: attrs.TotalEngine2GpcNgN1, Engine2PtcNfN2: attrs.TotalEngine2PtcNfN2, Engine2Ccc: attrs.TotalEngine2Ccc, Landing: attrs.TotalLanding, FlightReport: attrs.TotalFlightReport, HookRelease: attrs.TotalHookRelease, RotorBrakeCycle: attrs.TotalRotorBrakeCycle, } } func helicopterUsageResource(row *helicopterusage.HelicopterUsage) dto.HelicopterUsageResource { if row == nil { return dto.HelicopterUsageResource{} } return dto.HelicopterUsageResource{ Type: "helicopter_usage", ID: idString(row.ID), Attributes: dto.HelicopterUsageAttributes{ HelicopterID: idString(row.HelicopterID), TotalLanding: row.TotalLanding, TotalAirframeHours: row.TotalAirframeHours, TotalAirframeCycles: row.TotalAirframeCycles, TotalEngine1Hours: row.TotalEngine1Hours, TotalEngine1GpcNgN1: row.TotalEngine1GpcNgN1, TotalEngine1PtcNfN2: row.TotalEngine1PtcNfN2, TotalEngine1Ccc: row.TotalEngine1Ccc, TotalEngine2Hours: row.TotalEngine2Hours, TotalEngine2GpcNgN1: row.TotalEngine2GpcNgN1, TotalEngine2PtcNfN2: row.TotalEngine2PtcNfN2, TotalEngine2Ccc: row.TotalEngine2Ccc, TotalFlightReport: row.TotalFlightReport, TotalHookRelease: row.TotalHookRelease, TotalRotorBrakeCycle: row.TotalRotorBrakeCycle, CreatedAt: row.CreatedAt.UTC().Format(time.RFC3339), CreatedBy: uuidStringOrEmpty(row.CreatedBy), UpdatedAt: row.UpdatedAt.UTC().Format(time.RFC3339), UpdatedBy: uuidStringOrEmpty(row.UpdatedBy), DeletedAt: timeStringOrEmpty(row.DeletedAt), DeletedBy: uuidStringOrEmpty(row.DeletedBy), }, } } func writeHelicopterUsageErrors(c *fiber.Ctx, status int, errs []jsonapi.ErrorObject) error { for i := range errs { errs[i].Status = strconv.Itoa(status) } return response.WriteErrors(c, status, errs) }