Files
fm_be/internal/transport/http/handlers/fleet_status_handler.go
2026-07-16 22:16:45 +07:00

635 lines
25 KiB
Go

package handlers
import (
"context"
"errors"
"log/slog"
"sort"
"strconv"
"strings"
"time"
"github.com/gofiber/fiber/v2"
"gorm.io/gorm"
complaint "wucher/internal/domain/complaint"
easarelease "wucher/internal/domain/easa_release"
fleetstatus "wucher/internal/domain/fleet_status"
"wucher/internal/domain/helicopter"
"wucher/internal/domain/mcf"
"wucher/internal/shared/pkg/apperrorsx"
"wucher/internal/shared/pkg/userctx"
"wucher/internal/shared/pkg/uuidv7"
requestdto "wucher/internal/transport/http/dto/request"
responsedto "wucher/internal/transport/http/dto/response"
"wucher/internal/transport/http/jsonapi"
"wucher/internal/transport/http/response"
"wucher/internal/transport/http/validators"
)
type FleetStatusHandler struct {
svc fleetstatus.Service
helicopterSvc helicopter.Service
complaintSvc complaintLister
easaSvc easaReleaseLister
historySvc fleetHistoryLister
mcfSvc fleetMCFService
signoffSvc complaintSignoffLookup
storage fileManagerDownloadURLStorage
validate *validators.Validator
}
type FleetStatusDeps struct {
Helicopter helicopter.Service
Complaint complaintLister
EASA easaReleaseLister
History fleetHistoryLister
MCF fleetMCFService
Signoff complaintSignoffLookup
Storage fileManagerDownloadURLStorage
}
type complaintLister interface {
ListByHelicopter(ctx context.Context, helicopterID []byte, limit, offset int) ([]complaint.Complaint, int64, error)
ResolveUserNames(ctx context.Context, complaints []complaint.Complaint) (map[string]string, error)
ActiveGroundingByHelicopterIDs(ctx context.Context, helicopterIDs [][]byte) ([]complaint.Complaint, error)
OpenByHelicopterIDs(ctx context.Context, helicopterIDs [][]byte) ([]complaint.Complaint, error)
}
// fleetMCFService is the MCF access the fleet page needs: pending flags (for status)
// plus the latest MCF per helicopter (for the mcf object / placeholder).
type fleetMCFService interface {
mcfPendingChecker
LatestByHelicopterIDs(ctx context.Context, helicopterIDs [][]byte) (map[string]*mcf.MaintenanceCheckFlight, error)
}
type easaReleaseLister interface {
GetLatestByHelicopter(ctx context.Context, helicopterID []byte) (*easarelease.EASARelease, error)
}
func NewFleetStatusHandler(svc fleetstatus.Service, deps FleetStatusDeps) *FleetStatusHandler {
if svc == nil || deps.Helicopter == nil || deps.Complaint == nil || deps.EASA == nil ||
deps.History == nil || deps.MCF == nil || deps.Storage == nil {
panic("fleet status handler: missing required dependency")
}
return &FleetStatusHandler{
svc: svc,
helicopterSvc: deps.Helicopter,
complaintSvc: deps.Complaint,
easaSvc: deps.EASA,
historySvc: deps.History,
mcfSvc: deps.MCF,
signoffSvc: deps.Signoff,
storage: deps.Storage,
validate: validators.New(),
}
}
// CreateFleetStatus godoc
// @Summary Create fleet status
// @Description Create fleet status with maintenance schedules and file attachments.
// @Tags Fleet Status
// @Accept json
// @Produce json
// @Param request body requestdto.FleetStatusCreateRequest true "Fleet status create request"
// @Success 201 {object} responsedto.FleetStatusResponse
// @Failure 400 {object} jsonapi.ErrorDocument
// @Failure 422 {object} jsonapi.ErrorDocument
// @Router /api/v1/fleet-status/create [post]
func (h *FleetStatusHandler) Create(c *fiber.Ctx) error {
var req requestdto.FleetStatusCreateRequest
if err := c.BodyParser(&req); err != nil {
return writeFleetStatusAppError(c, apperrorsx.ErrFleetStatusInvalidJSON, "", nil)
}
if errs := h.validate.ValidateStruct(req); errs != nil {
return writeFleetStatusValidationErrors(c, fiber.StatusUnprocessableEntity, errs)
}
row, err := mapFleetStatusPayload(req.Data.Attributes, actorUserID(c))
if err != nil {
return writeFleetStatusAppError(c, apperrorsx.ErrFleetStatusInvalidPayload, "", nil)
}
if err := h.svc.Create(c.UserContext(), row); err != nil {
return writeFleetStatusServiceError(c, "FleetStatusHandler.Create", err)
}
created, err := h.svc.GetByID(c.UserContext(), row.ID)
if err != nil {
slog.WarnContext(c.UserContext(), "fleet status re-fetch after create failed", slog.Any("error", err), slog.String("id", idString(row.ID)))
}
if created == nil {
created = row
}
createdStatus := h.deriveHelicopterStatus(c.UserContext(), created.Helicopter, created.HelicopterID)
return response.Write(c, fiber.StatusCreated, fleetStatusDetailResource(c.UserContext(), h, created, createdStatus))
}
// UpdateFleetStatus godoc
// @Summary Update fleet status
// @Description Update fleet status by ID with full replacement for schedules and files.
// @Tags Fleet Status
// @Accept json
// @Produce json
// @Param id path string true "Fleet status ID (UUIDv7)"
// @Param request body requestdto.FleetStatusUpdateRequest true "Fleet status update request"
// @Success 200 {object} responsedto.FleetStatusResponse
// @Failure 400 {object} jsonapi.ErrorDocument
// @Failure 404 {object} jsonapi.ErrorDocument
// @Failure 422 {object} jsonapi.ErrorDocument
// @Router /api/v1/fleet-status/update/{id} [patch]
func (h *FleetStatusHandler) Update(c *fiber.Ctx) error {
idStr := c.Params("id")
id, err := uuidv7.ParseString(idStr)
if err != nil {
return writeFleetStatusAppError(c, apperrorsx.ErrFleetStatusInvalidID, "", &jsonapi.ErrorSource{Pointer: "/path/id"})
}
var req requestdto.FleetStatusUpdateRequest
if err := c.BodyParser(&req); err != nil {
return writeFleetStatusAppError(c, apperrorsx.ErrFleetStatusInvalidJSON, "", nil)
}
if errs := h.validate.ValidateStruct(req); errs != nil {
return writeFleetStatusValidationErrors(c, fiber.StatusUnprocessableEntity, errs)
}
if strings.TrimSpace(req.Data.ID) == "" {
return writeFleetStatusAppError(c, apperrorsx.ErrFleetStatusIDRequired, "", &jsonapi.ErrorSource{Pointer: "/data/id"})
}
if strings.TrimSpace(req.Data.ID) != idStr {
return writeFleetStatusAppError(c, apperrorsx.ErrFleetStatusIDMismatch, "", &jsonapi.ErrorSource{Pointer: "/data/id"})
}
existing, err := h.svc.GetByID(c.UserContext(), id)
if err != nil || existing == nil {
return writeFleetStatusAppError(c, apperrorsx.ErrFleetStatusNotFound, "", nil)
}
row, err := mergeFleetStatusPatch(existing, req.Data.Attributes, actorUserID(c))
if err != nil {
return writeFleetStatusAppError(c, apperrorsx.ErrFleetStatusInvalidPayload, "", nil)
}
if err := h.svc.Update(c.UserContext(), row); err != nil {
return writeFleetStatusServiceError(c, "FleetStatusHandler.Update", err)
}
if req.Data.Attributes.Summary != nil {
if err := h.svc.ReviseSummary(c.UserContext(), row.HelicopterID, fleetSummaryManualInput(req.Data.Attributes.Summary), actorUserID(c)); err != nil {
return writeFleetStatusServiceError(c, "FleetStatusHandler.Update", err)
}
}
updated, err := h.svc.GetByID(c.UserContext(), id)
if err != nil {
slog.WarnContext(c.UserContext(), "fleet status re-fetch after update failed", slog.Any("error", err), slog.String("id", idString(id)))
}
if updated == nil {
updated = row
}
updatedStatus := h.deriveHelicopterStatus(c.UserContext(), updated.Helicopter, updated.HelicopterID)
return response.Write(c, fiber.StatusOK, fleetStatusDetailResource(c.UserContext(), h, updated, updatedStatus))
}
// DeleteFleetStatus godoc
// @Summary Delete fleet status
// @Description Soft delete fleet status by ID.
// @Tags Fleet Status
// @Produce json
// @Param id path string true "Fleet status ID (UUIDv7)"
// @Success 200 {object} responsedto.FleetStatusDeleteResponse
// @Failure 400 {object} jsonapi.ErrorDocument
// @Failure 422 {object} jsonapi.ErrorDocument
// @Router /api/v1/fleet-status/delete/{id} [delete]
func (h *FleetStatusHandler) Delete(c *fiber.Ctx) error {
id, err := uuidv7.ParseString(c.Params("id"))
if err != nil {
return writeFleetStatusAppError(c, apperrorsx.ErrFleetStatusInvalidID, "", &jsonapi.ErrorSource{Pointer: "/path/id"})
}
if err := h.svc.Delete(c.UserContext(), id, actorUserID(c)); err != nil {
return writeFleetStatusServiceError(c, "FleetStatusHandler.Delete", err)
}
return response.Write(c, fiber.StatusOK, map[string]any{"data": map[string]any{"type": "fleet_status_delete", "attributes": map[string]bool{"deleted": true}}})
}
// GetFleetStatusByID godoc
// @Summary Get fleet status by ID
// @Tags Fleet Status
// @Produce json
// @Param id path string true "Fleet status ID (UUIDv7)"
// @Success 200 {object} responsedto.FleetStatusResponse
// @Failure 404 {object} jsonapi.ErrorDocument
// @Failure 422 {object} jsonapi.ErrorDocument
// @Router /api/v1/fleet-status/get/{id} [get]
func (h *FleetStatusHandler) GetByID(c *fiber.Ctx) error {
id, err := uuidv7.ParseString(c.Params("id"))
if err != nil {
return writeFleetStatusAppError(c, apperrorsx.ErrFleetStatusInvalidID, "", &jsonapi.ErrorSource{Pointer: "/path/id"})
}
row, err := h.svc.GetByID(c.UserContext(), id)
if err != nil || row == nil {
return writeFleetStatusAppError(c, apperrorsx.ErrFleetStatusNotFound, "", nil)
}
rowStatus := h.deriveHelicopterStatus(c.UserContext(), row.Helicopter, row.HelicopterID)
return response.Write(c, fiber.StatusOK, fleetStatusDetailResource(c.UserContext(), h, row, rowStatus))
}
// GetFleetStatusByHelicopterID godoc
// @Summary Get latest fleet status by helicopter ID
// @Tags Fleet Status
// @Produce json
// @Param helicopter_id path string true "Helicopter ID (UUIDv7)"
// @Success 200 {object} responsedto.FleetStatusResponse
// @Failure 404 {object} jsonapi.ErrorDocument
// @Failure 422 {object} jsonapi.ErrorDocument
// @Router /api/v1/fleet-status/get-by-helicopter/{helicopter_id} [get]
func (h *FleetStatusHandler) GetByHelicopterID(c *fiber.Ctx) error {
helicopterID, err := uuidv7.ParseString(c.Params("helicopter_id"))
if err != nil {
return writeFleetStatusAppError(c, apperrorsx.ErrFleetStatusInvalidID, "helicopter_id is invalid UUID", &jsonapi.ErrorSource{Pointer: "/path/helicopter_id"})
}
latest, err := h.svc.LatestByHelicopterIDs(c.UserContext(), [][]byte{helicopterID})
if err != nil {
return writeFleetStatusServiceError(c, "FleetStatusHandler.GetByHelicopterID", err)
}
row := latest[string(helicopterID)]
if row == nil {
return writeFleetStatusAppError(c, apperrorsx.ErrFleetStatusNotFound, "fleet status not found for helicopter", nil)
}
rowStatus := h.deriveHelicopterStatus(c.UserContext(), row.Helicopter, row.HelicopterID)
return response.Write(c, fiber.StatusOK, fleetStatusDetailResource(c.UserContext(), h, row, rowStatus))
}
// ListFleetStatus godoc
// @Summary List fleet status
// @Description Lists every helicopter from the master, each with its latest fleet status (or empty data when it has none). Paginated by helicopter.
// @Tags Fleet Status
// @Produce json
// @Param page query int false "Page number" default(1)
// @Param page_size query int false "Page size" default(20)
// @Param filter query string false "Search helicopters by designation/identifier/type"
// @Param sort query string false "Sort helicopters, example: designation"
// @Param status query string false "Filter by operational status; comma-separated for multiple (e.g. available,booked). Values: available, booked, aog, mcf"
// @Param helicopter_id query string false "Filter by helicopter ID (UUIDv7); combined with status uses AND logic"
// @Success 200 {object} responsedto.FleetStatusListResponse
// @Failure 400 {object} jsonapi.ErrorDocument
// @Router /api/v1/fleet-status/get-all [get]
func (h *FleetStatusHandler) List(c *fiber.Ctx) error {
page, _ := strconv.Atoi(c.Query("page", "1"))
if page < 1 {
page = 1
}
size, _ := strconv.Atoi(c.Query("page_size", "20"))
if size < 1 {
size = 20
}
offset := (page - 1) * size
statuses, err := parseHelicopterStatusFilter(c.Query("status"))
if err != nil {
return writeFleetStatusAppError(c, apperrorsx.ErrInvalidRequest, "invalid status filter", nil)
}
// If helicopter_id is provided, fetch that single helicopter and apply status
// filter in Go (AND logic). Pagination is irrelevant for a single result.
if raw := strings.TrimSpace(c.Query("helicopter_id")); raw != "" {
heliID, err := uuidv7.ParseString(raw)
if err != nil {
return writeFleetStatusAppError(c, apperrorsx.ErrInvalidRequest, "invalid helicopter_id", &jsonapi.ErrorSource{Pointer: "/query/helicopter_id"})
}
resources, err := h.singleHelicopterResources(c.UserContext(), heliID, statuses)
if err != nil {
return writeFleetStatusServiceError(c, "FleetStatusHandler.List", err)
}
return response.WriteWithMeta(c, fiber.StatusOK, resources, map[string]any{
"page_number": page,
"page_size": size,
"total": int64(len(resources)),
})
}
var groundedIDs [][]byte
if len(statuses) > 0 {
groundedIDs = groundedHelicopterIDs(c.UserContext(), h.complaintSvc, time.Now().UTC())
}
// Drive the listing from the helicopter master so each helicopter carries its
// latest fleet status (or empty data when it has none).
helis, total, err := h.helicopterSvc.List(c.UserContext(), c.Query("filter"), statuses, c.Query("sort"), size, offset, groundedIDs)
if err != nil {
return writeFleetStatusServiceError(c, "FleetStatusHandler.List", err)
}
resources, err := h.fleetStatusResourcesForHelicopters(c.UserContext(), helis)
if err != nil {
return writeFleetStatusServiceError(c, "FleetStatusHandler.List", err)
}
sortFleetStatusResourcesByStatus(resources)
meta := map[string]any{
"page_number": page,
"page_size": size,
"total": total,
}
return response.WriteWithMeta(c, fiber.StatusOK, resources, meta)
}
// ListFleetStatusDatatable godoc
// @Summary List fleet status (datatable)
// @Description Lists every helicopter from the master, each with its latest fleet status (or empty data when it has none). Paginated by helicopter.
// @Tags Fleet Status
// @Produce json
// @Param draw query int false "Datatable draw counter" default(1)
// @Param start query int false "Datatable start offset" default(0)
// @Param length query int false "Datatable page length" default(10)
// @Param search query string false "Search helicopters by designation/identifier/type"
// @Param sort query string false "Sort helicopters, example: designation"
// @Param status query string false "Filter by operational status; comma-separated for multiple (e.g. available,booked). Values: available, booked, aog, mcf"
// @Param helicopter_id query string false "Filter by helicopter ID (UUIDv7); combined with status uses AND logic"
// @Success 200 {object} responsedto.FleetStatusDataTableResponse
// @Failure 400 {object} jsonapi.ErrorDocument
// @Router /api/v1/fleet-status/get-all/dt [get]
func (h *FleetStatusHandler) ListDatatable(c *fiber.Ctx) error {
start, _ := strconv.Atoi(c.Query("start", "0"))
length, _ := strconv.Atoi(c.Query("length", "10"))
draw, _ := strconv.Atoi(c.Query("draw", "1"))
if length < 1 {
length = 10
}
statuses, err := parseHelicopterStatusFilter(c.Query("status"))
if err != nil {
return writeFleetStatusAppError(c, apperrorsx.ErrInvalidRequest, "invalid status filter", nil)
}
// If helicopter_id is provided, fetch that single helicopter and apply status
// filter in Go (AND logic).
if raw := strings.TrimSpace(c.Query("helicopter_id")); raw != "" {
heliID, err := uuidv7.ParseString(raw)
if err != nil {
return writeFleetStatusAppError(c, apperrorsx.ErrInvalidRequest, "invalid helicopter_id", &jsonapi.ErrorSource{Pointer: "/query/helicopter_id"})
}
resources, err := h.singleHelicopterResources(c.UserContext(), heliID, statuses)
if err != nil {
return writeFleetStatusServiceError(c, "FleetStatusHandler.ListDatatable", err)
}
total := int64(len(resources))
return response.WriteWithMeta(c, fiber.StatusOK, resources, map[string]any{
"draw": draw,
"records_total": total,
"records_filtered": total,
})
}
var groundedIDs [][]byte
if len(statuses) > 0 {
groundedIDs = groundedHelicopterIDs(c.UserContext(), h.complaintSvc, time.Now().UTC())
}
helis, total, err := h.helicopterSvc.List(c.UserContext(), c.Query("search"), statuses, c.Query("sort"), length, start, groundedIDs)
if err != nil {
return writeFleetStatusServiceError(c, "FleetStatusHandler.ListDatatable", err)
}
resources, err := h.fleetStatusResourcesForHelicopters(c.UserContext(), helis)
if err != nil {
return writeFleetStatusServiceError(c, "FleetStatusHandler.ListDatatable", err)
}
sortFleetStatusResourcesByStatus(resources)
meta := map[string]any{
"draw": draw,
"records_total": total,
"records_filtered": total,
}
return response.WriteWithMeta(c, fiber.StatusOK, resources, meta)
}
func fleetStatusResourceStatus(r responsedto.FleetStatusResource) string {
if r.Attributes.Helicopter != nil {
return r.Attributes.Helicopter.Status
}
return ""
}
// sortFleetStatusResourcesByStatus stably orders the resources by status rank, keeping
// the incoming (designation/sort) order within the same status.
func sortFleetStatusResourcesByStatus(resources []responsedto.FleetStatusResource) {
sort.SliceStable(resources, func(i, j int) bool {
return helicopter.StatusRank(fleetStatusResourceStatus(resources[i])) < helicopter.StatusRank(fleetStatusResourceStatus(resources[j]))
})
}
// MarkFleetStatusServiced godoc
// @Summary Mark fleet status serviced
// @Description Move current maintenance schedules into service logs, then clear active schedules.
// @Tags Fleet Status
// @Produce json
// @Param id path string true "Fleet status ID (UUIDv7)"
// @Success 200 {object} responsedto.FleetStatusMarkServicedResponse
// @Failure 400 {object} jsonapi.ErrorDocument
// @Failure 404 {object} jsonapi.ErrorDocument
// @Failure 422 {object} jsonapi.ErrorDocument
// @Router /api/v1/fleet-status/mark-serviced/{id} [post]
func (h *FleetStatusHandler) MarkServiced(c *fiber.Ctx) error {
id, err := uuidv7.ParseString(c.Params("id"))
if err != nil {
return writeFleetStatusAppError(c, apperrorsx.ErrFleetStatusInvalidID, "", &jsonapi.ErrorSource{Pointer: "/path/id"})
}
if err := h.svc.MarkServiced(c.UserContext(), id, time.Now().UTC(), actorUserID(c)); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return writeFleetStatusAppError(c, apperrorsx.ErrFleetStatusNotFound, "", nil)
}
return writeFleetStatusServiceError(c, "FleetStatusHandler.MarkServiced", err)
}
return response.Write(c, fiber.StatusOK, map[string]any{
"data": map[string]any{
"type": "fleet_status_mark_serviced",
"attributes": map[string]any{
"serviced": true,
},
},
})
}
// ListFleetStatusServiceHistory godoc
// @Summary List fleet history (timeline) by helicopter
// @Description Unified timeline: maintenance servicing events merged with complaint/MEL/NSR/action/MCF/EASA/AOG action events, newest first.
// @Tags Fleet Status
// @Produce json
// @Param helicopter_id path string true "Helicopter ID (UUIDv7)"
// @Param page query int false "Page number" default(1)
// @Param page_size query int false "Page size" default(20)
// @Success 200 {object} responsedto.FleetHistoryListResponse
// @Failure 400 {object} jsonapi.ErrorDocument
// @Failure 422 {object} jsonapi.ErrorDocument
// @Router /api/v1/fleet-status/history/{helicopter_id} [get]
func (h *FleetStatusHandler) ListHistoryByHelicopter(c *fiber.Ctx) error {
helicopterID, err := uuidv7.ParseString(c.Params("helicopter_id"))
if err != nil {
return writeFleetStatusAppError(c, apperrorsx.ErrFleetStatusInvalidID, "helicopter_id is invalid UUID", &jsonapi.ErrorSource{Pointer: "/path/helicopter_id"})
}
page, _ := strconv.Atoi(c.Query("page", "1"))
if page < 1 {
page = 1
}
size, _ := strconv.Atoi(c.Query("page_size", "20"))
if size < 1 {
size = 20
}
offset := (page - 1) * size
type timedEntry struct {
at time.Time
entry responsedto.FleetHistoryEntry
}
items := make([]timedEntry, 0)
logs, _, err := h.svc.ListServiceHistoryByHelicopterID(c.UserContext(), helicopterID, 1000, 0)
if err != nil {
return writeFleetStatusServiceError(c, "FleetStatusHandler.ListHistoryByHelicopter", err)
}
for i := range logs {
l := &logs[i]
items = append(items, timedEntry{at: l.ServicedAt.UTC(), entry: responsedto.FleetHistoryEntry{
At: l.ServicedAt.UTC().Format(time.RFC3339),
EntityType: "fleet_status",
EntityID: idString(l.FleetStatusID),
Action: "serviced",
Message: "Serviced " + strings.TrimSpace(l.InspectionType+" "+l.Type),
Actor: idString(l.CreatedBy),
ActorName: userctx.GetDisplayName(c.UserContext(), l.CreatedBy),
}})
}
events, _, herr := h.historySvc.ListByHelicopter(c.UserContext(), helicopterID, 0, 0)
if herr == nil {
for i := range events {
e := &events[i]
items = append(items, timedEntry{at: e.CreatedAt.UTC(), entry: responsedto.FleetHistoryEntry{
At: e.CreatedAt.UTC().Format(time.RFC3339),
EntityType: e.EntityType,
EntityID: idString(e.EntityID),
Action: e.Action,
Message: e.Message,
Actor: idString(e.Actor),
ActorName: userctx.GetDisplayName(c.UserContext(), e.Actor),
}})
}
}
sort.SliceStable(items, func(i, j int) bool { return items[i].at.After(items[j].at) })
total := int64(len(items))
start := offset
if start > len(items) {
start = len(items)
}
end := start + size
if end > len(items) {
end = len(items)
}
data := make([]responsedto.FleetHistoryEntry, 0, end-start)
for _, it := range items[start:end] {
data = append(data, it.entry)
}
res := responsedto.FleetHistoryListResponse{Data: data}
res.Meta.PageNumber = page
res.Meta.PageSize = size
res.Meta.Total = total
return response.Write(c, fiber.StatusOK, res)
}
// singleHelicopterResources fetches one helicopter by ID, builds its fleet
// status resource, and applies an optional status filter (AND logic). Returns
// an empty slice when the helicopter is not found or the status does not match.
func (h *FleetStatusHandler) singleHelicopterResources(ctx context.Context, helicopterID []byte, statuses []string) ([]responsedto.FleetStatusResource, error) {
heli, err := h.helicopterSvc.GetByID(ctx, helicopterID)
if err != nil {
return nil, err
}
if heli == nil {
return []responsedto.FleetStatusResource{}, nil
}
resources, err := h.fleetStatusResourcesForHelicopters(ctx, []helicopter.Helicopter{*heli})
if err != nil {
return nil, err
}
if len(statuses) > 0 && len(resources) > 0 {
heliStatus := ""
if resources[0].Attributes.Helicopter != nil {
heliStatus = resources[0].Attributes.Helicopter.Status
}
for _, s := range statuses {
if s == heliStatus {
return resources, nil
}
}
return []responsedto.FleetStatusResource{}, nil
}
return resources, nil
}
func writeFleetStatusAppError(c *fiber.Ctx, def apperrorsx.Def, detail string, source *jsonapi.ErrorSource) error {
appErr := apperrorsx.New(def)
if strings.TrimSpace(detail) != "" {
appErr.Message = detail
}
if source != nil {
appErr.Source = source
}
return apperrorsx.Write(c, appErr)
}
func writeFleetStatusValidationErrors(c *fiber.Ctx, status int, errs []jsonapi.ErrorObject) error {
statusStr := strconv.Itoa(status)
enriched := make([]jsonapi.ErrorObject, 0, len(errs))
for i := range errs {
e := errs[i]
e.Status = statusStr
if strings.TrimSpace(e.Code) == "" || strings.TrimSpace(e.ErrorCode) == "" {
code, errorCode := inferFleetStatusErrorCode(status, e.Title, e.Detail)
if strings.TrimSpace(e.Code) == "" {
e.Code = code
}
if strings.TrimSpace(e.ErrorCode) == "" {
e.ErrorCode = errorCode
}
}
enriched = append(enriched, e)
}
return jsonapi.WriteErrors(c, status, enriched)
}
func writeFleetStatusServiceError(c *fiber.Ctx, operation string, err error) error {
mapped := apperrorsx.Translate(apperrorsx.ModuleFleetStatus, err)
if mapped != nil && mapped.Code != apperrorsx.CodeInternalServerError {
logFleetStatusServiceError(c, operation, mapped.Status, mapped.Code, mapped.ErrorCode, err)
return apperrorsx.Write(c, mapped)
}
logFleetStatusServiceError(c, operation, fiber.StatusInternalServerError, apperrorsx.CodeInternalServerError, apperrorsx.ErrInternal.ErrorCode, err)
appErr := apperrorsx.New(apperrorsx.ErrInternal)
appErr.Message = "an internal error occurred"
return apperrorsx.Write(c, appErr)
}
func logFleetStatusServiceError(c *fiber.Ctx, operation string, status int, code, errorCode string, err error) {
requestID := requestIDFromCtx(c)
args := []any{
slog.String("request_id", requestID),
slog.String("operation", strings.TrimSpace(operation)),
slog.Int("status", status),
slog.String("code", strings.TrimSpace(code)),
slog.String("error_code", strings.TrimSpace(errorCode)),
slog.String("path", c.Path()),
slog.Any("cause", err),
}
if status >= fiber.StatusInternalServerError {
slog.ErrorContext(c.UserContext(), "fleet status service error", args...)
return
}
slog.WarnContext(c.UserContext(), "fleet status service error", args...)
}
func inferFleetStatusErrorCode(status int, _ string, _ string) (string, string) {
switch status {
case fiber.StatusNotFound:
return apperrorsx.CodeFleetStatusNotFound, apperrorsx.ErrFleetStatusNotFound.ErrorCode
case fiber.StatusUnprocessableEntity:
return apperrorsx.CodeFleetStatusInvalidPayload, apperrorsx.ErrFleetStatusInvalidPayload.ErrorCode
case fiber.StatusBadRequest:
return apperrorsx.CodeFleetStatusInvalidJSON, apperrorsx.ErrFleetStatusInvalidJSON.ErrorCode
default:
return apperrorsx.CodeInternalServerError, apperrorsx.ErrInternal.ErrorCode
}
}