diff --git a/internal/database/seed/seeder.go b/internal/database/seed/seeder.go index afa2a308..e6f77b15 100644 --- a/internal/database/seed/seeder.go +++ b/internal/database/seed/seeder.go @@ -50,7 +50,7 @@ func Run(db *gorm.DB) error { return err } - projectFlocks, err := seedProjectFlocks(tx, adminID, flocks, areas, productCategories, fcrs, locations) + projectFlocks, err := seedProjectFlocks(tx, adminID, flocks, areas, fcrs, locations) if err != nil { return err } @@ -239,33 +239,33 @@ func seedFlocks(tx *gorm.DB, createdBy uint) (map[string]uint, error) { return result, nil } -func seedProjectFlocks(tx *gorm.DB, createdBy uint, flocks, areas, productCategories, fcrs, locations map[string]uint) (map[string]uint, error) { +func seedProjectFlocks(tx *gorm.DB, createdBy uint, flocks, areas, fcrs, locations map[string]uint) (map[string]uint, error) { seeds := []struct { - Key string - Flock string - Area string - ProductCategory string - Fcr string - Location string - Period int + Key string + Flock string + Area string + Category utils.ProjectFlockCategory + Fcr string + Location string + Period int }{ { - Key: "Singaparna Period 1", - Flock: "Flock Priangan", - Area: "Priangan", - ProductCategory: "Day Old Chick", - Fcr: "FCR Layer", - Location: "Singaparna", - Period: 1, + Key: "Singaparna Period 1", + Flock: "Flock Priangan", + Area: "Priangan", + Category: utils.ProjectFlockCategoryGrowing, + Fcr: "FCR Layer", + Location: "Singaparna", + Period: 1, }, { - Key: "Cikaum Period 1", - Flock: "Flock Banten", - Area: "Banten", - ProductCategory: "Day Old Chick", - Fcr: "FCR Layer", - Location: "Cikaum", - Period: 1, + Key: "Cikaum Period 1", + Flock: "Flock Banten", + Area: "Banten", + Category: utils.ProjectFlockCategoryGrowing, + Fcr: "FCR Layer", + Location: "Cikaum", + Period: 1, }, } @@ -280,10 +280,6 @@ func seedProjectFlocks(tx *gorm.DB, createdBy uint, flocks, areas, productCatego if !ok { return nil, fmt.Errorf("area %s not seeded", seed.Area) } - categoryID, ok := productCategories[seed.ProductCategory] - if !ok { - return nil, fmt.Errorf("product category %s not seeded", seed.ProductCategory) - } fcrID, ok := fcrs[seed.Fcr] if !ok { return nil, fmt.Errorf("fcr %s not seeded", seed.Fcr) @@ -294,17 +290,17 @@ func seedProjectFlocks(tx *gorm.DB, createdBy uint, flocks, areas, productCatego } var projectFlock entity.ProjectFlock - err := tx.Where("flock_id = ? AND area_id = ? AND product_category_id = ? AND fcr_id = ? AND location_id = ? AND period = ?", - flockID, areaID, categoryID, fcrID, locationID, seed.Period).First(&projectFlock).Error + err := tx.Where("flock_id = ? AND area_id = ? AND category = ? AND fcr_id = ? AND location_id = ? AND period = ?", + flockID, areaID, seed.Category, fcrID, locationID, seed.Period).First(&projectFlock).Error if errors.Is(err, gorm.ErrRecordNotFound) { projectFlock = entity.ProjectFlock{ - FlockId: flockID, - AreaId: areaID, - ProductCategoryId: categoryID, - FcrId: fcrID, - LocationId: locationID, - Period: seed.Period, - CreatedBy: createdBy, + FlockId: flockID, + AreaId: areaID, + Category: string(seed.Category), + FcrId: fcrID, + LocationId: locationID, + Period: seed.Period, + CreatedBy: createdBy, } if err := tx.Create(&projectFlock).Error; err != nil { return nil, err @@ -313,12 +309,12 @@ func seedProjectFlocks(tx *gorm.DB, createdBy uint, flocks, areas, productCatego return nil, err } else { if err := tx.Model(&entity.ProjectFlock{}).Where("id = ?", projectFlock.Id).Updates(map[string]any{ - "flock_id": flockID, - "area_id": areaID, - "product_category_id": categoryID, - "fcr_id": fcrID, - "location_id": locationID, - "period": seed.Period, + "flock_id": flockID, + "area_id": areaID, + "category": string(seed.Category), + "fcr_id": fcrID, + "location_id": locationID, + "period": seed.Period, }).Error; err != nil { return nil, err } diff --git a/internal/entities/projectfloc.go b/internal/entities/projectfloc.go index 2d581e84..47362d42 100644 --- a/internal/entities/projectfloc.go +++ b/internal/entities/projectfloc.go @@ -7,23 +7,22 @@ import ( ) type ProjectFlock struct { - Id uint `gorm:"primaryKey"` - 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;uniqueIndex:idx_project_flocks_flock_period,priority:2"` - CreatedBy uint `gorm:"not null"` - CreatedAt time.Time `gorm:"autoCreateTime"` - UpdatedAt time.Time `gorm:"autoUpdateTime"` - DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` - Flock Flock `gorm:"foreignKey:FlockId;references:Id"` - Area Area `gorm:"foreignKey:AreaId;references:Id"` - ProductCategory ProductCategory `gorm:"foreignKey:ProductCategoryId;references:Id"` - Fcr Fcr `gorm:"foreignKey:FcrId;references:Id"` - Location Location `gorm:"foreignKey:LocationId;references:Id"` - CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"` - Kandangs []Kandang `gorm:"foreignKey:ProjectFlockId;references:Id"` - KandangHistory []ProjectFlockKandang `gorm:"foreignKey:ProjectFlockId;references:Id"` + Id uint `gorm:"primaryKey"` + FlockId uint `gorm:"not null;uniqueIndex:project_flocks_flock_period_unique"` + AreaId uint `gorm:"not null"` + Category string `gorm:"type:varchar(20);not null"` + FcrId uint `gorm:"not null"` + LocationId uint `gorm:"not null"` + Period int `gorm:"not null;uniqueIndex:project_flocks_flock_period_unique"` + CreatedBy uint `gorm:"not null"` + CreatedAt time.Time `gorm:"autoCreateTime"` + UpdatedAt time.Time `gorm:"autoUpdateTime"` + DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` + Flock Flock `gorm:"foreignKey:FlockId;references:Id"` + Area Area `gorm:"foreignKey:AreaId;references:Id"` + Fcr Fcr `gorm:"foreignKey:FcrId;references:Id"` + Location Location `gorm:"foreignKey:LocationId;references:Id"` + CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"` + Kandangs []Kandang `gorm:"foreignKey:ProjectFlockId;references:Id"` + KandangHistory []ProjectFlockKandang `gorm:"foreignKey:ProjectFlockId;references:Id"` } diff --git a/internal/modules/master/flocks/repositories/flock.repository.go b/internal/modules/master/flocks/repositories/flock.repository.go index 12f269fc..006fe541 100644 --- a/internal/modules/master/flocks/repositories/flock.repository.go +++ b/internal/modules/master/flocks/repositories/flock.repository.go @@ -1,21 +1,30 @@ package repository import ( - entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + "context" + "gitlab.com/mbugroup/lti-api.git/internal/common/repository" + entity "gitlab.com/mbugroup/lti-api.git/internal/entities" "gorm.io/gorm" ) type FlockRepository interface { repository.BaseRepository[entity.Flock] + NameExists(ctx context.Context, name string, excludeID *uint) (bool, error) } type FlockRepositoryImpl struct { *repository.BaseRepositoryImpl[entity.Flock] + db *gorm.DB } func NewFlockRepository(db *gorm.DB) FlockRepository { return &FlockRepositoryImpl{ BaseRepositoryImpl: repository.NewBaseRepository[entity.Flock](db), + db: db, } } + +func (r *FlockRepositoryImpl) NameExists(ctx context.Context, name string, excludeID *uint) (bool, error) { + return repository.ExistsByName[entity.Flock](ctx, r.db, name, excludeID) +} diff --git a/internal/modules/master/flocks/services/flock.service.go b/internal/modules/master/flocks/services/flock.service.go index 4c3c9b26..ad086920 100644 --- a/internal/modules/master/flocks/services/flock.service.go +++ b/internal/modules/master/flocks/services/flock.service.go @@ -2,6 +2,8 @@ package service import ( "errors" + "fmt" + "strings" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" repository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/repositories" @@ -79,8 +81,22 @@ func (s *flockService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity. return nil, err } + name := strings.TrimSpace(req.Name) + if name == "" { + return nil, fiber.NewError(fiber.StatusBadRequest, "Name is required") + } + + exists, err := s.Repository.NameExists(c.Context(), name, nil) + if err != nil { + s.Log.Errorf("Failed to check flock name: %+v", err) + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check flock name") + } + if exists { + return nil, fiber.NewError(fiber.StatusConflict, fmt.Sprintf("Flock with name %s already exists", name)) + } + createBody := &entity.Flock{ - Name: req.Name, + Name: name, CreatedBy: 1, } @@ -100,7 +116,20 @@ func (s flockService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) ( updateBody := make(map[string]any) if req.Name != nil { - updateBody["name"] = *req.Name + name := strings.TrimSpace(*req.Name) + if name == "" { + return nil, fiber.NewError(fiber.StatusBadRequest, "Name cannot be empty") + } + + exists, err := s.Repository.NameExists(c.Context(), name, &id) + if err != nil { + s.Log.Errorf("Failed to check flock name: %+v", err) + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check flock name") + } + if exists { + return nil, fiber.NewError(fiber.StatusConflict, fmt.Sprintf("Flock with name %s already exists", name)) + } + updateBody["name"] = name } if len(updateBody) == 0 { diff --git a/internal/modules/production/project_flocks/dto/projectflock.dto.go b/internal/modules/production/project_flocks/dto/projectflock.dto.go index a42caebf..fcf3d50c 100644 --- a/internal/modules/production/project_flocks/dto/projectflock.dto.go +++ b/internal/modules/production/project_flocks/dto/projectflock.dto.go @@ -4,75 +4,66 @@ import ( "time" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + areaDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/areas/dto" + fcrDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/fcrs/dto" + flockDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/dto" + kandangDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/dto" + locationDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/locations/dto" userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto" ) type ProjectFlockBaseDTO 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"` + Id uint `json:"id"` + Period int `json:"period"` + Category string `json:"category"` + Flock *flockDTO.FlockBaseDTO `json:"flock"` + Area *areaDTO.AreaBaseDTO `json:"area"` + Fcr *fcrDTO.FcrBaseDTO `json:"fcr"` + Location *locationDTO.LocationBaseDTO `json:"location"` } func ToProjectFlockBaseDTO(e entity.ProjectFlock) ProjectFlockBaseDTO { - return ProjectFlockBaseDTO{ - Id: e.Id, - // FlockId: e.FlockId, - // AreaId: e.AreaId, - // ProductCategoryId: e.ProductCategoryId, - // FcrId: e.FcrId, - // LocationId: e.LocationId, - Period: e.Period, + var flock *flockDTO.FlockBaseDTO + if e.Flock.Id != 0 { + mapped := flockDTO.ToFlockBaseDTO(e.Flock) + flock = &mapped } -} -type FlockSummaryDTO struct { - Id uint `json:"id"` - Name string `json:"name"` -} + var area *areaDTO.AreaBaseDTO + if e.Area.Id != 0 { + mapped := areaDTO.ToAreaBaseDTO(e.Area) + area = &mapped + } -type AreaSummaryDTO struct { - Id uint `json:"id"` - Name string `json:"name"` -} + var fcr *fcrDTO.FcrBaseDTO + if e.Fcr.Id != 0 { + mapped := fcrDTO.ToFcrBaseDTO(e.Fcr) + fcr = &mapped + } -type ProductCategorySummaryDTO struct { - Id uint `json:"id"` - Name string `json:"name"` - Code string `json:"code"` -} + var location *locationDTO.LocationBaseDTO + if e.Location.Id != 0 { + mapped := locationDTO.ToLocationBaseDTO(e.Location) + location = &mapped + } -type FcrSummaryDTO struct { - Id uint `json:"id"` - Name string `json:"name"` -} - -type LocationSummaryDTO struct { - Id uint `json:"id"` - Name string `json:"name"` - Address string `json:"address"` -} - -type KandangSummaryDTO struct { - Id uint `json:"id"` - Name string `json:"name"` - Status string `json:"status"` + return ProjectFlockBaseDTO{ + Id: e.Id, + Period: e.Period, + Category: e.Category, + Flock: flock, + Area: area, + Fcr: fcr, + Location: location, + } } type ProjectFlockListDTO struct { ProjectFlockBaseDTO - Flock *FlockSummaryDTO `json:"flock,omitempty"` - Area *AreaSummaryDTO `json:"area,omitempty"` - ProductCategory *ProductCategorySummaryDTO `json:"product_category,omitempty"` - Fcr *FcrSummaryDTO `json:"fcr,omitempty"` - Location *LocationSummaryDTO `json:"location,omitempty"` - Kandangs []KandangSummaryDTO `json:"kandangs,omitempty"` - CreatedUser *userDTO.UserBaseDTO `json:"created_user"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + Kandangs []kandangDTO.KandangBaseDTO `json:"kandangs,omitempty"` + CreatedUser *userDTO.UserBaseDTO `json:"created_user"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` } type ProjectFlockDetailDTO struct { @@ -80,8 +71,8 @@ type ProjectFlockDetailDTO struct { } type FlockPeriodSummaryDTO struct { - Flock FlockSummaryDTO `json:"flock"` - NextPeriod int `json:"next_period"` + Flock flockDTO.FlockBaseDTO `json:"flock"` + NextPeriod int `json:"next_period"` } func ToProjectFlockListDTO(e entity.ProjectFlock) ProjectFlockListDTO { @@ -91,62 +82,16 @@ func ToProjectFlockListDTO(e entity.ProjectFlock) ProjectFlockListDTO { createdUser = &mapped } - var flockSummary *FlockSummaryDTO - if e.Flock.Id != 0 { - summary := ToFlockSummaryDTO(e.Flock) - flockSummary = &summary - } - - var areaSummary *AreaSummaryDTO - if e.Area.Id != 0 { - areaSummary = &AreaSummaryDTO{ - Id: e.Area.Id, - Name: e.Area.Name, - } - } - - var categorySummary *ProductCategorySummaryDTO - if e.ProductCategory.Id != 0 { - categorySummary = &ProductCategorySummaryDTO{ - Id: e.ProductCategory.Id, - Name: e.ProductCategory.Name, - Code: e.ProductCategory.Code, - } - } - - var fcrSummary *FcrSummaryDTO - if e.Fcr.Id != 0 { - fcrSummary = &FcrSummaryDTO{ - Id: e.Fcr.Id, - Name: e.Fcr.Name, - } - } - - var locationSummary *LocationSummaryDTO - if e.Location.Id != 0 { - locationSummary = &LocationSummaryDTO{ - Id: e.Location.Id, - Name: e.Location.Name, - Address: e.Location.Address, - } - } - - kandangSummaries := make([]KandangSummaryDTO, len(e.Kandangs)) - for i, kandang := range e.Kandangs { - kandangSummaries[i] = KandangSummaryDTO{ - Id: kandang.Id, - Name: kandang.Name, - Status: kandang.Status, + var kandangSummaries []kandangDTO.KandangBaseDTO + if len(e.Kandangs) > 0 { + kandangSummaries = make([]kandangDTO.KandangBaseDTO, len(e.Kandangs)) + for i, kandang := range e.Kandangs { + kandangSummaries[i] = kandangDTO.ToKandangBaseDTO(kandang) } } return ProjectFlockListDTO{ ProjectFlockBaseDTO: ToProjectFlockBaseDTO(e), - Flock: flockSummary, - Area: areaSummary, - ProductCategory: categorySummary, - Fcr: fcrSummary, - Location: locationSummary, Kandangs: kandangSummaries, CreatedAt: e.CreatedAt, UpdatedAt: e.UpdatedAt, @@ -168,16 +113,9 @@ func ToProjectFlockDetailDTO(e entity.ProjectFlock) ProjectFlockDetailDTO { } } -func ToFlockSummaryDTO(e entity.Flock) FlockSummaryDTO { - return FlockSummaryDTO{ - Id: e.Id, - Name: e.Name, - } -} - func ToFlockPeriodSummaryDTO(flock entity.Flock, next int) FlockPeriodSummaryDTO { return FlockPeriodSummaryDTO{ - Flock: ToFlockSummaryDTO(flock), + Flock: flockDTO.ToFlockBaseDTO(flock), NextPeriod: next, } } diff --git a/internal/modules/production/project_flocks/services/projectflock.service.go b/internal/modules/production/project_flocks/services/projectflock.service.go index 8af6e452..18f00b7d 100644 --- a/internal/modules/production/project_flocks/services/projectflock.service.go +++ b/internal/modules/production/project_flocks/services/projectflock.service.go @@ -67,7 +67,6 @@ func (s projectflockService) withRelations(db *gorm.DB) *gorm.DB { Preload("CreatedUser"). Preload("Flock"). Preload("Area"). - Preload("ProductCategory"). Preload("Fcr"). Preload("Location"). Preload("Kandangs") @@ -115,15 +114,13 @@ func (s projectflockService) GetAll(c *fiber.Ctx, params *validation.Query) ([]e db = db. Joins("LEFT JOIN flocks ON flocks.id = project_flocks.flock_id"). Joins("LEFT JOIN areas ON areas.id = project_flocks.area_id"). - Joins("LEFT JOIN product_categories ON product_categories.id = project_flocks.product_category_id"). Joins("LEFT JOIN fcrs ON fcrs.id = project_flocks.fcr_id"). Joins("LEFT JOIN locations ON locations.id = project_flocks.location_id"). Joins("LEFT JOIN users AS created_users ON created_users.id = project_flocks.created_by"). Where(` LOWER(flocks.name) LIKE ? OR LOWER(areas.name) LIKE ? - OR LOWER(product_categories.name) LIKE ? - OR LOWER(product_categories.code) LIKE ? + OR LOWER(project_flocks.category) LIKE ? OR LOWER(fcrs.name) LIKE ? OR LOWER(locations.name) LIKE ? OR LOWER(locations.address) LIKE ? @@ -146,7 +143,6 @@ func (s projectflockService) GetAll(c *fiber.Ctx, params *validation.Query) ([]e likeQuery, likeQuery, likeQuery, - likeQuery, ) } for _, expr := range s.buildOrderExpressions(params.SortBy, params.SortOrder) { @@ -179,6 +175,11 @@ func (s *projectflockService) CreateOne(c *fiber.Ctx, req *validation.Create) (* return nil, err } + category, ok := utils.NormalizeProjectFlockCategory(req.Category) + if !ok { + return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid category") + } + if len(req.KandangIds) == 0 { return nil, fiber.NewError(fiber.StatusBadRequest, "kandang_ids is required") } @@ -186,7 +187,6 @@ func (s *projectflockService) CreateOne(c *fiber.Ctx, req *validation.Create) (* if err := common.EnsureRelations(c.Context(), common.RelationCheck{Name: "Flock", ID: &req.FlockId, Exists: relationExistsChecker[entity.Flock](s.Repository.DB())}, common.RelationCheck{Name: "Area", ID: &req.AreaId, Exists: relationExistsChecker[entity.Area](s.Repository.DB())}, - common.RelationCheck{Name: "Product category", ID: &req.ProductCategoryId, Exists: relationExistsChecker[entity.ProductCategory](s.Repository.DB())}, common.RelationCheck{Name: "FCR", ID: &req.FcrId, Exists: relationExistsChecker[entity.Fcr](s.Repository.DB())}, common.RelationCheck{Name: "Location", ID: &req.LocationId, Exists: relationExistsChecker[entity.Location](s.Repository.DB())}, ); err != nil { @@ -224,13 +224,13 @@ func (s *projectflockService) CreateOne(c *fiber.Ctx, req *validation.Create) (* } createBody := &entity.ProjectFlock{ - FlockId: req.FlockId, - AreaId: req.AreaId, - ProductCategoryId: req.ProductCategoryId, - FcrId: req.FcrId, - LocationId: req.LocationId, - Period: nextPeriod, - CreatedBy: 1, + FlockId: req.FlockId, + AreaId: req.AreaId, + Category: string(category), + FcrId: req.FcrId, + LocationId: req.LocationId, + Period: nextPeriod, + CreatedBy: 1, } if err := projectRepo.CreateOne(c.Context(), createBody, nil); err != nil { @@ -289,13 +289,12 @@ func (s projectflockService) UpdateOne(c *fiber.Ctx, req *validation.Update, id Exists: relationExistsChecker[entity.Area](s.Repository.DB()), }) } - if req.ProductCategoryId != nil { - updateBody["product_category_id"] = *req.ProductCategoryId - relationChecks = append(relationChecks, common.RelationCheck{ - Name: "Product category", - ID: req.ProductCategoryId, - Exists: relationExistsChecker[entity.ProductCategory](s.Repository.DB()), - }) + if req.Category != nil { + if normalized, ok := utils.NormalizeProjectFlockCategory(*req.Category); ok { + updateBody["category"] = string(normalized) + } else { + return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid category") + } } if req.FcrId != nil { updateBody["fcr_id"] = *req.FcrId @@ -541,7 +540,10 @@ func (s projectflockService) attachKandangs(ctx context.Context, tx *gorm.DB, pr if err := tx.Model(&entity.Kandang{}). Where("id IN ?", kandangIDs). - Updates(map[string]any{"project_flock_id": projectFlockID}).Error; err != nil { + Updates(map[string]any{ + "project_flock_id": projectFlockID, + "status": string(utils.KandangStatusPengajuan), + }).Error; err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to update kandangs") } diff --git a/internal/modules/production/project_flocks/validations/projectflock.validation.go b/internal/modules/production/project_flocks/validations/projectflock.validation.go index 0d8d3a80..bbe957b6 100644 --- a/internal/modules/production/project_flocks/validations/projectflock.validation.go +++ b/internal/modules/production/project_flocks/validations/projectflock.validation.go @@ -1,32 +1,32 @@ package validation type Create struct { - 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"` - KandangIds []uint `json:"kandang_ids" validate:"required,min=1,dive,gt=0"` + FlockId uint `json:"flock_id" validate:"required_strict,number,gt=0"` + AreaId uint `json:"area_id" validate:"required_strict,number,gt=0"` + Category string `json:"category" validate:"required_strict,oneof=growing laying GROWING LAYING"` + FcrId uint `json:"fcr_id" validate:"required_strict,number,gt=0"` + LocationId uint `json:"location_id" 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"` - 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"` - LocationId *uint `json:"location_id,omitempty" validate:"omitempty,number,gt=0"` - Period *int `json:"period,omitempty" validate:"omitempty,number,gt=0"` - KandangIds []uint `json:"kandang_ids,omitempty" validate:"omitempty,min=1,dive,gt=0"` + FlockId *uint `json:"flock_id,omitempty" validate:"omitempty,number,gt=0"` + AreaId *uint `json:"area_id,omitempty" validate:"omitempty,number,gt=0"` + Category *string `json:"category,omitempty" validate:"omitempty,oneof=growing laying GROWING LAYING"` + FcrId *uint `json:"fcr_id,omitempty" validate:"omitempty,number,gt=0"` + LocationId *uint `json:"location_id,omitempty" validate:"omitempty,number,gt=0"` + Period *int `json:"period,omitempty" validate:"omitempty,number,gt=0"` + KandangIds []uint `json:"kandang_ids,omitempty" validate:"omitempty,min=1,dive,gt=0"` } 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"` - SortBy string `query:"sort_by" validate:"omitempty,oneof=area location kandangs period"` - SortOrder string `query:"sort_order" validate:"omitempty,oneof=asc desc"` - AreaId uint `query:"area_id" validate:"omitempty,number,gt=0"` - LocationId uint `query:"location_id" validate:"omitempty,number,gt=0"` - Period int `query:"period" validate:"omitempty,number,gt=0"` + 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"` + SortBy string `query:"sort_by" validate:"omitempty,oneof=area location kandangs period"` + SortOrder string `query:"sort_order" validate:"omitempty,oneof=asc desc"` + AreaId uint `query:"area_id" validate:"omitempty,number,gt=0"` + LocationId uint `query:"location_id" validate:"omitempty,number,gt=0"` + Period int `query:"period" validate:"omitempty,number,gt=0"` KandangIds []uint `query:"kandang_id" validate:"omitempty,dive,gt=0"` } diff --git a/internal/utils/constant.go b/internal/utils/constant.go index dbc06660..d780d2ae 100644 --- a/internal/utils/constant.go +++ b/internal/utils/constant.go @@ -59,7 +59,6 @@ var allFlagTypes = func() map[FlagType]struct{} { return m }() - func AllFlagTypes() map[FlagType]struct{} { return allFlagTypes } @@ -76,8 +75,6 @@ const ( WarehouseTypeKandang WarehouseType = "KANDANG" ) - - // ------------------------------------------------------------------- // WarehouseType // ------------------------------------------------------------------- @@ -100,19 +97,29 @@ const ( SupplierCategorySapronak SupplierCategory = "SAPRONAK" ) - - // ------------------------------------------------------------------- -// Kandang Status +// Kandang Status // ------------------------------------------------------------------- type KandangStatus string const ( - KandangStatusNonActive KandangStatus = "NON_ACTIVE" - KandangStatusPengajuan KandangStatus = "PENGAJUAN" - KandangStatusActive KandangStatus = "ACTIVE" + KandangStatusNonActive KandangStatus = "NON_ACTIVE" + KandangStatusPengajuan KandangStatus = "PENGAJUAN" + KandangStatusActive KandangStatus = "ACTIVE" ) + +// ------------------------------------------------------------------- +// ProjectFlockCategory +// ------------------------------------------------------------------- + +type ProjectFlockCategory string + +const ( + ProjectFlockCategoryGrowing ProjectFlockCategory = "GROWING" + ProjectFlockCategoryLaying ProjectFlockCategory = "LAYING" +) + // ------------------------------------------------------------------- // Validators // ------------------------------------------------------------------- @@ -223,6 +230,21 @@ func IsValidCustomerSupplierType(v string) bool { return false } +func NormalizeProjectFlockCategory(v string) (ProjectFlockCategory, bool) { + normalized := ProjectFlockCategory(strings.ToUpper(strings.TrimSpace(v))) + switch normalized { + case ProjectFlockCategoryGrowing, ProjectFlockCategoryLaying: + return normalized, true + default: + return "", false + } +} + +func IsValidProjectFlockCategory(v string) bool { + _, ok := NormalizeProjectFlockCategory(v) + return ok +} + func IsValidSupplierCategory(v string) bool { switch SupplierCategory(v) { case SupplierCategoryBOP, SupplierCategorySapronak: diff --git a/test/integration/master_data/kandang_test.go b/test/integration/master_data/kandang_test.go index 580196d4..6f7c5ce7 100644 --- a/test/integration/master_data/kandang_test.go +++ b/test/integration/master_data/kandang_test.go @@ -8,6 +8,7 @@ import ( "github.com/gofiber/fiber/v2" "gitlab.com/mbugroup/lti-api.git/internal/entities" + "gitlab.com/mbugroup/lti-api.git/internal/utils" ) func TestKandangIntegration(t *testing.T) { @@ -51,20 +52,19 @@ func TestKandangIntegration(t *testing.T) { }) t.Run("cannot assign project floc with existing active kandang", func(t *testing.T) { - categoryID := createProductCategory(t, app, "DOC Category", "DOC1") fcrID := createFcr(t, app, "FCR For Floc", []map[string]any{ {"weight": 1.0, "fcr_number": 1.5, "mortality": 2.0}, }) flocID := createFlock(t, app, "Floc Test") projectFloc := entities.ProjectFlock{ - FlockId: flocID, - AreaId: areaID, - ProductCategoryId: categoryID, - FcrId: fcrID, - LocationId: locationID, - Period: 1, - CreatedBy: 1, + FlockId: flocID, + AreaId: areaID, + Category: string(utils.ProjectFlockCategoryGrowing), + FcrId: fcrID, + LocationId: locationID, + Period: 1, + CreatedBy: 1, } if err := db.Create(&projectFloc).Error; err != nil { t.Fatalf("failed to seed project floc: %v", err) diff --git a/test/integration/master_data/project_flock_test.go b/test/integration/master_data/project_flock_test.go index c5e0442c..fbeb804c 100644 --- a/test/integration/master_data/project_flock_test.go +++ b/test/integration/master_data/project_flock_test.go @@ -19,19 +19,18 @@ func TestProjectFlockSummary(t *testing.T) { areaID := createArea(t, app, "Area Project") locationID := createLocation(t, app, "Location Project", "Address", areaID) flockID := createFlock(t, app, "Flock Summary") - categoryID := createProductCategory(t, app, "DOC Summary", "DOCS") fcrID := createFcr(t, app, "FCR Summary", []map[string]any{ {"weight": 1.0, "fcr_number": 1.5, "mortality": 2.0}, }) kandangID := createKandang(t, app, "Kandang Summary", locationID, 1) createPayload := map[string]any{ - "flock_id": flockID, - "area_id": areaID, - "product_category_id": categoryID, - "fcr_id": fcrID, - "location_id": locationID, - "kandang_ids": []uint{kandangID}, + "flock_id": flockID, + "area_id": areaID, + "category": "growing", + "fcr_id": fcrID, + "location_id": locationID, + "kandang_ids": []uint{kandangID}, } resp, body := doJSONRequest(t, app, http.MethodPost, "/api/production/project_flocks", createPayload) if resp.StatusCode != fiber.StatusCreated { @@ -40,9 +39,10 @@ func TestProjectFlockSummary(t *testing.T) { var createResp struct { Data struct { - Id uint `json:"id"` - Period int `json:"period"` - Flock struct { + Id uint `json:"id"` + Period int `json:"period"` + Category string `json:"category"` + Flock struct { Id uint `json:"id"` Name string `json:"name"` } `json:"flock"` @@ -50,11 +50,6 @@ func TestProjectFlockSummary(t *testing.T) { Id uint `json:"id"` Name string `json:"name"` } `json:"area"` - ProductCategory struct { - Id uint `json:"id"` - Name string `json:"name"` - Code string `json:"code"` - } `json:"product_category"` Fcr struct { Id uint `json:"id"` Name string `json:"name"` @@ -86,19 +81,27 @@ func TestProjectFlockSummary(t *testing.T) { 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.Category != string(utils.ProjectFlockCategoryGrowing) { + t.Fatalf("expected category to be %s, got %s", utils.ProjectFlockCategoryGrowing, createResp.Data.Category) + } 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.Kandangs[0].Status == "" { - t.Fatalf("expected kandang status to be present, got %+v", createResp.Data.Kandangs[0]) + if createResp.Data.Kandangs[0].Status != string(utils.KandangStatusPengajuan) { + t.Fatalf("expected kandang status to be PENGAJUAN, got %s", createResp.Data.Kandangs[0].Status) } if createResp.Data.Period != 1 { t.Fatalf("expected period 1 to be assigned automatically, got %d", createResp.Data.Period) } + createdKandang := fetchKandang(t, db, kandangID) + if createdKandang.Status != string(utils.KandangStatusPengajuan) { + t.Fatalf("expected kandang status in DB to be PENGAJUAN, got %s", createdKandang.Status) + } + var pivotRecords []entities.ProjectFlockKandang if err := db.Where("project_flock_id = ?", createResp.Data.Id).Find(&pivotRecords).Error; err != nil { t.Fatalf("failed to fetch pivot records: %v", err) @@ -116,12 +119,12 @@ func TestProjectFlockSummary(t *testing.T) { 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}, + "flock_id": flockID, + "area_id": areaID, + "category": "laying", + "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 { @@ -129,8 +132,9 @@ func TestProjectFlockSummary(t *testing.T) { } var createRespSecond struct { Data struct { - Id uint `json:"id"` - Period int `json:"period"` + Id uint `json:"id"` + Period int `json:"period"` + Category string `json:"category"` } `json:"data"` } if err := json.Unmarshal(body, &createRespSecond); err != nil { @@ -139,6 +143,9 @@ func TestProjectFlockSummary(t *testing.T) { if createRespSecond.Data.Period != 2 { t.Fatalf("expected second period to be 2, got %d", createRespSecond.Data.Period) } + if createRespSecond.Data.Category != string(utils.ProjectFlockCategoryLaying) { + t.Fatalf("expected category to be %s, got %s", utils.ProjectFlockCategoryLaying, createRespSecond.Data.Category) + } pivotRecords = nil if err := db.Where("project_flock_id = ?", createRespSecond.Data.Id).Find(&pivotRecords).Error; err != nil { @@ -155,6 +162,11 @@ func TestProjectFlockSummary(t *testing.T) { t.Fatalf("expected second pivot DetachedAt to be nil, got %v", secondPivotRecord.DetachedAt) } + secondKandang := fetchKandang(t, db, secondKandangID) + if secondKandang.Status != string(utils.KandangStatusPengajuan) { + t.Fatalf("expected second kandang status in DB to be PENGAJUAN, got %s", secondKandang.Status) + } + 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, got %d: %s", resp.StatusCode, string(body)) @@ -202,7 +214,7 @@ func TestProjectFlockSummary(t *testing.T) { t.Fatalf("expected 200 when deleting second project flock, got %d: %s", resp.StatusCode, string(body)) } - secondKandang := fetchKandang(t, db, secondKandangID) + secondKandang = fetchKandang(t, db, secondKandangID) if secondKandang.ProjectFlockId != nil { t.Fatalf("expected second project_flock_id to be nil after delete, got %v", *secondKandang.ProjectFlockId) } @@ -245,19 +257,18 @@ func TestProjectFlockSearchByRelatedFields(t *testing.T) { areaID := createArea(t, app, "Area Search Target") locationID := createLocation(t, app, "Location Search Target", "Location Address Target", areaID) flockID := createFlock(t, app, "Flock Search Target") - categoryID := createProductCategory(t, app, "Category Search Target", "CATGT") fcrID := createFcr(t, app, "FCR Search Target", []map[string]any{ {"weight": 1.0, "fcr_number": 1.5, "mortality": 2.0}, }) kandangID := createKandang(t, app, "Kandang Search Target", locationID, 1) createPayload := map[string]any{ - "flock_id": flockID, - "area_id": areaID, - "product_category_id": categoryID, - "fcr_id": fcrID, - "location_id": locationID, - "kandang_ids": []uint{kandangID}, + "flock_id": flockID, + "area_id": areaID, + "category": "growing", + "fcr_id": fcrID, + "location_id": locationID, + "kandang_ids": []uint{kandangID}, } resp, body := doJSONRequest(t, app, http.MethodPost, "/api/production/project_flocks", createPayload) @@ -277,8 +288,8 @@ func TestProjectFlockSearchByRelatedFields(t *testing.T) { searchTerms := []string{ "Flock Search Target", "Area Search Target", - "Category Search Target", - "CATGT", + string(utils.ProjectFlockCategoryGrowing), + "growing", "FCR Search Target", "Kandang Search Target", "Location Search Target", @@ -329,7 +340,6 @@ func TestProjectFlockSorting(t *testing.T) { flockOne := createFlock(t, app, "Flock Sort One") flockTwo := createFlock(t, app, "Flock Sort Two") - categoryID := createProductCategory(t, app, "Category Sort", "CSORT") fcrID := createFcr(t, app, "FCR Sort", []map[string]any{ {"weight": 1.0, "fcr_number": 1.5, "mortality": 2.0}, }) @@ -339,12 +349,12 @@ func TestProjectFlockSorting(t *testing.T) { kandangThree := createKandang(t, app, "Kandang Sort Three", locationB, 1) projectOnePayload := map[string]any{ - "flock_id": flockOne, - "area_id": areaA, - "product_category_id": categoryID, - "fcr_id": fcrID, - "location_id": locationA, - "kandang_ids": []uint{kandangOne}, + "flock_id": flockOne, + "area_id": areaA, + "category": "growing", + "fcr_id": fcrID, + "location_id": locationA, + "kandang_ids": []uint{kandangOne}, } resp, body := doJSONRequest(t, app, http.MethodPost, "/api/production/project_flocks", projectOnePayload) if resp.StatusCode != fiber.StatusCreated { @@ -353,12 +363,12 @@ func TestProjectFlockSorting(t *testing.T) { projectOneID := parseProjectFlockID(t, body) projectTwoPayload := map[string]any{ - "flock_id": flockTwo, - "area_id": areaB, - "product_category_id": categoryID, - "fcr_id": fcrID, - "location_id": locationB, - "kandang_ids": []uint{kandangTwo, kandangThree}, + "flock_id": flockTwo, + "area_id": areaB, + "category": "laying", + "fcr_id": fcrID, + "location_id": locationB, + "kandang_ids": []uint{kandangTwo, kandangThree}, } resp, body = doJSONRequest(t, app, http.MethodPost, "/api/production/project_flocks", projectTwoPayload) if resp.StatusCode != fiber.StatusCreated {