package service import ( "errors" "fmt" "strings" commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository" commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories" KandangRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories" rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories" repository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/repositories" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/validations" rProjectFlock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories" "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 ChickinService interface { GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.ProjectChickin, int64, error) GetOne(ctx *fiber.Ctx, id uint) (*entity.ProjectChickin, error) CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.ProjectChickin, error) UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.ProjectChickin, error) DeleteOne(ctx *fiber.Ctx, id uint) error Approval(ctx *fiber.Ctx, req *validation.Approve) ([]entity.ProjectChickin, error) } type chickinService struct { Log *logrus.Logger Validate *validator.Validate Repository repository.ProjectChickinRepository KandangRepo KandangRepo.KandangRepository WarehouseRepo rWarehouse.WarehouseRepository ProductWarehouseRepo rProductWarehouse.ProductWarehouseRepository ProjectFlockRepo rProjectFlock.ProjectflockRepository ProjectflockKandangRepo rProjectFlock.ProjectFlockKandangRepository ProjectflockPopulationRepo rProjectFlock.ProjectFlockPopulationRepository ProjectChickinDetailRepo repository.ProjectChickinDetailRepository } func NewChickinService(repo repository.ProjectChickinRepository, kandangRepo KandangRepo.KandangRepository, warehouseRepo rWarehouse.WarehouseRepository, productWarehouseRepo rProductWarehouse.ProductWarehouseRepository, projectFlockRepo rProjectFlock.ProjectflockRepository, projectflockkandangRepo rProjectFlock.ProjectFlockKandangRepository, projectflockpopulationRepo rProjectFlock.ProjectFlockPopulationRepository, projectChickinDetailRepo repository.ProjectChickinDetailRepository, validate *validator.Validate) ChickinService { return &chickinService{ Log: utils.Log, Validate: validate, Repository: repo, KandangRepo: kandangRepo, WarehouseRepo: warehouseRepo, ProductWarehouseRepo: productWarehouseRepo, ProjectFlockRepo: projectFlockRepo, ProjectflockKandangRepo: projectflockkandangRepo, ProjectflockPopulationRepo: projectflockpopulationRepo, ProjectChickinDetailRepo: projectChickinDetailRepo, } } func (s chickinService) withRelations(db *gorm.DB) *gorm.DB { return db. Preload("CreatedUser"). Preload("ProjectFlockKandang.Kandang"). Preload("ProjectFlockKandang.Kandang.Location"). Preload("ProjectFlockKandang.Kandang.Location.Area"). Preload("ProjectFlockKandang.Kandang.Pic"). Preload("ProjectFlockKandang.ProjectFlock"). Preload("ProjectFlockKandang.ProjectFlock.Flock"). Preload("ProjectFlockKandang.ProjectFlock.Area"). Preload("ProjectFlockKandang.ProjectFlock.Fcr"). Preload("ProjectFlockKandang.ProjectFlock.Location"). Preload("ProjectFlockKandang.ProjectFlock.Location.Area") } func (s chickinService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.ProjectChickin, int64, error) { if err := s.Validate.Struct(params); err != nil { return nil, 0, err } offset := (params.Page - 1) * params.Limit chickins, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB { db = s.withRelations(db) if params.ProjectFlockKandangId != 0 { return db.Where("project_flock_kandang_id = ?", params.ProjectFlockKandangId) } return db.Order("created_at DESC").Order("updated_at DESC") }) if err != nil { s.Log.Errorf("Failed to get chickins: %+v", err) return nil, 0, err } return chickins, total, nil } func (s chickinService) GetOne(c *fiber.Ctx, id uint) (*entity.ProjectChickin, error) { chickin, err := s.Repository.GetByID(c.Context(), id, s.withRelations) if errors.Is(err, gorm.ErrRecordNotFound) { return nil, fiber.NewError(fiber.StatusNotFound, "Chickin not found") } if err != nil { s.Log.Errorf("Failed get chickin by id: %+v", err) return nil, err } return chickin, nil } func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.ProjectChickin, error) { if err := s.Validate.Struct(req); err != nil { return nil, err } projectFlockKandang, err := s.ProjectflockKandangRepo.GetByID(c.Context(), req.ProjectFlockKandangId) if err != nil { return nil, fiber.NewError(fiber.StatusNotFound, "Project Flock Kandang not found") } warehouse, err := s.WarehouseRepo.GetByKandangID(c.Context(), projectFlockKandang.KandangId) if err != nil { return nil, fiber.NewError(fiber.StatusNotFound, "Warehouse for Kandang not found") } var productWarehouses []entity.ProductWarehouse if strings.ToUpper(strings.TrimSpace(projectFlockKandang.ProjectFlock.Category)) == string(utils.ProjectFlockCategoryGrowing) { productWarehouses, err = s.ProductWarehouseRepo.GetByCategoryCodeAndWarehouseID(c.Context(), "DOC", warehouse.Id) if err != nil || len(productWarehouses) == 0 { return nil, fiber.NewError(fiber.StatusNotFound, "Product for growing category in the Kandang's warehouse not found") } } chickinDate, err := utils.ParseDateString(req.ChickInDate) if err != nil { return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid ChickInDate format") } actorID := uint(1) // todo nanti ambil dari auth context newChikins := make([]*entity.ProjectChickin, 0) for _, productWarehouse := range productWarehouses { if productWarehouse.Quantity > 0 { newChickin := &entity.ProjectChickin{ ProjectFlockKandangId: req.ProjectFlockKandangId, ChickInDate: chickinDate, UsageQty: 0, PendingUsageQty: productWarehouse.Quantity, ProductWarehouseId: productWarehouse.Id, Notes: req.Note, CreatedBy: actorID, } newChikins = append(newChikins, newChickin) } } if len(newChikins) == 0 { return nil, fiber.NewError(fiber.StatusBadRequest, "No chickins to create") } err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction)) productWarehouseTx := s.ProductWarehouseRepo.WithTx(dbTransaction) if err := s.Repository.WithTx(dbTransaction).CreateMany(c.Context(), newChikins, nil); err != nil { return err } latest, err := approvalSvcTx.LatestByTarget(c.Context(), utils.ApprovalWorkflowProjectFlockKandang, projectFlockKandang.Id, nil) if err != nil { return err } for _, chickin := range newChikins { updates := map[string]any{"quantity": 0} if err := productWarehouseTx.PatchOne(c.Context(), chickin.ProductWarehouseId, updates, nil); err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return fmt.Errorf("failed to update product warehouse quantity for id %d", chickin.ProductWarehouseId) } return err } } if latest == nil { action := entity.ApprovalActionCreated if _, err := approvalSvcTx.CreateApproval(c.Context(), utils.ApprovalWorkflowProjectFlockKandang, projectFlockKandang.Id, utils.ProjectFlockKandangStepPengajuan, &action, actorID, nil); err != nil { lower := strings.ToLower(err.Error()) if !(strings.Contains(lower, "duplicate") || strings.Contains(lower, "unique constraint") || strings.Contains(lower, "23505")) { return err } } } return nil }) if err != nil { return nil, fiber.NewError(fiber.StatusBadRequest, err.Error()) } return newChikins[0], nil } func (s chickinService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.ProjectChickin, error) { if err := s.Validate.Struct(req); err != nil { return nil, err } updateBody := make(map[string]any) if req.ChickInDate != "" { updateBody["chick_in_date"] = req.ChickInDate } if req.Note != "" { // entity uses `Notes` => column `notes` updateBody["notes"] = req.Note } 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, "Chickin not found") } s.Log.Errorf("Failed to update chickin: %+v", err) return nil, err } return s.GetOne(c, id) } func (s chickinService) DeleteOne(c *fiber.Ctx, id uint) error { // Simplified delete: directly call repository delete. Complex restore logic removed for now. if err := s.Repository.DeleteOne(c.Context(), id); err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return fiber.NewError(fiber.StatusNotFound, "Chickin not found") } return err } return nil } func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entity.ProjectChickin, error) { if err := s.Validate.Struct(req); err != nil { return nil, err } actorID := uint(1) // todo nanti ambil dari auth context var action entity.ApprovalAction switch strings.ToUpper(strings.TrimSpace(req.Action)) { case string(entity.ApprovalActionRejected): action = entity.ApprovalActionRejected case string(entity.ApprovalActionApproved): action = entity.ApprovalActionApproved default: return nil, fiber.NewError(fiber.StatusBadRequest, "action must be APPROVED or REJECTED") } approvableIDs := uniqueUintSlice(req.ApprovableIds) if len(approvableIDs) == 0 { return nil, fiber.NewError(fiber.StatusBadRequest, "approvable_ids must contain at least one id") } step := utils.ProjectFlockKandangStepPengajuan if action == entity.ApprovalActionApproved { step = utils.ProjectFlockKandangStepDisetujui } err := s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction)) ProjectFlockPopulationRepotx := s.ProjectflockPopulationRepo.WithTx(dbTransaction) chickinRepoTx := s.Repository.WithTx(dbTransaction) for _, approvableID := range approvableIDs { exists, err := s.ProjectflockKandangRepo.WithTx(dbTransaction).IdExists(c.Context(), approvableID) if err != nil { return err } if !exists { return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("ProjectFlockKandang %d not found", approvableID)) } if _, err := approvalSvc.CreateApproval( c.Context(), utils.ApprovalWorkflowProjectFlockKandang, approvableID, step, &action, actorID, req.Notes, ); err != nil { lower := strings.ToLower(err.Error()) if !(strings.Contains(lower, "duplicate") || strings.Contains(lower, "unique constraint") || strings.Contains(lower, "23505")) { return err } s.Log.Infof("ignored duplicate approval for kandang %d: %v", approvableID, err) } if action == entity.ApprovalActionApproved { var chickins []entity.ProjectChickin if err := chickinRepoTx.DB().WithContext(c.Context()).Where("project_flock_kandang_id = ?", approvableID).Find(&chickins).Error; err != nil { return err } for _, chickin := range chickins { population := &entity.ProjectFlockPopulation{ ProjectChickinId: chickin.Id, ProductWarehouseId: chickin.ProductWarehouseId, TotalQty: chickin.PendingUsageQty, TotalUsedQty: 0, Notes: chickin.Notes, CreatedBy: actorID, } if err := ProjectFlockPopulationRepotx.CreateOne(c.Context(), population, nil); err != nil { lower := strings.ToLower(err.Error()) if !(strings.Contains(lower, "duplicate") || strings.Contains(lower, "unique constraint") || strings.Contains(lower, "23505")) { return err } s.Log.Infof("ignored duplicate population for chickin %d: %v", chickin.Id, err) } } } } return nil }) if err != nil { if fiberErr, ok := err.(*fiber.Error); ok { return nil, fiberErr } if errors.Is(err, gorm.ErrRecordNotFound) { return nil, fiber.NewError(fiber.StatusNotFound, "Chickin not found") } s.Log.Errorf("Failed to record approval for chickins %+v: %+v", approvableIDs, err) return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to record approval") } updated := make([]entity.ProjectChickin, 0) for _, kandangID := range approvableIDs { var chickins []entity.ProjectChickin if err := s.Repository.DB().WithContext(c.Context()).Where("project_flock_kandang_id = ?", kandangID).Find(&chickins).Error; err != nil { return nil, err } updated = append(updated, chickins...) } return updated, nil } func uniqueUintSlice(values []uint) []uint { seen := make(map[uint]struct{}, len(values)) result := make([]uint, 0, len(values)) for _, v := range values { if _, ok := seen[v]; ok { continue } seen[v] = struct{}{} result = append(result, v) } return result }