feat(BE): approval_workflow, adjusment project_flocks, common, and migration

This commit is contained in:
Hafizh A. Y
2025-10-21 13:56:30 +07:00
parent 13c04460f0
commit 55b14f5fc7
30 changed files with 1379 additions and 159 deletions
@@ -7,13 +7,14 @@ import (
"strings"
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
common "gitlab.com/mbugroup/lti-api.git/internal/common/service"
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
flockRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/repositories"
kandangRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories"
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/validations"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
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"
@@ -28,15 +29,18 @@ type ProjectflockService interface {
UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.ProjectFlock, error)
DeleteOne(ctx *fiber.Ctx, id uint) error
GetFlockPeriodSummary(ctx *fiber.Ctx, flockID uint) (*FlockPeriodSummary, error)
Approval(ctx *fiber.Ctx, id uint, req *validation.Approve) (*entity.ProjectFlock, error)
}
type projectflockService struct {
Log *logrus.Logger
Validate *validator.Validate
Repository repository.ProjectflockRepository
FlockRepo flockRepository.FlockRepository
KandangRepo kandangRepository.KandangRepository
Log *logrus.Logger
Validate *validator.Validate
Repository repository.ProjectflockRepository
FlockRepo flockRepository.FlockRepository
KandangRepo kandangRepository.KandangRepository
PivotRepo repository.ProjectFlockKandangRepository
ApprovalSvc commonSvc.ApprovalService
approvalWorkflow approvalutils.ApprovalWorkflowKey
}
type FlockPeriodSummary struct {
@@ -49,15 +53,18 @@ func NewProjectflockService(
flockRepo flockRepository.FlockRepository,
kandangRepo kandangRepository.KandangRepository,
pivotRepo repository.ProjectFlockKandangRepository,
approvalSvc commonSvc.ApprovalService,
validate *validator.Validate,
) ProjectflockService {
return &projectflockService{
Log: utils.Log,
Validate: validate,
Repository: repo,
FlockRepo: flockRepo,
KandangRepo: kandangRepo,
Log: utils.Log,
Validate: validate,
Repository: repo,
FlockRepo: flockRepo,
KandangRepo: kandangRepo,
PivotRepo: pivotRepo,
ApprovalSvc: approvalSvc,
approvalWorkflow: utils.ApprovalWorkflowProjectFlock,
}
}
@@ -68,7 +75,7 @@ func (s projectflockService) withRelations(db *gorm.DB) *gorm.DB {
Preload("Area").
Preload("Fcr").
Preload("Location").
Preload("Kandangs")
Preload("Kandangs.Location")
}
func (s projectflockService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.ProjectFlock, int64, error) {
@@ -154,6 +161,27 @@ func (s projectflockService) GetAll(c *fiber.Ctx, params *validation.Query) ([]e
s.Log.Errorf("Failed to get projectflocks: %+v", err)
return nil, 0, err
}
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, func(db *gorm.DB) *gorm.DB {
return db.Preload("ActionUser")
})
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
}
}
}
}
return projectflocks, total, nil
}
@@ -166,6 +194,23 @@ func (s projectflockService) GetOne(c *fiber.Ctx, id uint) (*entity.ProjectFlock
s.Log.Errorf("Failed get projectflock by id: %+v", err)
return nil, err
}
if s.ApprovalSvc != nil {
approvals, err := s.ApprovalSvc.ListByTarget(c.Context(), s.approvalWorkflow, id, func(db *gorm.DB) *gorm.DB {
return db.Preload("ActionUser")
})
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
}
@@ -183,11 +228,11 @@ func (s *projectflockService) CreateOne(c *fiber.Ctx, req *validation.Create) (*
return nil, fiber.NewError(fiber.StatusBadRequest, "kandang_ids is required")
}
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: "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())},
if err := commonSvc.EnsureRelations(c.Context(),
commonSvc.RelationCheck{Name: "Flock", ID: &req.FlockId, Exists: relationExistsChecker[entity.Flock](s.Repository.DB())},
commonSvc.RelationCheck{Name: "Area", ID: &req.AreaId, Exists: relationExistsChecker[entity.Area](s.Repository.DB())},
commonSvc.RelationCheck{Name: "FCR", ID: &req.FcrId, Exists: relationExistsChecker[entity.Fcr](s.Repository.DB())},
commonSvc.RelationCheck{Name: "Location", ID: &req.LocationId, Exists: relationExistsChecker[entity.Location](s.Repository.DB())},
); err != nil {
return nil, err
}
@@ -209,19 +254,6 @@ func (s *projectflockService) CreateOne(c *fiber.Ctx, req *validation.Create) (*
}
}
tx := s.Repository.DB().Begin()
if tx.Error != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to start transaction")
}
projectRepo := repository.NewProjectflockRepository(tx)
nextPeriod, err := projectRepo.GetNextPeriodForFlock(c.Context(), req.FlockId)
if err != nil {
tx.Rollback()
s.Log.Errorf("Failed to determine next period for flock %d: %+v", req.FlockId, err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to determine next period")
}
createBody := &entity.ProjectFlock{
FlockId: req.FlockId,
AreaId: req.AreaId,
@@ -232,8 +264,60 @@ func (s *projectflockService) CreateOne(c *fiber.Ctx, req *validation.Create) (*
CreatedBy: 1,
}
if err := projectRepo.CreateOne(c.Context(), createBody, nil); err != nil {
tx.Rollback()
err = s.Repository.DB().WithContext(c.Context()).Transaction(func(tx *gorm.DB) error {
projectRepo := repository.NewProjectflockRepository(tx)
// kandangRepo := kandangRepository.NewKandangRepository(tx)
period, err := projectRepo.GetNextPeriodForFlock(c.Context(), req.FlockId)
if err != nil {
return err
}
createBody.Period = period
if err := projectRepo.CreateOne(c.Context(), createBody, nil); err != nil {
return err
}
// kandangUpdates := make([]*entity.Kandang, len(kandangs))
// for i := range kandangs {
// kandangs[i].ProjectFlockId = &createBody.Id
// kandangUpdates[i] = &kandangs[i]
// }
// if err := kandangRepo.UpdateMany(
// c.Context(),
// kandangUpdates,
// func(db *gorm.DB) *gorm.DB {
// return db.Select("project_flock_id")
// },
// ); err != nil {
// return err
// }
if err := tx.Model(&entity.Kandang{}).
Where("id IN ?", kandangIDs).
Updates(map[string]any{
"project_flock_id": createBody.Id,
"status": string(utils.KandangStatusPengajuan),
}).Error; err != nil {
return err
}
actorID := uint(1) //TODO: Change From Auth
action := entity.ApprovalActionCreated
approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(tx))
_, err = approvalSvcTx.CreateApproval(
c.Context(),
utils.ApprovalWorkflowProjectFlock,
createBody.Id,
utils.ProjectFlockStepPengajuan,
&action,
actorID,
nil,
)
return err
})
if err != nil {
if errors.Is(err, gorm.ErrDuplicatedKey) {
return nil, fiber.NewError(fiber.StatusConflict, "Project flock period already exists")
}
@@ -268,13 +352,14 @@ func (s projectflockService) UpdateOne(c *fiber.Ctx, req *validation.Update, id
s.Log.Errorf("Failed to fetch projectflock %d before update: %+v", id, err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock")
}
updateBody := make(map[string]any)
var relationChecks []common.RelationCheck
hasBodyChanges := false
var relationChecks []commonSvc.RelationCheck
if req.FlockId != nil {
updateBody["flock_id"] = *req.FlockId
relationChecks = append(relationChecks, common.RelationCheck{
hasBodyChanges = true
relationChecks = append(relationChecks, commonSvc.RelationCheck{
Name: "Flock",
ID: req.FlockId,
Exists: relationExistsChecker[entity.Flock](s.Repository.DB()),
@@ -282,7 +367,8 @@ func (s projectflockService) UpdateOne(c *fiber.Ctx, req *validation.Update, id
}
if req.AreaId != nil {
updateBody["area_id"] = *req.AreaId
relationChecks = append(relationChecks, common.RelationCheck{
hasBodyChanges = true
relationChecks = append(relationChecks, commonSvc.RelationCheck{
Name: "Area",
ID: req.AreaId,
Exists: relationExistsChecker[entity.Area](s.Repository.DB()),
@@ -297,7 +383,8 @@ func (s projectflockService) UpdateOne(c *fiber.Ctx, req *validation.Update, id
}
if req.FcrId != nil {
updateBody["fcr_id"] = *req.FcrId
relationChecks = append(relationChecks, common.RelationCheck{
hasBodyChanges = true
relationChecks = append(relationChecks, commonSvc.RelationCheck{
Name: "FCR",
ID: req.FcrId,
Exists: relationExistsChecker[entity.Fcr](s.Repository.DB()),
@@ -305,7 +392,8 @@ func (s projectflockService) UpdateOne(c *fiber.Ctx, req *validation.Update, id
}
if req.LocationId != nil {
updateBody["location_id"] = *req.LocationId
relationChecks = append(relationChecks, common.RelationCheck{
hasBodyChanges = true
relationChecks = append(relationChecks, commonSvc.RelationCheck{
Name: "Location",
ID: req.LocationId,
Exists: relationExistsChecker[entity.Location](s.Repository.DB()),
@@ -313,16 +401,19 @@ func (s projectflockService) UpdateOne(c *fiber.Ctx, req *validation.Update, id
}
if req.Period != nil {
updateBody["period"] = *req.Period
hasBodyChanges = true
}
if len(relationChecks) > 0 {
if err := common.EnsureRelations(c.Context(), relationChecks...); err != nil {
if err := commonSvc.EnsureRelations(c.Context(), relationChecks...); err != nil {
return nil, err
}
}
var newKandangIDs []uint
hasKandangChanges := false
if req.KandangIds != nil {
hasKandangChanges = true
if len(req.KandangIds) == 0 {
return nil, fiber.NewError(fiber.StatusBadRequest, "kandang_ids cannot be empty")
}
@@ -344,46 +435,47 @@ func (s projectflockService) UpdateOne(c *fiber.Ctx, req *validation.Update, id
}
}
tx := s.Repository.DB().Begin()
if tx.Error != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to start transaction")
hasChanges := hasBodyChanges || hasKandangChanges
if !hasChanges {
return s.GetOne(c, id)
}
projectRepo := repository.NewProjectflockRepository(tx)
if len(updateBody) > 0 {
if err := projectRepo.PatchOne(c.Context(), id, updateBody, nil); err != nil {
tx.Rollback()
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Projectflock not found")
err = s.Repository.DB().WithContext(c.Context()).Transaction(func(tx *gorm.DB) error {
projectRepo := repository.NewProjectflockRepository(tx)
if len(updateBody) > 0 {
if err := projectRepo.PatchOne(c.Context(), id, updateBody, nil); err != nil {
return err
}
s.Log.Errorf("Failed to update projectflock: %+v", err)
return nil, err
}
}
if req.KandangIds != nil {
existingIDs := make(map[uint]struct{}, len(existing.Kandangs))
for _, k := range existing.Kandangs {
existingIDs[k.Id] = struct{}{}
}
newSet := make(map[uint]struct{}, len(newKandangIDs))
for _, id := range newKandangIDs {
newSet[id] = struct{}{}
}
var toDetach []uint
for id := range existingIDs {
if _, ok := newSet[id]; !ok {
toDetach = append(toDetach, id)
} else {
if _, err := projectRepo.GetByID(c.Context(), id, nil); err != nil {
return err
}
}
var toAttach []uint
for id := range newSet {
if _, ok := existingIDs[id]; !ok {
toAttach = append(toAttach, id)
if req.KandangIds != nil {
existingIDs := make(map[uint]struct{}, len(existing.Kandangs))
for _, k := range existing.Kandangs {
existingIDs[k.Id] = struct{}{}
}
newSet := make(map[uint]struct{}, len(newKandangIDs))
for _, kid := range newKandangIDs {
newSet[kid] = struct{}{}
}
var toDetach []uint
for kid := range existingIDs {
if _, ok := newSet[kid]; !ok {
toDetach = append(toDetach, kid)
}
}
var toAttach []uint
for kid := range newSet {
if _, ok := existingIDs[kid]; !ok {
toAttach = append(toAttach, kid)
}
}
}
if len(toDetach) > 0 {
if err := s.detachKandangs(c.Context(), tx, id, toDetach, false); err != nil {
@@ -437,18 +529,21 @@ func (s projectflockService) DeleteOne(c *fiber.Ctx, id uint) error {
}
}
if err := repository.NewProjectflockRepository(tx).DeleteOne(c.Context(), id); err != nil {
tx.Rollback()
if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusNotFound, "Projectflock not found")
if err := repository.NewProjectflockRepository(tx).DeleteOne(c.Context(), id); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusNotFound, "Projectflock not found")
}
return err
}
s.Log.Errorf("Failed to delete projectflock: %+v", err)
return err
}
if err := tx.Commit().Error; err != nil {
tx.Rollback()
return fiber.NewError(fiber.StatusInternalServerError, "Failed to commit transaction")
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 err
}
return nil