package service import ( "context" "errors" "fmt" "strings" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" m "gitlab.com/mbugroup/lti-api.git/internal/middleware" repository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/production-standards/repositories" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/master/production-standards/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 ProductionStandardService interface { GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.ProductionStandard, int64, error) GetOne(ctx *fiber.Ctx, id uint) (*entity.ProductionStandard, error) CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.ProductionStandard, error) UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.ProductionStandard, error) DeleteOne(ctx *fiber.Ctx, id uint) error EnsureWeekStart(ctx context.Context, standardID uint, category string) error EnsureWeekAvailable(ctx context.Context, standardID uint, category string, day int) error } type productionStandardService struct { Log *logrus.Logger Validate *validator.Validate Repository repository.ProductionStandardRepository ProductionStandardDetailRepo repository.ProductionStandardDetailRepository StandardGrowthDetailRepo repository.StandardGrowthDetailRepository } func NewProductionStandardService( repo repository.ProductionStandardRepository, productionStandardDetailRepo repository.ProductionStandardDetailRepository, standardGrowthDetailRepo repository.StandardGrowthDetailRepository, validate *validator.Validate, ) ProductionStandardService { return &productionStandardService{ Log: utils.Log, Validate: validate, Repository: repo, ProductionStandardDetailRepo: productionStandardDetailRepo, StandardGrowthDetailRepo: standardGrowthDetailRepo, } } func (s productionStandardService) withRelations(db *gorm.DB) *gorm.DB { return db. Preload("CreatedUser"). Preload("ProductionStandardDetails"). Preload("StandardGrowthDetails") } func (s productionStandardService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.ProductionStandard, int64, error) { if err := s.Validate.Struct(params); err != nil { return nil, 0, err } offset := (params.Page - 1) * params.Limit productionStandards, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB { if params.Search != "" { return db.Where("name ILIKE ?", "%"+params.Search+"%") } if params.ProjectCategory != "" { return db.Where("project_category = ?", params.ProjectCategory) } return db.Order("created_at DESC").Order("updated_at DESC") }) if err != nil { s.Log.Errorf("Failed to get productionStandards: %+v", err) return nil, 0, err } return productionStandards, total, nil } func (s productionStandardService) GetOne(c *fiber.Ctx, id uint) (*entity.ProductionStandard, error) { productionStandard, err := s.Repository.GetByID(c.Context(), id, s.withRelations) if errors.Is(err, gorm.ErrRecordNotFound) { return nil, fiber.NewError(fiber.StatusNotFound, "ProductionStandard not found") } if err != nil { return nil, err } return productionStandard, nil } func (s *productionStandardService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.ProductionStandard, error) { if err := s.Validate.Struct(req); err != nil { return nil, err } actorID, err := m.ActorIDFromContext(c) if err != nil { return nil, err } nameExists, err := s.Repository.NameExists(c.Context(), req.Name, nil) if err != nil { return nil, err } if nameExists { return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Production standard with name '%s' already exists", req.Name)) } var createdStandard *entity.ProductionStandard err = s.Repository.DB().WithContext(c.Context()).Transaction(func(tx *gorm.DB) error { standardRepoTx := repository.NewProductionStandardRepository(tx) productionStandardDetailRepoTx := repository.NewProductionStandardDetailRepository(tx) standardGrowthDetailRepoTx := repository.NewStandardGrowthDetailRepository(tx) newStandard := &entity.ProductionStandard{ Name: req.Name, ProjectCategory: req.ProjectCategory, CreatedBy: actorID, } if err := standardRepoTx.CreateOne(c.Context(), newStandard, nil); err != nil { return fmt.Errorf("failed to create production standard: %w", err) } for _, detailReq := range req.Details { if detailReq.ProductionStandardUniformityDetails == nil { return fmt.Errorf("production_standard_uniformity_details is required in week %d", detailReq.Week) } if req.ProjectCategory == string(utils.ProjectFlockCategoryLaying) { if detailReq.ProductionStandardDetails == nil { return fmt.Errorf("production_standard_details is required for LAYING category in week %d", detailReq.Week) } productionStandardDetail := &entity.ProductionStandardDetail{ ProductionStandardId: newStandard.Id, Week: detailReq.Week, TargetHenDayProduction: detailReq.ProductionStandardDetails.TargetHenDayProduction, TargetHenHouseProduction: detailReq.ProductionStandardDetails.TargetHenHouseProduction, TargetEggWeight: detailReq.ProductionStandardDetails.TargetEggWeight, TargetEggMass: detailReq.ProductionStandardDetails.TargetEggMass, StandardFCR: detailReq.ProductionStandardDetails.StandardFCR, } if err := productionStandardDetailRepoTx.CreateOne(c.Context(), productionStandardDetail, nil); err != nil { return fmt.Errorf("failed to create production standard detail for week %d: %w", detailReq.Week, err) } } standardGrowthDetail := &entity.StandardGrowthDetail{ ProductionStandardId: newStandard.Id, Week: detailReq.Week, TargetMeanBw: detailReq.ProductionStandardUniformityDetails.TargetMeanBw, MaxDepletion: detailReq.ProductionStandardUniformityDetails.MaxDepletion, MinUniformity: detailReq.ProductionStandardUniformityDetails.MinUniformity, FeedIntake: detailReq.ProductionStandardUniformityDetails.FeedIntake, CreatedBy: actorID, } if err := standardGrowthDetailRepoTx.CreateOne(c.Context(), standardGrowthDetail, nil); err != nil { return fmt.Errorf("failed to create standard growth detail for week %d: %w", detailReq.Week, err) } } createdStandard = newStandard return nil }) if err != nil { s.Log.Errorf("Failed to create production standard: %+v", err) return nil, err } return s.GetOne(c, createdStandard.Id) } func (s productionStandardService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.ProductionStandard, error) { if err := s.Validate.Struct(req); err != nil { return nil, err } actorID, err := m.ActorIDFromContext(c) if err != nil { return nil, err } var updatedStandard *entity.ProductionStandard err = s.Repository.DB().WithContext(c.Context()).Transaction(func(tx *gorm.DB) error { standardRepoTx := repository.NewProductionStandardRepository(tx) productionStandardDetailRepoTx := repository.NewProductionStandardDetailRepository(tx) standardGrowthDetailRepoTx := repository.NewStandardGrowthDetailRepository(tx) existingStandard, err := standardRepoTx.GetByID(c.Context(), id, nil) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return fiber.NewError(fiber.StatusNotFound, "ProductionStandard not found") } return fmt.Errorf("failed to get production standard: %w", err) } updateBody := make(map[string]any) if req.Name != nil { nameExists, err := s.Repository.NameExists(c.Context(), *req.Name, &id) if err != nil { return err } if nameExists { return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Production standard with name '%s' already exists", *req.Name)) } updateBody["name"] = *req.Name } if req.ProjectCategory != nil { updateBody["project_category"] = *req.ProjectCategory } if len(updateBody) > 0 { if err := standardRepoTx.PatchOne(c.Context(), id, updateBody, nil); err != nil { return fmt.Errorf("failed to update production standard: %w", err) } } if req.Details != nil && len(req.Details) > 0 { projectCategory := existingStandard.ProjectCategory if req.ProjectCategory != nil { projectCategory = *req.ProjectCategory } if err := productionStandardDetailRepoTx.DeleteByProductionStandardID(c.Context(), id); err != nil { return fmt.Errorf("failed to delete old production standard details: %w", err) } if err := standardGrowthDetailRepoTx.DeleteByProductionStandardID(c.Context(), id); err != nil { return fmt.Errorf("failed to delete old standard growth details: %w", err) } for _, detailReq := range req.Details { if detailReq.ProductionStandardUniformityDetails == nil { return fmt.Errorf("production_standard_uniformity_details is required in week %d", detailReq.Week) } if projectCategory == "LAYING" { if detailReq.ProductionStandardDetails == nil { return fmt.Errorf("production_standard_details is required for LAYING category in week %d", detailReq.Week) } productionStandardDetail := &entity.ProductionStandardDetail{ ProductionStandardId: id, Week: detailReq.Week, TargetHenDayProduction: detailReq.ProductionStandardDetails.TargetHenDayProduction, TargetHenHouseProduction: detailReq.ProductionStandardDetails.TargetHenHouseProduction, TargetEggWeight: detailReq.ProductionStandardDetails.TargetEggWeight, TargetEggMass: detailReq.ProductionStandardDetails.TargetEggMass, StandardFCR: detailReq.ProductionStandardDetails.StandardFCR, } if err := productionStandardDetailRepoTx.CreateOne(c.Context(), productionStandardDetail, nil); err != nil { return fmt.Errorf("failed to create production standard detail for week %d: %w", detailReq.Week, err) } } standardGrowthDetail := &entity.StandardGrowthDetail{ ProductionStandardId: id, Week: detailReq.Week, TargetMeanBw: detailReq.ProductionStandardUniformityDetails.TargetMeanBw, MaxDepletion: detailReq.ProductionStandardUniformityDetails.MaxDepletion, MinUniformity: detailReq.ProductionStandardUniformityDetails.MinUniformity, FeedIntake: detailReq.ProductionStandardUniformityDetails.FeedIntake, CreatedBy: actorID, } if err := standardGrowthDetailRepoTx.CreateOne(c.Context(), standardGrowthDetail, nil); err != nil { return fmt.Errorf("failed to create standard growth detail for week %d: %w", detailReq.Week, err) } } } updatedStandard = existingStandard return nil }) if err != nil { return nil, err } return s.GetOne(c, updatedStandard.Id) } func (s productionStandardService) 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, "ProductionStandard not found") } return err } return nil } func (s productionStandardService) EnsureWeekStart(ctx context.Context, standardID uint, category string) error { if standardID == 0 || strings.TrimSpace(category) == "" { return nil } switch strings.ToUpper(category) { case string(utils.ProjectFlockCategoryLaying): details, err := s.ProductionStandardDetailRepo.GetByProductionStandardID(ctx, standardID) if err != nil { return err } startWeek := 0 if len(details) > 0 { startWeek = details[0].Week } if startWeek != 18 { return fiber.NewError(fiber.StatusBadRequest, "Week tidak sesuai dengan standart kategori project flock") } case string(utils.ProjectFlockCategoryGrowing): details, err := s.StandardGrowthDetailRepo.GetByProductionStandardID(ctx, standardID) if err != nil { return err } startWeek := 0 if len(details) > 0 { startWeek = details[0].Week } if startWeek != 1 { return fiber.NewError(fiber.StatusBadRequest, "Week tidak sesuai dengan standart kategori project flock") } } return nil } func (s productionStandardService) EnsureWeekAvailable(ctx context.Context, standardID uint, category string, day int) error { if standardID == 0 || day <= 0 { return nil } upperCategory := strings.ToUpper(category) weekBase := 1 if upperCategory == string(utils.ProjectFlockCategoryLaying) { weekBase = 18 } week := ((day - 1) / 7) + weekBase if week <= 0 { return nil } if upperCategory == string(utils.ProjectFlockCategoryLaying) { detail, err := s.ProductionStandardDetailRepo.GetByStandardIDAndWeek(ctx, standardID, week) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Standart production tidak tersedia untuk week %d", week)) } return err } if detail == nil { return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Standart production tidak tersedia untuk week %d", week)) } } growthDetail, err := s.StandardGrowthDetailRepo.GetByStandardIDAndWeek(ctx, standardID, week) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Standart production tidak tersedia untuk week %d", week)) } return err } if growthDetail == nil { return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Standart production tidak tersedia untuk week %d", week)) } return nil }