Files
lti-api/internal/modules/production/project_flocks/services/projectflock.service.go
T

1148 lines
39 KiB
Go

package service
import (
"context"
"errors"
"fmt"
"strconv"
"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"
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
productWarehouseRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
flockDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/dto"
flockRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/repositories"
kandangRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories"
nonstockRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/nonstocks/repositories"
warehouseRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
projectBudgetRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
pfutils "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/utils"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/validations"
recordingRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories"
uniformityRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/uniformities/repositories"
purchaseRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/repositories"
utils "gitlab.com/mbugroup/lti-api.git/internal/utils"
approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"github.com/sirupsen/logrus"
"gorm.io/gorm"
)
type ProjectflockService interface {
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.ProjectFlock, int64, map[uint]*flockDTO.FlockRelationDTO, error)
GetOne(ctx *fiber.Ctx, id uint) (*entity.ProjectFlock, *flockDTO.FlockRelationDTO, error)
CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.ProjectFlock, error)
GetAvailableDocQuantity(ctx *fiber.Ctx, kandangID uint) (float64, error)
GetWarehouseByKandangID(ctx *fiber.Ctx, kandangID uint) (*entity.Warehouse, error)
DeleteOne(ctx *fiber.Ctx, id uint) error
GetProjectFlockKandangByProjectAndKandang(ctx *fiber.Ctx, projectFlockID uint, kandangID uint) (*entity.ProjectFlockKandang, float64, error)
GetProjectFlockKandangPopulation(ctx *fiber.Ctx, projectFlockKandangID uint) (float64, error)
GetPeriodSummary(ctx *fiber.Ctx, locationID uint) ([]KandangPeriodSummary, error)
GetProjectPeriods(ctx *fiber.Ctx, projectIDs []uint) (map[uint]int, error)
Approval(ctx *fiber.Ctx, req *validation.Approve) ([]entity.ProjectFlock, error)
Resubmit(ctx *fiber.Ctx, req *validation.Resubmit, id uint) (*entity.ProjectFlock, error)
}
type projectflockService struct {
Log *logrus.Logger
Validate *validator.Validate
Repository repository.ProjectflockRepository
FlockRepo flockRepository.FlockRepository
KandangRepo kandangRepository.KandangRepository
NonstockRepo nonstockRepository.NonstockRepository
WarehouseRepo warehouseRepository.WarehouseRepository
ProductWarehouseRepo productWarehouseRepository.ProductWarehouseRepository
ProjectBudgetRepo projectBudgetRepository.ProjectBudgetRepository
PivotRepo repository.ProjectFlockKandangRepository
PopulationRepo repository.ProjectFlockPopulationRepository
RecordingRepo recordingRepo.RecordingRepository
ApprovalSvc commonSvc.ApprovalService
approvalWorkflow approvalutils.ApprovalWorkflowKey
}
type KandangPeriodSummary struct {
Id uint
Name string
Period int
}
func NewProjectflockService(
repo repository.ProjectflockRepository,
flockRepo flockRepository.FlockRepository,
kandangRepo kandangRepository.KandangRepository,
pivotRepo repository.ProjectFlockKandangRepository,
warehouseRepo warehouseRepository.WarehouseRepository,
productWarehouseRepo productWarehouseRepository.ProductWarehouseRepository,
projectBudgetRepo projectBudgetRepository.ProjectBudgetRepository,
nonstockRepo nonstockRepository.NonstockRepository,
populationRepo repository.ProjectFlockPopulationRepository,
recordingRepo recordingRepo.RecordingRepository,
approvalSvc commonSvc.ApprovalService,
validate *validator.Validate,
) ProjectflockService {
return &projectflockService{
Log: utils.Log,
Validate: validate,
Repository: repo,
FlockRepo: flockRepo,
KandangRepo: kandangRepo,
NonstockRepo: nonstockRepo,
WarehouseRepo: warehouseRepo,
ProductWarehouseRepo: productWarehouseRepo,
ProjectBudgetRepo: projectBudgetRepo,
PivotRepo: pivotRepo,
PopulationRepo: populationRepo,
RecordingRepo: recordingRepo,
ApprovalSvc: approvalSvc,
approvalWorkflow: utils.ApprovalWorkflowProjectFlock,
}
}
func (s projectflockService) approvalQueryModifier() func(*gorm.DB) *gorm.DB {
return func(db *gorm.DB) *gorm.DB {
return db.Preload("ActionUser")
}
}
func (s projectflockService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.ProjectFlock, int64, map[uint]*flockDTO.FlockRelationDTO, error) {
if err := s.Validate.Struct(params); err != nil {
return nil, 0, nil, err
}
offset := (params.Page - 1) * params.Limit
projectflocks, total, err := s.Repository.GetAllWithFilters(c.Context(), offset, params.Limit, params)
if err != nil {
s.Log.Errorf("Failed to get projectflocks: %+v", err)
return nil, 0, nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flocks")
}
if s.ApprovalSvc != nil && len(projectflocks) > 0 {
ids := make([]uint, len(projectflocks))
for i, item := range projectflocks {
ids[i] = item.Id
}
latestMap, err := s.ApprovalSvc.LatestByTargets(c.Context(), s.approvalWorkflow, ids, s.approvalQueryModifier())
if err != nil {
s.Log.Warnf("Unable to load latest approvals for projectflocks: %+v", err)
} else if len(latestMap) > 0 {
for i := range projectflocks {
if approval, ok := latestMap[projectflocks[i].Id]; ok {
projectflocks[i].LatestApproval = approval
}
}
}
}
flockMap := make(map[uint]*flockDTO.FlockRelationDTO)
for i := range projectflocks {
if projectflocks[i].FlockName != "" {
baseName := pfutils.DeriveBaseName(projectflocks[i].FlockName)
if baseName != "" {
flock, err := s.FlockRepo.GetByName(c.Context(), baseName)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
s.Log.Warnf("Failed to fetch flock %q: %+v", baseName, err)
} else if flock != nil {
flockMap[projectflocks[i].Id] = &flockDTO.FlockRelationDTO{
Id: flock.Id,
Name: flock.Name,
}
}
}
}
}
return projectflocks, total, flockMap, nil
}
func (s projectflockService) getOneEntityOnly(c *fiber.Ctx, id uint) (*entity.ProjectFlock, error) {
projectflock, err := s.Repository.GetByID(c.Context(), id, s.Repository.WithDefaultRelations())
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Projectflock not found")
}
if err != nil {
s.Log.Errorf("Failed get projectflock by id: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock")
}
if s.ApprovalSvc != nil {
approvals, err := s.ApprovalSvc.ListByTarget(c.Context(), s.approvalWorkflow, id, s.approvalQueryModifier())
if err != nil {
s.Log.Warnf("Unable to load approvals for projectflock %d: %+v", id, err)
} else if len(approvals) > 0 {
if projectflock.LatestApproval == nil {
latest := approvals[len(approvals)-1]
projectflock.LatestApproval = &latest
}
} else {
projectflock.LatestApproval = nil
}
}
return projectflock, nil
}
func (s projectflockService) GetOne(c *fiber.Ctx, id uint) (*entity.ProjectFlock, *flockDTO.FlockRelationDTO, error) {
projectflock, err := s.Repository.GetByID(c.Context(), id, s.Repository.WithDefaultRelations())
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil, fiber.NewError(fiber.StatusNotFound, "Projectflock not found")
}
if err != nil {
s.Log.Errorf("Failed get projectflock by id: %+v", err)
return nil, nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock")
}
if s.ApprovalSvc != nil {
approvals, err := s.ApprovalSvc.ListByTarget(c.Context(), s.approvalWorkflow, id, s.approvalQueryModifier())
if err != nil {
s.Log.Warnf("Unable to load approvals for projectflock %d: %+v", id, err)
} else if len(approvals) > 0 {
if projectflock.LatestApproval == nil {
latest := approvals[len(approvals)-1]
projectflock.LatestApproval = &latest
}
} else {
projectflock.LatestApproval = nil
}
}
// Fetch Flock master data for this ProjectFlock
var flockResult *flockDTO.FlockRelationDTO
if projectflock.FlockName != "" {
baseName := pfutils.DeriveBaseName(projectflock.FlockName)
if baseName != "" {
flock, err := s.FlockRepo.GetByName(c.Context(), baseName)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
s.Log.Warnf("Failed to fetch flock %q: %+v", baseName, err)
} else if flock != nil {
flockResult = &flockDTO.FlockRelationDTO{
Id: flock.Id,
Name: flock.Name,
}
}
}
}
return projectflock, flockResult, nil
}
func (s *projectflockService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.ProjectFlock, error) {
if err := s.Validate.Struct(req); err != nil {
return nil, err
}
actorID, err := m.ActorIDFromContext(c)
if err != nil {
return nil, err
}
cat := strings.ToUpper(req.Category)
if !utils.IsValidProjectFlockCategory(cat) {
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid category")
}
if len(req.KandangIds) == 0 {
return nil, fiber.NewError(fiber.StatusBadRequest, "kandang_ids is required")
}
baseName := strings.TrimSpace(req.FlockName)
if baseName == "" {
return nil, fiber.NewError(fiber.StatusBadRequest, "Flock name cannot be empty")
}
if err := commonSvc.EnsureRelations(c.Context(),
commonSvc.RelationCheck{Name: "Area", ID: &req.AreaId, Exists: s.Repository.AreaExists},
commonSvc.RelationCheck{Name: "FCR", ID: &req.FcrId, Exists: s.Repository.FcrExists},
commonSvc.RelationCheck{Name: "Production Standard", ID: &req.ProductionStandardId, Exists: s.Repository.ProductionStandardExists},
commonSvc.RelationCheck{Name: "Location", ID: &req.LocationId, Exists: s.Repository.LocationExists},
); err != nil {
return nil, err
}
var location entity.Location
if err := s.Repository.DB().WithContext(c.Context()).
Where("id = ? AND area_id = ?", req.LocationId, req.AreaId).
First(&location).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusBadRequest, "Lokasi tidak berada pada area yang diminta")
}
return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal memvalidasi relasi area-lokasi")
}
canonicalBase := baseName
if s.FlockRepo != nil {
baseFlock, err := s.ensureFlockByName(c.Context(), actorID, baseName)
if err != nil {
return nil, err
}
canonicalBase = baseFlock.Name
}
kandangIDs := uniqueUintSlice(req.KandangIds)
kandangs, err := s.KandangRepo.GetByIDs(c.Context(), kandangIDs, nil)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Some kandangs not found")
}
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch kandangs")
}
if len(kandangs) != len(kandangIDs) {
return nil, fiber.NewError(fiber.StatusNotFound, "Some kandangs not found")
}
for _, kandang := range kandangs {
if kandang.LocationId != req.LocationId {
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Kandang %d tidak berada pada lokasi yang sama dengan project flock", kandang.Id))
}
}
// larang kalau ada yg sudah terikat ke project lain
if linked, err := s.pivotRepo().HasKandangsLinkedToOtherProject(c.Context(), kandangIDs, nil); err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate kandangs linkage")
} else if linked {
return nil, fiber.NewError(fiber.StatusConflict, "Beberapa kandang sudah terikat dengan project flock lain")
}
createBody := &entity.ProjectFlock{
AreaId: req.AreaId,
Category: cat,
FcrId: req.FcrId,
ProductionStandardId: req.ProductionStandardId,
LocationId: req.LocationId,
CreatedBy: actorID,
}
err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
projectRepo := repository.NewProjectflockRepository(dbTransaction)
generatedName, _, err := s.generateSequentialFlockName(c.Context(), projectRepo, canonicalBase, 1, nil)
if err != nil {
return err
}
createBody.FlockName = generatedName
if err := projectRepo.CreateOne(c.Context(), createBody, nil); err != nil {
return err
}
periods, err := projectRepo.GetNextPeriodsForKandangs(c.Context(), kandangIDs)
if err != nil {
return err
}
if err := s.attachKandangs(c.Context(), dbTransaction, createBody.Id, kandangIDs, periods); err != nil {
return err
}
if err := s.UpsertProjectBudget(c.Context(), dbTransaction, createBody.Id, req.ProjectBudgets); err != nil {
return err
}
action := entity.ApprovalActionCreated
approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
_, err = approvalSvcTx.CreateApproval(
c.Context(),
utils.ApprovalWorkflowProjectFlock,
createBody.Id,
utils.ProjectFlockStepPengajuan,
&action,
actorID,
nil,
)
return err
})
if err != nil {
if fiberErr, ok := err.(*fiber.Error); ok {
return nil, fiberErr
}
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, fiber.NewError(fiber.StatusInternalServerError, "Failed to create project flock")
}
return s.getOneEntityOnly(c, createBody.Id)
}
func (s projectflockService) DeleteOne(c *fiber.Ctx, id uint) error {
existing, err := s.Repository.GetByID(c.Context(), id, s.Repository.WithDefaultRelations())
if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusNotFound, "Projectflock not found")
}
if err != nil {
s.Log.Errorf("Failed to fetch projectflock %d before delete: %+v", id, err)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock")
}
err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
if len(existing.Kandangs) > 0 {
ids := make([]uint, len(existing.Kandangs))
for i, k := range existing.Kandangs {
ids[i] = k.Id
}
if err := s.detachKandangs(c.Context(), dbTransaction, id, ids, true); err != nil {
return err
}
}
if err := repository.NewProjectflockRepository(dbTransaction).DeleteOne(c.Context(), id); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusNotFound, "Projectflock not found")
}
return err
}
return nil
})
if err != nil {
if fiberErr, ok := err.(*fiber.Error); ok {
return fiberErr
}
s.Log.Errorf("Failed to delete projectflock %d: %+v", id, err)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to delete project flock")
}
return nil
}
func (s projectflockService) GetProjectFlockKandangByProjectAndKandang(ctx *fiber.Ctx, projectFlockID uint, kandangID uint) (*entity.ProjectFlockKandang, float64, error) {
pfk, err := s.PivotRepo.GetByProjectFlockAndKandang(ctx.Context(), projectFlockID, kandangID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, 0, fiber.NewError(fiber.StatusNotFound, "ProjectFlockKandang not found")
}
s.Log.Errorf("Failed to fetch project_flock_kandang by project %d and kandang %d: %+v", projectFlockID, kandangID, err)
return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock kandang")
}
availableQuantity, err := s.GetAvailableDocQuantity(ctx, pfk.KandangId)
if err != nil {
return nil, 0, err
}
return pfk, availableQuantity, nil
}
func (s projectflockService) GetProjectFlockKandangPopulation(ctx *fiber.Ctx, projectFlockKandangID uint) (float64, error) {
if s.PopulationRepo == nil {
return 0, fiber.NewError(fiber.StatusInternalServerError, "Project flock population repository is not configured")
}
if projectFlockKandangID == 0 {
return 0, fiber.NewError(fiber.StatusBadRequest, "project_flock_kandang_id is required")
}
if s.RecordingRepo != nil {
latest, err := s.RecordingRepo.GetLatestByProjectFlockKandangID(ctx.Context(), projectFlockKandangID)
if err != nil {
s.Log.Errorf("Failed to fetch latest recording for project flock kandang %d: %+v", projectFlockKandangID, err)
return 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock kandang population")
}
if latest != nil && latest.TotalChickQty != nil && *latest.TotalChickQty > 0 {
return *latest.TotalChickQty, nil
}
}
total, err := s.PopulationRepo.GetAvailableQtyByProjectFlockKandangID(ctx.Context(), projectFlockKandangID)
if err != nil {
s.Log.Errorf("Failed to fetch project flock kandang population %d: %+v", projectFlockKandangID, err)
return 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock kandang population")
}
return total, nil
}
func (s projectflockService) GetProjectFlockKandangByParams(ctx *fiber.Ctx, idStr string, projectFlockIdStr string, kandangIdStr string) (*entity.ProjectFlockKandang, float64, error) {
idStr = strings.TrimSpace(idStr)
projectFlockIdStr = strings.TrimSpace(projectFlockIdStr)
kandangIdStr = strings.TrimSpace(kandangIdStr)
if idStr != "" {
id, err := strconv.Atoi(idStr)
if err != nil || id <= 0 {
return nil, 0, fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
}
pfk, err := s.PivotRepo.GetByID(ctx.Context(), uint(id))
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, 0, fiber.NewError(fiber.StatusNotFound, "ProjectFlockKandang not found")
}
s.Log.Errorf("Failed to fetch project_flock_kandang %d: %+v", id, err)
return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock kandang")
}
availableQuantity, err := s.GetAvailableDocQuantity(ctx, pfk.KandangId)
if err != nil {
return nil, 0, err
}
return pfk, availableQuantity, nil
}
if projectFlockIdStr == "" || kandangIdStr == "" {
return nil, 0, fiber.NewError(fiber.StatusBadRequest, "Missing lookup parameters")
}
pfid, err := strconv.Atoi(projectFlockIdStr)
if err != nil || pfid <= 0 {
return nil, 0, fiber.NewError(fiber.StatusBadRequest, "Invalid project_flock_id")
}
kid, err := strconv.Atoi(kandangIdStr)
if err != nil || kid <= 0 {
return nil, 0, fiber.NewError(fiber.StatusBadRequest, "Invalid kandang_id")
}
return s.GetProjectFlockKandangByProjectAndKandang(ctx, uint(pfid), uint(kid))
}
func (s projectflockService) GetAvailableDocQuantity(ctx *fiber.Ctx, kandangID uint) (float64, error) {
wh, err := s.WarehouseRepo.GetByKandangID(ctx.Context(), kandangID)
if err != nil {
return 0, err
}
productWarehouses, err := s.ProductWarehouseRepo.GetByCategoryCodeAndWarehouseID(ctx.Context(), "DOC", wh.Id)
if err != nil {
return 0, err
}
total := 0.0
for _, pw := range productWarehouses {
total += pw.Quantity
}
return total, nil
}
func (s projectflockService) GetWarehouseByKandangID(ctx *fiber.Ctx, kandangID uint) (*entity.Warehouse, error) {
if kandangID == 0 || s.WarehouseRepo == nil {
return nil, nil
}
var warehouse entity.Warehouse
err := s.WarehouseRepo.DB().WithContext(ctx.Context()).
Preload("Area").
Preload("Location").
Preload("Kandang").
Where("kandang_id = ?", kandangID).
Where("deleted_at IS NULL").
Order("id DESC").
First(&warehouse).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
if err != nil {
s.Log.Errorf("Failed to fetch warehouse for kandang %d: %+v", kandangID, err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch warehouse")
}
return &warehouse, nil
}
func (s projectflockService) GetProjectPeriods(c *fiber.Ctx, projectIDs []uint) (map[uint]int, error) {
if len(projectIDs) == 0 {
return map[uint]int{}, nil
}
return s.pivotRepo().ProjectPeriodsByProjectIDs(c.Context(), projectIDs)
}
func (s projectflockService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entity.ProjectFlock, error) {
if err := s.Validate.Struct(req); err != nil {
return nil, err
}
actorID, err := m.ActorIDFromContext(c)
if err != nil {
return nil, err
}
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.ProjectFlockStepPengajuan
if action == entity.ApprovalActionApproved {
step = utils.ProjectFlockStepAktif
}
err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
kandangRepoTx := kandangRepository.NewKandangRepository(dbTransaction)
projectRepoTx := repository.NewProjectflockRepository(dbTransaction)
projectFlockKandangRepoTx := repository.NewProjectFlockKandangRepository(dbTransaction)
for _, approvableID := range approvableIDs {
if _, err := projectRepoTx.GetByID(c.Context(), approvableID, nil); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Projectflock %d not found", approvableID))
}
return err
}
if _, err := approvalSvc.CreateApproval(
c.Context(),
utils.ApprovalWorkflowProjectFlock,
approvableID,
step,
&action,
actorID,
req.Notes,
); err != nil {
return err
}
switch action {
case entity.ApprovalActionApproved:
if err := kandangRepoTx.UpdateStatusByProjectFlockID(
c.Context(),
approvableID,
utils.KandangStatusActive,
); err != nil {
return err
}
pfks, err := projectFlockKandangRepoTx.GetByProjectFlockID(c.Context(), approvableID)
if err != nil {
return err
}
for _, pfk := range pfks {
latest, lerr := approvalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowProjectFlockKandang, pfk.Id, nil)
if lerr != nil {
return lerr
}
if latest != nil && latest.StepNumber == uint16(utils.ProjectFlockKandangStepDisetujui) {
continue
}
if _, aerr := approvalSvc.CreateApproval(
c.Context(),
utils.ApprovalWorkflowProjectFlockKandang,
pfk.Id,
utils.ProjectFlockKandangStepDisetujui,
&action,
actorID,
req.Notes,
); aerr != nil && !errors.Is(aerr, gorm.ErrDuplicatedKey) {
return aerr
}
}
case entity.ApprovalActionRejected:
if err := kandangRepoTx.UpdateStatusByProjectFlockID(
c.Context(),
approvableID,
utils.KandangStatusNonActive,
); err != nil {
return 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, "Projectflock not found")
}
s.Log.Errorf("Failed to record approval for projectflocks %+v: %+v", approvableIDs, err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to record approval")
}
updated := make([]entity.ProjectFlock, 0, len(approvableIDs))
for _, approvableID := range approvableIDs {
project, err := s.getOneEntityOnly(c, approvableID)
if err != nil {
return nil, err
}
updated = append(updated, *project)
}
return updated, nil
}
func (s projectflockService) GetPeriodSummary(c *fiber.Ctx, locationID uint) ([]KandangPeriodSummary, error) {
if locationID == 0 {
return nil, fiber.NewError(fiber.StatusBadRequest, "location_id is required")
}
exists, err := s.Repository.LocationExists(c.Context(), locationID)
if err != nil {
s.Log.Errorf("Failed to validate location %d: %+v", locationID, err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate location")
}
if !exists {
return nil, fiber.NewError(fiber.StatusNotFound, "Location not found")
}
rows, err := s.Repository.GetKandangPeriodSummaryRows(c.Context(), locationID)
if err != nil {
s.Log.Errorf("Failed to fetch kandang period summary for location %d: %+v", locationID, err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch kandang period summary")
}
summaries := make([]KandangPeriodSummary, 0, len(rows))
for _, row := range rows {
nextPeriod := 1
if row.LatestPeriod > 0 {
nextPeriod = row.LatestPeriod + 1
}
summaries = append(summaries, KandangPeriodSummary{
Id: row.Id,
Name: row.Name,
Period: nextPeriod,
})
}
return summaries, 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
}
func (s projectflockService) generateSequentialFlockName(ctx context.Context, repo repository.ProjectflockRepository, baseName string, startNumber int, excludeID *uint) (string, int, error) {
name := strings.TrimSpace(baseName)
if name == "" {
return "", 0, fiber.NewError(fiber.StatusBadRequest, "Base flock name cannot be empty")
}
number := startNumber
if number <= 0 {
number = 1
}
attempts := 0
for {
candidate := fmt.Sprintf("%s %03d", name, number)
exists, err := repo.ExistsByFlockName(ctx, candidate, excludeID)
if err != nil {
s.Log.Errorf("Failed checking project flock name uniqueness for %q: %+v", candidate, err)
return "", 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to generate flock name")
}
if !exists {
return candidate, number, nil
}
number++
attempts++
if attempts > 9999 {
return "", 0, fiber.NewError(fiber.StatusInternalServerError, "Unable to generate unique flock name")
}
}
}
func (s projectflockService) ensureFlockByName(ctx context.Context, actorID uint, name string) (*entity.Flock, error) {
trimmed := strings.TrimSpace(name)
if trimmed == "" {
return nil, fiber.NewError(fiber.StatusBadRequest, "Flock name cannot be empty")
}
flock, err := s.FlockRepo.GetByName(ctx, trimmed)
if err == nil {
return flock, nil
}
if !errors.Is(err, gorm.ErrRecordNotFound) {
s.Log.Errorf("Failed to fetch flock by name %q: %+v", trimmed, err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to prepare flock data")
}
newFlock := &entity.Flock{
Name: trimmed,
CreatedBy: actorID,
}
if err := s.FlockRepo.CreateOne(ctx, newFlock, nil); err != nil {
if errors.Is(err, gorm.ErrDuplicatedKey) {
return s.FlockRepo.GetByName(ctx, trimmed)
}
s.Log.Errorf("Failed to create flock %q: %+v", trimmed, err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to prepare flock data")
}
return newFlock, nil
}
func (s projectflockService) attachKandangs(ctx context.Context, dbTransaction *gorm.DB, projectFlockID uint, kandangIDs []uint, periods map[uint]int) error {
if len(kandangIDs) == 0 {
return nil
}
if err := s.kandangRepoWithTx(dbTransaction).UpdateStatusByIDs(ctx, kandangIDs, utils.KandangStatusPengajuan); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update kandangs status")
}
already, err := s.pivotRepoWithTx(dbTransaction).ListExistingKandangIDs(ctx, projectFlockID, kandangIDs)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to check existing pivot")
}
exists := make(map[uint]struct{}, len(already))
for _, id := range already {
exists[id] = struct{}{}
}
var toAttach []uint
seen := make(map[uint]struct{}, len(kandangIDs))
for _, id := range kandangIDs {
if _, ok := seen[id]; ok {
continue
}
seen[id] = struct{}{}
if _, ok := exists[id]; !ok {
toAttach = append(toAttach, id)
}
}
if len(toAttach) == 0 {
return nil
}
records := make([]*entity.ProjectFlockKandang, 0, len(toAttach))
for _, id := range toAttach {
period := periods[id]
if period <= 0 {
period = 1
}
records = append(records, &entity.ProjectFlockKandang{
ProjectFlockId: projectFlockID,
KandangId: id,
Period: period,
})
}
if err := s.pivotRepoWithTx(dbTransaction).CreateMany(ctx, records); err != nil {
if errors.Is(err, gorm.ErrDuplicatedKey) {
return fiber.NewError(fiber.StatusConflict, "Beberapa kandang sudah terhubung dengan project flock ini")
}
return fiber.NewError(fiber.StatusInternalServerError, "Failed to persist project flock history")
}
if err := s.ensureProjectFlockKandangProductWarehouses(ctx, dbTransaction, records); err != nil {
return err
}
return nil
}
func (s projectflockService) detachKandangs(ctx context.Context, dbTransaction *gorm.DB, projectFlockID uint, kandangIDs []uint, resetStatus bool) error {
if len(kandangIDs) == 0 {
return nil
}
// NOTE: Recording constraints are enforced via FK cascade; allow detachment even if recordings exist.
pfkIDs, err := s.pivotRepoWithTx(dbTransaction).ListIDsByProjectAndKandang(ctx, projectFlockID, kandangIDs)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to load project flock kandang ids")
}
if len(pfkIDs) > 0 {
uniformityRepo := uniformityRepository.NewUniformityRepository(s.Repository.DB())
if dbTransaction != nil {
uniformityRepo = uniformityRepository.NewUniformityRepository(dbTransaction)
}
if err := uniformityRepo.DeleteByProjectFlockKandangIDs(ctx, pfkIDs); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to remove uniformity data for project flock kandang")
}
db := s.Repository.DB()
if dbTransaction != nil {
db = dbTransaction
}
purchaseRepo := purchaseRepository.NewPurchaseRepository(db)
if err := purchaseRepo.SoftDeleteByProjectFlockKandangIDs(ctx, pfkIDs); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to soft delete purchases for project flock kandang")
}
pwRepo := s.ProductWarehouseRepo
if dbTransaction != nil {
pwRepo = productWarehouseRepository.NewProductWarehouseRepository(dbTransaction)
} else if pwRepo == nil {
pwRepo = productWarehouseRepository.NewProductWarehouseRepository(s.Repository.DB())
}
if err := pwRepo.DeleteByProjectFlockKandangIDs(ctx, pfkIDs); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to remove product warehouses for project flock kandang")
}
}
if resetStatus {
if err := s.kandangRepoWithTx(dbTransaction).UpdateStatusByIDs(ctx, kandangIDs, utils.KandangStatusNonActive); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update kandangs status")
}
}
if err := s.pivotRepoWithTx(dbTransaction).DeleteMany(ctx, projectFlockID, kandangIDs); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to persist project flock history")
}
return nil
}
func (s projectflockService) pivotRepoWithTx(dbTransaction *gorm.DB) repository.ProjectFlockKandangRepository {
if dbTransaction == nil {
return s.pivotRepo()
}
return s.pivotRepo().WithTx(dbTransaction)
}
func (s projectflockService) pivotRepo() repository.ProjectFlockKandangRepository {
if s.PivotRepo != nil {
return s.PivotRepo
}
return repository.NewProjectFlockKandangRepository(s.Repository.DB())
}
func (s projectflockService) kandangRepoWithTx(tx *gorm.DB) kandangRepository.KandangRepository {
if tx != nil {
return kandangRepository.NewKandangRepository(tx)
}
if s.KandangRepo != nil {
return s.KandangRepo
}
return kandangRepository.NewKandangRepository(s.Repository.DB())
}
func (s projectflockService) ensureProjectFlockKandangProductWarehouses(ctx context.Context, dbTransaction *gorm.DB, records []*entity.ProjectFlockKandang) error {
if len(records) == 0 {
return nil
}
projectFlockID := records[0].ProjectFlockId
if projectFlockID == 0 {
return fiber.NewError(fiber.StatusBadRequest, "Project flock id tidak ditemukan")
}
pwRepo := s.ProductWarehouseRepo
if dbTransaction != nil {
pwRepo = productWarehouseRepository.NewProductWarehouseRepository(dbTransaction)
} else if pwRepo == nil {
pwRepo = productWarehouseRepository.NewProductWarehouseRepository(s.Repository.DB())
}
warehouseRepo := s.WarehouseRepo
if dbTransaction != nil {
warehouseRepo = warehouseRepository.NewWarehouseRepository(dbTransaction)
} else if warehouseRepo == nil {
warehouseRepo = warehouseRepository.NewWarehouseRepository(s.Repository.DB())
}
db := s.Repository.DB()
if dbTransaction != nil {
db = dbTransaction
}
var category string
if err := db.WithContext(ctx).
Model(&entity.ProjectFlock{}).
Select("category").
Where("id = ?", projectFlockID).
Scan(&category).Error; err != nil {
return err
}
if strings.TrimSpace(category) == "" {
return fiber.NewError(fiber.StatusBadRequest, "Project flock category tidak ditemukan")
}
prefixes := []string{"AYAM-"}
if strings.EqualFold(category, string(utils.ProjectFlockCategoryLaying)) {
prefixes = append(prefixes, "TELUR")
}
invisibleOnly := false
productIDs, err := pwRepo.ListProductIDsByFlagPrefixes(ctx, prefixes, &invisibleOnly)
if err != nil {
return err
}
if len(productIDs) == 0 {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Product dengan flag %s tidak ditemukan", strings.Join(prefixes, ", ")))
}
for _, record := range records {
if record == nil || record.Id == 0 {
continue
}
warehouse, err := warehouseRepo.GetByKandangID(ctx, record.KandangId)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Warehouse untuk kandang %d belum tersedia", record.KandangId))
}
return err
}
for _, productID := range productIDs {
if _, err := pwRepo.GetByProductWarehouseAndProjectFlockKandang(ctx, productID, warehouse.Id, record.Id); err == nil {
continue
} else if !errors.Is(err, gorm.ErrRecordNotFound) {
return err
}
newPW := entity.ProductWarehouse{
ProductId: productID,
WarehouseId: warehouse.Id,
ProjectFlockKandangId: &record.Id,
Quantity: 0,
}
if err := pwRepo.CreateOne(ctx, &newPW, nil); err != nil {
return err
}
}
}
return nil
}
func (s projectflockService) Resubmit(c *fiber.Ctx, req *validation.Resubmit, id uint) (*entity.ProjectFlock, error) {
if err := s.Validate.Struct(req); err != nil {
return nil, err
}
actorID, err := m.ActorIDFromContext(c)
if err != nil {
return nil, err
}
existing, err := s.Repository.GetByID(c.Context(), id, s.Repository.WithDefaultRelations())
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Project flock tidak ditemukan")
}
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil data project flock")
}
kandangIDs := uniqueUintSlice(req.KandangIds)
kandangs, err := s.KandangRepo.GetByIDs(c.Context(), kandangIDs, nil)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Beberapa kandang tidak ditemukan")
}
return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil data kandang")
}
if len(kandangs) != len(kandangIDs) {
return nil, fiber.NewError(fiber.StatusNotFound, "Beberapa kandang tidak ditemukan")
}
for _, pb := range req.ProjectBudgets {
if err := commonSvc.EnsureRelations(c.Context(),
commonSvc.RelationCheck{Name: "Nonstock", ID: &pb.NonstockId, Exists: s.NonstockRepo.IdExists},
); err != nil {
return nil, err
}
}
err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
var period int = 1
if len(existing.KandangHistory) > 0 {
period = existing.KandangHistory[0].Period
}
periods := make(map[uint]int, len(kandangIDs))
for _, kandangID := range kandangIDs {
periods[kandangID] = period
}
if err := s.attachKandangs(c.Context(), dbTransaction, existing.Id, kandangIDs, periods); err != nil {
return err
}
if err := s.UpsertProjectBudget(c.Context(), dbTransaction, existing.Id, req.ProjectBudgets); err != nil {
return err
}
action := entity.ApprovalActionUpdated
_, err = approvalSvc.CreateApproval(
c.Context(),
utils.ApprovalWorkflowProjectFlock,
existing.Id,
utils.ProjectFlockStepPengajuan,
&action,
actorID,
nil,
)
return err
})
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, "Project flock tidak ditemukan")
}
return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal mengajukan ulang project flock")
}
return s.getOneEntityOnly(c, id)
}
func (s projectflockService) UpsertProjectBudget(ctx context.Context, dbTransaction *gorm.DB, projectFlockID uint, budgets []validation.ProjectBudget) error {
if len(budgets) == 0 {
return nil
}
budgetRepo := projectBudgetRepository.NewProjectBudgetRepository(dbTransaction)
nonstockMap := make(map[uint]bool)
relationChecks := make([]commonSvc.RelationCheck, 0, len(budgets))
for _, b := range budgets {
if nonstockMap[b.NonstockId] {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Duplicate nonstock_id: %d", b.NonstockId))
}
nonstockMap[b.NonstockId] = true
nonstockID := b.NonstockId
relationChecks = append(relationChecks, commonSvc.RelationCheck{
Name: "Nonstock",
ID: &nonstockID,
Exists: s.NonstockRepo.IdExists,
})
}
if err := commonSvc.EnsureRelations(ctx, relationChecks...); err != nil {
return err
}
if err := budgetRepo.DeleteMany(ctx, func(q *gorm.DB) *gorm.DB {
return q.Where("project_flock_id = ?", projectFlockID)
}); err != nil && err != gorm.ErrRecordNotFound {
return err
}
records := make([]*entity.ProjectBudget, 0, len(budgets))
for _, b := range budgets {
records = append(records, &entity.ProjectBudget{
ProjectFlockId: projectFlockID,
NonstockId: b.NonstockId,
Price: b.Price,
Qty: b.Qty,
})
}
if err := budgetRepo.CreateMany(ctx, records, nil); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to save project budgets")
}
return nil
}