diff --git a/api/http/handlers/presenter/create_survey.go b/api/http/handlers/presenter/create_survey.go new file mode 100644 index 0000000..6b64c9b --- /dev/null +++ b/api/http/handlers/presenter/create_survey.go @@ -0,0 +1,24 @@ +// api/http/dto/create_survey.go +package presenter + +import "time" + +type CreateSurveyRequest struct { + Title string `json:"title" validate:"required,min=3,max=100"` + CreationTime time.Time `json:"creation_time" validate:"required"` + StartTime *time.Time `json:"start_time" validate:"required"` + EndTime *time.Time `json:"end_time" validate:"required,gtfield=StartTime"` + RandomOrder bool `json:"random_order"` + AllowReturn bool `json:"allow_return"` + NumParticipationAttempts int `json:"num_participation_attempts" validate:"gte=1,lte=10"` + ResponseTime int `json:"response_time" validate:"gte=60,lte=86400"` // Between 1 minute and 1 day + AnonymityLevel string `json:"anonymity_level" validate:"required,oneof=visible_to_creator visible_to_creator_and_admins anonymous"` + DemographicRestrictions string `json:"demographic_restrictions" validate:"omitempty,json"` + ResponseModification bool `json:"response_modification"` +} + +type CreateSurveyResponse struct { + ID uint `json:"id"` + Title string `json:"title"` + OwnerID uint `json:"owner_id"` +} diff --git a/api/http/handlers/presenter/get_survey.go b/api/http/handlers/presenter/get_survey.go new file mode 100644 index 0000000..9761bc8 --- /dev/null +++ b/api/http/handlers/presenter/get_survey.go @@ -0,0 +1,19 @@ +// api/http/dto/get_survey.go +package presenter + +import "time" + +type GetSurveyResponse struct { + ID uint `json:"id"` + Title string `json:"title"` + CreationTime time.Time `json:"creation_time"` + StartTime *time.Time `json:"start_time"` + EndTime *time.Time `json:"end_time"` + RandomOrder bool `json:"random_order"` + AllowReturn bool `json:"allow_return"` + NumParticipationAttempts int `json:"num_participation_attempts"` + ResponseTime int `json:"response_time"` + AnonymityLevel string `json:"anonymity_level"` + DemographicRestrictions string `json:"demographic_restrictions"` + ResponseModification bool `json:"response_modification"` +} diff --git a/api/http/handlers/presenter/update_survey.go b/api/http/handlers/presenter/update_survey.go new file mode 100644 index 0000000..f029ad8 --- /dev/null +++ b/api/http/handlers/presenter/update_survey.go @@ -0,0 +1,22 @@ +// api/http/dto/update_survey.go +package presenter + +import "time" + +type UpdateSurveyRequest struct { + Title *string `json:"title,omitempty"` + StartTime *time.Time `json:"start_time,omitempty"` + EndTime *time.Time `json:"end_time,omitempty"` + RandomOrder *bool `json:"random_order,omitempty"` + AllowReturn *bool `json:"allow_return,omitempty"` + NumParticipationAttempts *int `json:"num_participation_attempts,omitempty"` + ResponseTime *int `json:"response_time,omitempty"` + AnonymityLevel *string `json:"anonymity_level,omitempty"` + DemographicRestrictions *string `json:"demographic_restrictions,omitempty"` + ResponseModification *bool `json:"response_modification,omitempty"` +} + +type UpdateSurveyResponse struct { + ID uint `json:"id"` + Title string `json:"title"` +} diff --git a/api/http/handlers/surevey.go b/api/http/handlers/surevey.go new file mode 100644 index 0000000..8ba64d9 --- /dev/null +++ b/api/http/handlers/surevey.go @@ -0,0 +1,174 @@ +// api/http/handlers/survey.go +package handlers + +import ( + "fmt" + "net/http" + "strconv" + + "golipors/api/http/handlers/presenter" + "golipors/api/http/mapper" + "golipors/internal/survey/port" + + "github.com/go-playground/validator/v10" + "github.com/gofiber/fiber/v2" +) + +var validate = validator.New() + +type SurveyHandler struct { + service port.Service +} + +func NewSurveyHandler(service port.Service) *SurveyHandler { + return &SurveyHandler{ + service: service, + } +} + +func (h *SurveyHandler) RegisterRoutes(api fiber.Router) { + api.Post("/surveys", h.CreateSurvey) + api.Get("/surveys/:id", h.GetSurveyByID) + api.Put("/surveys/:id", h.UpdateSurvey) + api.Delete("/surveys/:id", h.DeleteSurvey) +} + +// CreateSurvey handles POST /api/surveys +func (h *SurveyHandler) CreateSurvey(c *fiber.Ctx) error { + var req presenter.CreateSurveyRequest + + // Parse the request body + if err := c.BodyParser(&req); err != nil { + return c.Status(http.StatusBadRequest).JSON(fiber.Map{ + "error": "Invalid request body", + }) + } + + // Validate request input + if err := validate.Struct(req); err != nil { + validationErrors := make(map[string]string) + for _, e := range err.(validator.ValidationErrors) { + validationErrors[e.Field()] = fmt.Sprintf("Validation failed on '%s' with tag '%s'", e.Field(), e.Tag()) + } + return c.Status(http.StatusBadRequest).JSON(fiber.Map{ + "error": "Validation failed", + "details": validationErrors, + }) + } + + // Example: Retrieve ownerID from authenticated user context; here, we use a placeholder + ownerID := uint(1) + + // Convert from presenter to domain model + survey := mapper.CreateSurveyRequestToDomain(req, ownerID) + + // Create survey using the service + surveyID, err := h.service.CreateSurvey(c.Context(), survey) + if err != nil { + return c.Status(http.StatusInternalServerError).JSON(fiber.Map{ + "error": err.Error(), + }) + } + + // Build response using presenter + response := presenter.CreateSurveyResponse{ + ID: surveyID, + Title: survey.Title, + OwnerID: ownerID, + } + + return c.Status(http.StatusCreated).JSON(response) +} + +// GetSurveyByID handles GET /api/surveys/:id +func (h *SurveyHandler) GetSurveyByID(c *fiber.Ctx) error { + idParam := c.Params("id") + id, err := strconv.ParseUint(idParam, 10, 32) + if err != nil { + return c.Status(http.StatusBadRequest).JSON(fiber.Map{ + "error": "Invalid survey ID", + }) + } + + survey, err := h.service.GetSurveyByID(c.Context(), uint(id)) + if err != nil { + return c.Status(http.StatusInternalServerError).JSON(fiber.Map{ + "error": err.Error(), + }) + } + if survey == nil { + return c.Status(http.StatusNotFound).JSON(fiber.Map{ + "error": "Survey not found", + }) + } + + response := mapper.DomainSurveyToGetSurveyResponse(*survey) + return c.Status(http.StatusOK).JSON(response) +} + +// UpdateSurvey handles PUT /api/surveys/:id +func (h *SurveyHandler) UpdateSurvey(c *fiber.Ctx) error { + idParam := c.Params("id") + id, err := strconv.ParseUint(idParam, 10, 32) + if err != nil { + return c.Status(http.StatusBadRequest).JSON(fiber.Map{ + "error": "Invalid survey ID", + }) + } + + var req presenter.UpdateSurveyRequest + if err := c.BodyParser(&req); err != nil { + return c.Status(http.StatusBadRequest).JSON(fiber.Map{ + "error": "Invalid request body", + }) + } + + // Convert from presenter to domain model + survey := mapper.UpdateSurveyRequestToDomain(req) + survey.ID = uint(id) + + if err := h.service.UpdateSurvey(c.Context(), survey); err != nil { + if err.Error() == "survey not found" { + return c.Status(http.StatusNotFound).JSON(fiber.Map{ + "error": "Survey not found", + }) + } + return c.Status(http.StatusInternalServerError).JSON(fiber.Map{ + "error": err.Error(), + }) + } + + updatedSurvey, err := h.service.GetSurveyByID(c.Context(), uint(id)) + if err != nil || updatedSurvey == nil { + return c.Status(http.StatusInternalServerError).JSON(fiber.Map{ + "error": "Failed to retrieve updated survey", + }) + } + + response := mapper.DomainSurveyToUpdateSurveyResponse(*updatedSurvey) + return c.Status(http.StatusOK).JSON(response) +} + +// DeleteSurvey handles DELETE /api/surveys/:id +func (h *SurveyHandler) DeleteSurvey(c *fiber.Ctx) error { + idParam := c.Params("id") + id, err := strconv.ParseUint(idParam, 10, 32) + if err != nil { + return c.Status(http.StatusBadRequest).JSON(fiber.Map{ + "error": "Invalid survey ID", + }) + } + + if err := h.service.DeleteSurvey(c.Context(), uint(id)); err != nil { + if err.Error() == "survey not found" { + return c.Status(http.StatusNotFound).JSON(fiber.Map{ + "error": "Survey not found", + }) + } + return c.Status(http.StatusInternalServerError).JSON(fiber.Map{ + "error": err.Error(), + }) + } + + return c.SendStatus(http.StatusOK) +} diff --git a/api/http/mapper/survey_mapper.go b/api/http/mapper/survey_mapper.go new file mode 100644 index 0000000..8eeec3e --- /dev/null +++ b/api/http/mapper/survey_mapper.go @@ -0,0 +1,93 @@ +// api/http/mapper/survey_mapper.go +package mapper + +import ( + "golipors/api/http/handlers/presenter" + "golipors/internal/survey/domain" +) + +func CreateSurveyRequestToDomain(req presenter.CreateSurveyRequest, ownerID uint) domain.Survey { + return domain.Survey{ + Title: req.Title, + CreationTime: req.CreationTime, + StartTime: req.StartTime, + EndTime: req.EndTime, + RandomOrder: req.RandomOrder, + AllowReturn: req.AllowReturn, + NumParticipationAttempts: req.NumParticipationAttempts, + ResponseTime: req.ResponseTime, + AnonymityLevel: req.AnonymityLevel, + DemographicRestrictions: req.DemographicRestrictions, + ResponseModification: req.ResponseModification, + OwnerID: ownerID, + } +} + +func DomainSurveyToCreateSurveyResponse(survey domain.Survey) presenter.CreateSurveyResponse { + return presenter.CreateSurveyResponse{ + ID: survey.ID, + Title: survey.Title, + OwnerID: survey.OwnerID, + } +} + +func DomainSurveyToGetSurveyResponse(survey domain.Survey) presenter.GetSurveyResponse { + return presenter.GetSurveyResponse{ + ID: survey.ID, + Title: survey.Title, + CreationTime: survey.CreationTime, + StartTime: survey.StartTime, + EndTime: survey.EndTime, + RandomOrder: survey.RandomOrder, + AllowReturn: survey.AllowReturn, + NumParticipationAttempts: survey.NumParticipationAttempts, + ResponseTime: survey.ResponseTime, + AnonymityLevel: survey.AnonymityLevel, + DemographicRestrictions: survey.DemographicRestrictions, + ResponseModification: survey.ResponseModification, + } +} + +func UpdateSurveyRequestToDomain(req presenter.UpdateSurveyRequest) domain.Survey { + survey := domain.Survey{} + + if req.Title != nil { + survey.Title = *req.Title + } + if req.StartTime != nil { + survey.StartTime = req.StartTime + } + if req.EndTime != nil { + survey.EndTime = req.EndTime + } + if req.RandomOrder != nil { + survey.RandomOrder = *req.RandomOrder + } + if req.AllowReturn != nil { + survey.AllowReturn = *req.AllowReturn + } + if req.NumParticipationAttempts != nil { + survey.NumParticipationAttempts = *req.NumParticipationAttempts + } + if req.ResponseTime != nil { + survey.ResponseTime = *req.ResponseTime + } + if req.AnonymityLevel != nil { + survey.AnonymityLevel = *req.AnonymityLevel + } + if req.DemographicRestrictions != nil { + survey.DemographicRestrictions = *req.DemographicRestrictions + } + if req.ResponseModification != nil { + survey.ResponseModification = *req.ResponseModification + } + + return survey +} + +func DomainSurveyToUpdateSurveyResponse(survey domain.Survey) presenter.UpdateSurveyResponse { + return presenter.UpdateSurveyResponse{ + ID: survey.ID, + Title: survey.Title, + } +} diff --git a/internal/survey/domain/survey.go b/internal/survey/domain/survey.go new file mode 100644 index 0000000..fa3a9ed --- /dev/null +++ b/internal/survey/domain/survey.go @@ -0,0 +1,38 @@ +// internal/survey/domain/survey.go +package domain + +import ( + "errors" + "time" +) + +type Survey struct { + ID uint + Title string + CreationTime time.Time + StartTime *time.Time + EndTime *time.Time + RandomOrder bool + AllowReturn bool + NumParticipationAttempts int + ResponseTime int // in seconds + AnonymityLevel string + OwnerID uint + DemographicRestrictions string + ResponseModification bool + CreatedAt time.Time + UpdatedAt time.Time +} + +func (s *Survey) Validate() error { + if s.Title == "" { + return errors.New("title cannot be empty") + } + if s.CreationTime.IsZero() { + return errors.New("creation_time is required") + } + if s.AnonymityLevel == "" { + return errors.New("anonymity_level cannot be empty") + } + return nil +} diff --git a/internal/survey/port/repository.go b/internal/survey/port/repository.go new file mode 100644 index 0000000..f7a5913 --- /dev/null +++ b/internal/survey/port/repository.go @@ -0,0 +1,14 @@ +// internal/survey/port/repository.go +package port + +import ( + "context" + "golipors/internal/survey/domain" +) + +type Repository interface { + CreateSurvey(ctx context.Context, survey domain.Survey) (uint, error) + GetSurveyByID(ctx context.Context, id uint) (*domain.Survey, error) + UpdateSurvey(ctx context.Context, survey domain.Survey) error + DeleteSurvey(ctx context.Context, id uint) error +} diff --git a/internal/survey/port/service.go b/internal/survey/port/service.go new file mode 100644 index 0000000..cc2c995 --- /dev/null +++ b/internal/survey/port/service.go @@ -0,0 +1,14 @@ +// internal/survey/port/service.go +package port + +import ( + "context" + "golipors/internal/survey/domain" +) + +type Service interface { + CreateSurvey(ctx context.Context, survey domain.Survey) (uint, error) + GetSurveyByID(ctx context.Context, id uint) (*domain.Survey, error) + UpdateSurvey(ctx context.Context, survey domain.Survey) error + DeleteSurvey(ctx context.Context, id uint) error +} diff --git a/internal/survey/service.go b/internal/survey/service.go new file mode 100644 index 0000000..628492a --- /dev/null +++ b/internal/survey/service.go @@ -0,0 +1,82 @@ +// internal/survey/service/service.go +package service + +import ( + "context" + "errors" + "golipors/internal/survey/domain" + "golipors/internal/survey/port" +) + +type surveyService struct { + repository port.Repository +} + +func NewService(repository port.Repository) port.Service { + return &surveyService{ + repository: repository, + } +} + +func (s *surveyService) CreateSurvey(ctx context.Context, survey domain.Survey) (uint, error) { + if err := survey.Validate(); err != nil { + return 0, err + } + return s.repository.CreateSurvey(ctx, survey) +} + +func (s *surveyService) GetSurveyByID(ctx context.Context, id uint) (*domain.Survey, error) { + return s.repository.GetSurveyByID(ctx, id) +} + +func (s *surveyService) UpdateSurvey(ctx context.Context, survey domain.Survey) error { + existingSurvey, err := s.repository.GetSurveyByID(ctx, survey.ID) + if err != nil { + return err + } + if existingSurvey == nil { + return errors.New("survey not found") + } + + // Merge existing and updated survey data + if survey.Title != "" { + existingSurvey.Title = survey.Title + } + if survey.StartTime != nil { + existingSurvey.StartTime = survey.StartTime + } + if survey.EndTime != nil { + existingSurvey.EndTime = survey.EndTime + } + if survey.RandomOrder != existingSurvey.RandomOrder { + existingSurvey.RandomOrder = survey.RandomOrder + } + if survey.AllowReturn != existingSurvey.AllowReturn { + existingSurvey.AllowReturn = survey.AllowReturn + } + if survey.NumParticipationAttempts != 0 { + existingSurvey.NumParticipationAttempts = survey.NumParticipationAttempts + } + if survey.ResponseTime != 0 { + existingSurvey.ResponseTime = survey.ResponseTime + } + if survey.AnonymityLevel != "" { + existingSurvey.AnonymityLevel = survey.AnonymityLevel + } + if survey.DemographicRestrictions != "" { + existingSurvey.DemographicRestrictions = survey.DemographicRestrictions + } + if survey.ResponseModification != existingSurvey.ResponseModification { + existingSurvey.ResponseModification = survey.ResponseModification + } + + if err := existingSurvey.Validate(); err != nil { + return err + } + + return s.repository.UpdateSurvey(ctx, *existingSurvey) +} + +func (s *surveyService) DeleteSurvey(ctx context.Context, id uint) error { + return s.repository.DeleteSurvey(ctx, id) +} diff --git a/pkg/adapters/storage/mapper/survey_mapper.go b/pkg/adapters/storage/mapper/survey_mapper.go new file mode 100644 index 0000000..6e574ce --- /dev/null +++ b/pkg/adapters/storage/mapper/survey_mapper.go @@ -0,0 +1,47 @@ +// pkg/adapters/storage/mapper/survey_mapper.go +package mapper + +import ( + "golipors/internal/survey/domain" + "golipors/pkg/adapters/storage/types" +) + +func DomainToModel(survey domain.Survey) types.Survey { + return types.Survey{ + ID: survey.ID, + Title: survey.Title, + CreationTime: survey.CreationTime, + StartTime: survey.StartTime, + EndTime: survey.EndTime, + RandomOrder: survey.RandomOrder, + AllowReturn: survey.AllowReturn, + NumParticipationAttempts: survey.NumParticipationAttempts, + ResponseTime: survey.ResponseTime, + AnonymityLevel: survey.AnonymityLevel, + OwnerID: survey.OwnerID, + DemographicRestrictions: survey.DemographicRestrictions, + ResponseModification: survey.ResponseModification, + CreatedAt: survey.CreatedAt, + UpdatedAt: survey.UpdatedAt, + } +} + +func ModelToDomain(survey types.Survey) domain.Survey { + return domain.Survey{ + ID: survey.ID, + Title: survey.Title, + CreationTime: survey.CreationTime, + StartTime: survey.StartTime, + EndTime: survey.EndTime, + RandomOrder: survey.RandomOrder, + AllowReturn: survey.AllowReturn, + NumParticipationAttempts: survey.NumParticipationAttempts, + ResponseTime: survey.ResponseTime, + AnonymityLevel: survey.AnonymityLevel, + OwnerID: survey.OwnerID, + DemographicRestrictions: survey.DemographicRestrictions, + ResponseModification: survey.ResponseModification, + CreatedAt: survey.CreatedAt, + UpdatedAt: survey.UpdatedAt, + } +} diff --git a/pkg/adapters/storage/survey_repository.go b/pkg/adapters/storage/survey_repository.go new file mode 100644 index 0000000..a9ea7ab --- /dev/null +++ b/pkg/adapters/storage/survey_repository.go @@ -0,0 +1,65 @@ +// pkg/adapters/storage/survey_repository.go +package storage + +import ( + "context" + "errors" + "golipors/internal/survey/domain" + "golipors/internal/survey/port" + "golipors/pkg/adapters/storage/mapper" + "golipors/pkg/adapters/storage/migrations" + "golipors/pkg/adapters/storage/types" + + "github.com/go-gormigrate/gormigrate/v2" + "gorm.io/gorm" +) + +type surveyRepository struct { + db *gorm.DB +} + +func NewSurveyRepository(db *gorm.DB) port.Repository { + return &surveyRepository{ + db: db, + } +} + +func (r *surveyRepository) CreateSurvey(ctx context.Context, survey domain.Survey) (uint, error) { + model := mapper.DomainToModel(survey) + if err := r.db.WithContext(ctx).Create(&model).Error; err != nil { + return 0, err + } + return model.ID, nil +} + +func (r *surveyRepository) GetSurveyByID(ctx context.Context, id uint) (*domain.Survey, error) { + var model types.Survey + if err := r.db.WithContext(ctx).First(&model, id).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, nil + } + return nil, err + } + survey := mapper.ModelToDomain(model) + return &survey, nil +} + +func (r *surveyRepository) UpdateSurvey(ctx context.Context, survey domain.Survey) error { + model := mapper.DomainToModel(survey) + if err := r.db.WithContext(ctx).Model(&types.Survey{}).Where("id = ?", survey.ID).Updates(&model).Error; err != nil { + return err + } + return nil +} + +func (r *surveyRepository) DeleteSurvey(ctx context.Context, id uint) error { + if err := r.db.WithContext(ctx).Delete(&types.Survey{}, id).Error; err != nil { + return err + } + return nil +} + +func (r *surveyRepository) RunMigrations() error { + migrator := gormigrate.New(r.db, gormigrate.DefaultOptions, migrations.GetUserMigrations()) + return migrator.Migrate() +} diff --git a/pkg/adapters/storage/types/survey.go b/pkg/adapters/storage/types/survey.go new file mode 100644 index 0000000..eb23671 --- /dev/null +++ b/pkg/adapters/storage/types/survey.go @@ -0,0 +1,24 @@ +// pkg/adapters/storage/models/survey.go +package types + +import ( + "time" +) + +type Survey struct { + ID uint `gorm:"primaryKey"` + Title string `gorm:"not null"` + CreationTime time.Time `gorm:"not null"` + StartTime *time.Time + EndTime *time.Time + RandomOrder bool `gorm:"default:false"` + AllowReturn bool `gorm:"default:false"` + NumParticipationAttempts int `gorm:"default:1"` + ResponseTime int // in seconds + AnonymityLevel string `gorm:"not null"` + OwnerID uint + DemographicRestrictions string `gorm:"type:text"` + ResponseModification bool `gorm:"default:false"` + CreatedAt time.Time + UpdatedAt time.Time +} diff --git a/tests/CreateSurvey.js b/tests/CreateSurvey.js new file mode 100644 index 0000000..0457912 --- /dev/null +++ b/tests/CreateSurvey.js @@ -0,0 +1,16 @@ +POST /api/surveys +Content-Type: application/json + +{ + "title": "Customer Feedback Survey", + "creation_time": "2024-12-01T00:00:00Z", + "start_time": "2024-12-05T00:00:00Z", + "end_time": "2024-12-31T23:59:59Z", + "random_order": true, + "allow_return": false, + "num_participation_attempts": 3, + "response_time": 1800, + "anonymity_level": "visible_to_creator_and_admins", + "demographic_restrictions": "{\"age\": \"18-65\", \"location\": \"USA\"}", + "response_modification": true +} diff --git a/tests/DeleteSurvay.js b/tests/DeleteSurvay.js new file mode 100644 index 0000000..2712004 --- /dev/null +++ b/tests/DeleteSurvay.js @@ -0,0 +1 @@ +DELETE / api / surveys / 1; diff --git a/tests/GetSurveyByID.js b/tests/GetSurveyByID.js new file mode 100644 index 0000000..cec31b6 --- /dev/null +++ b/tests/GetSurveyByID.js @@ -0,0 +1 @@ +GET / api / surveys / 1; diff --git a/tests/UpdateSurvey.js b/tests/UpdateSurvey.js new file mode 100644 index 0000000..9a8a685 --- /dev/null +++ b/tests/UpdateSurvey.js @@ -0,0 +1,8 @@ +PUT /api/surveys/1 +Content-Type: application/json + +{ + "title": "Updated Customer Feedback Survey", + "start_time": "2024-12-10T00:00:00Z", + "end_time": "2024-12-25T23:59:59Z" +}