init push

This commit is contained in:
2026-07-16 22:16:45 +07:00
commit 8b068bdb10
1021 changed files with 332816 additions and 0 deletions

View 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,
},
}
}