init push
This commit is contained in:
256
internal/transport/http/handlers/action_signoff_handler.go
Normal file
256
internal/transport/http/handlers/action_signoff_handler.go
Normal file
@@ -0,0 +1,256 @@
|
||||
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,
|
||||
},
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user