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
@@ -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"`
}