package handlers import ( "context" "errors" "strings" "time" "github.com/gofiber/fiber/v2" actionsignoff "wucher/internal/domain/action_signoff" complaint "wucher/internal/domain/complaint" "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" ) // ActionSignoffHandler serves the corrective-action sign-off (sign/unsign) for a // helicopter — both complaint-linked and complaint-independent. type ActionSignoffHandler struct { svc actionsignoff.Service complaintSvc actionSignoffComplaintLookup } type actionSignoffComplaintLookup interface { GetByID(ctx context.Context, id []byte) (*complaint.Complaint, error) } func NewActionSignoffHandler(svc actionsignoff.Service) *ActionSignoffHandler { return &ActionSignoffHandler{svc: svc} } // WithComplaintService wires the complaint lookup used to validate a complaint-linked // sign-off (complaint exists and has action_taken recorded). func (h *ActionSignoffHandler) WithComplaintService(svc actionSignoffComplaintLookup) *ActionSignoffHandler { h.complaintSvc = svc return h } func (h *ActionSignoffHandler) resource(ctx context.Context, row *actionsignoff.ActionSignoff) responsedto.ActionSignoffResource { if row == nil { return responsedto.ActionSignoffResource{} } id, _ := uuidv7.BytesToString(row.ID) out := responsedto.ActionSignoffResource{ Type: "action_signoff", ID: id, Attributes: responsedto.ActionSignoffAttributes{ HelicopterID: idString(row.HelicopterID), FlightID: idString(row.FlightID), Signed: row.IsSigned(), SignedBy: idString(row.SignedBy), }, } if row.SignedAt != nil { v := row.SignedAt.UTC().Format(time.RFC3339) out.Attributes.SignedAt = &v } if n := userctx.GetDisplayName(ctx, row.SignedBy); n != "" { out.Attributes.SignedByName = &n } if s := userctx.GetShortName(ctx, row.SignedBy); s != "" { out.Attributes.SignedByShort = &s } if !row.CreatedAt.IsZero() { out.Attributes.CreatedAt = row.CreatedAt.UTC().Format(time.RFC3339) } if !row.UpdatedAt.IsZero() { out.Attributes.UpdatedAt = row.UpdatedAt.UTC().Format(time.RFC3339) } return out } func actionSignoffHelicopterID(c *fiber.Ctx) ([]byte, *apperrorsx.AppError) { id, err := uuidv7.ParseString(strings.TrimSpace(c.Params("helicopter_id"))) if err != nil { appErr := apperrorsx.New(apperrorsx.ErrActionSignoffInvalidUUID).WithCause(err) appErr.Source = &jsonapi.ErrorSource{Pointer: "/path/helicopter_id"} return nil, appErr } return id, nil } func actionSignoffFlightID(c *fiber.Ctx) ([]byte, *apperrorsx.AppError) { id, err := uuidv7.ParseString(strings.TrimSpace(c.Params("flight_id"))) if err != nil { appErr := apperrorsx.New(apperrorsx.ErrActionSignoffInvalidUUID).WithCause(err) appErr.Source = &jsonapi.ErrorSource{Pointer: "/path/flight_id"} return nil, appErr } return id, nil } // actionSignoffBodyFlightID parses the REQUIRED flight_id from the sign body — the // flight the sign-off is attributed to (standalone) or performed in (complaint-linked). func actionSignoffBodyFlightID(req requestdto.ActionSignoffSignRequest) ([]byte, *apperrorsx.AppError) { raw := "" if req.Data.Attributes.FlightID != nil { raw = strings.TrimSpace(*req.Data.Attributes.FlightID) } if raw == "" { appErr := apperrorsx.New(apperrorsx.ErrActionSignoffInvalidUUID) appErr.Source = &jsonapi.ErrorSource{Pointer: "/data/attributes/flight_id"} return nil, appErr } id, err := uuidv7.ParseString(raw) if err != nil { appErr := apperrorsx.New(apperrorsx.ErrActionSignoffInvalidUUID).WithCause(err) appErr.Source = &jsonapi.ErrorSource{Pointer: "/data/attributes/flight_id"} return nil, appErr } return id, nil } // GetByHelicopter godoc // @Summary Get the current corrective-action sign-off for a helicopter // @Description Returns the fleet-level (complaint-independent) corrective-action sign-off state for a helicopter — `signed`, `signed_at`, `signed_by`. When none exists yet it returns an unsigned placeholder. A signed sign-off is what gates the EASA maintenance release. // @Tags Complaints - Sign Off // @Produce json // @Param flight_id path string true "Flight ID (UUIDv7)" // @Success 200 {object} responsedto.ActionSignoffResponse // @Router /api/v1/action-signoffs/get-by-flight/{flight_id} [get] func (h *ActionSignoffHandler) GetByFlight(c *fiber.Ctx) error { flightID, aerr := actionSignoffFlightID(c) if aerr != nil { return apperrorsx.Write(c, aerr) } row, err := h.svc.GetByFlight(c.UserContext(), flightID) if err != nil { return apperrorsx.Write(c, apperrorsx.New(apperrorsx.ErrActionSignoffNotFound).WithCause(err)) } if row == nil { // No row yet: return an unsigned placeholder so the client can render. return response.Write(c, fiber.StatusOK, responsedto.ActionSignoffResource{ Type: "action_signoff", Attributes: responsedto.ActionSignoffAttributes{ FlightID: idString(flightID), Signed: false, }, }) } return response.Write(c, fiber.StatusOK, h.resource(c.UserContext(), row)) } // Sign godoc // @Summary Toggle the corrective-action sign-off (per complaint or fleet-wide) // @Description Toggles the corrective-action sign-off for a helicopter. `sign` (optional, default true) chooses the direction: true signs, false unsigns. Send `complaint_id` to target a specific complaint's sign-off — signing it requires `action_taken` recorded (else 422 `ACTION_SIGNOFF_ACTION_REQUIRED`), and 404 `ACTION_SIGNOFF_COMPLAINT_NOT_FOUND` if the complaint does not exist. Omit `complaint_id` for a fleet-wide (complaint-independent) sign-off. A signed sign-off is what allows an EASA maintenance release to be created for the helicopter. // @Tags Complaints - Sign Off // @Accept json // @Produce json // @Param helicopter_id path string true "Helicopter ID (UUIDv7)" // @Param request body requestdto.ActionSignoffSignRequest false "Optional complaint_id + note" // @Success 200 {object} responsedto.ActionSignoffResponse // @Failure 404 {object} jsonapi.ErrorDocument // @Failure 422 {object} jsonapi.ErrorDocument // @Router /api/v1/action-signoffs/sign/{helicopter_id} [post] func (h *ActionSignoffHandler) Sign(c *fiber.Ctx) error { helicopterID, err := actionSignoffHelicopterID(c) if err != nil { return apperrorsx.Write(c, err) } // Body is optional — the sign-off can be checked with nothing filled. var req requestdto.ActionSignoffSignRequest _ = c.BodyParser(&req) actor := actorUserID(c) // sign toggles the state: true (or omitted) signs, false unsigns. signWanted := req.Data.Attributes.Sign == nil || *req.Data.Attributes.Sign complaintIDRaw := "" if req.Data.Attributes.ComplaintID != nil { complaintIDRaw = strings.TrimSpace(*req.Data.Attributes.ComplaintID) } // Complaint-linked toggle: sign/unsign that complaint's corrective action. if complaintIDRaw != "" { complaintID, perr := uuidv7.ParseString(complaintIDRaw) if perr != nil { appErr := apperrorsx.New(apperrorsx.ErrActionSignoffInvalidUUID).WithCause(perr) appErr.Source = &jsonapi.ErrorSource{Pointer: "/data/attributes/complaint_id"} return apperrorsx.Write(c, appErr) } if !signWanted { row, serr := h.svc.UnsignByComplaint(c.UserContext(), complaintID) if serr != nil { return apperrorsx.Write(c, apperrorsx.New(apperrorsx.ErrActionSignoffUnsignFailed).WithCause(serr)) } if row == nil { return response.Write(c, fiber.StatusOK, unsignedActionSignoffPlaceholder(helicopterID)) } return response.Write(c, fiber.StatusOK, h.resource(c.UserContext(), row)) } if h.complaintSvc == nil { return apperrorsx.Write(c, apperrorsx.New(apperrorsx.ErrActionSignoffSignFailed)) } comp, cerr := h.complaintSvc.GetByID(c.UserContext(), complaintID) if cerr != nil { return apperrorsx.Write(c, apperrorsx.New(apperrorsx.ErrActionSignoffSignFailed).WithCause(cerr)) } if comp == nil { return apperrorsx.Write(c, apperrorsx.New(apperrorsx.ErrActionSignoffComplaintNotFound)) } // The complaint's corrective action must be recorded before it can be signed off. if comp.ActionTaken == nil || strings.TrimSpace(*comp.ActionTaken) == "" { appErr := apperrorsx.New(apperrorsx.ErrActionSignoffActionRequired) appErr.Source = &jsonapi.ErrorSource{Pointer: "/data/attributes/complaint_id"} return apperrorsx.Write(c, appErr) } flightID, ferr := actionSignoffBodyFlightID(req) if ferr != nil { return apperrorsx.Write(c, ferr) } row, serr := h.svc.SignForComplaint(c.UserContext(), complaintID, flightID, comp.HelicopterID, actor) if serr != nil { return apperrorsx.Write(c, apperrorsx.New(apperrorsx.ErrActionSignoffSignFailed).WithCause(serr)) } return response.Write(c, fiber.StatusOK, h.resource(c.UserContext(), row)) } // Complaint-independent (fleet) toggle — scoped to a flight. flightID, ferr := actionSignoffBodyFlightID(req) if ferr != nil { return apperrorsx.Write(c, ferr) } if !signWanted { row, serr := h.svc.Unsign(c.UserContext(), flightID, actor) if errors.Is(serr, actionsignoff.ErrNotFound) { return response.Write(c, fiber.StatusOK, unsignedActionSignoffPlaceholder(helicopterID)) } if serr != nil { return apperrorsx.Write(c, apperrorsx.New(apperrorsx.ErrActionSignoffUnsignFailed).WithCause(serr)) } return response.Write(c, fiber.StatusOK, h.resource(c.UserContext(), row)) } // Signing a fleet sign-off — this is what allows an EASA release for the flight. row, serr := h.svc.Sign(c.UserContext(), flightID, helicopterID, actor) if serr != nil { return apperrorsx.Write(c, apperrorsx.New(apperrorsx.ErrActionSignoffSignFailed).WithCause(serr)) } return response.Write(c, fiber.StatusOK, h.resource(c.UserContext(), row)) } // unsignedActionSignoffPlaceholder renders an unsigned resource for a helicopter that // has no sign-off row (e.g. unsigning something that was never signed). func unsignedActionSignoffPlaceholder(helicopterID []byte) responsedto.ActionSignoffResource { return responsedto.ActionSignoffResource{ Type: "action_signoff", Attributes: responsedto.ActionSignoffAttributes{ HelicopterID: idString(helicopterID), Signed: false, }, } }