feat/BE/US-279/Closing unfinished

This commit is contained in:
ragilap
2025-12-01 16:49:13 +07:00
parent dbeb0b62cb
commit d76db26a4d
14 changed files with 320 additions and 99 deletions
@@ -2,9 +2,11 @@ package repository
import (
"context"
"errors"
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
"gorm.io/gorm"
)
@@ -13,6 +15,8 @@ type ExpenseRepository interface {
IdExists(ctx context.Context, id uint64) (bool, error)
GetNextSequence(ctx context.Context) (int, error)
GetWithSupplier(ctx context.Context, id uint64) (*entity.Expense, error)
WithProjectFlockKandangFilter(pfkID uint) func(*gorm.DB) *gorm.DB
CountUnfinishedByProjectFlockKandang(ctx context.Context, pfkID uint, isFinished func(*entity.Approval) bool) (int64, error)
}
type ExpenseRepositoryImpl struct {
@@ -49,3 +53,51 @@ func (r *ExpenseRepositoryImpl) GetWithSupplier(ctx context.Context, id uint64)
}
return &expense, nil
}
func (r *ExpenseRepositoryImpl) WithProjectFlockKandangFilter(pfkID uint) func(*gorm.DB) *gorm.DB {
return func(db *gorm.DB) *gorm.DB {
if pfkID == 0 {
return db
}
return db.Joins("JOIN expense_nonstocks ON expense_nonstocks.expense_id = expenses.id").
Where("expense_nonstocks.project_flock_kandang_id = ?", pfkID)
}
}
func (r *ExpenseRepositoryImpl) CountUnfinishedByProjectFlockKandang(ctx context.Context, pfkID uint, isFinished func(*entity.Approval) bool) (int64, error) {
if pfkID == 0 {
return 0, nil
}
var ids []uint64
if err := r.DB().WithContext(ctx).
Table("expenses").
Scopes(r.WithProjectFlockKandangFilter(pfkID)).
Group("expenses.id").
Pluck("expenses.id", &ids).Error; err != nil {
return 0, err
}
if len(ids) == 0 {
return 0, nil
}
var unfinished int64
for _, id := range ids {
var latest entity.Approval
err := r.DB().WithContext(ctx).
Table("approvals").
Where("approvable_type = ? AND approvable_id = ?", utils.ApprovalWorkflowExpense.String(), id).
Order("action_at DESC").
Limit(1).
First(&latest).Error
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return 0, err
}
if isFinished != nil {
if !isFinished(&latest) {
unfinished++
}
}
}
return unfinished, nil
}
@@ -84,3 +84,29 @@ 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
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Status closing kandang diperbarui",
// Data: dto.ToProjectFlockKandangDetailDTO(*result),
Data: result,
})
}
@@ -12,6 +12,7 @@ import (
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"
@@ -36,7 +37,8 @@ func (ProjectFlockKandangModule) RegisterRoutes(router fiber.Router, db *gorm.DB
panic(fmt.Sprintf("failed to register project flock kandang 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, validate)
userService := sUser.NewUserService(userRepo, validate)
ProjectFlockKandangRoutes(router, userService, projectFlockKandangService)
@@ -22,5 +22,5 @@ func ProjectFlockKandangRoutes(v1 fiber.Router, u user.UserService, s projectFlo
route.Get("/", ctrl.GetAll)
route.Get("/:id", ctrl.GetOne)
route.Post("/:id/closing", ctrl.Closing)
}
@@ -2,24 +2,28 @@ package service
import (
"errors"
"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"
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"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"github.com/sirupsen/logrus"
"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)
Closing(ctx *fiber.Ctx, id uint, req *validation.Closing) (*entity.ProjectFlockKandang, error)
}
// Note: map[uint]float64 adalah mapping dari ProductWarehouse ID ke calculated available quantity
@@ -29,17 +33,19 @@ type projectFlockKandangService struct {
Validate *validator.Validate
Repository repository.ProjectFlockKandangRepository
ApprovalSvc commonSvc.ApprovalService
ExpenseRepo expenseRepo.ExpenseRepository
WarehouseRepo rWarehouse.WarehouseRepository
ProductWarehouseRepo rProductWarehouse.ProductWarehouseRepository
PopulationRepo repository.ProjectFlockPopulationRepository
}
func NewProjectFlockKandangService(repo repository.ProjectFlockKandangRepository, approvalSvc commonSvc.ApprovalService, warehouseRepo rWarehouse.WarehouseRepository, productWarehouseRepo rProductWarehouse.ProductWarehouseRepository, populationRepo repository.ProjectFlockPopulationRepository, validate *validator.Validate) ProjectFlockKandangService {
func NewProjectFlockKandangService(repo repository.ProjectFlockKandangRepository, approvalSvc commonSvc.ApprovalService, expenseRepo expenseRepo.ExpenseRepository, warehouseRepo rWarehouse.WarehouseRepository, productWarehouseRepo rProductWarehouse.ProductWarehouseRepository, populationRepo repository.ProjectFlockPopulationRepository, validate *validator.Validate) ProjectFlockKandangService {
return &projectFlockKandangService{
Log: utils.Log,
Validate: validate,
Repository: repo,
ApprovalSvc: approvalSvc,
ExpenseRepo: expenseRepo,
WarehouseRepo: warehouseRepo,
ProductWarehouseRepo: productWarehouseRepo,
PopulationRepo: populationRepo,
@@ -166,6 +172,99 @@ func (s projectFlockKandangService) getAvailableQuantities(c *fiber.Ctx, project
return result, 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.StepNumber != uint16(utils.ProjectFlockStepAktif) || latest.Action == nil || *latest.Action != entity.ApprovalActionApproved {
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, 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")
}
}
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.ApprovalSvc != nil {
closeAction := entity.ApprovalActionApproved
if _, aerr := s.ApprovalSvc.CreateApproval(
c.Context(),
utils.ApprovalWorkflowProjectFlockKandang,
id,
utils.ProjectFlockKandangStepDisetujui,
&closeAction,
actorID,
nil,
); aerr != nil {
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
}
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"`
}
@@ -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,6 +15,7 @@ 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)
@@ -23,9 +25,9 @@ type ProjectFlockKandangRepository interface {
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
}
type projectFlockKandangRepositoryImpl struct {
@@ -251,6 +253,32 @@ func (r *projectFlockKandangRepositoryImpl) GetActiveByKandangID(ctx context.Con
return record, nil
}
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) {
if len(kandangIDs) == 0 {
return nil, nil
@@ -1,8 +0,0 @@
package recordings
const (
PermissionRecordingRead = "recording.read"
PermissionRecordingCreate = "recording.write"
PermissionRecordingUpdate = "recording.update"
PermissionRecordingDelete = "recording.delete"
)