Merge branch 'feat/BE/Sprint-7' of https://gitlab.com/mbugroup/lti-api into feat/BE/US-304/permission-middleware-adjustment

This commit is contained in:
ragilap
2025-12-19 14:19:40 +07:00
87 changed files with 4782 additions and 1261 deletions
@@ -41,8 +41,8 @@ func (ChickinModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *
approvalRepo := commonRepo.NewApprovalRepository(db)
approvalService := commonSvc.NewApprovalService(approvalRepo)
if err := approvalService.RegisterWorkflowSteps(utils.ApprovalWorkflowProjectFlockKandang, utils.ProjectFlockKandangApprovalSteps); err != nil {
panic(fmt.Sprintf("failed to register project flock kandang approval workflow: %v", err))
if err := approvalService.RegisterWorkflowSteps(utils.ApprovalWorkflowChickin, utils.ChickinApprovalSteps); err != nil {
panic(fmt.Sprintf("failed to register chickin approval workflow: %v", err))
}
chickinService := sChickin.NewChickinService(chickinRepo, kandangRepo, warehouseRepo, productWarehouseRepo, projectFlockRepo, projectflockkandangrepo, projectflockpopulationrepo, chickinDetailRepo, validate)
@@ -11,6 +11,7 @@ import (
type ProjectChickinRepository interface {
repository.BaseRepository[entity.ProjectChickin]
GetFirstByProjectFlockID(ctx context.Context, projectFlockID uint) (*entity.ProjectChickin, error)
GetByProjectFlockID(ctx context.Context, projectFlockID uint) ([]entity.ProjectChickin, error)
GetByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) ([]entity.ProjectChickin, error)
GetPendingByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) ([]entity.ProjectChickin, error)
GetTotalPendingUsageQtyByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (float64, error)
@@ -40,6 +41,16 @@ func (r *ChickinRepositoryImpl) GetFirstByProjectFlockID(ctx context.Context, pr
return &chickin, nil
}
func (r *ChickinRepositoryImpl) GetByProjectFlockID(ctx context.Context, projectFlockID uint) ([]entity.ProjectChickin, error) {
var chickins []entity.ProjectChickin
err := r.db.WithContext(ctx).
Joins("JOIN project_flock_kandangs ON project_flock_kandangs.id = project_chickins.project_flock_kandang_id").
Where("project_flock_kandangs.project_flock_id = ?", projectFlockID).
Order("project_chickins.created_at DESC").
Find(&chickins).Error
return chickins, err
}
func (r *ChickinRepositoryImpl) GetByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) ([]entity.ProjectChickin, error) {
var chickins []entity.ProjectChickin
err := r.db.WithContext(ctx).
@@ -190,14 +190,14 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) ([]enti
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create chickins")
}
latest, err := approvalSvcTx.LatestByTarget(c.Context(), utils.ApprovalWorkflowProjectFlockKandang, projectFlockKandang.Id, nil)
latest, err := approvalSvcTx.LatestByTarget(c.Context(), utils.ApprovalWorkflowChickin, projectFlockKandang.Id, nil)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get latest approval")
}
if category == string(utils.ProjectFlockCategoryLaying) {
for _, chickin := range newChikins {
updates := map[string]any{"quantity": gorm.Expr("quantity - ?", chickin.PendingUsageQty)}
updates := map[string]any{"qty": gorm.Expr("qty - ?", chickin.PendingUsageQty)}
if err := productWarehouseTx.PatchOne(c.Context(), chickin.ProductWarehouseId, updates, nil); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
@@ -218,9 +218,9 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) ([]enti
if latest == nil {
if _, err := approvalSvcTx.CreateApproval(
c.Context(),
utils.ApprovalWorkflowProjectFlockKandang,
utils.ApprovalWorkflowChickin,
projectFlockKandang.Id,
utils.ProjectFlockKandangStepPengajuan,
utils.ChickinStepPengajuan,
&approvalAction,
actorID,
nil); err != nil {
@@ -228,12 +228,12 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) ([]enti
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create approval")
}
}
} else if latest.StepNumber != uint16(utils.ProjectFlockKandangStepPengajuan) {
} else if latest.StepNumber != uint16(utils.ChickinStepPengajuan) {
if _, err := approvalSvcTx.CreateApproval(
c.Context(),
utils.ApprovalWorkflowProjectFlockKandang,
utils.ApprovalWorkflowChickin,
projectFlockKandang.Id,
utils.ProjectFlockKandangStepPengajuan,
utils.ChickinStepPengajuan,
&approvalAction,
actorID,
nil); err != nil {
@@ -388,7 +388,7 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit
return nil, err
}
latestApproval, err := approvalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowProjectFlockKandang, id, nil)
latestApproval, err := approvalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowChickin, id, nil)
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check approval status")
@@ -396,14 +396,14 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit
if latestApproval == nil {
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("No approval found for ProjectFlockKandang %d - chickins must be created first", id))
}
if latestApproval.StepNumber != uint16(utils.ProjectFlockKandangStepPengajuan) {
if latestApproval.StepNumber != uint16(utils.ChickinStepPengajuan) {
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("ProjectFlockKandang %d cannot be approved - current status is not in PENGAJUAN stage", id))
}
}
step := utils.ProjectFlockKandangStepPengajuan
step := utils.ChickinStepPengajuan
if action == entity.ApprovalActionApproved {
step = utils.ProjectFlockKandangStepDisetujui
step = utils.ChickinStepDisetujui
}
err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
@@ -415,7 +415,7 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit
for _, approvableID := range approvableIDs {
if _, err := approvalSvc.CreateApproval(
c.Context(),
utils.ApprovalWorkflowProjectFlockKandang,
utils.ApprovalWorkflowChickin,
approvableID,
step,
&action,
@@ -498,7 +498,7 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit
for _, chickin := range chickins {
if categoryForRejection == string(utils.ProjectFlockCategoryGrowing) {
updates := map[string]any{"quantity": gorm.Expr("quantity + ?", chickin.PendingUsageQty)}
updates := map[string]any{"qty": gorm.Expr("qty + ?", chickin.PendingUsageQty)}
if err := productWarehouseTx.PatchOne(c.Context(), chickin.ProductWarehouseId, updates, nil); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
@@ -600,7 +600,7 @@ func (s *chickinService) convertChickinsToTarget(ctx *fiber.Ctx, chickins []enti
if chickin.ProductWarehouseId != targetPW.Id {
if err := productWarehouseTx.PatchOne(ctx.Context(), chickin.ProductWarehouseId, map[string]any{
"quantity": gorm.Expr("quantity - ?", quantityToConvert),
"qty": gorm.Expr("qty - ?", quantityToConvert),
}, nil); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Source product warehouse %d not found", chickin.ProductWarehouseId))
@@ -610,7 +610,7 @@ func (s *chickinService) convertChickinsToTarget(ctx *fiber.Ctx, chickins []enti
}
if err := productWarehouseTx.PatchOne(ctx.Context(), targetPW.Id, map[string]any{
"quantity": gorm.Expr("quantity + ?", quantityToConvert),
"qty": gorm.Expr("qty + ?", quantityToConvert),
}, nil); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Target product warehouse %d not found", targetPW.Id))
@@ -84,3 +84,57 @@ func (u *ProjectFlockKandangController) GetOne(c *fiber.Ctx) error {
Data: dto.ToProjectFlockKandangDetailDTOWithAvailableQty(*result, availableQtys, productWarehouses),
})
}
func (u *ProjectFlockKandangController) Closing(c *fiber.Ctx) error {
id, err := strconv.Atoi(c.Params("id"))
if err != nil || id <= 0 {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
}
req := new(validation.Closing)
if err := c.BodyParser(req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
}
result, err := u.ProjectFlockKandangService.Closing(c, uint(id), req)
if err != nil {
return err
}
detail, availableQtys, productWarehouses, err := u.ProjectFlockKandangService.GetOne(c, result.Id)
if err != nil {
return err
}
detailDTO := dto.ToProjectFlockKandangDetailDTOWithAvailableQty(*detail, availableQtys, productWarehouses)
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Status closing kandang diperbarui",
Data: fiber.Map{
"detail": detailDTO,
"approval": detailDTO.Approval,
},
})
}
func (u *ProjectFlockKandangController) CheckClosing(c *fiber.Ctx) error {
id, err := strconv.Atoi(c.Params("id"))
if err != nil || id <= 0 {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
}
result, err := u.ProjectFlockKandangService.CheckClosing(c, uint(id))
if err != nil {
return err
}
return c.Status(fiber.StatusOK).JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Cek persyaratan closing kandang",
Data: result,
})
}
@@ -9,9 +9,10 @@ import (
sProjectFlockKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project-flock-kandangs/services"
rProjectFlockKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
rKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories"
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
rExpense "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories"
rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
@@ -28,15 +29,17 @@ func (ProjectFlockKandangModule) RegisterRoutes(router fiber.Router, db *gorm.DB
userRepo := rUser.NewUserRepository(db)
warehouseRepo := rWarehouse.NewWarehouseRepository(db)
productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db)
kandangRepo := rKandang.NewKandangRepository(db)
approvalRepo := commonRepo.NewApprovalRepository(db)
approvalService := commonSvc.NewApprovalService(approvalRepo)
// register workflow steps for project flock kandang approvals
// register workflow steps for chickin approvals
if err := approvalService.RegisterWorkflowSteps(utils.ApprovalWorkflowProjectFlockKandang, utils.ProjectFlockKandangApprovalSteps); err != nil {
panic(fmt.Sprintf("failed to register project flock kandang approval workflow: %v", err))
panic(fmt.Sprintf("failed to register chickin approval workflow: %v", err))
}
projectFlockKandangService := sProjectFlockKandang.NewProjectFlockKandangService(projectFlockKandangRepo, approvalService, warehouseRepo, productWarehouseRepo, projectFlockPopulationRepo, validate)
expenseRepo := rExpense.NewExpenseRepository(db)
projectFlockKandangService := sProjectFlockKandang.NewProjectFlockKandangService(projectFlockKandangRepo, approvalService, expenseRepo, warehouseRepo, productWarehouseRepo, projectFlockPopulationRepo,kandangRepo, validate)
userService := sUser.NewUserService(userRepo, validate)
ProjectFlockKandangRoutes(router, userService, projectFlockKandangService)
@@ -16,5 +16,8 @@ func ProjectFlockKandangRoutes(v1 fiber.Router, u user.UserService, s projectFlo
route.Use(m.Auth(u))
route.Get("/",m.RequirePermissions(m.P_ProjectFlockKandangsGetAll), ctrl.GetAll)
route.Get("/:id",m.RequirePermissions(m.P_ProjectFlockKandangsGetOne), ctrl.GetOne)
// route.Post("/:id/closing", m.RequirePermissions(m.PermissionProjectFlockClosing), ctrl.Closing)
// route.Get("/:id/closing/check", m.RequirePermissions(m.PermissionProjectFlockClosing), ctrl.CheckClosing)
route.Post("/:id/closing", ctrl.Closing)
route.Get("/:id/closing/check", ctrl.CheckClosing)
}
@@ -2,47 +2,84 @@ package service
import (
"errors"
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"
rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project-flock-kandangs/validations"
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
"fmt"
"strings"
"time"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"github.com/sirupsen/logrus"
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"
expenseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories"
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"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project-flock-kandangs/validations"
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
"gorm.io/gorm"
)
type ProjectFlockKandangService interface {
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.ProjectFlockKandang, int64, error)
GetOne(ctx *fiber.Ctx, id uint) (*entity.ProjectFlockKandang, map[uint]float64, []entity.ProductWarehouse, error)
CheckClosing(ctx *fiber.Ctx, id uint) (*ClosingCheckResult, error)
Closing(ctx *fiber.Ctx, id uint, req *validation.Closing) (*entity.ProjectFlockKandang, error)
GetProjectFlockKandangClosingDate(c *fiber.Ctx, id uint) (*time.Time, error)
}
// Note: map[uint]float64 adalah mapping dari ProductWarehouse ID ke calculated available quantity
type projectFlockKandangService struct {
Log *logrus.Logger
Validate *validator.Validate
Repository repository.ProjectFlockKandangRepository
ApprovalSvc commonSvc.ApprovalService
ExpenseRepo expenseRepo.ExpenseRepository
WarehouseRepo rWarehouse.WarehouseRepository
ProductWarehouseRepo rProductWarehouse.ProductWarehouseRepository
PopulationRepo repository.ProjectFlockPopulationRepository
KandangRepo kandangRepo.KandangRepository
}
func NewProjectFlockKandangService(repo repository.ProjectFlockKandangRepository, approvalSvc commonSvc.ApprovalService, warehouseRepo rWarehouse.WarehouseRepository, productWarehouseRepo rProductWarehouse.ProductWarehouseRepository, populationRepo repository.ProjectFlockPopulationRepository, validate *validator.Validate) ProjectFlockKandangService {
type ClosingCheckResult struct {
UnfinishedExpenses int64 `json:"unfinished_expenses"`
StockRemaining []StockRemainingDetail `json:"stock_remaining"`
Expenses []ExpenseSummary `json:"expenses"`
}
type StockRemainingDetail struct {
FlagName string `json:"flag_name"`
ProductWarehouseId uint `json:"product_warehouse_id"`
ProductId uint `json:"product_id"`
ProductName string `json:"product_name"`
ProductCategory string `json:"product_category"`
Uom string `json:"uom"`
Quantity float64 `json:"quantity"`
}
type ExpenseSummary struct {
Id uint64 `json:"id"`
PoNumber string `json:"po_number"`
Category string `json:"category"`
Total float64 `json:"total"`
Status string `json:"status"`
StepName string `json:"step_name"`
Step uint16 `json:"step"`
Reference string `json:"reference_number"`
}
func NewProjectFlockKandangService(repo repository.ProjectFlockKandangRepository, approvalSvc commonSvc.ApprovalService, expenseRepo expenseRepo.ExpenseRepository, warehouseRepo rWarehouse.WarehouseRepository, productWarehouseRepo rProductWarehouse.ProductWarehouseRepository, populationRepo repository.ProjectFlockPopulationRepository, kandangRepo kandangRepo.KandangRepository, validate *validator.Validate) ProjectFlockKandangService {
return &projectFlockKandangService{
Log: utils.Log,
Validate: validate,
Repository: repo,
ApprovalSvc: approvalSvc,
ExpenseRepo: expenseRepo,
WarehouseRepo: warehouseRepo,
ProductWarehouseRepo: productWarehouseRepo,
PopulationRepo: populationRepo,
KandangRepo: kandangRepo,
}
}
@@ -166,6 +203,351 @@ func (s projectFlockKandangService) getAvailableQuantities(c *fiber.Ctx, project
return result, nil
}
func (s projectFlockKandangService) CheckClosing(c *fiber.Ctx, id uint) (*ClosingCheckResult, error) {
pfk, err := s.Repository.GetByID(c.Context(), id)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "ProjectFlockKandang not found")
}
return nil, err
}
var unfinished int64
if s.ExpenseRepo != nil && s.ApprovalSvc != nil {
count, err := s.ExpenseRepo.CountUnfinishedByProjectFlockKandang(c.Context(), pfk.Id, pfk.KandangId, func(appr *entity.Approval) bool {
return appr != nil && appr.StepNumber == uint16(utils.ExpenseStepSelesai) && appr.Action != nil && *appr.Action == entity.ApprovalActionApproved
})
if err != nil {
return nil, err
}
unfinished = count
}
stockRemain := make([]StockRemainingDetail, 0)
if s.WarehouseRepo != nil && s.ProductWarehouseRepo != nil {
warehouse, werr := s.WarehouseRepo.GetByKandangID(c.Context(), pfk.KandangId)
if werr != nil {
return nil, werr
}
for _, flagName := range []utils.FlagType{utils.FlagPakan, utils.FlagOVK} {
productWarehouses, pwErr := s.ProductWarehouseRepo.GetByFlagAndWarehouseID(c.Context(), string(flagName), warehouse.Id)
if pwErr != nil {
return nil, pwErr
}
for _, pw := range productWarehouses {
if pw.Quantity > 0 {
category := ""
if pw.Product.ProductCategory.Id != 0 {
category = pw.Product.ProductCategory.Name
}
uomName := ""
if pw.Product.Uom.Id != 0 {
uomName = pw.Product.Uom.Name
}
stockRemain = append(stockRemain, StockRemainingDetail{
FlagName: string(flagName),
ProductWarehouseId: pw.Id,
ProductId: pw.ProductId,
ProductName: pw.Product.Name,
ProductCategory: category,
Uom: uomName,
Quantity: pw.Quantity,
})
}
}
}
}
expenseSummaries := make([]ExpenseSummary, 0)
if s.ExpenseRepo != nil {
var expenses []entity.Expense
if err := s.ExpenseRepo.DB().WithContext(c.Context()).
Scopes(s.ExpenseRepo.WithProjectFlockKandangFilter(pfk.Id, pfk.KandangId)).
Preload("Nonstocks").
Find(&expenses).Error; err != nil {
return nil, err
}
if len(expenses) > 0 && s.ApprovalSvc != nil {
ids := make([]uint, 0, len(expenses))
for _, e := range expenses {
ids = append(ids, uint(e.Id))
}
latestMap, err := s.ApprovalSvc.LatestByTargets(c.Context(), utils.ApprovalWorkflowExpense, ids, nil)
if err == nil {
for i := range expenses {
if latest, ok := latestMap[uint(expenses[i].Id)]; ok {
expenses[i].LatestApproval = latest
}
}
}
}
for _, exp := range expenses {
total := 0.0
for _, ns := range exp.Nonstocks {
total += ns.Qty * ns.Price
}
status := "Pending"
stepName := ""
stepNum := uint16(0)
if exp.LatestApproval != nil {
stepName = exp.LatestApproval.StepName
stepNum = exp.LatestApproval.StepNumber
if exp.LatestApproval.Action != nil {
status = string(*exp.LatestApproval.Action)
} else if stepName != "" {
status = stepName
}
}
expenseSummaries = append(expenseSummaries, ExpenseSummary{
Id: exp.Id,
PoNumber: exp.PoNumber,
Category: exp.Category,
Total: total,
Status: status,
StepName: stepName,
Step: stepNum,
Reference: exp.ReferenceNumber,
})
}
}
return &ClosingCheckResult{
UnfinishedExpenses: unfinished,
StockRemaining: stockRemain,
Expenses: expenseSummaries,
}, nil
}
// getProjectFlockKandangClosingDate mengembalikan tanggal closing PFK jika sudah di-close.
func (s projectFlockKandangService) GetProjectFlockKandangClosingDate(c *fiber.Ctx, id uint) (*time.Time, error) {
if id == 0 {
return nil, nil
}
pfk, err := s.Repository.GetByID(c.Context(), id)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
return nil, err
}
return pfk.ClosedAt, nil
}
func (s projectFlockKandangService) Closing(c *fiber.Ctx, id uint, req *validation.Closing) (*entity.ProjectFlockKandang, error) {
if err := s.Validate.Struct(req); err != nil {
return nil, err
}
actorID, err := m.ActorIDFromContext(c)
if err != nil {
return nil, err
}
pfk, err := s.Repository.GetByID(c.Context(), id)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "ProjectFlockKandang not found")
}
return nil, err
}
if s.ApprovalSvc != nil {
latest, aerr := s.ApprovalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowProjectFlock, pfk.ProjectFlockId, nil)
if aerr != nil {
return nil, aerr
}
if latest == nil || latest.Action == nil || *latest.Action != entity.ApprovalActionApproved {
return nil, fiber.NewError(fiber.StatusBadRequest, "Project flock belum berstatus aktif")
}
if latest.StepNumber != uint16(utils.ProjectFlockStepAktif) && latest.StepNumber != uint16(utils.ProjectFlockStepSelesai) {
return nil, fiber.NewError(fiber.StatusBadRequest, "Project flock belum berstatus aktif")
}
}
action := strings.ToLower(strings.TrimSpace(req.Action))
now := time.Now()
switch action {
case "close":
if pfk.ClosedAt != nil {
return nil, fiber.NewError(fiber.StatusConflict, "Kandang sudah closed")
}
if s.ExpenseRepo != nil && s.ApprovalSvc != nil {
unfinished, err := s.ExpenseRepo.CountUnfinishedByProjectFlockKandang(c.Context(), pfk.Id, pfk.KandangId, func(appr *entity.Approval) bool {
return appr != nil && appr.StepNumber == uint16(utils.ExpenseStepSelesai) && appr.Action != nil && *appr.Action == entity.ApprovalActionApproved
})
if err != nil {
return nil, err
}
if unfinished > 0 {
return nil, fiber.NewError(fiber.StatusBadRequest, "Masih ada expense belum selesai untuk kandang ini")
}
}
if s.WarehouseRepo != nil && s.ProductWarehouseRepo != nil {
warehouse, werr := s.WarehouseRepo.GetByKandangID(c.Context(), pfk.KandangId)
if werr != nil {
return nil, werr
}
for _, flagName := range []utils.FlagType{utils.FlagPakan, utils.FlagOVK} {
productWarehouses, pwErr := s.ProductWarehouseRepo.GetByFlagAndWarehouseID(c.Context(), string(flagName), warehouse.Id)
if pwErr != nil {
return nil, pwErr
}
for _, pw := range productWarehouses {
if pw.Quantity > 0 {
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Stok %s masih tersedia (product warehouse %d: %.2f)", flagName, pw.Id, pw.Quantity))
}
}
}
}
closeTime := now
if req.ClosedDate != nil {
parsed, perr := utils.ParseDateString(strings.TrimSpace(*req.ClosedDate))
if perr != nil {
return nil, fiber.NewError(fiber.StatusBadRequest, "closed_date tidak valid, gunakan format YYYY-MM-DD")
}
closeTime = parsed
}
if err := s.Repository.UpdateClosedAt(c.Context(), id, &closeTime); err != nil {
return nil, err
}
if s.KandangRepo != nil {
if err := s.KandangRepo.UpdateStatusByIDs(
c.Context(),
[]uint{pfk.KandangId},
utils.KandangStatusNonActive,
); err != nil {
return nil, err
}
}
if s.ApprovalSvc != nil {
closeAction := entity.ApprovalActionApproved
// Hindari duplikasi jika approval terakhir sudah Closed + Approved
latestPFK, lerr := s.ApprovalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowProjectFlockKandang, id, nil)
if lerr != nil {
return nil, lerr
}
shouldCreate := true
if latestPFK != nil &&
latestPFK.StepNumber == uint16(utils.ProjectFlockKandangStepClosed) &&
latestPFK.Action != nil && *latestPFK.Action == closeAction {
shouldCreate = false
}
if shouldCreate {
if _, aerr := s.ApprovalSvc.CreateApproval(
c.Context(),
utils.ApprovalWorkflowProjectFlockKandang,
id,
utils.ProjectFlockKandangStepClosed,
&closeAction,
actorID,
nil,
); aerr != nil {
return nil, aerr
}
}
// Jika semua kandang dalam project sudah ditutup, set approval project flock ke SELESAI.
pfks, ferr := s.Repository.GetByProjectFlockID(c.Context(), pfk.ProjectFlockId)
if ferr != nil {
return nil, ferr
}
allClosed := true
for _, item := range pfks {
if item.ClosedAt == nil {
allClosed = false
break
}
}
if allClosed {
latestPF, lerr := s.ApprovalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowProjectFlock, pfk.ProjectFlockId, nil)
if lerr != nil {
return nil, lerr
}
if latestPF == nil || latestPF.StepNumber != uint16(utils.ProjectFlockStepSelesai) {
if _, aerr := s.ApprovalSvc.CreateApproval(
c.Context(),
utils.ApprovalWorkflowProjectFlock,
pfk.ProjectFlockId,
utils.ProjectFlockStepSelesai,
&closeAction,
actorID,
nil,
); aerr != nil && !errors.Is(aerr, gorm.ErrDuplicatedKey) {
return nil, aerr
}
}
}
}
case "unclose":
if pfk.ClosedAt == nil {
return nil, fiber.NewError(fiber.StatusConflict, "Kandang belum closed")
}
openNewer, err := s.Repository.HasOpenNewerPeriod(c.Context(), pfk.KandangId, pfk.Period, &pfk.Id)
if err != nil {
return nil, err
}
if openNewer {
return nil, fiber.NewError(fiber.StatusBadRequest, "Tidak dapat un-close: ada periode yang sedang berjalan")
}
if err := s.Repository.UpdateClosedAt(c.Context(), id, nil); err != nil {
return nil, err
}
if s.KandangRepo != nil {
if err := s.KandangRepo.UpdateStatusByIDs(
c.Context(),
[]uint{pfk.KandangId},
utils.KandangStatusActive,
); err != nil {
return nil, err
}
}
if s.ApprovalSvc != nil {
reopenAction := entity.ApprovalActionUpdated
// Hindari duplikasi jika approval terakhir sudah Disetujui + Updated
latestPFK, lerr := s.ApprovalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowProjectFlockKandang, id, nil)
if lerr != nil {
return nil, lerr
}
shouldCreate := true
if latestPFK != nil &&
latestPFK.StepNumber == uint16(utils.ProjectFlockKandangStepDisetujui) &&
latestPFK.Action != nil && *latestPFK.Action == reopenAction {
shouldCreate = false
}
if shouldCreate {
if _, aerr := s.ApprovalSvc.CreateApproval(
c.Context(),
utils.ApprovalWorkflowProjectFlockKandang,
id,
utils.ProjectFlockKandangStepDisetujui,
&reopenAction,
actorID,
nil,
); aerr != nil && !errors.Is(aerr, gorm.ErrDuplicatedKey) {
return nil, aerr
}
}
}
default:
return nil, fiber.NewError(fiber.StatusBadRequest, "action harus close atau unclose")
}
return s.Repository.GetByID(c.Context(), id)
}
func (s projectFlockKandangService) calculateAvailableQuantityForProductWarehouse(c *fiber.Ctx, projectFlockKandang *entity.ProjectFlockKandang, productWarehouse *entity.ProductWarehouse) (float64, error) {
availableQty := productWarehouse.Quantity
@@ -22,3 +22,8 @@ type Query struct {
SortOrder string `query:"sort_order" validate:"omitempty,oneof=ASC DESC"`
StepName string `query:"step_name" validate:"omitempty,max=50"`
}
type Closing struct {
Action string `json:"action" validate:"required,oneof=close unclose"`
ClosedDate *string `json:"closed_date,omitempty"`
}
@@ -165,33 +165,6 @@ func (u *ProjectflockController) CreateOne(c *fiber.Ctx) error {
})
}
func (u *ProjectflockController) UpdateOne(c *fiber.Ctx) error {
req := new(validation.Update)
param := c.Params("id")
id, err := strconv.Atoi(param)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
}
if err := c.BodyParser(req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
}
result, err := u.ProjectflockService.UpdateOne(c, req, uint(id))
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Update projectflock successfully",
Data: dto.ToProjectFlockListDTO(*result),
})
}
func (u *ProjectflockController) DeleteOne(c *fiber.Ctx) error {
param := c.Params("id")
@@ -308,7 +281,6 @@ func (u *ProjectflockController) LookupProjectFlockKandang(c *fiber.Ctx) error {
dtoResult := dto.ToProjectFlockKandangDTO(*result)
dtoResult.AvailableQuantity = float64(availableStock)
// populate available quantity for each kandang inside project_flock
if dtoResult.ProjectFlock != nil {
for i := range dtoResult.ProjectFlock.Kandangs {
kand := &dtoResult.ProjectFlock.Kandangs[i]
@@ -319,7 +291,7 @@ func (u *ProjectflockController) LookupProjectFlockKandang(c *fiber.Ctx) error {
kand.AvailableQuantity = q
}
}
// remove inner kandangs from project_flock to avoid duplication
dtoResult.ProjectFlock.Kandangs = nil
}
@@ -1,6 +1,8 @@
package repository
import (
"context"
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gorm.io/gorm"
@@ -8,6 +10,7 @@ import (
type ProjectBudgetRepository interface {
repository.BaseRepository[entity.ProjectBudget]
GetByProjectFlockID(ctx context.Context, projectFlockID uint) ([]entity.ProjectBudget, error)
}
type ProjectBudgetRepositoryImpl struct {
@@ -21,3 +24,13 @@ func NewProjectBudgetRepository(db *gorm.DB) ProjectBudgetRepository {
db: db,
}
}
func (r *ProjectBudgetRepositoryImpl) GetByProjectFlockID(ctx context.Context, projectFlockID uint) ([]entity.ProjectBudget, error) {
var budgets []entity.ProjectBudget
err := r.db.WithContext(ctx).
Where("project_flock_id = ?", projectFlockID).
Preload("Nonstock").
Preload("Nonstock.Uom").
Find(&budgets).Error
return budgets, err
}
@@ -3,6 +3,7 @@ package repository
import (
"context"
"strings"
"time"
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
@@ -14,18 +15,21 @@ type ProjectFlockKandangRepository interface {
GetByID(ctx context.Context, id uint) (*entity.ProjectFlockKandang, error)
GetByProjectFlockAndKandang(ctx context.Context, projectFlockID uint, kandangID uint) (*entity.ProjectFlockKandang, error)
GetActiveByKandangID(ctx context.Context, kandangID uint) (*entity.ProjectFlockKandang, error)
UpdateClosedAt(ctx context.Context, id uint, t *time.Time) error
CreateMany(ctx context.Context, records []*entity.ProjectFlockKandang) error
DeleteMany(ctx context.Context, projectFlockID uint, kandangIDs []uint) error
GetAll(ctx context.Context, offset int, limit int, modifier func(*gorm.DB) *gorm.DB) ([]entity.ProjectFlockKandang, int64, error)
GetAllWithFilters(ctx context.Context, offset int, limit int, params interface{}) ([]entity.ProjectFlockKandang, int64, error)
GetByProjectFlockID(ctx context.Context, projectFlockID uint) ([]entity.ProjectFlockKandang, error)
ListExistingKandangIDs(ctx context.Context, projectFlockID uint, kandangIDs []uint) ([]uint, error)
HasKandangsLinkedToOtherProject(ctx context.Context, kandangIDs []uint, exceptProjectID *uint) (bool, error)
FindKandangsWithRecordings(ctx context.Context, projectFlockID uint, kandangIDs []uint) ([]entity.Kandang, error)
MaxPeriodByBaseName(ctx context.Context, baseName string) (int, error)
ProjectPeriodsByProjectIDs(ctx context.Context, projectIDs []uint) (map[uint]int, error)
HasOpenNewerPeriod(ctx context.Context, kandangID uint, currentPeriod int, excludeID *uint) (bool, error)
WithTx(tx *gorm.DB) ProjectFlockKandangRepository
IdExists(ctx context.Context, id uint) (bool, error)
DB() *gorm.DB
IdExists(ctx context.Context, id uint) (bool, error)
}
type projectFlockKandangRepositoryImpl struct {
@@ -75,6 +79,16 @@ func (r *projectFlockKandangRepositoryImpl) GetAll(ctx context.Context, offset i
return records, total, nil
}
func (r *projectFlockKandangRepositoryImpl) GetByProjectFlockID(ctx context.Context, projectFlockID uint) ([]entity.ProjectFlockKandang, error) {
var records []entity.ProjectFlockKandang
if err := r.db.WithContext(ctx).
Where("project_flock_id = ?", projectFlockID).
Find(&records).Error; err != nil {
return nil, err
}
return records, nil
}
func (r *projectFlockKandangRepositoryImpl) GetAllWithFilters(ctx context.Context, offset int, limit int, params interface{}) ([]entity.ProjectFlockKandang, int64, error) {
var records []entity.ProjectFlockKandang
var total int64
@@ -104,10 +118,10 @@ func (r *projectFlockKandangRepositoryImpl) GetAllWithFilters(ctx context.Contex
AND "approvals"."approvable_type" = ?
AND LOWER("approvals"."step_name") = LOWER(?)
AND "approvals"."id" IN (
SELECT "id" FROM "approvals"
WHERE "approvable_id" = "project_flock_kandangs"."id"
AND "approvable_type" = ?
ORDER BY "action_at" DESC
SELECT "approvals"."id" FROM "approvals"
WHERE "approvals"."approvable_id" = "project_flock_kandangs"."id"
AND "approvals"."approvable_type" = ?
ORDER BY "approvals"."id" DESC
LIMIT 1
)
)
@@ -223,32 +237,57 @@ func (r *projectFlockKandangRepositoryImpl) GetByProjectFlockAndKandang(ctx cont
}
func (r *projectFlockKandangRepositoryImpl) GetActiveByKandangID(ctx context.Context, kandangID uint) (*entity.ProjectFlockKandang, error) {
record := new(entity.ProjectFlockKandang)
latestApprovalSubQuery := r.db.
Table("approvals").
Select("DISTINCT ON (approvable_id) approvable_id, step_name, id").
Where("approvable_type = ?", "PROJECT_FLOCKS").
Order("approvable_id, id DESC")
var pfkID uint
if err := r.db.WithContext(ctx).
Model(&entity.ProjectFlockKandang{}).
Joins("JOIN project_flocks ON project_flocks.id = project_flock_kandangs.project_flock_id").
Joins(`
INNER JOIN (
SELECT DISTINCT ON (approvable_id) approvable_id, step_name, action_at
FROM approvals
WHERE approvable_type = 'PROJECT_FLOCKS'
ORDER BY approvable_id, action_at DESC
) latest_approval ON latest_approval.approvable_id = project_flocks.id
`).
Joins("JOIN (?) AS latest_approval ON latest_approval.approvable_id = project_flocks.id", latestApprovalSubQuery).
Where("project_flock_kandangs.kandang_id = ?", kandangID).
Where("project_flock_kandangs.closed_at IS NULL").
Where("LOWER(latest_approval.step_name) = LOWER(?)", "Aktif").
Order("project_flock_kandangs.id DESC").
Preload("ProjectFlock").
Preload("ProjectFlock.Fcr").
Preload("ProjectFlock.Area").
Preload("ProjectFlock.Location").
Preload("ProjectFlock.CreatedUser").
Preload("ProjectFlock.Kandangs").
Preload("ProjectFlock.KandangHistory").
Preload("Kandang").
First(record).Error; err != nil {
Limit(1).
Pluck("project_flock_kandangs.id", &pfkID).Error; err != nil {
return nil, err
}
return record, nil
if pfkID == 0 {
return nil, gorm.ErrRecordNotFound
}
return r.GetByID(ctx, pfkID)
}
func (r *projectFlockKandangRepositoryImpl) UpdateClosedAt(ctx context.Context, id uint, t *time.Time) error {
return r.db.WithContext(ctx).
Model(&entity.ProjectFlockKandang{}).
Where("id = ?", id).
Update("closed_at", t).Error
}
func (r *projectFlockKandangRepositoryImpl) HasOpenNewerPeriod(ctx context.Context, kandangID uint, currentPeriod int, excludeID *uint) (bool, error) {
if kandangID == 0 {
return false, nil
}
q := r.db.WithContext(ctx).
Table("project_flock_kandangs").
Where("kandang_id = ?", kandangID).
Where("period > ?", currentPeriod).
Where("closed_at IS NULL")
if excludeID != nil {
q = q.Where("id <> ?", *excludeID)
}
var count int64
if err := q.Count(&count).Error; err != nil {
return false, err
}
return count > 0, nil
}
func (r *projectFlockKandangRepositoryImpl) ListExistingKandangIDs(ctx context.Context, projectFlockID uint, kandangIDs []uint) ([]uint, error) {
@@ -269,7 +308,8 @@ func (r *projectFlockKandangRepositoryImpl) HasKandangsLinkedToOtherProject(ctx
}
q := r.db.WithContext(ctx).
Table("project_flock_kandangs").
Where("kandang_id IN ?", kandangIDs)
Where("kandang_id IN ?", kandangIDs).
Where("closed_at IS NULL")
if exceptProjectID != nil {
q = q.Where("project_flock_id <> ?", *exceptProjectID)
}
@@ -18,7 +18,6 @@ func ProjectflockRoutes(v1 fiber.Router, u user.UserService, s projectflock.Proj
route.Get("/",m.RequirePermissions(m.P_ProjectFlockGetAll),ctrl.GetAll)
route.Post("/",m.RequirePermissions(m.P_ProjectFlockCreate), ctrl.CreateOne)
route.Get("/:id",m.RequirePermissions(m.P_ProjectFlockGetOne), ctrl.GetOne)
route.Patch("/:id",m.RequirePermissions(m.P_ProjectFlockUpdate), ctrl.UpdateOne)
route.Delete("/:id",m.RequirePermissions(m.P_ProjectFlockGetAll), ctrl.DeleteOne)
route.Get("/kandangs/lookup",m.RequirePermissions(m.P_ProjectFlockLookup), ctrl.LookupProjectFlockKandang)
route.Post("/approvals",m.RequirePermissions(m.P_ProjectFlockApprove), ctrl.Approval)
@@ -34,7 +34,6 @@ 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)
UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.ProjectFlock, error)
GetAvailableDocQuantity(ctx *fiber.Ctx, kandangID uint) (float64, error)
DeleteOne(ctx *fiber.Ctx, id uint) error
GetProjectFlockKandangByProjectAndKandang(ctx *fiber.Ctx, projectFlockID uint, kandangID uint) (*entity.ProjectFlockKandang, float64, error)
@@ -255,6 +254,16 @@ func (s *projectflockService) CreateOne(c *fiber.Ctx, req *validation.Create) (*
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)
@@ -348,365 +357,6 @@ func (s *projectflockService) CreateOne(c *fiber.Ctx, req *validation.Create) (*
return s.getOneEntityOnly(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
}
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, "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)
hasBodyChanges := false
var relationChecks []commonSvc.RelationCheck
existingBase := pfutils.DeriveBaseName(existing.FlockName)
targetBaseName := existingBase
needFlockNameRegenerate := false
if req.FlockName != nil {
trimmed := strings.TrimSpace(*req.FlockName)
if trimmed == "" {
return nil, fiber.NewError(fiber.StatusBadRequest, "Flock name cannot be empty")
}
canonicalBase := trimmed
if s.FlockRepo != nil {
flockEntity, err := s.ensureFlockByName(c.Context(), actorID, trimmed)
if err != nil {
return nil, err
}
canonicalBase = flockEntity.Name
}
if !strings.EqualFold(canonicalBase, existingBase) {
needFlockNameRegenerate = true
targetBaseName = canonicalBase
hasBodyChanges = true
}
}
if req.AreaId != nil {
updateBody["area_id"] = *req.AreaId
hasBodyChanges = true
relationChecks = append(relationChecks, commonSvc.RelationCheck{
Name: "Area",
ID: req.AreaId,
Exists: s.Repository.AreaExists,
})
}
if req.Category != nil {
cat := strings.ToUpper(*req.Category)
if !utils.IsValidProjectFlockCategory(cat) {
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid category")
}
updateBody["category"] = cat
}
if req.FcrId != nil {
updateBody["fcr_id"] = *req.FcrId
hasBodyChanges = true
relationChecks = append(relationChecks, commonSvc.RelationCheck{
Name: "FCR",
ID: req.FcrId,
Exists: s.Repository.FcrExists,
})
}
if req.LocationId != nil {
updateBody["location_id"] = *req.LocationId
hasBodyChanges = true
relationChecks = append(relationChecks, commonSvc.RelationCheck{
Name: "Location",
ID: req.LocationId,
Exists: s.Repository.LocationExists,
})
}
if len(relationChecks) > 0 {
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")
}
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")
}
targetLocationID := existing.LocationId
if req.LocationId != nil && *req.LocationId > 0 {
targetLocationID = *req.LocationId
}
for _, kandang := range kandangs {
if kandang.LocationId != targetLocationID {
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Kandang %d tidak berada pada lokasi yang sama dengan project flock", kandang.Id))
}
}
if linked, err := s.pivotRepo().HasKandangsLinkedToOtherProject(c.Context(), newKandangIDs, &id); 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")
}
}
hasChanges := hasBodyChanges || hasKandangChanges
if !hasChanges {
return s.getOneEntityOnly(c, id)
}
err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
projectRepo := repository.NewProjectflockRepository(dbTransaction)
baseForGeneration := targetBaseName
if strings.TrimSpace(baseForGeneration) == "" {
baseForGeneration = existingBase
}
if strings.TrimSpace(baseForGeneration) == "" {
baseForGeneration = strings.TrimSpace(existing.FlockName)
}
if needFlockNameRegenerate {
newName, _, err := s.generateSequentialFlockName(c.Context(), projectRepo, baseForGeneration, 1, &id)
if err != nil {
return err
}
updateBody["flock_name"] = newName
}
if len(updateBody) > 0 {
if err := projectRepo.PatchOne(c.Context(), id, updateBody, nil); err != nil {
return err
}
} else {
if _, err := projectRepo.GetByID(c.Context(), id, nil); err != nil {
return 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 _, 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(), dbTransaction, id, toDetach, true); err != nil {
return err
}
}
if len(toAttach) > 0 {
currentPeriod, err := projectRepo.GetCurrentProjectPeriod(c.Context(), id)
if err != nil {
return err
}
periods := make(map[uint]int, len(toAttach))
if currentPeriod > 0 {
for _, kid := range toAttach {
periods[kid] = currentPeriod
}
} else {
periods, err = projectRepo.GetNextPeriodsForKandangs(c.Context(), toAttach)
if err != nil {
return err
}
}
if err := s.attachKandangs(c.Context(), dbTransaction, id, toAttach, periods); err != nil {
return err
}
}
}
if hasChanges {
approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
if approvalSvc != nil {
latestBeforeReset, err := approvalSvc.LatestByTarget(c.Context(), s.approvalWorkflow, id, nil)
if err != nil {
return err
}
shouldRecordUpdate := latestBeforeReset == nil ||
latestBeforeReset.StepNumber != uint16(utils.ProjectFlockStepPengajuan) ||
latestBeforeReset.Action == nil ||
(latestBeforeReset.Action != nil && *latestBeforeReset.Action != entity.ApprovalActionUpdated)
if shouldRecordUpdate {
action := entity.ApprovalActionUpdated
if _, err := approvalSvc.CreateApproval(
c.Context(),
utils.ApprovalWorkflowProjectFlock,
id,
utils.ProjectFlockStepPengajuan,
&action,
actorID,
nil,
); 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 update projectflock %d: %+v", id, err)
if errors.Is(err, gorm.ErrDuplicatedKey) {
return nil, fiber.NewError(fiber.StatusConflict, "Project flock period already exists")
}
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to update project flock")
}
return s.getOneEntityOnly(c, id)
}
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)
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
}
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) DeleteOne(c *fiber.Ctx, id uint) error {
existing, err := s.Repository.GetByID(c.Context(), id, s.Repository.WithDefaultRelations())
if errors.Is(err, gorm.ErrRecordNotFound) {
@@ -827,6 +477,27 @@ func (s projectflockService) GetAvailableDocQuantity(ctx *fiber.Ctx, kandangID u
return total, nil
}
// getProjectFlockClosingDate mengembalikan tanggal closing Project Flock jika sudah mencapai step SELESAI (Approved).
// func (s projectflockService) getProjectFlockClosingDate(ctx context.Context, projectFlockID uint) (*time.Time, error) {
// if projectFlockID == 0 || s.ApprovalSvc == nil {
// return nil, nil
// }
// latest, err := s.ApprovalSvc.LatestByTarget(ctx, utils.ApprovalWorkflowProjectFlock, projectFlockID, nil)
// if err != nil {
// return nil, err
// }
// if latest == nil || latest.Action == nil || *latest.Action != entity.ApprovalActionApproved {
// return nil, nil
// }
// if latest.StepNumber != uint16(utils.ProjectFlockStepSelesai) {
// return nil, nil
// }
// t := latest.ActionAt
// return &t, nil
// }
func (s projectflockService) GetProjectPeriods(c *fiber.Ctx, projectIDs []uint) (map[uint]int, error) {
if len(projectIDs) == 0 {
return map[uint]int{}, nil
@@ -834,6 +505,133 @@ func (s projectflockService) GetProjectPeriods(c *fiber.Ctx, projectIDs []uint)
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")
@@ -10,15 +10,6 @@ type Create struct {
ProjectBudgets []ProjectBudget `json:"project_budgets" validate:"required,min=1,dive"`
}
type Update struct {
FlockName *string `json:"flock_name,omitempty" validate:"omitempty"`
AreaId *uint `json:"area_id,omitempty" validate:"omitempty,number,gt=0"`
Category *string `json:"category,omitempty" validate:"omitempty"`
FcrId *uint `json:"fcr_id,omitempty" validate:"omitempty,number,gt=0"`
LocationId *uint `json:"location_id,omitempty" validate:"omitempty,number,gt=0"`
KandangIds []uint `json:"kandang_ids,omitempty" validate:"omitempty,min=1,dive,gt=0"`
}
type Query struct {
Page int `query:"page" validate:"omitempty,number,min=1"`
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"`
@@ -1,8 +0,0 @@
package recordings
const (
PermissionRecordingRead = "recording.read"
PermissionRecordingCreate = "recording.write"
PermissionRecordingUpdate = "recording.update"
PermissionRecordingDelete = "recording.delete"
)