diff --git a/internal/database/migrations/20251107120000_add_project_flock_period_unique.down.sql b/internal/database/migrations/20251107120000_add_project_flock_period_unique.down.sql new file mode 100644 index 00000000..f3cb3ddf --- /dev/null +++ b/internal/database/migrations/20251107120000_add_project_flock_period_unique.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS project_flocks_flock_period_unique; diff --git a/internal/database/migrations/20251107120000_add_project_flock_period_unique.up.sql b/internal/database/migrations/20251107120000_add_project_flock_period_unique.up.sql new file mode 100644 index 00000000..40cebe2d --- /dev/null +++ b/internal/database/migrations/20251107120000_add_project_flock_period_unique.up.sql @@ -0,0 +1,3 @@ +CREATE UNIQUE INDEX project_flocks_flock_period_unique +ON project_flocks (flock_id, period) +WHERE deleted_at IS NULL; diff --git a/internal/entities/projectfloc.go b/internal/entities/projectfloc.go index 5332e336..eee7392a 100644 --- a/internal/entities/projectfloc.go +++ b/internal/entities/projectfloc.go @@ -8,12 +8,12 @@ import ( type ProjectFlock struct { Id uint `gorm:"primaryKey"` - FlockId uint `gorm:"not null"` + FlockId uint `gorm:"not null;uniqueIndex:idx_project_flocks_flock_period,priority:1"` AreaId uint `gorm:"not null"` ProductCategoryId uint `gorm:"not null"` FcrId uint `gorm:"not null"` LocationId uint `gorm:"not null"` - Period int `gorm:"not null"` + Period int `gorm:"not null;uniqueIndex:idx_project_flocks_flock_period,priority:2"` CreatedBy uint `gorm:"not null"` CreatedAt time.Time `gorm:"autoCreateTime"` UpdatedAt time.Time `gorm:"autoUpdateTime"` diff --git a/internal/entities/recording.go b/internal/entities/recording.go new file mode 100644 index 00000000..a6cf61b0 --- /dev/null +++ b/internal/entities/recording.go @@ -0,0 +1,18 @@ +package entities + +import ( + "time" + + "gorm.io/gorm" +) + +type Recording struct { + Id uint `gorm:"primaryKey"` + Name string `gorm:"not null;uniqueIndex:idx_name,where:deleted_at IS NULL"` + CreatedBy uint `gorm:"not null"` + CreatedAt time.Time `gorm:"autoCreateTime"` + UpdatedAt time.Time `gorm:"autoUpdateTime"` + DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` + + CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"` +} diff --git a/internal/modules/production/project_flocks/repositories/projectflock.repository.go b/internal/modules/production/project_flocks/repositories/projectflock.repository.go index 2d5abe0c..dde9ed35 100644 --- a/internal/modules/production/project_flocks/repositories/projectflock.repository.go +++ b/internal/modules/production/project_flocks/repositories/projectflock.repository.go @@ -57,7 +57,6 @@ func (r *ProjectflockRepositoryImpl) GetMaxPeriodByFlock(ctx context.Context, fl var max int if err := r.DB().WithContext(ctx). Model(&entity.ProjectFlock{}). - Unscoped(). Where("flock_id = ?", flockID). Select("COALESCE(MAX(period), 0)"). Scan(&max).Error; err != nil { diff --git a/internal/modules/production/project_flocks/services/projectflock.service.go b/internal/modules/production/project_flocks/services/projectflock.service.go index d8a98a32..4ad9d21d 100644 --- a/internal/modules/production/project_flocks/services/projectflock.service.go +++ b/internal/modules/production/project_flocks/services/projectflock.service.go @@ -15,6 +15,7 @@ import ( "github.com/gofiber/fiber/v2" "github.com/sirupsen/logrus" "gorm.io/gorm" + "gorm.io/gorm/clause" ) type ProjectflockService interface { @@ -30,12 +31,12 @@ type projectflockService struct { Log *logrus.Logger Validate *validator.Validate Repository repository.ProjectflockRepository - FlockRepo flockRepository.FlockRepository + FlockRepo flockRepository.FlockRepository KandangRepo kandangRepository.KandangRepository } type FlockPeriodSummary struct { - Flock entity.Flock + Flock entity.Flock NextPeriod int } @@ -49,7 +50,7 @@ func NewProjectflockService( Log: utils.Log, Validate: validate, Repository: repo, - FlockRepo: flockRepo, + FlockRepo: flockRepo, KandangRepo: kandangRepo, } } @@ -127,19 +128,33 @@ func (s *projectflockService) CreateOne(c *fiber.Ctx, req *validation.Create) (* return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to start transaction") } + var nextPeriod int + periodQuery := tx.Model(&entity.ProjectFlock{}). + Where("flock_id = ?", req.FlockId). + Clauses(clause.Locking{Strength: "UPDATE"}) + if err := periodQuery.Select("COALESCE(MAX(period), 0)").Scan(&nextPeriod).Error; err != nil { + tx.Rollback() + s.Log.Errorf("Failed to determine next period for flock %d: %+v", req.FlockId, err) + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to determine next period") + } + nextPeriod++ + projectRepo := s.Repository.WithTx(tx) createBody := &entity.ProjectFlock{ - FlockId: req.FlockId, + FlockId: req.FlockId, AreaId: req.AreaId, ProductCategoryId: req.ProductCategoryId, FcrId: req.FcrId, LocationId: req.LocationId, - Period: req.Period, + Period: nextPeriod, CreatedBy: 1, } if err := projectRepo.CreateOne(c.Context(), createBody, nil); err != nil { tx.Rollback() + if errors.Is(err, gorm.ErrDuplicatedKey) { + return nil, fiber.NewError(fiber.StatusConflict, "Project flock period already exists") + } s.Log.Errorf("Failed to create projectflock: %+v", err) return nil, err } @@ -353,7 +368,7 @@ func (s projectflockService) GetFlockPeriodSummary(c *fiber.Ctx, flockID uint) ( } return &FlockPeriodSummary{ - Flock: *flock, + Flock: *flock, NextPeriod: maxPeriod + 1, }, nil } diff --git a/internal/modules/production/project_flocks/validations/projectflock.validation.go b/internal/modules/production/project_flocks/validations/projectflock.validation.go index cab30918..8c1f7d06 100644 --- a/internal/modules/production/project_flocks/validations/projectflock.validation.go +++ b/internal/modules/production/project_flocks/validations/projectflock.validation.go @@ -1,17 +1,16 @@ package validation type Create struct { - FlockId uint `json:"flock_id" validate:"required_strict,number,gt=0"` + FlockId uint `json:"flock_id" validate:"required_strict,number,gt=0"` AreaId uint `json:"area_id" validate:"required_strict,number,gt=0"` ProductCategoryId uint `json:"product_category_id" validate:"required_strict,number,gt=0"` FcrId uint `json:"fcr_id" validate:"required_strict,number,gt=0"` LocationId uint `json:"location_id" validate:"required_strict,number,gt=0"` - Period int `json:"period" validate:"required_strict,number,gt=0"` KandangIds []uint `json:"kandang_ids" validate:"required,min=1,dive,gt=0"` } type Update struct { - FlockId *uint `json:"flock_id,omitempty" validate:"omitempty,number,gt=0"` + FlockId *uint `json:"flock_id,omitempty" validate:"omitempty,number,gt=0"` AreaId *uint `json:"area_id,omitempty" validate:"omitempty,number,gt=0"` ProductCategoryId *uint `json:"product_category_id,omitempty" validate:"omitempty,number,gt=0"` FcrId *uint `json:"fcr_id,omitempty" validate:"omitempty,number,gt=0"` diff --git a/internal/modules/production/recordings/controllers/recording.controller.go b/internal/modules/production/recordings/controllers/recording.controller.go new file mode 100644 index 00000000..1215e8fc --- /dev/null +++ b/internal/modules/production/recordings/controllers/recording.controller.go @@ -0,0 +1,140 @@ +package controller + +import ( + "math" + "strconv" + + "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/dto" + service "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/services" + validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/validations" + "gitlab.com/mbugroup/lti-api.git/internal/response" + + "github.com/gofiber/fiber/v2" +) + +type RecordingController struct { + RecordingService service.RecordingService +} + +func NewRecordingController(recordingService service.RecordingService) *RecordingController { + return &RecordingController{ + RecordingService: recordingService, + } +} + +func (u *RecordingController) GetAll(c *fiber.Ctx) error { + query := &validation.Query{ + Page: c.QueryInt("page", 1), + Limit: c.QueryInt("limit", 10), + Search: c.Query("search", ""), + } + + result, totalResults, err := u.RecordingService.GetAll(c, query) + if err != nil { + return err + } + + return c.Status(fiber.StatusOK). + JSON(response.SuccessWithPaginate[dto.RecordingListDTO]{ + Code: fiber.StatusOK, + Status: "success", + Message: "Get all recordings successfully", + Meta: response.Meta{ + Page: query.Page, + Limit: query.Limit, + TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))), + TotalResults: totalResults, + }, + Data: dto.ToRecordingListDTOs(result), + }) +} + +func (u *RecordingController) GetOne(c *fiber.Ctx) error { + param := c.Params("id") + + id, err := strconv.Atoi(param) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, "Invalid Id") + } + + result, err := u.RecordingService.GetOne(c, uint(id)) + if err != nil { + return err + } + + return c.Status(fiber.StatusOK). + JSON(response.Success{ + Code: fiber.StatusOK, + Status: "success", + Message: "Get recording successfully", + Data: dto.ToRecordingListDTO(*result), + }) +} + +func (u *RecordingController) CreateOne(c *fiber.Ctx) error { + req := new(validation.Create) + + if err := c.BodyParser(req); err != nil { + return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") + } + + result, err := u.RecordingService.CreateOne(c, req) + if err != nil { + return err + } + + return c.Status(fiber.StatusCreated). + JSON(response.Success{ + Code: fiber.StatusCreated, + Status: "success", + Message: "Create recording successfully", + Data: dto.ToRecordingListDTO(*result), + }) +} + +func (u *RecordingController) UpdateOne(c *fiber.Ctx) error { + req := new(validation.Update) + param := c.Params("id") + + id, err := strconv.Atoi(param) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, "Invalid Id") + } + + if err := c.BodyParser(req); err != nil { + return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") + } + + result, err := u.RecordingService.UpdateOne(c, req, uint(id)) + if err != nil { + return err + } + + return c.Status(fiber.StatusOK). + JSON(response.Success{ + Code: fiber.StatusOK, + Status: "success", + Message: "Update recording successfully", + Data: dto.ToRecordingListDTO(*result), + }) +} + +func (u *RecordingController) DeleteOne(c *fiber.Ctx) error { + param := c.Params("id") + + id, err := strconv.Atoi(param) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, "Invalid Id") + } + + if err := u.RecordingService.DeleteOne(c, uint(id)); err != nil { + return err + } + + return c.Status(fiber.StatusOK). + JSON(response.Common{ + Code: fiber.StatusOK, + Status: "success", + Message: "Delete recording successfully", + }) +} diff --git a/internal/modules/production/recordings/dto/recording.dto.go b/internal/modules/production/recordings/dto/recording.dto.go new file mode 100644 index 00000000..7dbdec98 --- /dev/null +++ b/internal/modules/production/recordings/dto/recording.dto.go @@ -0,0 +1,64 @@ +package dto + +import ( + "time" + + entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto" +) + +// === DTO Structs === + +type RecordingBaseDTO struct { + Id uint `json:"id"` + Name string `json:"name"` +} + +type RecordingListDTO struct { + RecordingBaseDTO + CreatedUser *userDTO.UserBaseDTO `json:"created_user"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +type RecordingDetailDTO struct { + RecordingListDTO +} + +// === Mapper Functions === + +func ToRecordingBaseDTO(e entity.Recording) RecordingBaseDTO { + return RecordingBaseDTO{ + Id: e.Id, + Name: e.Name, + } +} + +func ToRecordingListDTO(e entity.Recording) RecordingListDTO { + var createdUser *userDTO.UserBaseDTO + if e.CreatedUser.Id != 0 { + mapped := userDTO.ToUserBaseDTO(e.CreatedUser) + createdUser = &mapped + } + + return RecordingListDTO{ + RecordingBaseDTO: ToRecordingBaseDTO(e), + CreatedAt: e.CreatedAt, + UpdatedAt: e.UpdatedAt, + CreatedUser: createdUser, + } +} + +func ToRecordingListDTOs(e []entity.Recording) []RecordingListDTO { + result := make([]RecordingListDTO, len(e)) + for i, r := range e { + result[i] = ToRecordingListDTO(r) + } + return result +} + +func ToRecordingDetailDTO(e entity.Recording) RecordingDetailDTO { + return RecordingDetailDTO{ + RecordingListDTO: ToRecordingListDTO(e), + } +} diff --git a/internal/modules/production/recordings/module.go b/internal/modules/production/recordings/module.go new file mode 100644 index 00000000..36ae8dd7 --- /dev/null +++ b/internal/modules/production/recordings/module.go @@ -0,0 +1,26 @@ +package recordings + +import ( + "github.com/go-playground/validator/v10" + "github.com/gofiber/fiber/v2" + "gorm.io/gorm" + + rRecording "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories" + sRecording "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/services" + + rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories" + sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" +) + +type RecordingModule struct{} + +func (RecordingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) { + recordingRepo := rRecording.NewRecordingRepository(db) + userRepo := rUser.NewUserRepository(db) + + recordingService := sRecording.NewRecordingService(recordingRepo, validate) + userService := sUser.NewUserService(userRepo, validate) + + RecordingRoutes(router, userService, recordingService) +} + diff --git a/internal/modules/production/recordings/repositories/recording.repository.go b/internal/modules/production/recordings/repositories/recording.repository.go new file mode 100644 index 00000000..8dd114d1 --- /dev/null +++ b/internal/modules/production/recordings/repositories/recording.repository.go @@ -0,0 +1,21 @@ +package repository + +import ( + entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + "gitlab.com/mbugroup/lti-api.git/internal/common/repository" + "gorm.io/gorm" +) + +type RecordingRepository interface { + repository.BaseRepository[entity.Recording] +} + +type RecordingRepositoryImpl struct { + *repository.BaseRepositoryImpl[entity.Recording] +} + +func NewRecordingRepository(db *gorm.DB) RecordingRepository { + return &RecordingRepositoryImpl{ + BaseRepositoryImpl: repository.NewBaseRepository[entity.Recording](db), + } +} diff --git a/internal/modules/production/recordings/route.go b/internal/modules/production/recordings/route.go new file mode 100644 index 00000000..6852a1ba --- /dev/null +++ b/internal/modules/production/recordings/route.go @@ -0,0 +1,28 @@ +package recordings + +import ( + // m "gitlab.com/mbugroup/lti-api.git/internal/middleware" + controller "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/controllers" + recording "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/services" + user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" + + "github.com/gofiber/fiber/v2" +) + +func RecordingRoutes(v1 fiber.Router, u user.UserService, s recording.RecordingService) { + ctrl := controller.NewRecordingController(s) + + route := v1.Group("/recordings") + + // route.Get("/", m.Auth(u), ctrl.GetAll) + // route.Post("/", m.Auth(u), ctrl.CreateOne) + // route.Get("/:id", m.Auth(u), ctrl.GetOne) + // route.Patch("/:id", m.Auth(u), ctrl.UpdateOne) + // route.Delete("/:id", m.Auth(u), ctrl.DeleteOne) + + route.Get("/", ctrl.GetAll) + route.Post("/", ctrl.CreateOne) + route.Get("/:id", ctrl.GetOne) + route.Patch("/:id", ctrl.UpdateOne) + route.Delete("/:id", ctrl.DeleteOne) +} diff --git a/internal/modules/production/recordings/services/recording.service.go b/internal/modules/production/recordings/services/recording.service.go new file mode 100644 index 00000000..84220bd2 --- /dev/null +++ b/internal/modules/production/recordings/services/recording.service.go @@ -0,0 +1,129 @@ +package service + +import ( + "errors" + + entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + repository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories" + validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/validations" + "gitlab.com/mbugroup/lti-api.git/internal/utils" + + "github.com/go-playground/validator/v10" + "github.com/gofiber/fiber/v2" + "github.com/sirupsen/logrus" + "gorm.io/gorm" +) + +type RecordingService interface { + GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.Recording, int64, error) + GetOne(ctx *fiber.Ctx, id uint) (*entity.Recording, error) + CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.Recording, error) + UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.Recording, error) + DeleteOne(ctx *fiber.Ctx, id uint) error +} + +type recordingService struct { + Log *logrus.Logger + Validate *validator.Validate + Repository repository.RecordingRepository +} + +func NewRecordingService(repo repository.RecordingRepository, validate *validator.Validate) RecordingService { + return &recordingService{ + Log: utils.Log, + Validate: validate, + Repository: repo, + } +} + +func (s recordingService) withRelations(db *gorm.DB) *gorm.DB { + return db.Preload("CreatedUser") +} + +func (s recordingService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.Recording, int64, error) { + if err := s.Validate.Struct(params); err != nil { + return nil, 0, err + } + + offset := (params.Page - 1) * params.Limit + + recordings, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB { + db = s.withRelations(db) + if params.Search != "" { + return db.Where("name LIKE ?", "%"+params.Search+"%") + } + return db.Order("created_at DESC").Order("updated_at DESC") + }) + + if err != nil { + s.Log.Errorf("Failed to get recordings: %+v", err) + return nil, 0, err + } + return recordings, total, nil +} + +func (s recordingService) GetOne(c *fiber.Ctx, id uint) (*entity.Recording, error) { + recording, err := s.Repository.GetByID(c.Context(), id, s.withRelations) + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fiber.NewError(fiber.StatusNotFound, "Recording not found") + } + if err != nil { + s.Log.Errorf("Failed get recording by id: %+v", err) + return nil, err + } + return recording, nil +} + +func (s *recordingService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.Recording, error) { + if err := s.Validate.Struct(req); err != nil { + return nil, err + } + + createBody := &entity.Recording{ + Name: req.Name, + } + + if err := s.Repository.CreateOne(c.Context(), createBody, nil); err != nil { + s.Log.Errorf("Failed to create recording: %+v", err) + return nil, err + } + + return s.GetOne(c, createBody.Id) +} + +func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.Recording, error) { + if err := s.Validate.Struct(req); err != nil { + return nil, err + } + + updateBody := make(map[string]any) + + if req.Name != nil { + updateBody["name"] = *req.Name + } + + if len(updateBody) == 0 { + return s.GetOne(c, id) + } + + if err := s.Repository.PatchOne(c.Context(), id, updateBody, nil); err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fiber.NewError(fiber.StatusNotFound, "Recording not found") + } + s.Log.Errorf("Failed to update recording: %+v", err) + return nil, err + } + + return s.GetOne(c, id) +} + +func (s recordingService) DeleteOne(c *fiber.Ctx, id uint) error { + if err := s.Repository.DeleteOne(c.Context(), id); err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fiber.NewError(fiber.StatusNotFound, "Recording not found") + } + s.Log.Errorf("Failed to delete recording: %+v", err) + return err + } + return nil +} diff --git a/internal/modules/production/recordings/validations/recording.validation.go b/internal/modules/production/recordings/validations/recording.validation.go new file mode 100644 index 00000000..95505746 --- /dev/null +++ b/internal/modules/production/recordings/validations/recording.validation.go @@ -0,0 +1,15 @@ +package validation + +type Create struct { + Name string `json:"name" validate:"required_strict,min=3"` +} + +type Update struct { + Name *string `json:"name,omitempty" validate:"omitempty"` +} + +type Query struct { + Page int `query:"page" validate:"omitempty,number,min=1"` + Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"` + Search string `query:"search" validate:"omitempty,max=50"` +} diff --git a/internal/modules/production/route.go b/internal/modules/production/route.go index f93bc877..73bbe8da 100644 --- a/internal/modules/production/route.go +++ b/internal/modules/production/route.go @@ -8,6 +8,7 @@ import ( "gorm.io/gorm" projectflocks "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks" + recordings "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings" // MODULE IMPORTS ) @@ -16,6 +17,7 @@ func RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Valida allModules := []modules.Module{ projectflocks.ProjectflockModule{}, + recordings.RecordingModule{}, // MODULE REGISTRY } diff --git a/test/integration/master_data/project_flock_test.go b/test/integration/master_data/project_flock_test.go index 5ba6d2fe..22c73a5d 100644 --- a/test/integration/master_data/project_flock_test.go +++ b/test/integration/master_data/project_flock_test.go @@ -22,12 +22,11 @@ func TestProjectFlockSummary(t *testing.T) { kandangID := createKandang(t, app, "Kandang Summary", locationID, 1) createPayload := map[string]any{ - "flock_id": flockID, + "flock_id": flockID, "area_id": areaID, "product_category_id": categoryID, "fcr_id": fcrID, "location_id": locationID, - "period": 1, "kandang_ids": []uint{kandangID}, } resp, body := doJSONRequest(t, app, http.MethodPost, "/api/production/project_flocks", createPayload) @@ -37,14 +36,9 @@ func TestProjectFlockSummary(t *testing.T) { var createResp struct { Data struct { - Id uint `json:"id"` - FlockId uint `json:"flock_id"` - AreaId uint `json:"area_id"` - ProductCategoryId uint `json:"product_category_id"` - FcrId uint `json:"fcr_id"` - LocationId uint `json:"location_id"` - Period int `json:"period"` - Flock struct { + Id uint `json:"id"` + Period int `json:"period"` + Flock struct { Id uint `json:"id"` Name string `json:"name"` } `json:"flock"` @@ -82,18 +76,47 @@ func TestProjectFlockSummary(t *testing.T) { if err := json.Unmarshal(body, &createResp); err != nil { t.Fatalf("failed to parse create response: %v", err) } - if createResp.Data.FlockId != flockID || createResp.Data.Flock.Name == "" { + if createResp.Data.Flock.Id != flockID || createResp.Data.Flock.Name == "" { t.Fatalf("expected flock detail to be present, got %+v", createResp.Data.Flock) } - if createResp.Data.AreaId != areaID || createResp.Data.Area.Name == "" { + if createResp.Data.Area.Id != areaID || createResp.Data.Area.Name == "" { t.Fatalf("expected area detail to be present, got %+v", createResp.Data.Area) } - if createResp.Data.LocationId != locationID || createResp.Data.Location.Name == "" { + if createResp.Data.Location.Id != locationID || createResp.Data.Location.Name == "" { t.Fatalf("expected location detail to be present, got %+v", createResp.Data.Location) } if len(createResp.Data.Kandangs) != 1 || createResp.Data.Kandangs[0].Id != kandangID { t.Fatalf("expected kandang detail to be present, got %+v", createResp.Data.Kandangs) } + if createResp.Data.Period != 1 { + t.Fatalf("expected period 1 to be assigned automatically, got %d", createResp.Data.Period) + } + + secondKandangID := createKandang(t, app, "Kandang Summary 2", locationID, 1) + secondPayload := map[string]any{ + "flock_id": flockID, + "area_id": areaID, + "product_category_id": categoryID, + "fcr_id": fcrID, + "location_id": locationID, + "kandang_ids": []uint{secondKandangID}, + } + resp, body = doJSONRequest(t, app, http.MethodPost, "/api/production/project_flocks", secondPayload) + if resp.StatusCode != fiber.StatusCreated { + t.Fatalf("expected 201 when creating second project flock, got %d: %s", resp.StatusCode, string(body)) + } + var createRespSecond struct { + Data struct { + Id uint `json:"id"` + Period int `json:"period"` + } `json:"data"` + } + if err := json.Unmarshal(body, &createRespSecond); err != nil { + t.Fatalf("failed to parse second create response: %v", err) + } + if createRespSecond.Data.Period != 2 { + t.Fatalf("expected second period to be 2, got %d", createRespSecond.Data.Period) + } resp, body = doJSONRequest(t, app, http.MethodGet, "/api/production/project_flocks/flocks/"+uintToString(flockID)+"/periods", nil) if resp.StatusCode != fiber.StatusOK { @@ -109,8 +132,31 @@ func TestProjectFlockSummary(t *testing.T) { t.Fatalf("failed to parse summary response: %v", err) } - if summary.Data.NextPeriod != 2 { - t.Fatalf("expected next_period 2, got %d", summary.Data.NextPeriod) + if summary.Data.NextPeriod != 3 { + t.Fatalf("expected next_period 3, got %d", summary.Data.NextPeriod) + } + + resp, body = doJSONRequest(t, app, http.MethodDelete, "/api/production/project_flocks/"+uintToString(createResp.Data.Id), nil) + if resp.StatusCode != fiber.StatusOK { + t.Fatalf("expected 200 when deleting first project flock, got %d: %s", resp.StatusCode, string(body)) + } + + resp, body = doJSONRequest(t, app, http.MethodDelete, "/api/production/project_flocks/"+uintToString(createRespSecond.Data.Id), nil) + if resp.StatusCode != fiber.StatusOK { + t.Fatalf("expected 200 when deleting second project flock, got %d: %s", resp.StatusCode, string(body)) + } + + resp, body = doJSONRequest(t, app, http.MethodGet, "/api/production/project_flocks/flocks/"+uintToString(flockID)+"/periods", nil) + if resp.StatusCode != fiber.StatusOK { + t.Fatalf("expected 200 when fetching summary after delete, got %d: %s", resp.StatusCode, string(body)) + } + + if err := json.Unmarshal(body, &summary); err != nil { + t.Fatalf("failed to parse summary response after delete: %v", err) + } + + if summary.Data.NextPeriod != 1 { + t.Fatalf("expected next_period 1 after soft deletes, got %d", summary.Data.NextPeriod) } }