2071 lines
72 KiB
Go
2071 lines
72 KiB
Go
package handlers
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"math"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/gofiber/fiber/v2"
|
|
|
|
"wucher/internal/domain/base"
|
|
filemanager "wucher/internal/domain/file_manager"
|
|
appservice "wucher/internal/service"
|
|
"wucher/internal/shared/pkg/apperrors"
|
|
"wucher/internal/shared/pkg/apperrorsx"
|
|
"wucher/internal/shared/pkg/attachmentresolver"
|
|
"wucher/internal/shared/pkg/userctx"
|
|
"wucher/internal/shared/pkg/uuidv7"
|
|
"wucher/internal/transport/http/dto"
|
|
"wucher/internal/transport/http/jsonapi"
|
|
"wucher/internal/transport/http/response"
|
|
"wucher/internal/transport/http/validators"
|
|
)
|
|
|
|
const (
|
|
basePhotoAttachmentRefType = "base_photo"
|
|
basePhotoAttachmentCategory = "photo"
|
|
)
|
|
|
|
type basePresignStorage interface {
|
|
PresignGetObject(ctx context.Context, objectKey string, ttl time.Duration) (string, error)
|
|
}
|
|
|
|
type BaseHandler struct {
|
|
svc base.Service
|
|
validate *validators.Validator
|
|
fileManager filemanager.Service
|
|
storage basePresignStorage
|
|
}
|
|
|
|
type baseDetailedWriter interface {
|
|
CreateDetailed(context.Context, appservice.BaseCreateInput) (*appservice.BaseWriteResult, error)
|
|
UpdateDetailed(context.Context, appservice.BaseUpdateInput) (*appservice.BaseWriteResult, error)
|
|
}
|
|
|
|
func NewBaseHandler(svc base.Service) *BaseHandler {
|
|
return &BaseHandler{svc: svc, validate: validators.New()}
|
|
}
|
|
|
|
func (h *BaseHandler) WithFileManagerService(fileManager filemanager.Service) *BaseHandler {
|
|
h.fileManager = fileManager
|
|
return h
|
|
}
|
|
|
|
func (h *BaseHandler) WithFileStorage(storage basePresignStorage) *BaseHandler {
|
|
h.storage = storage
|
|
return h
|
|
}
|
|
|
|
func (h *BaseHandler) WithFileResolver(fileManager filemanager.Service, storage basePresignStorage) *BaseHandler {
|
|
h.fileManager = fileManager
|
|
h.storage = storage
|
|
return h
|
|
}
|
|
|
|
func (h *BaseHandler) createBaseWithService(c *fiber.Ctx, svc baseDetailedWriter) bool {
|
|
var req dto.BaseCreateRequest
|
|
if err := c.BodyParser(&req); err != nil {
|
|
_ = writeBaseErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
|
|
Status: "400",
|
|
Title: "Invalid JSON",
|
|
Detail: safeInternalDetail(),
|
|
}})
|
|
return true
|
|
}
|
|
|
|
categoryType, errObj := resolveBaseCategoryType(c, req.Data.Attributes.BaseCategory, "/data/attributes/base_category")
|
|
if errObj != nil {
|
|
_ = writeBaseErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{*errObj})
|
|
return true
|
|
}
|
|
if errObj := validateBaseCoordinates(req.Data.Attributes.Latitude, req.Data.Attributes.Longitude, "/data/attributes/latitude", "/data/attributes/longitude"); errObj != nil {
|
|
_ = writeBaseErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{*errObj})
|
|
return true
|
|
}
|
|
baseName := strings.TrimSpace(req.Data.Attributes.BaseName)
|
|
if baseName == "" {
|
|
_ = writeBaseErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{
|
|
Status: "422",
|
|
Title: "Validation error",
|
|
Detail: "base is required",
|
|
Source: &jsonapi.ErrorSource{Pointer: "/data/attributes/base"},
|
|
}})
|
|
return true
|
|
}
|
|
createdBy, errObj := parseOptionalUUID(req.Data.Attributes.CreatedBy, "/data/attributes/created_by")
|
|
if errObj != nil {
|
|
_ = writeBaseErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{*errObj})
|
|
return true
|
|
}
|
|
updatedBy, errObj := parseOptionalUUID(req.Data.Attributes.UpdatedBy, "/data/attributes/updated_by")
|
|
if errObj != nil {
|
|
_ = writeBaseErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{*errObj})
|
|
return true
|
|
}
|
|
defaultStartTimeType, errObj := normalizeBaseShiftTimeType(stringOrDefault(req.Data.Attributes.DefaultStartTimeType, ""), "/data/attributes/default_start_time_type")
|
|
if errObj != nil {
|
|
_ = writeBaseErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{*errObj})
|
|
return true
|
|
}
|
|
defaultEndTimeType, errObj := normalizeBaseShiftTimeType(stringOrDefault(req.Data.Attributes.DefaultEndTimeType, ""), "/data/attributes/default_end_time_type")
|
|
if errObj != nil {
|
|
_ = writeBaseErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{*errObj})
|
|
return true
|
|
}
|
|
defaultShiftStart, defaultShiftEnd, errObj := parseBaseDefaultShiftRange(
|
|
req.Data.Attributes.DefaultShiftStart,
|
|
req.Data.Attributes.DefaultShiftEnd,
|
|
req.Data.Attributes.DefaultShiftTime,
|
|
defaultStartTimeType,
|
|
defaultEndTimeType,
|
|
true,
|
|
"/data/attributes/default_shift_start",
|
|
"/data/attributes/default_shift_end",
|
|
"/data/attributes/default_shift_time",
|
|
)
|
|
if errObj != nil {
|
|
_ = writeBaseErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{*errObj})
|
|
return true
|
|
}
|
|
operationalShiftTimes, errObj := parseBaseOperationalShiftTimes(req.Data.Attributes.OperationalShiftTimes, "/data/attributes/operational_shift_times")
|
|
if errObj != nil {
|
|
_ = writeBaseErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{*errObj})
|
|
return true
|
|
}
|
|
hemsEDCIDs, errObj := parseUUIDList(req.Data.Attributes.HEMSEDCContactIDs, "/data/attributes/hems_edc_contact_ids")
|
|
if errObj != nil {
|
|
_ = writeBaseErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{*errObj})
|
|
return true
|
|
}
|
|
medPaxIDs, errObj := parseUUIDList(req.Data.Attributes.MedPaxContactIDs, "/data/attributes/med_pax_contact_ids")
|
|
if errObj != nil {
|
|
_ = writeBaseErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{*errObj})
|
|
return true
|
|
}
|
|
pilotIDs, errObj := parseUUIDList(req.Data.Attributes.ResponsiblePilotContactIDs, "/data/attributes/responsible_pilot_contact_ids")
|
|
if errObj != nil {
|
|
_ = writeBaseErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{*errObj})
|
|
return true
|
|
}
|
|
|
|
modelID := uuidv7.MustBytes()
|
|
fotoAttachmentID := []byte(nil)
|
|
if req.Data.Attributes.FileUUID != nil {
|
|
resolved, handled := h.resolveBaseFotoAttachmentID(c, modelID, req.Data.Attributes.FileUUID)
|
|
if handled {
|
|
return true
|
|
}
|
|
fotoAttachmentID = resolved
|
|
}
|
|
res, err := svc.CreateDetailed(c.UserContext(), appservice.BaseCreateInput{
|
|
ID: modelID,
|
|
CategoryType: categoryType,
|
|
BaseName: baseName,
|
|
BaseAbbreviation: stringOrDefault(req.Data.Attributes.BaseAbbreviation, ""),
|
|
Address: stringOrDefault(req.Data.Attributes.Address, ""),
|
|
Latitude: float64OrDefault(req.Data.Attributes.Latitude, 0),
|
|
Longitude: float64OrDefault(req.Data.Attributes.Longitude, 0),
|
|
LandlineNumber: stringOrDefault(req.Data.Attributes.LandlineNumber, ""),
|
|
MobileNumber: stringOrDefault(req.Data.Attributes.MobileNumber, ""),
|
|
Email: stringOrDefault(req.Data.Attributes.Email, ""),
|
|
SortKey: req.Data.Attributes.SortKey,
|
|
SMSAlert: boolOrDefault(req.Data.Attributes.SMSAlert, false),
|
|
Checklist: boolOrDefault(req.Data.Attributes.Checklist, false),
|
|
LegTime: stringOrDefault(req.Data.Attributes.LegTime, ""),
|
|
DefaultStartTimeType: defaultStartTimeType,
|
|
DefaultEndTimeType: defaultEndTimeType,
|
|
DefaultShiftStart: defaultShiftStart,
|
|
DefaultShiftEnd: defaultShiftEnd,
|
|
UTC: stringOrDefault(req.Data.Attributes.UTC, ""),
|
|
Dry: boolOrDefault(req.Data.Attributes.Dry, false),
|
|
ControlCenter: boolOrDefault(req.Data.Attributes.ControlCenter, false),
|
|
DUL: boolOrDefault(req.Data.Attributes.DUL, false),
|
|
Notes: stringOrDefault(req.Data.Attributes.Notes, ""),
|
|
HEMSEDCContactIDs: hemsEDCIDs,
|
|
MedPaxContactIDs: medPaxIDs,
|
|
ResponsiblePilotContactIDs: pilotIDs,
|
|
OperationalShiftTimes: operationalShiftTimes,
|
|
IsActive: boolOrDefault(req.Data.Attributes.IsActive, true),
|
|
CreatedBy: createdBy,
|
|
UpdatedBy: updatedBy,
|
|
FotoAttachmentID: fotoAttachmentID,
|
|
})
|
|
if err != nil {
|
|
var writeErr *appservice.BaseWriteError
|
|
if errors.As(err, &writeErr) {
|
|
errObj := jsonapi.ErrorObject{
|
|
Status: strconv.Itoa(writeErr.Status),
|
|
Title: writeErr.Title,
|
|
Detail: writeErr.Detail,
|
|
}
|
|
if strings.TrimSpace(writeErr.Pointer) != "" {
|
|
errObj.Source = &jsonapi.ErrorSource{Pointer: writeErr.Pointer}
|
|
}
|
|
_ = writeBaseErrors(c, writeErr.Status, []jsonapi.ErrorObject{{
|
|
Status: errObj.Status, Title: errObj.Title, Detail: errObj.Detail, Source: errObj.Source,
|
|
}})
|
|
return true
|
|
}
|
|
_ = writeBaseErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
|
|
Status: "400",
|
|
Title: "Create failed",
|
|
Detail: safeInternalDetail(),
|
|
}})
|
|
return true
|
|
}
|
|
if len(res.PreviousFotoAttachmentID) == 16 {
|
|
_ = syncAttachmentLifecycleSwap(c.UserContext(), h.fileManager, res.PreviousFotoAttachmentID, res.Row.FotoAttachmentID, actorUserID(c))
|
|
}
|
|
_ = response.Write(c, fiber.StatusCreated, h.baseResourceByCategory(c.UserContext(), res.Row, res.CategoryType))
|
|
return true
|
|
}
|
|
|
|
func (h *BaseHandler) updateBaseWithService(c *fiber.Ctx, svc baseDetailedWriter) bool {
|
|
uuidStr := c.Params("uuid")
|
|
id, err := uuidv7.ParseString(uuidStr)
|
|
if err != nil {
|
|
_ = writeBaseErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{
|
|
Status: "422",
|
|
Title: "Validation error",
|
|
Detail: "uuid is invalid UUID",
|
|
Source: &jsonapi.ErrorSource{Pointer: "/path/uuid"},
|
|
}})
|
|
return true
|
|
}
|
|
|
|
var req dto.BaseUpdateRequest
|
|
if err := c.BodyParser(&req); err != nil {
|
|
_ = writeBaseErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
|
|
Status: "400",
|
|
Title: "Invalid JSON",
|
|
Detail: safeInternalDetail(),
|
|
}})
|
|
return true
|
|
}
|
|
|
|
attrs := req.Data.Attributes
|
|
categoryType, errObj := resolveBaseCategoryType(c, attrs.BaseCategory, "/data/attributes/base_category")
|
|
if errObj != nil {
|
|
_ = writeBaseErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{*errObj})
|
|
return true
|
|
}
|
|
if errObj := validateOptionalBaseCoordinates(attrs.Latitude, attrs.Longitude, "/data/attributes/latitude", "/data/attributes/longitude"); errObj != nil {
|
|
_ = writeBaseErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{*errObj})
|
|
return true
|
|
}
|
|
if attrs.BaseName == nil && attrs.BaseAbbreviation == nil && attrs.Address == nil && attrs.Latitude == nil && attrs.Longitude == nil && attrs.LandlineNumber == nil &&
|
|
attrs.MobileNumber == nil && attrs.Email == nil && attrs.SMSAlert == nil && attrs.Checklist == nil &&
|
|
!attrs.SortKey.Set &&
|
|
attrs.LegTime == nil && attrs.DefaultStartTimeType == nil && attrs.DefaultEndTimeType == nil &&
|
|
attrs.DefaultShiftStart == nil && attrs.DefaultShiftEnd == nil && attrs.DefaultShiftTime == nil && attrs.ShiftTime == nil &&
|
|
attrs.UTC == nil && attrs.FileUUID == nil && attrs.Dry == nil &&
|
|
attrs.ControlCenter == nil && attrs.DUL == nil && attrs.Notes == nil && attrs.IsActive == nil &&
|
|
attrs.HEMSEDCContactIDs == nil && attrs.MedPaxContactIDs == nil &&
|
|
attrs.ResponsiblePilotContactIDs == nil &&
|
|
attrs.OperationalShiftTimes == nil &&
|
|
attrs.CreatedBy == nil && attrs.UpdatedBy == nil {
|
|
_ = writeBaseErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{
|
|
Status: "422",
|
|
Title: "Validation error",
|
|
Detail: "at least one attribute must be provided",
|
|
Source: &jsonapi.ErrorSource{Pointer: "/data/attributes"},
|
|
}})
|
|
return true
|
|
}
|
|
|
|
var defaultShiftStart *time.Time
|
|
var defaultShiftEnd *time.Time
|
|
var defaultStartTimeType *string
|
|
var defaultEndTimeType *string
|
|
if attrs.DefaultStartTimeType != nil || attrs.DefaultEndTimeType != nil || attrs.DefaultShiftStart != nil || attrs.DefaultShiftEnd != nil || attrs.DefaultShiftTime != nil {
|
|
startType, errObj := normalizeBaseShiftTimeType(stringOrDefault(attrs.DefaultStartTimeType, ""), "/data/attributes/default_start_time_type")
|
|
if errObj != nil {
|
|
_ = writeBaseErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{*errObj})
|
|
return true
|
|
}
|
|
endType, errObj := normalizeBaseShiftTimeType(stringOrDefault(attrs.DefaultEndTimeType, ""), "/data/attributes/default_end_time_type")
|
|
if errObj != nil {
|
|
_ = writeBaseErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{*errObj})
|
|
return true
|
|
}
|
|
parsedStart, parsedEnd, errObj := parseBaseDefaultShiftRange(
|
|
attrs.DefaultShiftStart,
|
|
attrs.DefaultShiftEnd,
|
|
attrs.DefaultShiftTime,
|
|
startType,
|
|
endType,
|
|
false,
|
|
"/data/attributes/default_shift_start",
|
|
"/data/attributes/default_shift_end",
|
|
"/data/attributes/default_shift_time",
|
|
)
|
|
if errObj != nil {
|
|
_ = writeBaseErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{*errObj})
|
|
return true
|
|
}
|
|
if attrs.DefaultShiftStart != nil || attrs.DefaultShiftTime != nil {
|
|
defaultShiftStart = parsedStart
|
|
}
|
|
if attrs.DefaultShiftEnd != nil || attrs.DefaultShiftTime != nil {
|
|
defaultShiftEnd = parsedEnd
|
|
}
|
|
if attrs.DefaultStartTimeType != nil {
|
|
defaultStartTimeType = &startType
|
|
}
|
|
if attrs.DefaultEndTimeType != nil {
|
|
defaultEndTimeType = &endType
|
|
}
|
|
}
|
|
if attrs.ShiftTime != nil && (strings.TrimSpace(attrs.ShiftTime.Start) != "" || strings.TrimSpace(attrs.ShiftTime.End) != "") {
|
|
if strings.TrimSpace(attrs.ShiftTime.Start) != "" {
|
|
start, errObj := parseOptionalClock(&attrs.ShiftTime.Start, "/data/attributes/shift_time/start")
|
|
if errObj != nil {
|
|
_ = writeBaseErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{*errObj})
|
|
return true
|
|
}
|
|
defaultShiftStart = start
|
|
}
|
|
if strings.TrimSpace(attrs.ShiftTime.End) != "" {
|
|
end, errObj := parseOptionalClock(&attrs.ShiftTime.End, "/data/attributes/shift_time/end")
|
|
if errObj != nil {
|
|
_ = writeBaseErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{*errObj})
|
|
return true
|
|
}
|
|
defaultShiftEnd = end
|
|
}
|
|
}
|
|
operationalShiftTimesSet := attrs.OperationalShiftTimes != nil
|
|
operationalShiftTimes, errObj := parseBaseOperationalShiftTimes(attrs.OperationalShiftTimes, "/data/attributes/operational_shift_times")
|
|
if errObj != nil {
|
|
_ = writeBaseErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{*errObj})
|
|
return true
|
|
}
|
|
|
|
var fotoAttachmentID []byte
|
|
fotoAttachmentSet := false
|
|
if attrs.FileUUID != nil {
|
|
fotoAttachmentSet = true
|
|
if hasBlankOptionalString(attrs.FileUUID) {
|
|
fotoAttachmentID = nil
|
|
} else {
|
|
resolved, handled := h.resolveExistingBaseFotoAttachmentOrFileID(c, id, attrs.FileUUID)
|
|
if handled {
|
|
return true
|
|
}
|
|
fotoAttachmentID = resolved
|
|
}
|
|
}
|
|
|
|
hemsEDCSet := attrs.HEMSEDCContactIDs != nil
|
|
medPaxSet := attrs.MedPaxContactIDs != nil
|
|
pilotSet := attrs.ResponsiblePilotContactIDs != nil
|
|
var hemsEDCIDs [][]byte
|
|
var medPaxIDs [][]byte
|
|
var pilotIDs [][]byte
|
|
if hemsEDCSet {
|
|
var errObj *jsonapi.ErrorObject
|
|
hemsEDCIDs, errObj = parseUUIDList(*attrs.HEMSEDCContactIDs, "/data/attributes/hems_edc_contact_ids")
|
|
if errObj != nil {
|
|
_ = writeBaseErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{*errObj})
|
|
return true
|
|
}
|
|
}
|
|
if medPaxSet {
|
|
var errObj *jsonapi.ErrorObject
|
|
medPaxIDs, errObj = parseUUIDList(*attrs.MedPaxContactIDs, "/data/attributes/med_pax_contact_ids")
|
|
if errObj != nil {
|
|
_ = writeBaseErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{*errObj})
|
|
return true
|
|
}
|
|
}
|
|
if pilotSet {
|
|
var errObj *jsonapi.ErrorObject
|
|
pilotIDs, errObj = parseUUIDList(*attrs.ResponsiblePilotContactIDs, "/data/attributes/responsible_pilot_contact_ids")
|
|
if errObj != nil {
|
|
_ = writeBaseErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{*errObj})
|
|
return true
|
|
}
|
|
}
|
|
|
|
var sortKeyValue *int
|
|
sortKeyValid := false
|
|
if attrs.SortKey.Set {
|
|
sortKeyVal := attrs.SortKey.Value
|
|
sortKeyValue = &sortKeyVal
|
|
sortKeyValid = attrs.SortKey.Valid
|
|
}
|
|
|
|
createdBySet := attrs.CreatedBy != nil
|
|
updatedBySet := attrs.UpdatedBy != nil
|
|
var createdBy []byte
|
|
var updatedBy []byte
|
|
if createdBySet {
|
|
parsed, errObj := parseUUIDOrClear(*attrs.CreatedBy, "/data/attributes/created_by")
|
|
if errObj != nil {
|
|
_ = writeBaseErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{*errObj})
|
|
return true
|
|
}
|
|
createdBy = parsed
|
|
}
|
|
if updatedBySet {
|
|
parsed, errObj := parseUUIDOrClear(*attrs.UpdatedBy, "/data/attributes/updated_by")
|
|
if errObj != nil {
|
|
_ = writeBaseErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{*errObj})
|
|
return true
|
|
}
|
|
updatedBy = parsed
|
|
}
|
|
|
|
res, err := svc.UpdateDetailed(c.UserContext(), appservice.BaseUpdateInput{
|
|
ID: id,
|
|
CategoryType: categoryType,
|
|
BaseName: attrs.BaseName,
|
|
BaseAbbreviation: attrs.BaseAbbreviation,
|
|
Address: attrs.Address,
|
|
Latitude: attrs.Latitude,
|
|
Longitude: attrs.Longitude,
|
|
LandlineNumber: attrs.LandlineNumber,
|
|
MobileNumber: attrs.MobileNumber,
|
|
Email: attrs.Email,
|
|
SortKeySet: attrs.SortKey.Set,
|
|
SortKeyValid: sortKeyValid,
|
|
SortKeyValue: sortKeyValue,
|
|
SMSAlert: attrs.SMSAlert,
|
|
Checklist: attrs.Checklist,
|
|
LegTime: attrs.LegTime,
|
|
DefaultStartTimeType: defaultStartTimeType,
|
|
DefaultEndTimeType: defaultEndTimeType,
|
|
DefaultShiftStart: defaultShiftStart,
|
|
DefaultShiftEnd: defaultShiftEnd,
|
|
UTC: attrs.UTC,
|
|
Dry: attrs.Dry,
|
|
ControlCenter: attrs.ControlCenter,
|
|
DUL: attrs.DUL,
|
|
Notes: attrs.Notes,
|
|
HEMSEDCContactIDs: hemsEDCIDs,
|
|
HEMSEDCContactIDsSet: hemsEDCSet,
|
|
MedPaxContactIDs: medPaxIDs,
|
|
MedPaxContactIDsSet: medPaxSet,
|
|
ResponsiblePilotContactIDs: pilotIDs,
|
|
ResponsiblePilotContactIDsSet: pilotSet,
|
|
OperationalShiftTimes: operationalShiftTimes,
|
|
OperationalShiftTimesSet: operationalShiftTimesSet,
|
|
IsActive: attrs.IsActive,
|
|
CreatedBy: createdBy,
|
|
CreatedBySet: createdBySet,
|
|
UpdatedBy: updatedBy,
|
|
UpdatedBySet: updatedBySet,
|
|
FotoAttachmentID: fotoAttachmentID,
|
|
FotoAttachmentSet: fotoAttachmentSet,
|
|
})
|
|
if err != nil {
|
|
var writeErr *appservice.BaseWriteError
|
|
if errors.As(err, &writeErr) {
|
|
errObj := jsonapi.ErrorObject{
|
|
Status: strconv.Itoa(writeErr.Status),
|
|
Title: writeErr.Title,
|
|
Detail: writeErr.Detail,
|
|
}
|
|
if strings.TrimSpace(writeErr.Pointer) != "" {
|
|
errObj.Source = &jsonapi.ErrorSource{Pointer: writeErr.Pointer}
|
|
}
|
|
_ = writeBaseErrors(c, writeErr.Status, []jsonapi.ErrorObject{errObj})
|
|
return true
|
|
}
|
|
_ = writeBaseErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
|
|
Status: "400",
|
|
Title: "Update failed",
|
|
Detail: safeInternalDetail(),
|
|
}})
|
|
return true
|
|
}
|
|
if err := syncAttachmentLifecycleSwap(c.UserContext(), h.fileManager, res.PreviousFotoAttachmentID, res.Row.FotoAttachmentID, actorUserID(c)); err != nil {
|
|
_ = writeBaseErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
|
|
Status: "400",
|
|
Title: "Update failed",
|
|
Detail: safeInternalDetail(),
|
|
}})
|
|
return true
|
|
}
|
|
_ = response.Write(c, fiber.StatusOK, h.baseResourceByCategory(c.UserContext(), res.Row, res.CategoryType))
|
|
return true
|
|
}
|
|
|
|
// CreateBase godoc
|
|
// @Summary Create base
|
|
// @Tags Bases
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Description Example request:
|
|
// @Description {"data":{"type":"base","attributes":{"base":"Lude Testing 2","base_abbreviation":"LOIG","base_category":"regular","address":"Lude Airfield","latitude":47.3769,"longitude":8.5417,"sortkey":1,"sms_alert":false,"checklist":false,"leg_time":"00:20","default_start_time_type":"FIXED","default_end_time_type":"FIXED","default_shift_start":"06:00","default_shift_end":"21:00","utc":"0","notes":"Main base","operational_shift_times":[{"date_start":"2026-06-15","date_end":"2026-06-15","start_time_type":"BMCT","end_time_type":"ECET"}]}}}
|
|
// @Param request body dto.BaseCreateRequest true "JSON:API base create request"
|
|
// @Success 201 {object} dto.BaseResponse
|
|
// @Failure 422 {object} dto.ErrorResponse
|
|
// @Failure 400 {object} dto.ErrorResponse
|
|
// @Router /api/v1/bases/create [post]
|
|
func (h *BaseHandler) CreateBase(c *fiber.Ctx) error {
|
|
svc, ok := h.svc.(baseDetailedWriter)
|
|
if !ok {
|
|
return writeBaseErrors(c, fiber.StatusInternalServerError, []jsonapi.ErrorObject{{
|
|
Status: "500",
|
|
Title: "Create failed",
|
|
Detail: "base service is not configured",
|
|
}})
|
|
}
|
|
if h.createBaseWithService(c, svc) {
|
|
return nil
|
|
}
|
|
|
|
var req dto.BaseCreateRequest
|
|
if err := c.BodyParser(&req); err != nil {
|
|
return writeBaseErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
|
|
Status: "400",
|
|
Title: "Invalid JSON",
|
|
Detail: safeInternalDetail(),
|
|
}})
|
|
}
|
|
categoryType, errObj := resolveBaseCategoryType(c, req.Data.Attributes.BaseCategory, "/data/attributes/base_category")
|
|
if errObj != nil {
|
|
return writeBaseErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{*errObj})
|
|
}
|
|
if errObj := validateBaseCoordinates(req.Data.Attributes.Latitude, req.Data.Attributes.Longitude, "/data/attributes/latitude", "/data/attributes/longitude"); errObj != nil {
|
|
return writeBaseErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{*errObj})
|
|
}
|
|
|
|
baseName := strings.TrimSpace(req.Data.Attributes.BaseName)
|
|
if baseName == "" {
|
|
return writeBaseErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{
|
|
Status: "422",
|
|
Title: "Validation error",
|
|
Detail: "base is required",
|
|
Source: &jsonapi.ErrorSource{Pointer: "/data/attributes/base"},
|
|
}})
|
|
}
|
|
|
|
createdBy, errObj := parseOptionalUUID(req.Data.Attributes.CreatedBy, "/data/attributes/created_by")
|
|
if errObj != nil {
|
|
return writeBaseErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{*errObj})
|
|
}
|
|
updatedBy, errObj := parseOptionalUUID(req.Data.Attributes.UpdatedBy, "/data/attributes/updated_by")
|
|
if errObj != nil {
|
|
return writeBaseErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{*errObj})
|
|
}
|
|
defaultStartTimeType, errObj := normalizeBaseShiftTimeType(stringOrDefault(req.Data.Attributes.DefaultStartTimeType, ""), "/data/attributes/default_start_time_type")
|
|
if errObj != nil {
|
|
return writeBaseErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{*errObj})
|
|
}
|
|
defaultEndTimeType, errObj := normalizeBaseShiftTimeType(stringOrDefault(req.Data.Attributes.DefaultEndTimeType, ""), "/data/attributes/default_end_time_type")
|
|
if errObj != nil {
|
|
return writeBaseErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{*errObj})
|
|
}
|
|
defaultShiftStart, defaultShiftEnd, errObj := parseBaseDefaultShiftRange(
|
|
req.Data.Attributes.DefaultShiftStart,
|
|
req.Data.Attributes.DefaultShiftEnd,
|
|
req.Data.Attributes.DefaultShiftTime,
|
|
defaultStartTimeType,
|
|
defaultEndTimeType,
|
|
true,
|
|
"/data/attributes/default_shift_start",
|
|
"/data/attributes/default_shift_end",
|
|
"/data/attributes/default_shift_time",
|
|
)
|
|
if errObj != nil {
|
|
return writeBaseErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{*errObj})
|
|
}
|
|
|
|
row := &base.Base{ID: uuidv7.MustBytes()}
|
|
*row = *dto.MapBaseCreateAttributesToDomain(req.Data.Attributes)
|
|
row.ID = uuidv7.MustBytes()
|
|
row.BaseName = baseName
|
|
row.DefaultStartTimeType = defaultStartTimeType
|
|
row.DefaultEndTimeType = defaultEndTimeType
|
|
row.DefaultShiftStart = clockStringOrEmpty(defaultShiftStart)
|
|
row.DefaultShiftEnd = clockStringOrEmpty(defaultShiftEnd)
|
|
if req.Data.Attributes.FileUUID != nil {
|
|
fotoAttachmentID, resolveErr := h.resolveBaseFotoAttachmentID(c, row.ID, req.Data.Attributes.FileUUID)
|
|
if resolveErr {
|
|
return nil
|
|
}
|
|
row.FotoAttachmentID = fotoAttachmentID
|
|
}
|
|
hemsEDCIDs, errObj := parseUUIDList(req.Data.Attributes.HEMSEDCContactIDs, "/data/attributes/hems_edc_contact_ids")
|
|
if errObj != nil {
|
|
return writeBaseErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{*errObj})
|
|
}
|
|
medPaxIDs, errObj := parseUUIDList(req.Data.Attributes.MedPaxContactIDs, "/data/attributes/med_pax_contact_ids")
|
|
if errObj != nil {
|
|
return writeBaseErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{*errObj})
|
|
}
|
|
pilotIDs, errObj := parseUUIDList(req.Data.Attributes.ResponsiblePilotContactIDs, "/data/attributes/responsible_pilot_contact_ids")
|
|
if errObj != nil {
|
|
return writeBaseErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{*errObj})
|
|
}
|
|
row.HEMSEDCContactIDs = hemsEDCIDs
|
|
row.MedPaxContactIDs = medPaxIDs
|
|
row.ResponsiblePilotContactIDs = pilotIDs
|
|
row.CreatedBy = createdBy
|
|
row.UpdatedBy = updatedBy
|
|
|
|
if err := h.svc.CreateBase(c.UserContext(), row, categoryType); err != nil {
|
|
return writeBaseErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
|
|
Status: "400",
|
|
Title: "Create failed",
|
|
Detail: safeInternalDetail(),
|
|
}})
|
|
}
|
|
if len(row.FotoAttachmentID) == 16 {
|
|
if err := syncAttachmentLifecycleSwap(c.UserContext(), h.fileManager, nil, row.FotoAttachmentID, actorUserID(c)); err != nil {
|
|
return writeBaseErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
|
|
Status: "400",
|
|
Title: "Create failed",
|
|
Detail: safeInternalDetail(),
|
|
}})
|
|
}
|
|
}
|
|
|
|
created, err := h.svc.GetBaseByID(c.UserContext(), row.ID, categoryType)
|
|
if err == nil && created != nil {
|
|
return response.Write(c, fiber.StatusCreated, h.baseResourceByCategory(c.UserContext(), created, categoryType))
|
|
}
|
|
return response.Write(c, fiber.StatusCreated, h.baseResourceByCategory(c.UserContext(), row, categoryType))
|
|
}
|
|
|
|
// UpdateBase godoc
|
|
// @Summary Update base
|
|
// @Tags Bases
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param uuid path string true "Base UUID (UUIDv7)"
|
|
// @Description Example request:
|
|
// @Description {"data":{"type":"hems_base","id":"<uuid>","attributes":{"base":"Lude Testing 2","base_abbreviation":"LOIG","base_category":"regular","address":"Lude Airfield","latitude":47.3769,"longitude":8.5417,"sortkey":1,"sms_alert":false,"checklist":false,"leg_time":"00:20","default_start_time_type":"FIXED","default_end_time_type":"FIXED","default_shift_start":"06:00","default_shift_end":"21:00","utc":"0","notes":"Main base","operational_shift_times":[{"date_start":"2026-06-15","date_end":"2026-06-15","start_time_type":"BMCT","end_time_type":"ECET"}]}}}
|
|
// @Param request body dto.BaseUpdateRequest true "JSON:API base update request"
|
|
// @Success 200 {object} dto.BaseResponse
|
|
// @Failure 422 {object} dto.ErrorResponse
|
|
// @Failure 404 {object} dto.ErrorResponse
|
|
// @Router /api/v1/bases/update/{uuid} [patch]
|
|
func (h *BaseHandler) UpdateBase(c *fiber.Ctx) error {
|
|
svc, ok := h.svc.(baseDetailedWriter)
|
|
if !ok {
|
|
return writeBaseErrors(c, fiber.StatusInternalServerError, []jsonapi.ErrorObject{{
|
|
Status: "500",
|
|
Title: "Update failed",
|
|
Detail: "base service is not configured",
|
|
}})
|
|
}
|
|
if h.updateBaseWithService(c, svc) {
|
|
return nil
|
|
}
|
|
|
|
uuidStr := c.Params("uuid")
|
|
id, err := uuidv7.ParseString(uuidStr)
|
|
if err != nil {
|
|
return writeBaseErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{
|
|
Status: "422",
|
|
Title: "Validation error",
|
|
Detail: "uuid is invalid UUID",
|
|
Source: &jsonapi.ErrorSource{Pointer: "/path/uuid"},
|
|
}})
|
|
}
|
|
|
|
var req dto.BaseUpdateRequest
|
|
rawBody := append([]byte(nil), c.Body()...)
|
|
if err := c.BodyParser(&req); err != nil {
|
|
return writeBaseErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
|
|
Status: "400",
|
|
Title: "Invalid JSON",
|
|
Detail: safeInternalDetail(),
|
|
}})
|
|
}
|
|
|
|
attrs := req.Data.Attributes
|
|
if attrs.ShiftTime == nil {
|
|
attrs.ShiftTime = extractBaseShiftTimeFromUpdateBody(rawBody)
|
|
}
|
|
categoryType, errObj := resolveBaseCategoryType(c, attrs.BaseCategory, "/data/attributes/base_category")
|
|
if errObj != nil {
|
|
return writeBaseErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{*errObj})
|
|
}
|
|
if errObj := validateOptionalBaseCoordinates(attrs.Latitude, attrs.Longitude, "/data/attributes/latitude", "/data/attributes/longitude"); errObj != nil {
|
|
return writeBaseErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{*errObj})
|
|
}
|
|
|
|
if attrs.BaseName == nil && attrs.BaseAbbreviation == nil && attrs.Address == nil && attrs.LandlineNumber == nil &&
|
|
attrs.MobileNumber == nil && attrs.Email == nil && attrs.SMSAlert == nil && attrs.Checklist == nil &&
|
|
!attrs.SortKey.Set &&
|
|
attrs.LegTime == nil && attrs.DefaultStartTimeType == nil && attrs.DefaultEndTimeType == nil &&
|
|
attrs.DefaultShiftStart == nil && attrs.DefaultShiftEnd == nil && attrs.DefaultShiftTime == nil && attrs.ShiftTime == nil &&
|
|
attrs.UTC == nil && attrs.FileUUID == nil && attrs.Dry == nil &&
|
|
attrs.ControlCenter == nil && attrs.DUL == nil && attrs.Notes == nil && attrs.IsActive == nil &&
|
|
attrs.HEMSEDCContactIDs == nil && attrs.MedPaxContactIDs == nil &&
|
|
attrs.ResponsiblePilotContactIDs == nil &&
|
|
attrs.CreatedBy == nil && attrs.UpdatedBy == nil {
|
|
return writeBaseErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{
|
|
Status: "422",
|
|
Title: "Validation error",
|
|
Detail: "at least one attribute must be provided",
|
|
Source: &jsonapi.ErrorSource{Pointer: "/data/attributes"},
|
|
}})
|
|
}
|
|
|
|
existing, err := h.svc.GetBaseByID(c.UserContext(), id, categoryType)
|
|
if err != nil || existing == nil {
|
|
return writeBaseErrors(c, fiber.StatusNotFound, []jsonapi.ErrorObject{{
|
|
Status: "404",
|
|
Title: "Not found",
|
|
Detail: categoryNotFoundDetail(categoryType),
|
|
}})
|
|
}
|
|
prevFotoAttachmentID := append([]byte(nil), existing.FotoAttachmentID...)
|
|
|
|
if attrs.BaseName != nil {
|
|
v := strings.TrimSpace(*attrs.BaseName)
|
|
if v == "" {
|
|
return writeBaseErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{
|
|
Status: "422",
|
|
Title: "Validation error",
|
|
Detail: "base cannot be empty",
|
|
Source: &jsonapi.ErrorSource{Pointer: "/data/attributes/base"},
|
|
}})
|
|
}
|
|
existing.BaseName = v
|
|
}
|
|
dto.ApplyBaseUpdateAttributesToDomain(existing, attrs)
|
|
if attrs.DefaultStartTimeType != nil {
|
|
parsed, errObj := normalizeBaseShiftTimeType(*attrs.DefaultStartTimeType, "/data/attributes/default_start_time_type")
|
|
if errObj != nil {
|
|
return writeBaseErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{*errObj})
|
|
}
|
|
existing.DefaultStartTimeType = parsed
|
|
}
|
|
if attrs.DefaultEndTimeType != nil {
|
|
parsed, errObj := normalizeBaseShiftTimeType(*attrs.DefaultEndTimeType, "/data/attributes/default_end_time_type")
|
|
if errObj != nil {
|
|
return writeBaseErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{*errObj})
|
|
}
|
|
existing.DefaultEndTimeType = parsed
|
|
}
|
|
if attrs.DefaultShiftStart != nil || attrs.DefaultShiftEnd != nil || attrs.DefaultShiftTime != nil {
|
|
parsedStart, parsedEnd, errObj := parseBaseDefaultShiftRange(
|
|
attrs.DefaultShiftStart,
|
|
attrs.DefaultShiftEnd,
|
|
attrs.DefaultShiftTime,
|
|
normalizeBaseShiftTimeTypeOrFixed(existing.DefaultStartTimeType),
|
|
normalizeBaseShiftTimeTypeOrFixed(existing.DefaultEndTimeType),
|
|
false,
|
|
"/data/attributes/default_shift_start",
|
|
"/data/attributes/default_shift_end",
|
|
"/data/attributes/default_shift_time",
|
|
)
|
|
if errObj != nil {
|
|
return writeBaseErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{*errObj})
|
|
}
|
|
if attrs.DefaultShiftStart != nil || attrs.DefaultShiftTime != nil {
|
|
existing.DefaultShiftStart = clockStringOrEmpty(parsedStart)
|
|
}
|
|
if attrs.DefaultShiftEnd != nil || attrs.DefaultShiftTime != nil {
|
|
existing.DefaultShiftEnd = clockStringOrEmpty(parsedEnd)
|
|
}
|
|
}
|
|
if normalizeBaseShiftTimeTypeOrFixed(existing.DefaultStartTimeType) != base.ShiftTimeTypeFixed {
|
|
existing.DefaultShiftStart = ""
|
|
}
|
|
if normalizeBaseShiftTimeTypeOrFixed(existing.DefaultEndTimeType) != base.ShiftTimeTypeFixed {
|
|
existing.DefaultShiftEnd = ""
|
|
}
|
|
if attrs.FileUUID != nil {
|
|
if hasBlankOptionalString(attrs.FileUUID) {
|
|
existing.FotoAttachmentID = nil
|
|
} else {
|
|
fotoAttachmentID, resolveErr := h.resolveExistingBaseFotoAttachmentOrFileID(c, existing.ID, attrs.FileUUID)
|
|
if resolveErr {
|
|
return nil
|
|
}
|
|
existing.FotoAttachmentID = fotoAttachmentID
|
|
}
|
|
}
|
|
if attrs.HEMSEDCContactIDs != nil {
|
|
parsed, errObj := parseUUIDList(*attrs.HEMSEDCContactIDs, "/data/attributes/hems_edc_contact_ids")
|
|
if errObj != nil {
|
|
return writeBaseErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{*errObj})
|
|
}
|
|
existing.HEMSEDCContactIDs = parsed
|
|
}
|
|
if attrs.MedPaxContactIDs != nil {
|
|
parsed, errObj := parseUUIDList(*attrs.MedPaxContactIDs, "/data/attributes/med_pax_contact_ids")
|
|
if errObj != nil {
|
|
return writeBaseErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{*errObj})
|
|
}
|
|
existing.MedPaxContactIDs = parsed
|
|
}
|
|
if attrs.ResponsiblePilotContactIDs != nil {
|
|
parsed, errObj := parseUUIDList(*attrs.ResponsiblePilotContactIDs, "/data/attributes/responsible_pilot_contact_ids")
|
|
if errObj != nil {
|
|
return writeBaseErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{*errObj})
|
|
}
|
|
existing.ResponsiblePilotContactIDs = parsed
|
|
}
|
|
if attrs.CreatedBy != nil {
|
|
parsed, errObj := parseUUIDOrClear(*attrs.CreatedBy, "/data/attributes/created_by")
|
|
if errObj != nil {
|
|
return writeBaseErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{*errObj})
|
|
}
|
|
existing.CreatedBy = parsed
|
|
}
|
|
if attrs.UpdatedBy != nil {
|
|
parsed, errObj := parseUUIDOrClear(*attrs.UpdatedBy, "/data/attributes/updated_by")
|
|
if errObj != nil {
|
|
return writeBaseErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{*errObj})
|
|
}
|
|
existing.UpdatedBy = parsed
|
|
}
|
|
|
|
// Convert shift_time object to individual fields (handler responsibility for HTTP format translation)
|
|
if attrs.ShiftTime != nil && (attrs.ShiftTime.Start != "" || attrs.ShiftTime.End != "") {
|
|
if attrs.ShiftTime.Start != "" {
|
|
start, errObj := parseOptionalClock(&attrs.ShiftTime.Start, "/data/attributes/shift_time/start")
|
|
if errObj != nil {
|
|
return writeBaseErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{*errObj})
|
|
}
|
|
existing.DefaultShiftStart = clockStringOrEmpty(start)
|
|
}
|
|
if attrs.ShiftTime.End != "" {
|
|
end, errObj := parseOptionalClock(&attrs.ShiftTime.End, "/data/attributes/shift_time/end")
|
|
if errObj != nil {
|
|
return writeBaseErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{*errObj})
|
|
}
|
|
existing.DefaultShiftEnd = clockStringOrEmpty(end)
|
|
}
|
|
}
|
|
|
|
if err := h.svc.UpdateBase(c.UserContext(), existing, categoryType); err != nil {
|
|
return writeBaseErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
|
|
Status: "400",
|
|
Title: "Update failed",
|
|
Detail: safeInternalDetail(),
|
|
}})
|
|
}
|
|
if err := syncAttachmentLifecycleSwap(c.UserContext(), h.fileManager, prevFotoAttachmentID, existing.FotoAttachmentID, actorUserID(c)); err != nil {
|
|
return writeBaseErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
|
|
Status: "400",
|
|
Title: "Update failed",
|
|
Detail: safeInternalDetail(),
|
|
}})
|
|
}
|
|
|
|
updated, err := h.svc.GetBaseByID(c.UserContext(), existing.ID, categoryType)
|
|
if err == nil && updated != nil {
|
|
if attrs.ShiftTime != nil && (attrs.ShiftTime.Start != "" || attrs.ShiftTime.End != "") {
|
|
updated.DefaultShiftStart = existing.DefaultShiftStart
|
|
updated.DefaultShiftEnd = existing.DefaultShiftEnd
|
|
}
|
|
if attrs.OperationalShiftTimes != nil {
|
|
updated.OperationalShiftTimes = existing.OperationalShiftTimes
|
|
}
|
|
return response.Write(c, fiber.StatusOK, h.baseResourceByCategory(c.UserContext(), updated, categoryType))
|
|
}
|
|
return response.Write(c, fiber.StatusOK, h.baseResourceByCategory(c.UserContext(), existing, categoryType))
|
|
}
|
|
|
|
// DeleteBase godoc
|
|
// @Summary Delete base
|
|
// @Tags Bases
|
|
// @Produce json
|
|
// @Param uuid path string true "Base UUID (UUIDv7)"
|
|
// @Success 200 {object} dto.BaseDeleteResponse
|
|
// @Failure 422 {object} dto.ErrorResponse
|
|
// @Failure 404 {object} dto.ErrorResponse
|
|
// @Router /api/v1/bases/delete/{uuid} [delete]
|
|
func (h *BaseHandler) DeleteBase(c *fiber.Ctx) error {
|
|
categoryType, errObj := parseBaseCategoryTypeQuery(c)
|
|
if errObj != nil {
|
|
return writeBaseErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{*errObj})
|
|
}
|
|
|
|
uuidStr := c.Params("uuid")
|
|
id, err := uuidv7.ParseString(uuidStr)
|
|
if err != nil {
|
|
return writeBaseErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{
|
|
Status: "422",
|
|
Title: "Validation error",
|
|
Detail: "uuid is invalid UUID",
|
|
Source: &jsonapi.ErrorSource{Pointer: "/path/uuid"},
|
|
}})
|
|
}
|
|
|
|
row, err := h.svc.DeleteBase(c.UserContext(), id, categoryType)
|
|
if err != nil {
|
|
if _, ok := apperrors.AsDeleteConflict(err); ok {
|
|
def := apperrorsx.ErrBaseRelationDelete
|
|
return jsonapi.WriteErrors(c, fiber.StatusConflict, []jsonapi.ErrorObject{{
|
|
Status: "409",
|
|
Code: def.Code,
|
|
ErrorCode: def.ErrorCode,
|
|
Title: def.Title,
|
|
Detail: err.Error(),
|
|
}})
|
|
}
|
|
return writeDeleteError(c, err)
|
|
}
|
|
if row == nil {
|
|
return writeBaseErrors(c, fiber.StatusNotFound, []jsonapi.ErrorObject{{
|
|
Status: "404",
|
|
Title: "Not found",
|
|
Detail: "base not found",
|
|
}})
|
|
}
|
|
if row == nil {
|
|
return writeBaseErrors(c, fiber.StatusNotFound, []jsonapi.ErrorObject{{
|
|
Status: "404",
|
|
Title: "Not found",
|
|
Detail: categoryNotFoundDetail(categoryType),
|
|
}})
|
|
}
|
|
if row != nil && len(row.FotoAttachmentID) == 16 {
|
|
if err := syncAttachmentLifecycleClear(c.UserContext(), h.fileManager, row.FotoAttachmentID, actorUserID(c)); err != nil {
|
|
return writeBaseErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
|
|
Status: "400",
|
|
Title: "Delete failed",
|
|
Detail: safeInternalDetail(),
|
|
}})
|
|
}
|
|
}
|
|
|
|
deleteType := "base_delete"
|
|
if categoryType == base.CategoryKeyHEMS {
|
|
deleteType = "hems_base_delete"
|
|
}
|
|
|
|
idStr, _ := uuidv7.BytesToString(id)
|
|
if row == nil {
|
|
// fallback minimal response when no row is available
|
|
return response.Write(c, fiber.StatusOK, jsonapi.Resource{
|
|
Type: deleteType,
|
|
ID: idStr,
|
|
Attributes: map[string]any{"deleted": true},
|
|
})
|
|
}
|
|
|
|
// minimal attributes required for delete response per request
|
|
attrs := map[string]any{
|
|
"base": row.BaseName,
|
|
"base_abbreviation": row.BaseAbbreviation,
|
|
"base_category": baseCategoryFromRowOrType(row, categoryType),
|
|
"created_at": row.CreatedAt.UTC().Format(time.RFC3339),
|
|
"created_by": bytesUUIDToString(row.CreatedBy),
|
|
"updated_at": row.UpdatedAt.UTC().Format(time.RFC3339),
|
|
"updated_by": bytesUUIDToString(row.UpdatedBy),
|
|
"deleted_at": timeStringOrEmpty(row.DeletedAt),
|
|
"deleted_by": bytesUUIDToString(row.DeletedBy),
|
|
}
|
|
|
|
return response.Write(c, fiber.StatusOK, jsonapi.Resource{
|
|
Type: deleteType,
|
|
ID: idStr,
|
|
Attributes: attrs,
|
|
})
|
|
}
|
|
|
|
// GetBaseByID godoc
|
|
// @Summary Get base by ID
|
|
// @Tags Bases
|
|
// @Produce json
|
|
// @Param uuid path string true "Base UUID (UUIDv7)"
|
|
// @Success 200 {object} dto.BaseResponse
|
|
// @Failure 404 {object} dto.ErrorResponse
|
|
// @Router /api/v1/bases/get/{uuid} [get]
|
|
func (h *BaseHandler) GetBase(c *fiber.Ctx) error {
|
|
categoryType, errObj := parseBaseCategoryTypeQuery(c)
|
|
if errObj != nil {
|
|
return writeBaseErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{*errObj})
|
|
}
|
|
|
|
uuidStr := c.Params("uuid")
|
|
id, err := uuidv7.ParseString(uuidStr)
|
|
if err != nil {
|
|
return writeBaseErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{
|
|
Status: "422",
|
|
Title: "Validation error",
|
|
Detail: "uuid is invalid UUID",
|
|
Source: &jsonapi.ErrorSource{Pointer: "/path/uuid"},
|
|
}})
|
|
}
|
|
|
|
row, err := h.svc.GetBaseByID(c.UserContext(), id, categoryType)
|
|
if err != nil || row == nil {
|
|
return writeBaseErrors(c, fiber.StatusNotFound, []jsonapi.ErrorObject{{
|
|
Status: "404",
|
|
Title: "Not found",
|
|
Detail: categoryNotFoundDetail(categoryType),
|
|
}})
|
|
}
|
|
|
|
return response.Write(c, fiber.StatusOK, h.baseResourceByCategory(c.UserContext(), row, categoryType))
|
|
}
|
|
|
|
// ListBases godoc
|
|
// @Summary List bases
|
|
// @Description JSON:API list with filtering and sorting. This endpoint returns all bases.
|
|
// @Tags Bases
|
|
// @Produce json
|
|
// @Param category_type query string false "Base category" Enums(regular,hems)
|
|
// @Param base_category query string false "Alias filter for base category" Enums(regular,hems)
|
|
// @Param search query string false "Search by base/base_abbreviation/address/email/phone"
|
|
// @Param dul query boolean false "Filter by DUL flag (true/false)"
|
|
// @Param page query int false "Ignored"
|
|
// @Param size query int false "Ignored"
|
|
// @Param limit query int false "Ignored"
|
|
// @Param sort query string false "Sort order"
|
|
// @Success 200 {object} dto.BaseListResponse
|
|
// @Router /api/v1/bases/get-all [get]
|
|
func (h *BaseHandler) ListBases(c *fiber.Ctx) error {
|
|
categoryType, errObj := parseOptionalBaseCategoryTypeQuery(c)
|
|
if errObj != nil {
|
|
return writeBaseErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{*errObj})
|
|
}
|
|
dulFilter, errObj := parseOptionalBoolQueryParam(c, "dul")
|
|
if errObj != nil {
|
|
return writeBaseErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{*errObj})
|
|
}
|
|
|
|
filter := c.Query("search")
|
|
sort := c.Query("sort")
|
|
|
|
rows, total, err := h.svc.ListBases(c.UserContext(), filter, sort, 0, 0, categoryType, dulFilter)
|
|
if err != nil {
|
|
return writeBaseErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
|
|
Status: "400",
|
|
Title: "List failed",
|
|
Detail: safeInternalDetail(),
|
|
}})
|
|
}
|
|
|
|
data := make([]dto.BaseResource, 0, len(rows))
|
|
for i := range rows {
|
|
data = append(data, h.baseResourceByCategory(c.UserContext(), &rows[i], categoryType))
|
|
}
|
|
|
|
meta := map[string]any{
|
|
"total": total,
|
|
}
|
|
return response.WriteWithMeta(c, fiber.StatusOK, data, meta)
|
|
}
|
|
|
|
// ListBasesDatatable godoc
|
|
// @Summary List bases (datatable)
|
|
// @Tags Bases
|
|
// @Produce json
|
|
// @Param category_type query string false "Base category" Enums(regular,hems)
|
|
// @Param base_category query string false "Alias filter for base category" Enums(regular,hems)
|
|
// @Param page query int false "Page number (default 1)"
|
|
// @Param size query int false "Page size (default 20, max 100)"
|
|
// @Param limit query int false "Page size override (same as size)"
|
|
// @Param draw query int false "Draw counter (optional)"
|
|
// @Param search query string false "Search term"
|
|
// @Param dul query boolean false "Filter by DUL flag (true/false)"
|
|
// @Success 200 {object} dto.BaseDataTableResponse
|
|
// @Router /api/v1/bases/get-all/dt [get]
|
|
func (h *BaseHandler) ListBasesDatatable(c *fiber.Ctx) error {
|
|
categoryType, errObj := parseOptionalBaseCategoryTypeQuery(c)
|
|
if errObj != nil {
|
|
return writeBaseErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{*errObj})
|
|
}
|
|
dulFilter, errObj := parseOptionalBoolQueryParam(c, "dul")
|
|
if errObj != nil {
|
|
return writeBaseErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{*errObj})
|
|
}
|
|
|
|
pageNumber := parseIntDefault(c.Query("page"), 1)
|
|
if pageNumber < 1 {
|
|
pageNumber = 1
|
|
}
|
|
length := parseIntDefault(c.Query("limit"), 0)
|
|
if length <= 0 {
|
|
length = parseIntDefault(c.Query("size"), 20)
|
|
}
|
|
if length > 100 {
|
|
length = 100
|
|
}
|
|
if length < 1 {
|
|
length = 20
|
|
}
|
|
start := (pageNumber - 1) * length
|
|
draw := parseIntDefault(c.Query("draw"), pageNumber)
|
|
search := c.Query("search")
|
|
|
|
rows, total, err := h.svc.ListBases(c.UserContext(), search, "", length, start, categoryType, dulFilter)
|
|
if err != nil {
|
|
return writeBaseErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
|
|
Status: "400",
|
|
Title: "List failed",
|
|
Detail: safeInternalDetail(),
|
|
}})
|
|
}
|
|
|
|
data := make([]dto.BaseResource, 0, len(rows))
|
|
for i := range rows {
|
|
data = append(data, h.baseResourceByCategory(c.UserContext(), &rows[i], categoryType))
|
|
}
|
|
|
|
meta := map[string]any{
|
|
"draw": draw,
|
|
"records_total": total,
|
|
"records_filtered": total,
|
|
}
|
|
return response.WriteWithMeta(c, fiber.StatusOK, data, meta)
|
|
}
|
|
|
|
func (h *BaseHandler) baseResource(ctx context.Context, row *base.Base, categoryType string) dto.BaseResource {
|
|
baseRow := row
|
|
id := ""
|
|
defaultShiftStart := ""
|
|
defaultShiftEnd := ""
|
|
defaultStartTimeType := base.ShiftTimeTypeFixed
|
|
defaultEndTimeType := base.ShiftTimeTypeFixed
|
|
if baseRow != nil {
|
|
id, _ = uuidv7.BytesToString(baseRow.ID)
|
|
defaultStartTimeType = normalizeBaseShiftTimeTypeOrFixed(baseRow.DefaultStartTimeType)
|
|
defaultEndTimeType = normalizeBaseShiftTimeTypeOrFixed(baseRow.DefaultEndTimeType)
|
|
defaultShiftStart = dto.DisplayBaseShiftValue(defaultStartTimeType, baseRow.DefaultShiftStart)
|
|
defaultShiftEnd = dto.DisplayBaseShiftValue(defaultEndTimeType, baseRow.DefaultShiftEnd)
|
|
}
|
|
baseCategory := baseCategoryFromRowOrType(baseRow, categoryType)
|
|
resourceType := "base"
|
|
if baseCategory == base.CategoryKeyHEMS {
|
|
resourceType = "hems_base"
|
|
}
|
|
var foto *dto.BaseFoto
|
|
fotoUUID := ""
|
|
if baseRow != nil {
|
|
fotoUUID = idString(baseRow.FotoAttachmentID)
|
|
}
|
|
fotoDownloadURL := ""
|
|
fotoThumbnailURL := ""
|
|
var fotoOriginalSizeBytes *int64
|
|
if baseRow != nil && baseRow.FotoAttachment != nil && baseRow.FotoAttachment.File != nil {
|
|
asset := resolveFileManagerDownloadURLs(ctx, h.storage, baseRow.FotoAttachment.File)
|
|
fotoDownloadURL = strings.TrimSpace(asset.OriginalURL)
|
|
fotoThumbnailURL = strings.TrimSpace(asset.ThumbnailURL)
|
|
fotoOriginalSizeBytes = int64Ptr(asset.OriginalSizeBytes)
|
|
}
|
|
if fotoUUID != "" || fotoDownloadURL != "" || fotoThumbnailURL != "" || fotoOriginalSizeBytes != nil {
|
|
foto = &dto.BaseFoto{
|
|
UUID: fotoUUID,
|
|
DownloadURL: fotoDownloadURL,
|
|
ThumbnailDownloadURL: fotoThumbnailURL,
|
|
OriginalSizeBytes: fotoOriginalSizeBytes,
|
|
}
|
|
}
|
|
var hemsEDCIDs []string
|
|
var hemsEDCs []dto.BaseContact
|
|
var medPaxIDs []string
|
|
var medPax []dto.BaseContact
|
|
var pilotIDs []string
|
|
var pilots []dto.BaseContact
|
|
if baseRow != nil {
|
|
hemsEDCIDs = uuidListToStrings(baseRow.HEMSEDCContactIDs)
|
|
hemsEDCs = baseContactResources(baseRow.HEMSEDCs)
|
|
medPaxIDs = uuidListToStrings(baseRow.MedPaxContactIDs)
|
|
medPax = baseContactResources(baseRow.MedPax)
|
|
pilotIDs = uuidListToStrings(baseRow.ResponsiblePilotContactIDs)
|
|
pilots = baseContactResources(baseRow.ResponsiblePilots)
|
|
}
|
|
operationalShiftTimes := []dto.BaseOperationalShiftTime(nil)
|
|
if baseRow != nil {
|
|
operationalShiftTimes = dto.MapBaseOperationalShiftTimesToDTO(baseRow.OperationalShiftTimes)
|
|
}
|
|
|
|
return dto.MapBaseToResource(baseRow, dto.BaseResponseMappingInput{
|
|
ResourceType: resourceType,
|
|
BaseCategory: baseCategory,
|
|
ID: id,
|
|
DefaultStartTimeType: defaultStartTimeType,
|
|
DefaultEndTimeType: defaultEndTimeType,
|
|
ShiftTime: dto.BaseShiftTime{Start: defaultShiftStart, End: defaultShiftEnd},
|
|
Foto: foto,
|
|
Latitude: float64FromBase(baseRow),
|
|
Longitude: float64FromBaseLongitude(baseRow),
|
|
HEMSEDCContactIDs: hemsEDCIDs,
|
|
HEMSEDCs: hemsEDCs,
|
|
MedPaxContactIDs: medPaxIDs,
|
|
MedPax: medPax,
|
|
ResponsiblePilotContactIDs: pilotIDs,
|
|
ResponsiblePilots: pilots,
|
|
OperationalShiftTimes: operationalShiftTimes,
|
|
})
|
|
}
|
|
|
|
func float64FromBase(row *base.Base) float64 {
|
|
if row == nil {
|
|
return 0
|
|
}
|
|
return row.Latitude
|
|
}
|
|
|
|
func float64FromBaseLongitude(row *base.Base) float64 {
|
|
if row == nil {
|
|
return 0
|
|
}
|
|
return row.Longitude
|
|
}
|
|
|
|
func cloneIntPointerBase(v *int) *int {
|
|
if v == nil {
|
|
return nil
|
|
}
|
|
copied := *v
|
|
return &copied
|
|
}
|
|
|
|
func preferStringPtr(primary, fallback *string) *string {
|
|
if primary != nil {
|
|
return primary
|
|
}
|
|
return fallback
|
|
}
|
|
|
|
func parseUUIDList(values []string, pointer string) ([][]byte, *jsonapi.ErrorObject) {
|
|
out := make([][]byte, 0, len(values))
|
|
seen := map[string]struct{}{}
|
|
for i := range values {
|
|
v := strings.TrimSpace(values[i])
|
|
if v == "" {
|
|
continue
|
|
}
|
|
id, err := uuidv7.ParseString(v)
|
|
if err != nil {
|
|
return nil, &jsonapi.ErrorObject{
|
|
Status: "422",
|
|
Title: "Validation error",
|
|
Detail: "value is invalid UUID",
|
|
Source: &jsonapi.ErrorSource{Pointer: pointer},
|
|
}
|
|
}
|
|
key := string(id)
|
|
if _, ok := seen[key]; ok {
|
|
continue
|
|
}
|
|
seen[key] = struct{}{}
|
|
out = append(out, id)
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func uuidListToStrings(values [][]byte) []string {
|
|
if len(values) == 0 {
|
|
return nil
|
|
}
|
|
out := make([]string, 0, len(values))
|
|
for i := range values {
|
|
id, err := uuidv7.BytesToString(values[i])
|
|
if err != nil {
|
|
continue
|
|
}
|
|
out = append(out, id)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func baseContactResources(values []base.BaseContactPerson) []dto.BaseContact {
|
|
if len(values) == 0 {
|
|
return nil
|
|
}
|
|
out := make([]dto.BaseContact, 0, len(values))
|
|
for i := range values {
|
|
id, err := uuidv7.BytesToString(values[i].ContactID)
|
|
if err != nil || strings.TrimSpace(id) == "" {
|
|
continue
|
|
}
|
|
out = append(out, dto.BaseContact{
|
|
ID: id,
|
|
FirstName: values[i].FirstName,
|
|
LastName: values[i].LastName,
|
|
})
|
|
}
|
|
if len(out) == 0 {
|
|
return nil
|
|
}
|
|
return out
|
|
}
|
|
|
|
func (h *BaseHandler) baseResourceByCategory(ctx context.Context, row *base.Base, categoryType string) dto.BaseResource {
|
|
return h.baseResource(ctx, row, categoryType)
|
|
}
|
|
|
|
func (h *BaseHandler) resolveBaseFotoAttachmentID(
|
|
c *fiber.Ctx,
|
|
baseID []byte,
|
|
rawFileUUID *string,
|
|
) ([]byte, bool) {
|
|
return h.resolveAttachmentIDFromInput(c, attachmentresolver.Params{
|
|
FileManager: h.fileManager,
|
|
|
|
RawFileID: rawFileUUID,
|
|
|
|
FileField: "file_uuid",
|
|
|
|
RequireValue: false,
|
|
|
|
RefType: basePhotoAttachmentRefType,
|
|
RefID: idString(baseID),
|
|
Category: ptrString(basePhotoAttachmentCategory),
|
|
|
|
IsPrimary: true,
|
|
ActorID: actorUserID(c),
|
|
|
|
ValidateAttachmentExists: false,
|
|
}, "Update failed")
|
|
}
|
|
|
|
func (h *BaseHandler) resolveExistingBaseFotoAttachmentOrFileID(
|
|
c *fiber.Ctx,
|
|
baseID []byte,
|
|
rawUUID *string,
|
|
) ([]byte, bool) {
|
|
if hasNonEmptyOptionalString(rawUUID) && h.fileManager != nil {
|
|
candidateID, err := uuidv7.ParseString(strings.TrimSpace(*rawUUID))
|
|
if err == nil {
|
|
attachment, getErr := h.fileManager.GetAttachmentByID(c.UserContext(), candidateID)
|
|
if getErr != nil {
|
|
_ = writeBaseErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
|
|
Status: "400",
|
|
Title: "Update failed",
|
|
Detail: getErr.Error(),
|
|
}})
|
|
return nil, true
|
|
}
|
|
if attachment != nil {
|
|
return candidateID, false
|
|
}
|
|
}
|
|
}
|
|
return h.resolveBaseFotoAttachmentID(c, baseID, rawUUID)
|
|
}
|
|
|
|
func (h *BaseHandler) resolveAttachmentIDFromInput(
|
|
c *fiber.Ctx,
|
|
params attachmentresolver.Params,
|
|
errorTitle string,
|
|
) ([]byte, bool) {
|
|
attachmentID, err := attachmentresolver.Resolve(c.UserContext(), params)
|
|
if err == nil {
|
|
return attachmentID, false
|
|
}
|
|
|
|
var resolveErr *attachmentresolver.ResolveError
|
|
if errors.As(err, &resolveErr) {
|
|
switch resolveErr.Code {
|
|
case attachmentresolver.ErrorInvalidAttachmentUUID:
|
|
_ = writeBaseErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{
|
|
Status: "422",
|
|
Title: "Validation error",
|
|
Detail: fmt.Sprintf("%s is invalid UUID", fieldOrDefault(resolveErr.AttachmentField, "attachment_id")),
|
|
Source: &jsonapi.ErrorSource{Pointer: "/data/attributes/" + fieldOrDefault(resolveErr.AttachmentField, "attachment_id")},
|
|
}})
|
|
return nil, true
|
|
case attachmentresolver.ErrorInvalidFileUUID:
|
|
_ = writeBaseErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{
|
|
Status: "422",
|
|
Title: "Validation error",
|
|
Detail: fmt.Sprintf("%s is invalid UUID", fieldOrDefault(resolveErr.FileField, "file_id")),
|
|
Source: &jsonapi.ErrorSource{Pointer: "/data/attributes/" + fieldOrDefault(resolveErr.FileField, "file_id")},
|
|
}})
|
|
return nil, true
|
|
case attachmentresolver.ErrorAttachmentNotFound:
|
|
_ = writeBaseErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{
|
|
Status: "422",
|
|
Title: "Validation error",
|
|
Detail: fmt.Sprintf("%s not found", fieldOrDefault(resolveErr.AttachmentField, "attachment_id")),
|
|
Source: &jsonapi.ErrorSource{Pointer: "/data/attributes/" + fieldOrDefault(resolveErr.AttachmentField, "attachment_id")},
|
|
}})
|
|
return nil, true
|
|
case attachmentresolver.ErrorRequireValue:
|
|
_ = writeBaseErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{
|
|
Status: "422",
|
|
Title: "Validation error",
|
|
Detail: fmt.Sprintf("%s or %s is required", fieldOrDefault(resolveErr.AttachmentField, "attachment_id"), fieldOrDefault(resolveErr.FileField, "file_id")),
|
|
Source: &jsonapi.ErrorSource{Pointer: "/data/attributes"},
|
|
}})
|
|
return nil, true
|
|
case attachmentresolver.ErrorCannotResolveFromFile:
|
|
_ = writeBaseErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{
|
|
Status: "422",
|
|
Title: "Validation error",
|
|
Detail: fmt.Sprintf("cannot resolve attachment from %s", fieldOrDefault(resolveErr.FileField, "file_id")),
|
|
Source: &jsonapi.ErrorSource{Pointer: "/data/attributes/" + fieldOrDefault(resolveErr.FileField, "file_id")},
|
|
}})
|
|
return nil, true
|
|
case attachmentresolver.ErrorFileNotReadyForAttachment:
|
|
_ = writeBaseErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{
|
|
Status: "422",
|
|
Title: "Validation error",
|
|
Detail: fmt.Sprintf("%s is not ready yet; wait for file.updated SSE event", fieldOrDefault(resolveErr.FileField, "file_id")),
|
|
Source: &jsonapi.ErrorSource{Pointer: "/data/attributes/" + fieldOrDefault(resolveErr.FileField, "file_id")},
|
|
}})
|
|
return nil, true
|
|
case attachmentresolver.ErrorFileManagerUnavailable:
|
|
_ = writeBaseErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
|
|
Status: "400",
|
|
Title: errorTitle,
|
|
Detail: resolveErr.Error(),
|
|
}})
|
|
return nil, true
|
|
}
|
|
}
|
|
|
|
_ = writeBaseErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
|
|
Status: "400",
|
|
Title: errorTitle,
|
|
Detail: safeInternalDetail(),
|
|
}})
|
|
return nil, true
|
|
}
|
|
|
|
func parseBaseShiftRange(
|
|
startRaw *string,
|
|
endRaw *string,
|
|
legacyRaw *string,
|
|
startPointer string,
|
|
endPointer string,
|
|
legacyPointer string,
|
|
) (*time.Time, *time.Time, *jsonapi.ErrorObject) {
|
|
start, errObj := parseOptionalClock(startRaw, startPointer)
|
|
if errObj != nil {
|
|
return nil, nil, errObj
|
|
}
|
|
end, errObj := parseOptionalClock(endRaw, endPointer)
|
|
if errObj != nil {
|
|
return nil, nil, errObj
|
|
}
|
|
if legacyRaw == nil {
|
|
return start, end, nil
|
|
}
|
|
|
|
legacy := strings.TrimSpace(*legacyRaw)
|
|
if legacy == "" {
|
|
return start, end, nil
|
|
}
|
|
legacyStartRaw, legacyEndRaw := extractBaseShiftRangeTokens(legacy)
|
|
if legacyStartRaw == "" && legacyEndRaw == "" {
|
|
return nil, nil, &jsonapi.ErrorObject{
|
|
Status: "422",
|
|
Title: "Validation error",
|
|
Detail: "default_shift_time must contain HH:MM or HH:MM:SS range",
|
|
Source: &jsonapi.ErrorSource{Pointer: legacyPointer},
|
|
}
|
|
}
|
|
if startRaw == nil && legacyStartRaw != "" {
|
|
start, errObj = parseOptionalClock(&legacyStartRaw, legacyPointer)
|
|
if errObj != nil {
|
|
return nil, nil, errObj
|
|
}
|
|
}
|
|
if endRaw == nil && legacyEndRaw != "" {
|
|
end, errObj = parseOptionalClock(&legacyEndRaw, legacyPointer)
|
|
if errObj != nil {
|
|
return nil, nil, errObj
|
|
}
|
|
}
|
|
return start, end, nil
|
|
}
|
|
|
|
func parseBaseDefaultShiftRange(
|
|
startRaw *string,
|
|
endRaw *string,
|
|
legacyRaw *string,
|
|
startTimeType string,
|
|
endTimeType string,
|
|
requireFixedTimes bool,
|
|
startPointer string,
|
|
endPointer string,
|
|
legacyPointer string,
|
|
) (*time.Time, *time.Time, *jsonapi.ErrorObject) {
|
|
start, end, errObj := parseBaseShiftRange(startRaw, endRaw, legacyRaw, startPointer, endPointer, legacyPointer)
|
|
if errObj != nil {
|
|
return nil, nil, errObj
|
|
}
|
|
startTimeType = normalizeBaseShiftTimeTypeOrFixed(startTimeType)
|
|
endTimeType = normalizeBaseShiftTimeTypeOrFixed(endTimeType)
|
|
if requireFixedTimes && startTimeType == base.ShiftTimeTypeFixed && start == nil {
|
|
return nil, nil, &jsonapi.ErrorObject{
|
|
Status: "422",
|
|
Title: "Validation error",
|
|
Detail: "default_shift_start is required when default_start_time_type is FIXED",
|
|
Source: &jsonapi.ErrorSource{Pointer: startPointer},
|
|
}
|
|
}
|
|
if requireFixedTimes && endTimeType == base.ShiftTimeTypeFixed && end == nil {
|
|
return nil, nil, &jsonapi.ErrorObject{
|
|
Status: "422",
|
|
Title: "Validation error",
|
|
Detail: "default_shift_end is required when default_end_time_type is FIXED",
|
|
Source: &jsonapi.ErrorSource{Pointer: endPointer},
|
|
}
|
|
}
|
|
if start != nil && end != nil && start.After(*end) {
|
|
return nil, nil, &jsonapi.ErrorObject{
|
|
Status: "422",
|
|
Title: "Validation error",
|
|
Detail: "default_shift_start must be earlier than default_shift_end",
|
|
Source: &jsonapi.ErrorSource{Pointer: legacyPointer},
|
|
}
|
|
}
|
|
if startTimeType != base.ShiftTimeTypeFixed {
|
|
start = nil
|
|
}
|
|
if endTimeType != base.ShiftTimeTypeFixed {
|
|
end = nil
|
|
}
|
|
return start, end, nil
|
|
}
|
|
|
|
func parseBaseOperationalShiftTimes(items []dto.BaseOperationalShiftTime, basePointer string) ([]appservice.BaseOperationalShiftTimeInput, *jsonapi.ErrorObject) {
|
|
if len(items) == 0 {
|
|
return nil, nil
|
|
}
|
|
out := make([]appservice.BaseOperationalShiftTimeInput, 0, len(items))
|
|
for i := range items {
|
|
idxPointer := fmt.Sprintf("%s/%d", basePointer, i)
|
|
startRaw := strings.TrimSpace(items[i].DateStart)
|
|
endRaw := strings.TrimSpace(items[i].DateEnd)
|
|
if startRaw == "" || endRaw == "" {
|
|
return nil, &jsonapi.ErrorObject{
|
|
Status: "422",
|
|
Title: "Validation error",
|
|
Detail: "date_start and date_end are required",
|
|
Source: &jsonapi.ErrorSource{Pointer: idxPointer + "/date_start"},
|
|
}
|
|
}
|
|
dateStart, err := time.Parse("2006-01-02", startRaw)
|
|
if err != nil {
|
|
return nil, &jsonapi.ErrorObject{
|
|
Status: "422",
|
|
Title: "Validation error",
|
|
Detail: "date_start must be YYYY-MM-DD",
|
|
Source: &jsonapi.ErrorSource{Pointer: idxPointer + "/date_start"},
|
|
}
|
|
}
|
|
dateEnd, err := time.Parse("2006-01-02", endRaw)
|
|
if err != nil {
|
|
return nil, &jsonapi.ErrorObject{
|
|
Status: "422",
|
|
Title: "Validation error",
|
|
Detail: "date_end must be YYYY-MM-DD",
|
|
Source: &jsonapi.ErrorSource{Pointer: idxPointer + "/date_end"},
|
|
}
|
|
}
|
|
if dateEnd.Before(dateStart) {
|
|
return nil, &jsonapi.ErrorObject{
|
|
Status: "422",
|
|
Title: "Validation error",
|
|
Detail: "date_end must be >= date_start",
|
|
Source: &jsonapi.ErrorSource{Pointer: idxPointer + "/date_end"},
|
|
}
|
|
}
|
|
shiftStartRaw := strings.TrimSpace(items[i].ShiftTime.Start)
|
|
shiftEndRaw := strings.TrimSpace(items[i].ShiftTime.End)
|
|
startTimeType, errObj := normalizeBaseShiftTimeType(items[i].StartTimeType, idxPointer+"/start_time_type")
|
|
if errObj != nil {
|
|
return nil, errObj
|
|
}
|
|
endTimeType, errObj := normalizeBaseShiftTimeType(items[i].EndTimeType, idxPointer+"/end_time_type")
|
|
if errObj != nil {
|
|
return nil, errObj
|
|
}
|
|
if startTimeType == base.ShiftTimeTypeFixed && shiftStartRaw == "" {
|
|
return nil, &jsonapi.ErrorObject{
|
|
Status: "422",
|
|
Title: "Validation error",
|
|
Detail: "shift_time.start is required when start_time_type is FIXED",
|
|
Source: &jsonapi.ErrorSource{Pointer: idxPointer + "/shift_time/start"},
|
|
}
|
|
}
|
|
if endTimeType == base.ShiftTimeTypeFixed && shiftEndRaw == "" {
|
|
return nil, &jsonapi.ErrorObject{
|
|
Status: "422",
|
|
Title: "Validation error",
|
|
Detail: "shift_time.end is required when end_time_type is FIXED",
|
|
Source: &jsonapi.ErrorSource{Pointer: idxPointer + "/shift_time/end"},
|
|
}
|
|
}
|
|
var start *time.Time
|
|
var end *time.Time
|
|
var clockErr *jsonapi.ErrorObject
|
|
if shiftStartRaw != "" {
|
|
start, clockErr = parseOptionalClock(&shiftStartRaw, idxPointer+"/shift_time/start")
|
|
if clockErr != nil {
|
|
return nil, clockErr
|
|
}
|
|
}
|
|
if shiftEndRaw != "" {
|
|
end, clockErr = parseOptionalClock(&shiftEndRaw, idxPointer+"/shift_time/end")
|
|
if clockErr != nil {
|
|
return nil, clockErr
|
|
}
|
|
}
|
|
if start != nil && end != nil && start.After(*end) {
|
|
return nil, &jsonapi.ErrorObject{
|
|
Status: "422",
|
|
Title: "Validation error",
|
|
Detail: "shift_time.start must be earlier than shift_time.end",
|
|
Source: &jsonapi.ErrorSource{Pointer: idxPointer + "/shift_time"},
|
|
}
|
|
}
|
|
out = append(out, appservice.BaseOperationalShiftTimeInput{
|
|
DateStart: dateStart.UTC(),
|
|
DateEnd: dateEnd.UTC(),
|
|
StartTimeType: startTimeType,
|
|
EndTimeType: endTimeType,
|
|
ShiftStart: start,
|
|
ShiftEnd: end,
|
|
})
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func validateBaseCoordinates(lat, lon *float64, latPointer, lonPointer string) *jsonapi.ErrorObject {
|
|
if lat == nil || lon == nil {
|
|
return &jsonapi.ErrorObject{
|
|
Status: "422",
|
|
Title: "Validation error",
|
|
Detail: "latitude and longitude are required",
|
|
Source: &jsonapi.ErrorSource{Pointer: latPointer},
|
|
}
|
|
}
|
|
if errObj := validateCoordinateRange(*lat, -90, 90, "latitude", latPointer); errObj != nil {
|
|
return errObj
|
|
}
|
|
if errObj := validateCoordinateRange(*lon, -180, 180, "longitude", lonPointer); errObj != nil {
|
|
return errObj
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validateOptionalBaseCoordinates(lat, lon *float64, latPointer, lonPointer string) *jsonapi.ErrorObject {
|
|
if lat == nil && lon == nil {
|
|
return nil
|
|
}
|
|
if lat == nil || lon == nil {
|
|
pointer := latPointer
|
|
if lat == nil {
|
|
pointer = latPointer
|
|
} else {
|
|
pointer = lonPointer
|
|
}
|
|
return &jsonapi.ErrorObject{
|
|
Status: "422",
|
|
Title: "Validation error",
|
|
Detail: "latitude and longitude must be provided together",
|
|
Source: &jsonapi.ErrorSource{Pointer: pointer},
|
|
}
|
|
}
|
|
if errObj := validateCoordinateRange(*lat, -90, 90, "latitude", latPointer); errObj != nil {
|
|
return errObj
|
|
}
|
|
if errObj := validateCoordinateRange(*lon, -180, 180, "longitude", lonPointer); errObj != nil {
|
|
return errObj
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validateCoordinateRange(value, min, max float64, label, pointer string) *jsonapi.ErrorObject {
|
|
if math.IsNaN(value) || value < min || value > max {
|
|
return &jsonapi.ErrorObject{
|
|
Status: "422",
|
|
Title: "Validation error",
|
|
Detail: fmt.Sprintf("%s must be between %.0f and %.0f", label, min, max),
|
|
Source: &jsonapi.ErrorSource{Pointer: pointer},
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func extractBaseShiftTimeFromUpdateBody(body []byte) *dto.BaseShiftTime {
|
|
var payload struct {
|
|
Data struct {
|
|
Attributes struct {
|
|
ShiftTime *dto.BaseShiftTime `json:"shift_time"`
|
|
} `json:"attributes"`
|
|
} `json:"data"`
|
|
}
|
|
if err := json.Unmarshal(body, &payload); err != nil {
|
|
return nil
|
|
}
|
|
return payload.Data.Attributes.ShiftTime
|
|
}
|
|
|
|
func parseOptionalClock(v *string, pointer string) (*time.Time, *jsonapi.ErrorObject) {
|
|
if v == nil {
|
|
return nil, nil
|
|
}
|
|
s := strings.TrimSpace(*v)
|
|
if s == "" {
|
|
return nil, nil
|
|
}
|
|
if len(s) == 5 {
|
|
s += ":00"
|
|
}
|
|
parsed, err := time.Parse("15:04:05", s)
|
|
if err != nil {
|
|
return nil, &jsonapi.ErrorObject{
|
|
Status: "422",
|
|
Title: "Validation error",
|
|
Detail: "time must be HH:MM or HH:MM:SS",
|
|
Source: &jsonapi.ErrorSource{Pointer: pointer},
|
|
}
|
|
}
|
|
val := time.Date(2000, 1, 1, parsed.Hour(), parsed.Minute(), parsed.Second(), 0, time.UTC)
|
|
return &val, nil
|
|
}
|
|
|
|
func normalizeBaseShiftTimeType(raw string, pointer string) (string, *jsonapi.ErrorObject) {
|
|
switch strings.ToUpper(strings.TrimSpace(raw)) {
|
|
case "", base.ShiftTimeTypeFixed:
|
|
return base.ShiftTimeTypeFixed, nil
|
|
case base.ShiftTimeTypeBMCT:
|
|
return base.ShiftTimeTypeBMCT, nil
|
|
case base.ShiftTimeTypeECET:
|
|
return base.ShiftTimeTypeECET, nil
|
|
default:
|
|
return "", &jsonapi.ErrorObject{
|
|
Status: "422",
|
|
Title: "Validation error",
|
|
Detail: "start_time_type and end_time_type must be FIXED, BMCT, or ECET",
|
|
Source: &jsonapi.ErrorSource{Pointer: pointer},
|
|
}
|
|
}
|
|
}
|
|
|
|
func normalizeBaseShiftTimeTypeOrFixed(raw string) string {
|
|
value, errObj := normalizeBaseShiftTimeType(raw, "")
|
|
if errObj != nil {
|
|
return strings.ToUpper(strings.TrimSpace(raw))
|
|
}
|
|
return value
|
|
}
|
|
|
|
func float64OrDefault(v *float64, d float64) float64 {
|
|
if v == nil {
|
|
return d
|
|
}
|
|
return *v
|
|
}
|
|
|
|
func extractBaseShiftRangeTokens(raw string) (string, string) {
|
|
tokens := extractBaseShiftClockTokens(raw)
|
|
if len(tokens) == 0 {
|
|
return "", ""
|
|
}
|
|
if len(tokens) == 1 {
|
|
return tokens[0], tokens[0]
|
|
}
|
|
return tokens[0], tokens[1]
|
|
}
|
|
|
|
func extractBaseShiftClockTokens(raw string) []string {
|
|
s := strings.TrimSpace(raw)
|
|
if s == "" {
|
|
return nil
|
|
}
|
|
out := make([]string, 0, 2)
|
|
for i := 0; i < len(s) && len(out) < 2; i++ {
|
|
if i+5 > len(s) || !isDigitBaseClock(s[i]) || !isDigitBaseClock(s[i+1]) || s[i+2] != ':' || !isDigitBaseClock(s[i+3]) || !isDigitBaseClock(s[i+4]) {
|
|
continue
|
|
}
|
|
token := s[i : i+5]
|
|
if i+8 <= len(s) && s[i+5] == ':' && isDigitBaseClock(s[i+6]) && isDigitBaseClock(s[i+7]) {
|
|
token = s[i : i+8]
|
|
}
|
|
out = append(out, token)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func isDigitBaseClock(b byte) bool {
|
|
return b >= '0' && b <= '9'
|
|
}
|
|
|
|
func clockStringOrEmpty(v any) string {
|
|
switch t := v.(type) {
|
|
case nil:
|
|
return ""
|
|
case string:
|
|
return normalizeBaseShiftDisplay(t)
|
|
case *time.Time:
|
|
if t == nil {
|
|
return ""
|
|
}
|
|
return normalizeBaseShiftDisplay(t.Format("15:04:05"))
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
func normalizeBaseShiftDisplay(raw string) string {
|
|
s := strings.TrimSpace(raw)
|
|
if len(s) >= 5 && isDigitBaseClock(s[0]) && isDigitBaseClock(s[1]) && s[2] == ':' && isDigitBaseClock(s[3]) && isDigitBaseClock(s[4]) {
|
|
return s[:5]
|
|
}
|
|
return s
|
|
}
|
|
|
|
func baseCategoryFromRowOrType(row *base.Base, categoryType string) string {
|
|
if row != nil {
|
|
key := strings.TrimSpace(row.BaseCategory.Key)
|
|
if key != "" {
|
|
if fromRelation, ok := base.NormalizeCategoryType(key); ok {
|
|
return fromRelation
|
|
}
|
|
}
|
|
}
|
|
if fromType, ok := base.NormalizeCategoryType(categoryType); ok {
|
|
return fromType
|
|
}
|
|
return base.CategoryKeyRegular
|
|
}
|
|
|
|
func parseBaseCategoryTypeQuery(c *fiber.Ctx) (string, *jsonapi.ErrorObject) {
|
|
raw := baseCategoryQueryValue(c)
|
|
categoryType, ok := base.NormalizeCategoryType(raw)
|
|
if !ok {
|
|
return "", &jsonapi.ErrorObject{
|
|
Status: "422",
|
|
Title: "Validation error",
|
|
Detail: "category_type/base_category must be one of: regular, hems",
|
|
Source: &jsonapi.ErrorSource{Pointer: "/query/category_type"},
|
|
}
|
|
}
|
|
return categoryType, nil
|
|
}
|
|
|
|
func parseOptionalBaseCategoryTypeQuery(c *fiber.Ctx) (string, *jsonapi.ErrorObject) {
|
|
raw := baseCategoryQueryValue(c)
|
|
if raw == "" {
|
|
return "", nil
|
|
}
|
|
categoryType, ok := base.NormalizeCategoryType(raw)
|
|
if !ok {
|
|
return "", &jsonapi.ErrorObject{
|
|
Status: "422",
|
|
Title: "Validation error",
|
|
Detail: "category_type/base_category must be one of: regular, hems",
|
|
Source: &jsonapi.ErrorSource{Pointer: "/query/category_type"},
|
|
}
|
|
}
|
|
return categoryType, nil
|
|
}
|
|
|
|
func parseOptionalBoolQueryParam(c *fiber.Ctx, queryKey string) (*bool, *jsonapi.ErrorObject) {
|
|
raw := strings.TrimSpace(c.Query(queryKey))
|
|
if raw == "" {
|
|
return nil, nil
|
|
}
|
|
switch strings.ToLower(raw) {
|
|
case "true", "1":
|
|
v := true
|
|
return &v, nil
|
|
case "false", "0":
|
|
v := false
|
|
return &v, nil
|
|
default:
|
|
return nil, &jsonapi.ErrorObject{
|
|
Status: "422",
|
|
Title: "Validation error",
|
|
Detail: queryKey + " must be true or false",
|
|
Source: &jsonapi.ErrorSource{Pointer: "/query/" + queryKey},
|
|
}
|
|
}
|
|
}
|
|
|
|
func baseCategoryQueryValue(c *fiber.Ctx) string {
|
|
raw := strings.TrimSpace(c.Query("category_type"))
|
|
if raw == "" {
|
|
raw = strings.TrimSpace(c.Query("base_category"))
|
|
}
|
|
if raw == "" {
|
|
raw = strings.TrimSpace(c.Query("category"))
|
|
}
|
|
return raw
|
|
}
|
|
|
|
func resolveBaseCategoryType(c *fiber.Ctx, bodyCategory *string, pointer string) (string, *jsonapi.ErrorObject) {
|
|
if bodyCategory != nil {
|
|
categoryType, ok := base.NormalizeCategoryType(*bodyCategory)
|
|
if !ok {
|
|
return "", &jsonapi.ErrorObject{
|
|
Status: "422",
|
|
Title: "Validation error",
|
|
Detail: "base_category must be one of: regular, hems",
|
|
Source: &jsonapi.ErrorSource{Pointer: pointer},
|
|
}
|
|
}
|
|
return categoryType, nil
|
|
}
|
|
return parseBaseCategoryTypeQuery(c)
|
|
}
|
|
|
|
func categoryNotFoundDetail(categoryType string) string {
|
|
if categoryType == base.CategoryKeyHEMS {
|
|
return "hems base not found"
|
|
}
|
|
return "base not found"
|
|
}
|
|
|
|
func parseOptionalUUID(v *string, pointer string) ([]byte, *jsonapi.ErrorObject) {
|
|
if v == nil {
|
|
return nil, nil
|
|
}
|
|
s := strings.TrimSpace(*v)
|
|
if s == "" {
|
|
return nil, nil
|
|
}
|
|
parsed, err := uuidv7.ParseString(s)
|
|
if err != nil {
|
|
return nil, &jsonapi.ErrorObject{
|
|
Status: "422",
|
|
Title: "Validation error",
|
|
Detail: "invalid UUID",
|
|
Source: &jsonapi.ErrorSource{Pointer: pointer},
|
|
}
|
|
}
|
|
return parsed, nil
|
|
}
|
|
|
|
func parseUUIDOrClear(v string, pointer string) ([]byte, *jsonapi.ErrorObject) {
|
|
s := strings.TrimSpace(v)
|
|
if s == "" {
|
|
return nil, nil
|
|
}
|
|
parsed, err := uuidv7.ParseString(s)
|
|
if err != nil {
|
|
return nil, &jsonapi.ErrorObject{
|
|
Status: "422",
|
|
Title: "Validation error",
|
|
Detail: "invalid UUID",
|
|
Source: &jsonapi.ErrorSource{Pointer: pointer},
|
|
}
|
|
}
|
|
return parsed, nil
|
|
}
|
|
|
|
func bytesUUIDToString(raw []byte) string {
|
|
if len(raw) == 0 {
|
|
return ""
|
|
}
|
|
if name := userctx.GetDisplayName(context.TODO(), raw); name != "" {
|
|
return name
|
|
}
|
|
s, err := uuidv7.BytesToString(raw)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
return s
|
|
}
|
|
|
|
func writeBaseErrors(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
|
|
def := inferBaseDef(status, e.Title, e.Detail)
|
|
if strings.TrimSpace(e.Code) == "" {
|
|
e.Code = def.Code
|
|
}
|
|
if strings.TrimSpace(e.ErrorCode) == "" {
|
|
e.ErrorCode = def.ErrorCode
|
|
}
|
|
if strings.TrimSpace(e.Title) == "" {
|
|
e.Title = def.Title
|
|
}
|
|
if e.Source != nil && shouldPreserveBaseDetail(e.Source.Pointer) {
|
|
if strings.TrimSpace(e.Detail) == "" {
|
|
e.Detail = def.Message
|
|
}
|
|
} else {
|
|
e.Detail = def.Message
|
|
}
|
|
enriched = append(enriched, e)
|
|
}
|
|
return response.WriteErrors(c, status, enriched)
|
|
}
|
|
|
|
func shouldPreserveBaseDetail(pointer string) bool {
|
|
lowerPointer := strings.ToLower(strings.TrimSpace(pointer))
|
|
return strings.Contains(lowerPointer, "operational_shift_times") ||
|
|
strings.Contains(lowerPointer, "latitude") ||
|
|
strings.Contains(lowerPointer, "longitude") ||
|
|
strings.Contains(lowerPointer, "twilight") ||
|
|
strings.Contains(lowerPointer, "shift_time")
|
|
}
|
|
|
|
func inferBaseDef(status int, title, detail string) apperrorsx.Def {
|
|
lowerTitle := strings.ToLower(strings.TrimSpace(title))
|
|
lowerDetail := strings.ToLower(strings.TrimSpace(detail))
|
|
|
|
switch status {
|
|
case fiber.StatusNotFound:
|
|
return apperrorsx.ErrBaseNotFound
|
|
case fiber.StatusConflict:
|
|
return apperrorsx.ErrBaseRelationDelete
|
|
case fiber.StatusUnprocessableEntity:
|
|
switch {
|
|
case strings.Contains(lowerDetail, "uuid is invalid uuid"), strings.Contains(lowerDetail, "invalid uuid"):
|
|
return apperrorsx.ErrBaseInvalidUUID
|
|
case strings.Contains(lowerDetail, "base_category must be one of"),
|
|
strings.Contains(lowerDetail, "category_type/base_category must be one of"):
|
|
return apperrorsx.ErrBaseInvalidCategory
|
|
case strings.Contains(lowerDetail, "at least one attribute must be provided"):
|
|
return apperrorsx.ErrBaseAttributesRequired
|
|
case strings.Contains(lowerDetail, "base is required"), strings.Contains(lowerDetail, "base cannot be empty"):
|
|
return apperrorsx.ErrBaseNameRequired
|
|
case strings.Contains(lowerDetail, "default_shift_start is required"):
|
|
return apperrorsx.ErrBaseDefaultShiftStartRequired
|
|
case strings.Contains(lowerDetail, "default_shift_end is required"):
|
|
return apperrorsx.ErrBaseDefaultShiftEndRequired
|
|
case strings.Contains(lowerDetail, "time must be"), strings.Contains(lowerDetail, "default_shift_time must contain"):
|
|
return apperrorsx.ErrBaseInvalidTime
|
|
case strings.Contains(lowerDetail, "contact id"), strings.Contains(lowerDetail, "value is invalid uuid"):
|
|
return apperrorsx.ErrBaseInvalidContactIDs
|
|
case strings.Contains(lowerDetail, "shift_time.start must be earlier than shift_time.end"),
|
|
strings.Contains(lowerDetail, "shift_time.start is required when start_time_type is fixed"),
|
|
strings.Contains(lowerDetail, "shift_time.end is required when end_time_type is fixed"),
|
|
strings.Contains(lowerDetail, "latitude"),
|
|
strings.Contains(lowerDetail, "longitude"),
|
|
strings.Contains(lowerDetail, "coordinates"),
|
|
strings.Contains(lowerDetail, "twilight"):
|
|
def := apperrorsx.ErrBaseInvalidPayload
|
|
def.Message = detail
|
|
return def
|
|
default:
|
|
return apperrorsx.ErrBaseInvalidPayload
|
|
}
|
|
case fiber.StatusBadRequest:
|
|
switch {
|
|
case strings.Contains(lowerTitle, "invalid json"):
|
|
return apperrorsx.ErrBaseInvalidJSON
|
|
case strings.Contains(lowerTitle, "create failed"):
|
|
return apperrorsx.ErrBaseCreateFailed
|
|
case strings.Contains(lowerTitle, "update failed"):
|
|
return apperrorsx.ErrBaseUpdateFailed
|
|
case strings.Contains(lowerTitle, "delete failed"):
|
|
return apperrorsx.ErrBaseDeleteFailed
|
|
case strings.Contains(lowerTitle, "list failed"):
|
|
return apperrorsx.ErrBaseListFailed
|
|
default:
|
|
return apperrorsx.ErrBaseInvalidPayload
|
|
}
|
|
default:
|
|
return apperrorsx.ErrInternal
|
|
}
|
|
}
|
|
|
|
func inferBaseErrorCode(status int, title, detail string) (string, string) {
|
|
def := inferBaseDef(status, title, detail)
|
|
return def.Code, def.ErrorCode
|
|
}
|