257 lines
10 KiB
Go
257 lines
10 KiB
Go
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,
|
|
},
|
|
}
|
|
}
|