mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-20 13:31:56 +00:00
434 lines
14 KiB
Go
434 lines
14 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
|
|
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
|
common "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"
|
|
|
|
"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, error)
|
|
GetOne(ctx *fiber.Ctx, id uint) (*entity.ProjectFlock, error)
|
|
CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.ProjectFlock, error)
|
|
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)
|
|
}
|
|
|
|
type projectflockService struct {
|
|
Log *logrus.Logger
|
|
Validate *validator.Validate
|
|
Repository repository.ProjectflockRepository
|
|
FlockRepo flockRepository.FlockRepository
|
|
KandangRepo kandangRepository.KandangRepository
|
|
}
|
|
|
|
type FlockPeriodSummary struct {
|
|
Flock entity.Flock
|
|
NextPeriod int
|
|
}
|
|
|
|
func NewProjectflockService(
|
|
repo repository.ProjectflockRepository,
|
|
flockRepo flockRepository.FlockRepository,
|
|
kandangRepo kandangRepository.KandangRepository,
|
|
validate *validator.Validate,
|
|
) ProjectflockService {
|
|
return &projectflockService{
|
|
Log: utils.Log,
|
|
Validate: validate,
|
|
Repository: repo,
|
|
FlockRepo: flockRepo,
|
|
KandangRepo: kandangRepo,
|
|
}
|
|
}
|
|
|
|
func (s projectflockService) withRelations(db *gorm.DB) *gorm.DB {
|
|
return db.
|
|
Preload("CreatedUser").
|
|
Preload("Flock").
|
|
Preload("Area").
|
|
Preload("ProductCategory").
|
|
Preload("Fcr").
|
|
Preload("Location").
|
|
Preload("Kandangs")
|
|
}
|
|
|
|
func (s projectflockService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.ProjectFlock, int64, error) {
|
|
if err := s.Validate.Struct(params); err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
offset := (params.Page - 1) * params.Limit
|
|
|
|
projectflocks, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
|
|
db = s.withRelations(db)
|
|
return db.Order("created_at DESC").Order("updated_at DESC")
|
|
})
|
|
|
|
if err != nil {
|
|
s.Log.Errorf("Failed to get projectflocks: %+v", err)
|
|
return nil, 0, err
|
|
}
|
|
return projectflocks, total, nil
|
|
}
|
|
|
|
func (s projectflockService) GetOne(c *fiber.Ctx, id uint) (*entity.ProjectFlock, error) {
|
|
projectflock, err := s.Repository.GetByID(c.Context(), id, s.withRelations)
|
|
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, err
|
|
}
|
|
return projectflock, 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
|
|
}
|
|
|
|
if len(req.KandangIds) == 0 {
|
|
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: "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 {
|
|
return nil, err
|
|
}
|
|
|
|
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.ProjectFlockId != nil {
|
|
return nil, fiber.NewError(fiber.StatusConflict, fmt.Sprintf("Kandang %s sudah memiliki project flock", kandang.Name))
|
|
}
|
|
}
|
|
|
|
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,
|
|
ProductCategoryId: req.ProductCategoryId,
|
|
FcrId: req.FcrId,
|
|
LocationId: req.LocationId,
|
|
Period: nextPeriod,
|
|
CreatedBy: 1,
|
|
}
|
|
|
|
if err := projectRepo.CreateOne(c.Context(), createBody, nil); err != nil {
|
|
tx.Rollback()
|
|
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, err
|
|
}
|
|
|
|
if err := tx.Model(&entity.Kandang{}).
|
|
Where("id IN ?", kandangIDs).
|
|
Updates(map[string]any{"project_flock_id": createBody.Id}).Error; err != nil {
|
|
tx.Rollback()
|
|
s.Log.Errorf("Failed to assign kandangs to projectflock: %+v", err)
|
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to assign kandangs")
|
|
}
|
|
|
|
if err := tx.Commit().Error; err != nil {
|
|
tx.Rollback()
|
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to commit transaction")
|
|
}
|
|
|
|
return s.GetOne(c, createBody.Id)
|
|
}
|
|
|
|
func (s projectflockService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.ProjectFlock, error) {
|
|
if err := s.Validate.Struct(req); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
existing, err := s.Repository.GetByID(c.Context(), id, s.withRelations)
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return nil, fiber.NewError(fiber.StatusNotFound, "Projectflock not found")
|
|
}
|
|
if err != nil {
|
|
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
|
|
|
|
if req.FlockId != nil {
|
|
updateBody["flock_id"] = *req.FlockId
|
|
relationChecks = append(relationChecks, common.RelationCheck{
|
|
Name: "Flock",
|
|
ID: req.FlockId,
|
|
Exists: relationExistsChecker[entity.Flock](s.Repository.DB()),
|
|
})
|
|
}
|
|
if req.AreaId != nil {
|
|
updateBody["area_id"] = *req.AreaId
|
|
relationChecks = append(relationChecks, common.RelationCheck{
|
|
Name: "Area",
|
|
ID: req.AreaId,
|
|
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.FcrId != nil {
|
|
updateBody["fcr_id"] = *req.FcrId
|
|
relationChecks = append(relationChecks, common.RelationCheck{
|
|
Name: "FCR",
|
|
ID: req.FcrId,
|
|
Exists: relationExistsChecker[entity.Fcr](s.Repository.DB()),
|
|
})
|
|
}
|
|
if req.LocationId != nil {
|
|
updateBody["location_id"] = *req.LocationId
|
|
relationChecks = append(relationChecks, common.RelationCheck{
|
|
Name: "Location",
|
|
ID: req.LocationId,
|
|
Exists: relationExistsChecker[entity.Location](s.Repository.DB()),
|
|
})
|
|
}
|
|
if req.Period != nil {
|
|
updateBody["period"] = *req.Period
|
|
}
|
|
|
|
if len(relationChecks) > 0 {
|
|
if err := common.EnsureRelations(c.Context(), relationChecks...); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
var newKandangIDs []uint
|
|
if req.KandangIds != nil {
|
|
if len(req.KandangIds) == 0 {
|
|
return nil, fiber.NewError(fiber.StatusBadRequest, "kandang_ids cannot be empty")
|
|
}
|
|
newKandangIDs = uniqueUintSlice(req.KandangIds)
|
|
kandangs, err := s.KandangRepo.GetByIDs(c.Context(), newKandangIDs, 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(newKandangIDs) {
|
|
return nil, fiber.NewError(fiber.StatusNotFound, "Some kandangs not found")
|
|
}
|
|
for _, k := range kandangs {
|
|
if k.ProjectFlockId != nil && *k.ProjectFlockId != id {
|
|
return nil, fiber.NewError(fiber.StatusConflict, fmt.Sprintf("Kandang %s sudah terikat dengan project flock lain", k.Name))
|
|
}
|
|
}
|
|
}
|
|
|
|
tx := s.Repository.DB().Begin()
|
|
if tx.Error != nil {
|
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to start transaction")
|
|
}
|
|
|
|
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")
|
|
}
|
|
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)
|
|
}
|
|
}
|
|
|
|
var toAttach []uint
|
|
for id := range newSet {
|
|
if _, ok := existingIDs[id]; !ok {
|
|
toAttach = append(toAttach, id)
|
|
}
|
|
}
|
|
|
|
if len(toDetach) > 0 {
|
|
if err := tx.Model(&entity.Kandang{}).
|
|
Where("id IN ?", toDetach).
|
|
Updates(map[string]any{"project_flock_id": nil}).Error; err != nil {
|
|
tx.Rollback()
|
|
s.Log.Errorf("Failed to detach kandangs: %+v", err)
|
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to update kandangs")
|
|
}
|
|
}
|
|
|
|
if len(toAttach) > 0 {
|
|
if err := tx.Model(&entity.Kandang{}).
|
|
Where("id IN ?", toAttach).
|
|
Updates(map[string]any{"project_flock_id": id}).Error; err != nil {
|
|
tx.Rollback()
|
|
s.Log.Errorf("Failed to attach kandangs: %+v", err)
|
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to update kandangs")
|
|
}
|
|
}
|
|
}
|
|
|
|
if err := tx.Commit().Error; err != nil {
|
|
tx.Rollback()
|
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to commit transaction")
|
|
}
|
|
|
|
return s.GetOne(c, id)
|
|
}
|
|
|
|
func (s projectflockService) DeleteOne(c *fiber.Ctx, id uint) error {
|
|
existing, err := s.Repository.GetByID(c.Context(), id, s.withRelations)
|
|
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")
|
|
}
|
|
|
|
tx := s.Repository.DB().Begin()
|
|
if tx.Error != nil {
|
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to start transaction")
|
|
}
|
|
|
|
if len(existing.Kandangs) > 0 {
|
|
ids := make([]uint, len(existing.Kandangs))
|
|
for i, k := range existing.Kandangs {
|
|
ids[i] = k.Id
|
|
}
|
|
if err := tx.Model(&entity.Kandang{}).
|
|
Where("id IN ?", ids).
|
|
Updates(map[string]any{"project_flock_id": nil}).Error; err != nil {
|
|
tx.Rollback()
|
|
s.Log.Errorf("Failed to detach kandangs before delete: %+v", err)
|
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update kandangs")
|
|
}
|
|
}
|
|
|
|
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")
|
|
}
|
|
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
|
|
}
|
|
|
|
func (s projectflockService) GetFlockPeriodSummary(c *fiber.Ctx, flockID uint) (*FlockPeriodSummary, error) {
|
|
flock, err := s.FlockRepo.GetByID(c.Context(), flockID, func(db *gorm.DB) *gorm.DB {
|
|
return db.Preload("CreatedUser")
|
|
})
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return nil, fiber.NewError(fiber.StatusNotFound, "Flock not found")
|
|
}
|
|
if err != nil {
|
|
s.Log.Errorf("Failed get flock %d for period summary: %+v", flockID, err)
|
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch flock")
|
|
}
|
|
|
|
maxPeriod, err := s.Repository.GetMaxPeriodByFlock(c.Context(), flockID)
|
|
if err != nil {
|
|
s.Log.Errorf("Failed to compute next period for flock %d: %+v", flockID, err)
|
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to compute next period")
|
|
}
|
|
|
|
return &FlockPeriodSummary{
|
|
Flock: *flock,
|
|
NextPeriod: maxPeriod + 1,
|
|
}, 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 relationExistsChecker[T any](db *gorm.DB) func(context.Context, uint) (bool, error) {
|
|
return func(ctx context.Context, id uint) (bool, error) {
|
|
return commonRepo.Exists[T](ctx, db, id)
|
|
}
|
|
}
|