package handlers import ( "context" "errors" "strconv" "strings" "time" "github.com/gofiber/fiber/v2" "gorm.io/gorm" filemanager "wucher/internal/domain/file_manager" mastersettings "wucher/internal/domain/master_settings" "wucher/internal/shared/pkg/apperrorsx" "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" "wucher/internal/transport/http/validators" ) type MasterSettingsHandler struct { svc mastersettings.Service fileSvc filemanager.Service storage filemanager.ObjectStorage validate *validators.Validator } type managedFileAsset struct { OriginalURL *string ThumbnailURL *string OriginalSizeBytes *int64 } func NewMasterSettingsHandler(svc mastersettings.Service, fileSvc filemanager.Service, storage filemanager.ObjectStorage) *MasterSettingsHandler { return &MasterSettingsHandler{ svc: svc, fileSvc: fileSvc, storage: storage, validate: validators.New(), } } // CreateMasterSettings godoc // @Summary Create master settings // @Description Create a master settings row. // @Tags Master Settings // @Accept json // @Produce json // @Param request body requestdto.MasterSettingsCreateRequest true "JSON:API master settings create request" // @Success 201 {object} responsedto.MasterSettingsResponse // @Failure 422 {object} jsonapi.ErrorDocument // @Failure 400 {object} jsonapi.ErrorDocument // @Router /api/v1/master-settings/create [post] func (h *MasterSettingsHandler) Create(c *fiber.Ctx) error { var req requestdto.MasterSettingsCreateRequest if err := c.BodyParser(&req); err != nil { return writeMasterSettingsErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{ Status: "400", Title: "Invalid JSON", Detail: safeInternalDetail(), }}) } if errs := h.validate.ValidateStruct(req); errs != nil { return writeMasterSettingsErrors(c, fiber.StatusUnprocessableEntity, errs) } companyName := strings.TrimSpace(req.Data.Attributes.CompanyName) if companyName == "" { return writeMasterSettingsErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{ Status: "422", Title: "Validation error", Detail: "company_name is required", Source: &jsonapi.ErrorSource{Pointer: "/data/attributes/company_name"}, }}) } logoFileID, statusCode, errObj := h.resolveLogoFileID(c.UserContext(), req.Data.Attributes.LogoFileUUID, "/data/attributes/logo_file_uuid") if errObj != nil { return writeMasterSettingsErrors(c, statusCode, []jsonapi.ErrorObject{*errObj}) } coverFileID, statusCode, errObj := h.resolveCoverFileID(c.UserContext(), req.Data.Attributes.CoverFileUUID, "/data/attributes/cover_file_uuid") if errObj != nil { return writeMasterSettingsErrors(c, statusCode, []jsonapi.ErrorObject{*errObj}) } row := &mastersettings.MasterSettings{ CompanyName: companyName, Title: optionalTrimmedString(req.Data.Attributes.Title), Subtitle: optionalTrimmedString(req.Data.Attributes.Subtitle), LogoFileID: logoFileID, CoverFileID: coverFileID, LogoURL: nil, CreatedBy: actorUserID(c), UpdatedBy: actorUserID(c), } if err := h.svc.Create(c.UserContext(), row); err != nil { return writeMasterSettingsErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{ Status: "400", Title: "Create failed", Detail: safeInternalDetail(), }}) } if err := syncFileLifecycleSwap(c.UserContext(), h.fileSvc, nil, row.LogoFileID, actorUserID(c)); err != nil { return writeMasterSettingsErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{ Status: "400", Title: "Create failed", Detail: safeInternalDetail(), }}) } if err := syncFileLifecycleSwap(c.UserContext(), h.fileSvc, nil, row.CoverFileID, actorUserID(c)); err != nil { return writeMasterSettingsErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{ Status: "400", Title: "Create failed", Detail: safeInternalDetail(), }}) } created, err := h.svc.GetByID(c.UserContext(), row.ID) if err == nil && created != nil { return response.Write(c, fiber.StatusCreated, h.masterSettingsResource(c, created)) } return response.Write(c, fiber.StatusCreated, h.masterSettingsResource(c, row)) } // GetMasterSettings godoc // @Summary Get current master settings // @Description Return singleton master settings object (latest active row). // @Tags Master Settings // @Produce json // @Success 200 {object} responsedto.MasterSettingsResponse // @Failure 400 {object} jsonapi.ErrorDocument // @Router /api/v1/master-settings/get [get] func (h *MasterSettingsHandler) Get(c *fiber.Ctx) error { rows, _, err := h.svc.List(c.UserContext(), "", "-updated_at", 1, 0) if err != nil { return writeMasterSettingsErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{ Status: "400", Title: "Get failed", Detail: safeInternalDetail(), }}) } if len(rows) == 0 { return response.Write(c, fiber.StatusOK, responsedto.MasterSettingsResponse{ Data: responsedto.MasterSettingsResource{ Type: "master_settings", ID: "", Attributes: responsedto.MasterSettingsAttributes{ CompanyName: "", }, }, }) } return response.Write(c, fiber.StatusOK, h.masterSettingsResource(c, &rows[0])) } // UpsertMicrosoftEntraSSO godoc // @Summary Upsert Microsoft Entra SSO settings // @Description Create or update Microsoft Entra SSO settings as master settings. // @Tags Master Settings // @Accept json // @Produce json // @Param request body requestdto.MasterSettingsMicrosoftEntraUpsertRequest true "JSON:API Microsoft Entra SSO upsert request" // @Success 200 {object} responsedto.MasterSettingsMicrosoftEntraResponse // @Failure 422 {object} jsonapi.ErrorDocument // @Failure 400 {object} jsonapi.ErrorDocument // @Router /api/v1/master-settings/sso/microsoft [put] func (h *MasterSettingsHandler) UpsertMicrosoftEntraSSO(c *fiber.Ctx) error { var req requestdto.MasterSettingsMicrosoftEntraUpsertRequest if err := c.BodyParser(&req); err != nil { return writeMasterSettingsErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{ Status: "400", Title: "Invalid JSON", Detail: safeInternalDetail(), }}) } if errs := h.validate.ValidateStruct(req); errs != nil { return writeMasterSettingsErrors(c, fiber.StatusUnprocessableEntity, errs) } view, err := h.svc.UpsertMicrosoftEntraConfig(c.UserContext(), mastersettings.MicrosoftEntraConfigInput{ TenantID: req.Data.Attributes.TenantID, ClientID: req.Data.Attributes.ClientID, ClientSecret: req.Data.Attributes.ClientSecret, RedirectURL: req.Data.Attributes.RedirectURL, Authority: req.Data.Attributes.Authority, Scopes: req.Data.Attributes.Scopes, }, actorUserID(c)) if err != nil { status := fiber.StatusBadRequest title := "Upsert failed" if isMicrosoftEntraValidationError(err) { status = fiber.StatusUnprocessableEntity title = "Validation error" } return writeMasterSettingsErrors(c, status, []jsonapi.ErrorObject{{ Status: strconv.Itoa(status), Title: title, Detail: safeInternalDetail(), }}) } return response.Write(c, fiber.StatusOK, h.microsoftEntraResource(view)) } // GetMicrosoftEntraSSO godoc // @Summary Get Microsoft Entra SSO settings // @Description Get Microsoft Entra SSO settings from master settings. // @Tags Master Settings // @Produce json // @Success 200 {object} responsedto.MasterSettingsMicrosoftEntraResponse // @Failure 404 {object} jsonapi.ErrorDocument // @Failure 400 {object} jsonapi.ErrorDocument // @Router /api/v1/master-settings/sso/microsoft [get] func (h *MasterSettingsHandler) GetMicrosoftEntraSSO(c *fiber.Ctx) error { view, err := h.svc.GetMicrosoftEntraConfig(c.UserContext()) if err != nil { return writeMasterSettingsErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{ Status: "400", Title: "Get failed", Detail: safeInternalDetail(), }}) } if view == nil { return writeMasterSettingsErrors(c, fiber.StatusNotFound, []jsonapi.ErrorObject{{ Status: "404", Title: "Not found", Detail: "microsoft entra sso settings not found", }}) } return response.Write(c, fiber.StatusOK, h.microsoftEntraResource(view)) } // UpdateMasterSettings godoc // @Summary Update master settings (partial) // @Description Patch master settings by ID. // @Tags Master Settings // @Accept json // @Produce json // @Param uuid path string true "Master Settings UUID (UUIDv7)" // @Param request body requestdto.MasterSettingsUpdateRequest true "JSON:API master settings update request" // @Success 200 {object} responsedto.MasterSettingsResponse // @Failure 422 {object} jsonapi.ErrorDocument // @Failure 404 {object} jsonapi.ErrorDocument // @Router /api/v1/master-settings/update/{uuid} [patch] func (h *MasterSettingsHandler) Update(c *fiber.Ctx) error { uuidStr := c.Params("uuid") id, err := uuidv7.ParseString(uuidStr) if err != nil { return writeMasterSettingsErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{ Status: "422", Title: "Validation error", Detail: "uuid is invalid UUID", Source: &jsonapi.ErrorSource{Pointer: "/path/uuid"}, }}) } var req requestdto.MasterSettingsUpdateRequest if err := c.BodyParser(&req); err != nil { return writeMasterSettingsErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{ Status: "400", Title: "Invalid JSON", Detail: safeInternalDetail(), }}) } if errs := h.validate.ValidateStruct(req); errs != nil { return writeMasterSettingsErrors(c, fiber.StatusUnprocessableEntity, errs) } if strings.TrimSpace(req.Data.ID) == "" { return writeMasterSettingsErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{ Status: "422", Title: "Validation error", Detail: "id is required", Source: &jsonapi.ErrorSource{Pointer: "/data/id"}, }}) } if req.Data.ID != uuidStr { return writeMasterSettingsErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{ Status: "422", Title: "Validation error", Detail: "id does not match path uuid", Source: &jsonapi.ErrorSource{Pointer: "/data/id"}, }}) } attrs := req.Data.Attributes if attrs.CompanyName == nil && attrs.Title == nil && attrs.Subtitle == nil && attrs.LogoFileUUID == nil && attrs.CoverFileUUID == nil { return writeMasterSettingsErrors(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.GetByID(c.UserContext(), id) if err != nil || existing == nil { return writeMasterSettingsErrors(c, fiber.StatusNotFound, []jsonapi.ErrorObject{{ Status: "404", Title: "Not found", Detail: "master settings not found", }}) } prevLogoFileID := append([]byte(nil), existing.LogoFileID...) prevCoverFileID := append([]byte(nil), existing.CoverFileID...) if attrs.CompanyName != nil { v := strings.TrimSpace(*attrs.CompanyName) if v == "" { return writeMasterSettingsErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{ Status: "422", Title: "Validation error", Detail: "company_name cannot be empty", Source: &jsonapi.ErrorSource{Pointer: "/data/attributes/company_name"}, }}) } existing.CompanyName = v } if attrs.Title != nil { existing.Title = strings.TrimSpace(*attrs.Title) } if attrs.Subtitle != nil { existing.Subtitle = strings.TrimSpace(*attrs.Subtitle) } if attrs.LogoFileUUID != nil { if hasBlankOptionalString(attrs.LogoFileUUID) { existing.LogoFileID = nil } else { logoFileID, statusCode, errObj := h.resolveLogoFileID(c.UserContext(), attrs.LogoFileUUID, "/data/attributes/logo_file_uuid") if errObj != nil { return writeMasterSettingsErrors(c, statusCode, []jsonapi.ErrorObject{*errObj}) } existing.LogoFileID = logoFileID // Keep manual URL disabled once logo is managed by file manager metadata. existing.LogoURL = nil } } if attrs.CoverFileUUID != nil { if hasBlankOptionalString(attrs.CoverFileUUID) { existing.CoverFileID = nil } else { coverFileID, statusCode, errObj := h.resolveCoverFileID(c.UserContext(), attrs.CoverFileUUID, "/data/attributes/cover_file_uuid") if errObj != nil { return writeMasterSettingsErrors(c, statusCode, []jsonapi.ErrorObject{*errObj}) } existing.CoverFileID = coverFileID } } existing.UpdatedBy = actorUserID(c) if err := h.svc.Update(c.UserContext(), existing); err != nil { return writeMasterSettingsErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{ Status: "400", Title: "Update failed", Detail: safeInternalDetail(), }}) } if err := syncFileLifecycleSwap(c.UserContext(), h.fileSvc, prevLogoFileID, existing.LogoFileID, actorUserID(c)); err != nil { return writeMasterSettingsErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{ Status: "400", Title: "Update failed", Detail: safeInternalDetail(), }}) } if err := syncFileLifecycleSwap(c.UserContext(), h.fileSvc, prevCoverFileID, existing.CoverFileID, actorUserID(c)); err != nil { return writeMasterSettingsErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{ Status: "400", Title: "Update failed", Detail: safeInternalDetail(), }}) } updated, err := h.svc.GetByID(c.UserContext(), existing.ID) if err == nil && updated != nil { return response.Write(c, fiber.StatusOK, h.masterSettingsResource(c, updated)) } return response.Write(c, fiber.StatusOK, h.masterSettingsResource(c, existing)) } // DeleteMasterSettings godoc // @Summary Delete master settings // @Tags Master Settings // @Produce json // @Param uuid path string true "Master Settings UUID (UUIDv7)" // @Success 200 {object} jsonapi.Resource // @Failure 422 {object} jsonapi.ErrorDocument // @Failure 404 {object} jsonapi.ErrorDocument // @Router /api/v1/master-settings/delete/{uuid} [delete] func (h *MasterSettingsHandler) Delete(c *fiber.Ctx) error { uuidStr := c.Params("uuid") id, err := uuidv7.ParseString(uuidStr) if err != nil { return writeMasterSettingsErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{ Status: "422", Title: "Validation error", Detail: "uuid is invalid UUID", Source: &jsonapi.ErrorSource{Pointer: "/path/uuid"}, }}) } existing, _ := h.svc.GetByID(c.UserContext(), id) var prevLogoFileID []byte var prevCoverFileID []byte if existing != nil { prevLogoFileID = append([]byte(nil), existing.LogoFileID...) prevCoverFileID = append([]byte(nil), existing.CoverFileID...) } if err := h.svc.Delete(c.UserContext(), id, actorUserID(c)); err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return writeMasterSettingsErrors(c, fiber.StatusNotFound, []jsonapi.ErrorObject{{ Status: "404", Title: "Not found", Detail: "master settings not found", }}) } return writeMasterSettingsErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{ Status: "400", Title: "Delete failed", Detail: safeInternalDetail(), }}) } if err := syncFileLifecycleSwap(c.UserContext(), h.fileSvc, prevLogoFileID, nil, actorUserID(c)); err != nil { return writeMasterSettingsErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{ Status: "400", Title: "Delete failed", Detail: safeInternalDetail(), }}) } if err := syncFileLifecycleSwap(c.UserContext(), h.fileSvc, prevCoverFileID, nil, actorUserID(c)); err != nil { return writeMasterSettingsErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{ Status: "400", Title: "Delete failed", Detail: safeInternalDetail(), }}) } return response.Write(c, fiber.StatusOK, jsonapi.Resource{ Type: "master_settings_delete", Attributes: map[string]any{ "deleted": true, }, }) } func (h *MasterSettingsHandler) masterSettingsResource(c *fiber.Ctx, row *mastersettings.MasterSettings) responsedto.MasterSettingsResource { logoFileUUID := uuidStringPtrOrNil(row.LogoFileID) logoURL := h.resolveLogoURL(c, row, logoFileUUID) logoThumbURL := h.resolveLogoThumbnailURL(c, row, logoFileUUID) logoSize := h.resolveLogoOriginalSize(c, row, logoFileUUID) coverFileUUID := uuidStringPtrOrNil(row.CoverFileID) coverURL := h.resolveCoverURL(c, row, coverFileUUID) coverThumbURL := h.resolveCoverThumbnailURL(c, row, coverFileUUID) coverSize := h.resolveCoverOriginalSize(c, row, coverFileUUID) return responsedto.MasterSettingsResource{ Type: "master_settings", ID: idString(row.ID), Attributes: responsedto.MasterSettingsAttributes{ CompanyName: row.CompanyName, Title: row.Title, Subtitle: row.Subtitle, LogoFileUUID: logoFileUUID, LogoURL: logoURL, LogoThumbURL: logoThumbURL, LogoSizeBytes: logoSize, CoverFileUUID: coverFileUUID, CoverURL: coverURL, CoverThumbURL: coverThumbURL, CoverSizeBytes: coverSize, CreatedAt: row.CreatedAt.UTC().Format(time.RFC3339), CreatedBy: uuidStringOrEmpty(row.CreatedBy), UpdatedAt: row.UpdatedAt.UTC().Format(time.RFC3339), UpdatedBy: uuidStringOrEmpty(row.UpdatedBy), DeletedAt: timeStringOrEmpty(row.DeletedAt), DeletedBy: uuidStringOrEmpty(row.DeletedBy), }, } } func optionalTrimmedString(v *string) string { if v == nil { return "" } return strings.TrimSpace(*v) } func (h *MasterSettingsHandler) resolveLogoURL(c *fiber.Ctx, row *mastersettings.MasterSettings, logoFileUUID *string) *string { legacyURL := strings.TrimSpace(masterSettingsDerefString(row.LogoURL)) if logoFileUUID == nil || strings.TrimSpace(*logoFileUUID) == "" { if legacyURL == "" { return nil } return stringPointer(legacyURL) } return h.resolveManagedFileAsset(c, row.LogoFileID, logoFileUUID).OriginalURL } func (h *MasterSettingsHandler) resolveCoverURL(c *fiber.Ctx, row *mastersettings.MasterSettings, coverFileUUID *string) *string { return h.resolveManagedFileAsset(c, row.CoverFileID, coverFileUUID).OriginalURL } func (h *MasterSettingsHandler) resolveManagedFileURL(c *fiber.Ctx, fileID []byte, fileUUID *string) *string { return h.resolveManagedFileAsset(c, fileID, fileUUID).OriginalURL } func (h *MasterSettingsHandler) resolveLogoThumbnailURL(c *fiber.Ctx, row *mastersettings.MasterSettings, logoFileUUID *string) *string { if row == nil { return nil } return h.resolveManagedFileAsset(c, row.LogoFileID, logoFileUUID).ThumbnailURL } func (h *MasterSettingsHandler) resolveCoverThumbnailURL(c *fiber.Ctx, row *mastersettings.MasterSettings, coverFileUUID *string) *string { if row == nil { return nil } return h.resolveManagedFileAsset(c, row.CoverFileID, coverFileUUID).ThumbnailURL } func (h *MasterSettingsHandler) resolveLogoOriginalSize(c *fiber.Ctx, row *mastersettings.MasterSettings, logoFileUUID *string) *int64 { if row == nil { return nil } return h.resolveManagedFileAsset(c, row.LogoFileID, logoFileUUID).OriginalSizeBytes } func (h *MasterSettingsHandler) resolveCoverOriginalSize(c *fiber.Ctx, row *mastersettings.MasterSettings, coverFileUUID *string) *int64 { if row == nil { return nil } return h.resolveManagedFileAsset(c, row.CoverFileID, coverFileUUID).OriginalSizeBytes } func (h *MasterSettingsHandler) resolveManagedFileAsset(c *fiber.Ctx, fileID []byte, fileUUID *string) managedFileAsset { if fileUUID == nil || strings.TrimSpace(*fileUUID) == "" { return managedFileAsset{} } if h.fileSvc == nil { return managedFileAsset{} } fileRow, err := h.fileSvc.GetFileByID(c.UserContext(), fileID) if err != nil || fileRow == nil { return managedFileAsset{} } resolved := resolveFileManagerDownloadURLs(c.UserContext(), h.storage, fileRow) asset := managedFileAsset{ OriginalSizeBytes: int64Ptr(resolved.OriginalSizeBytes), } if url := strings.TrimSpace(resolved.OriginalURL); url != "" { asset.OriginalURL = stringPointer(url) } if url := strings.TrimSpace(resolved.ThumbnailURL); url != "" { asset.ThumbnailURL = stringPointer(url) } return asset } func stringPointer(v string) *string { return &v } func masterSettingsDerefString(v *string) string { if v == nil { return "" } return *v } func uuidStringPtrOrNil(id []byte) *string { v := idString(id) if strings.TrimSpace(v) == "" { return nil } return &v } func (h *MasterSettingsHandler) microsoftEntraResource(view *mastersettings.MicrosoftEntraConfigView) responsedto.MasterSettingsMicrosoftEntraResource { if view == nil { return responsedto.MasterSettingsMicrosoftEntraResource{Type: "master_settings_microsoft_entra_sso"} } return responsedto.MasterSettingsMicrosoftEntraResource{ Type: "master_settings_microsoft_entra_sso", Attributes: responsedto.MasterSettingsMicrosoftEntraAttributes{ TenantID: view.TenantID, ClientID: view.ClientID, ClientSecretMasked: view.ClientSecretMasked, RedirectURL: view.RedirectURL, Authority: view.Authority, Scopes: view.Scopes, }, } } func isMicrosoftEntraValidationError(err error) bool { if err == nil { return false } msg := err.Error() return strings.Contains(msg, "MS_ENTRA_") || strings.Contains(msg, "required") || strings.Contains(msg, "valid absolute URL") } func (h *MasterSettingsHandler) resolveLogoFileID(ctx context.Context, raw *string, pointer string) ([]byte, int, *jsonapi.ErrorObject) { return h.resolveManagedFileID(ctx, raw, pointer, "logo_file_uuid") } func (h *MasterSettingsHandler) resolveCoverFileID(ctx context.Context, raw *string, pointer string) ([]byte, int, *jsonapi.ErrorObject) { return h.resolveManagedFileID(ctx, raw, pointer, "cover_file_uuid") } func (h *MasterSettingsHandler) resolveManagedFileID(ctx context.Context, raw *string, pointer, fieldName string) ([]byte, int, *jsonapi.ErrorObject) { if raw == nil { return nil, 0, nil } v := strings.TrimSpace(*raw) if v == "" { return nil, 0, nil } id, err := uuidv7.ParseString(v) if err != nil { return nil, fiber.StatusUnprocessableEntity, &jsonapi.ErrorObject{ Status: "422", Title: "Validation error", Detail: fieldName + " is invalid UUID", Source: &jsonapi.ErrorSource{Pointer: pointer}, } } if h.fileSvc == nil { return nil, fiber.StatusServiceUnavailable, &jsonapi.ErrorObject{ Status: "503", Title: "File manager unavailable", Detail: "file manager service is not configured", Source: &jsonapi.ErrorSource{Pointer: pointer}, } } fileRow, err := h.fileSvc.GetFileByID(ctx, id) if err != nil { return nil, fiber.StatusBadRequest, &jsonapi.ErrorObject{ Status: "400", Title: "Validation error", Detail: safeInternalDetail(), Source: &jsonapi.ErrorSource{Pointer: pointer}, } } if fileRow == nil { return nil, fiber.StatusUnprocessableEntity, &jsonapi.ErrorObject{ Status: "422", Title: "Validation error", Detail: fieldName + " is not found", Source: &jsonapi.ErrorSource{Pointer: pointer}, } } if !strings.EqualFold(strings.TrimSpace(fileRow.Status), filemanager.FileStatusReady) { return nil, fiber.StatusUnprocessableEntity, &jsonapi.ErrorObject{ Status: "422", Title: "Validation error", Detail: fieldName + " is not ready. finalize upload via /api/v1/file-manager/files/create", Source: &jsonapi.ErrorSource{Pointer: pointer}, } } return id, 0, nil } func writeMasterSettingsErrors(c *fiber.Ctx, status int, errs []jsonapi.ErrorObject) error { enriched := make([]jsonapi.ErrorObject, 0, len(errs)) statusStr := strconv.Itoa(status) for i := range errs { e := errs[i] e.Status = statusStr def := inferMasterSettingsDef(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 } e.Detail = def.Message enriched = append(enriched, e) } return response.WriteErrors(c, status, enriched) } func inferMasterSettingsDef(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.ErrMasterSettingsNotFound case fiber.StatusUnprocessableEntity: switch { case strings.Contains(lowerDetail, "uuid is invalid"): return apperrorsx.ErrMasterSettingsInvalidUUID case strings.Contains(lowerDetail, "id is required"): return apperrorsx.ErrMasterSettingsIDRequired case strings.Contains(lowerDetail, "id does not match"): return apperrorsx.ErrMasterSettingsIDMismatch case strings.Contains(lowerDetail, "at least one attribute"): return apperrorsx.ErrMasterSettingsAttributesRequired case strings.Contains(lowerDetail, "company_name is required"), strings.Contains(lowerDetail, "company_name cannot be empty"): return apperrorsx.ErrMasterSettingsCompanyNameRequired case strings.Contains(lowerDetail, "logo_file_uuid"): return apperrorsx.ErrMasterSettingsLogoFileInvalid case strings.Contains(lowerDetail, "cover_file_uuid"): return apperrorsx.ErrMasterSettingsCoverFileInvalid default: return apperrorsx.ErrMasterSettingsInvalidPayload } case fiber.StatusBadRequest: switch { case strings.Contains(lowerTitle, "invalid json"): return apperrorsx.ErrMasterSettingsInvalidJSON case strings.Contains(lowerTitle, "create failed"): return apperrorsx.ErrMasterSettingsCreateFailed case strings.Contains(lowerTitle, "update failed"): return apperrorsx.ErrMasterSettingsUpdateFailed case strings.Contains(lowerTitle, "delete failed"): return apperrorsx.ErrMasterSettingsDeleteFailed case strings.Contains(lowerTitle, "get failed"): return apperrorsx.ErrMasterSettingsGetFailed case strings.Contains(lowerTitle, "upsert failed"): return apperrorsx.ErrMasterSettingsMicrosoftUpsert default: return apperrorsx.ErrMasterSettingsInvalidPayload } default: return apperrorsx.ErrInternal } }