From 3422fceec759cf140ac7eef58b2a166daea122fa Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Sun, 11 Jan 2026 20:10:19 +0700 Subject: [PATCH 1/2] feat(BE-ExpenseApproval): add unit vice president approval step and permissions --- internal/middleware/permissions.go | 25 +++++++------- .../controllers/expense.controller.go | 2 ++ internal/modules/expenses/route.go | 3 ++ .../expenses/services/expense.service.go | 17 +++++++--- .../services/transfer_expense_bridge.go | 34 +++++++------------ .../purchases/services/expense_bridge.go | 3 ++ internal/utils/constant.go | 24 +++++++------ 7 files changed, 60 insertions(+), 48 deletions(-) diff --git a/internal/middleware/permissions.go b/internal/middleware/permissions.go index e9148927..5820db27 100644 --- a/internal/middleware/permissions.go +++ b/internal/middleware/permissions.go @@ -19,18 +19,19 @@ const ( ) const ( - P_ExpenseGetAll = "lti.expense.list" - P_ExpenseCreateOne = "lti.expense.create" - P_ExpenseUpdateOne = "lti.expense.update" - P_ExpenseGetOne = "lti.expense.detail" - P_ExpenseDeleteOne = "lti.expense.delete" - P_ExpenseApprovalManager = "lti.expense.approve.manager" - P_ExpenseApprovalFinance = "lti.expense.approve.finance" - P_ExpenseCreateRealizations = "lti.expense.create.realization" - P_ExpenseUpdateRealizations = "lti.expense.update.realization" - P_ExpenseCompleteExpense = "lti.expense.complete.expense" - P_ExpenseDocument = "lti.expense.document" - P_ExpenseDocumentRealizations = "lti.expense.document.realization" + P_ExpenseGetAll = "lti.expense.list" + P_ExpenseCreateOne = "lti.expense.create" + P_ExpenseUpdateOne = "lti.expense.update" + P_ExpenseGetOne = "lti.expense.detail" + P_ExpenseDeleteOne = "lti.expense.delete" + P_ExpenseApprovalManager = "lti.expense.approve.manager" + P_ExpenseApprovalFinance = "lti.expense.approve.finance" + P_ExpenseApprovalUnitVicePresident = "lti.expense.approve.unit_vice_president" + P_ExpenseCreateRealizations = "lti.expense.create.realization" + P_ExpenseUpdateRealizations = "lti.expense.update.realization" + P_ExpenseCompleteExpense = "lti.expense.complete.expense" + P_ExpenseDocument = "lti.expense.document" + P_ExpenseDocumentRealizations = "lti.expense.document.realization" ) const ( P_AdjustmentGetAll = "lti.inventory.list" diff --git a/internal/modules/expenses/controllers/expense.controller.go b/internal/modules/expenses/controllers/expense.controller.go index 666642ca..125aeb0c 100644 --- a/internal/modules/expenses/controllers/expense.controller.go +++ b/internal/modules/expenses/controllers/expense.controller.go @@ -233,6 +233,8 @@ func (u *ExpenseController) Approval(c *fiber.Ctx) error { approvalType = "manager" } else if strings.Contains(path, "/approvals/finance") { approvalType = "finance" + } else if strings.Contains(path, "/approvals/unit-vice-president") { + approvalType = "unit-vice-president" } else { return fiber.NewError(fiber.StatusBadRequest, "Invalid approval path") } diff --git a/internal/modules/expenses/route.go b/internal/modules/expenses/route.go index 9c22bde3..cfb4dd23 100644 --- a/internal/modules/expenses/route.go +++ b/internal/modules/expenses/route.go @@ -27,8 +27,11 @@ func ExpenseRoutes(v1 fiber.Router, u user.UserService, s expense.ExpenseService route.Get("/:id", m.RequirePermissions(m.P_ExpenseGetOne), ctrl.GetOne) route.Patch("/:id", m.RequirePermissions(m.P_ExpenseUpdateOne), ctrl.UpdateOne) route.Delete("/:id", m.RequirePermissions(m.P_ExpenseDeleteOne), ctrl.DeleteOne) + route.Post("/approvals/manager", m.RequirePermissions(m.P_ExpenseApprovalManager), ctrl.Approval) route.Post("/approvals/finance", m.RequirePermissions(m.P_ExpenseApprovalFinance), ctrl.Approval) + route.Post("/approvals/unit-vice-president", m.RequirePermissions(m.P_ExpenseApprovalUnitVicePresident), ctrl.Approval) + route.Post("/:id/realizations", m.RequirePermissions(m.P_ExpenseCreateRealizations), ctrl.CreateRealization) route.Patch("/:id/realizations", m.RequirePermissions(m.P_ExpenseUpdateRealizations), ctrl.UpdateRealization) route.Post("/:id/complete", m.RequirePermissions(m.P_ExpenseCompleteExpense), ctrl.CompleteExpense) diff --git a/internal/modules/expenses/services/expense.service.go b/internal/modules/expenses/services/expense.service.go index 3e50da26..8afbac28 100644 --- a/internal/modules/expenses/services/expense.service.go +++ b/internal/modules/expenses/services/expense.service.go @@ -1055,15 +1055,24 @@ func (s *expenseService) Approval(c *fiber.Ctx, req *validation.ApprovalRequest, if latestApproval.StepNumber != uint16(utils.ExpenseStepPengajuan) { currentStepName := utils.ExpenseApprovalSteps[approvalutils.ApprovalStep(latestApproval.StepNumber)] return fiber.NewError(fiber.StatusBadRequest, - fmt.Sprintf("Cannot process at Manager step. Latest approval is at %s step. Expected previous step: Pengajuan", currentStepName)) + fmt.Sprintf("Cannot process at Head Area step. Latest approval is at %s step. Expected previous step: Pengajuan", currentStepName)) } - } else if approvalType == "finance" { + } else if approvalType == "unit-vice-president" { - stepNumber = utils.ExpenseStepFinance + stepNumber = utils.ExpenseStepUnitVicePresident if latestApproval.StepNumber != uint16(utils.ExpenseStepManager) { currentStepName := utils.ExpenseApprovalSteps[approvalutils.ApprovalStep(latestApproval.StepNumber)] return fiber.NewError(fiber.StatusBadRequest, - fmt.Sprintf("Cannot process at Finance step. Latest approval is at %s step. Expected previous step: Manager", currentStepName)) + fmt.Sprintf("Cannot process at Unit Vice President step. Latest approval is at %s step. Expected previous step: Head Area", currentStepName)) + } + + } else if approvalType == "finance" { + + stepNumber = utils.ExpenseStepFinance + if latestApproval.StepNumber != uint16(utils.ExpenseStepUnitVicePresident) { + currentStepName := utils.ExpenseApprovalSteps[approvalutils.ApprovalStep(latestApproval.StepNumber)] + return fiber.NewError(fiber.StatusBadRequest, + fmt.Sprintf("Cannot process at Finance step. Latest approval is at %s step. Expected previous step: Unit Vice President", currentStepName)) } } else { return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid approval type: %v", approvalType)) diff --git a/internal/modules/inventory/transfers/services/transfer_expense_bridge.go b/internal/modules/inventory/transfers/services/transfer_expense_bridge.go index 90350c18..d4322be6 100644 --- a/internal/modules/inventory/transfers/services/transfer_expense_bridge.go +++ b/internal/modules/inventory/transfers/services/transfer_expense_bridge.go @@ -39,12 +39,12 @@ type TransferExpenseReceivingPayload struct { } type groupedTransferItem struct { - detail *entity.StockTransferDetail - payload TransferExpenseReceivingPayload - projectFK *uint - kandangID *uint - totalPrice float64 - shippingCostTotal float64 + detail *entity.StockTransferDetail + payload TransferExpenseReceivingPayload + projectFK *uint + kandangID *uint + totalPrice float64 + shippingCostTotal float64 } func groupingKey(supplierID uint, date time.Time, warehouseID uint) string { @@ -84,7 +84,6 @@ func (b *transferExpenseBridge) OnItemsDeleted(ctx context.Context, _ uint64, it expenseIDs := make(map[uint64]struct{}) expenseNonstockIDs := make([]uint64, 0) - for _, item := range items { if item.ExpenseNonstockId != nil && *item.ExpenseNonstockId != 0 { expenseNonstockIDs = append(expenseNonstockIDs, *item.ExpenseNonstockId) @@ -92,7 +91,7 @@ func (b *transferExpenseBridge) OnItemsDeleted(ctx context.Context, _ uint64, it } if len(expenseNonstockIDs) > 0 { - + for _, nsID := range expenseNonstockIDs { var expenseID uint64 if err := tx.Model(&entity.ExpenseNonstock{}). @@ -106,13 +105,11 @@ func (b *transferExpenseBridge) OnItemsDeleted(ctx context.Context, _ uint64, it } } - if err := tx.Delete(&entity.ExpenseNonstock{}, expenseNonstockIDs).Error; err != nil { return err } } - approvalRepoTx := commonRepo.NewApprovalRepository(tx) for expenseID := range expenseIDs { var count int64 @@ -122,7 +119,6 @@ func (b *transferExpenseBridge) OnItemsDeleted(ctx context.Context, _ uint64, it return err } - if count == 0 { if err := approvalRepoTx.DeleteByTarget(ctx, utils.ApprovalWorkflowExpense.String(), uint(expenseID)); err != nil { return err @@ -220,7 +216,6 @@ func (b *transferExpenseBridge) createExpenseViaService( for _, gi := range items { note := fmt.Sprintf("stock_transfer_detail:%d", gi.detail.Id) - price := gi.shippingCostTotal if gi.payload.TransportPerItem != nil { price = *gi.payload.TransportPerItem * gi.payload.DeliveredQty @@ -228,7 +223,7 @@ func (b *transferExpenseBridge) createExpenseViaService( costItems = append(costItems, expenseValidation.CostItem{ NonstockID: expeditionNonstockID, - Quantity: 1, + Quantity: 1, Price: price, Notes: note, }) @@ -251,7 +246,6 @@ func (b *transferExpenseBridge) createExpenseViaService( return nil, err } - action := entity.ApprovalActionApproved actorID := uint(transfer.CreatedBy) if actorID == 0 { @@ -261,6 +255,9 @@ func (b *transferExpenseBridge) createExpenseViaService( if _, err := approvalSvc.CreateApproval(ctx, utils.ApprovalWorkflowExpense, uint(detail.Id), utils.ExpenseStepManager, &action, actorID, nil); err != nil { return nil, err } + if _, err := approvalSvc.CreateApproval(ctx, utils.ApprovalWorkflowExpense, uint(detail.Id), utils.ExpenseStepUnitVicePresident, &action, actorID, nil); err != nil { + return nil, err + } if _, err := approvalSvc.CreateApproval(ctx, utils.ApprovalWorkflowExpense, uint(detail.Id), utils.ExpenseStepFinance, &action, actorID, nil); err != nil { return nil, err } @@ -328,7 +325,6 @@ func (b *transferExpenseBridge) OnItemsDelivered(c *fiber.Ctx, transferID uint64 ctx := c.Context() - transfer, err := b.transferRepo.GetByID(ctx, uint(transferID), func(db *gorm.DB) *gorm.DB { return db. Preload("Details"). @@ -348,11 +344,10 @@ func (b *transferExpenseBridge) OnItemsDelivered(c *fiber.Ctx, transferID uint64 for i := range transfer.Details { detailMap[transfer.Details[i].Id] = &transfer.Details[i] - for _, deliveryItem := range transfer.Details[i].DeliveryItems { if deliveryItem.StockTransferDelivery != nil { shippingCostMap[transfer.Details[i].Id] = deliveryItem.StockTransferDelivery.ShippingCostTotal - break + break } } } @@ -395,17 +390,14 @@ func (b *transferExpenseBridge) OnItemsDelivered(c *fiber.Ctx, transferID uint64 } } - shippingCostTotal := shippingCostMap[detail.Id] - totalPrice := shippingCostTotal if payload.TransportPerItem != nil { - + totalPrice = *payload.TransportPerItem * payload.DeliveredQty } - warehouseID := uint(payload.WarehouseID) if warehouseID == 0 && transfer.ToWarehouse != nil { warehouseID = uint(transfer.ToWarehouse.Id) diff --git a/internal/modules/purchases/services/expense_bridge.go b/internal/modules/purchases/services/expense_bridge.go index 23b95c58..094b99c1 100644 --- a/internal/modules/purchases/services/expense_bridge.go +++ b/internal/modules/purchases/services/expense_bridge.go @@ -621,6 +621,9 @@ func (b *expenseBridge) createExpenseViaService( if _, err := approvalSvc.CreateApproval(ctx, utils.ApprovalWorkflowExpense, uint(detail.Id), utils.ExpenseStepManager, &action, actorID, nil); err != nil { return nil, err } + if _, err := approvalSvc.CreateApproval(ctx, utils.ApprovalWorkflowExpense, uint(detail.Id), utils.ExpenseStepUnitVicePresident, &action, actorID, nil); err != nil { + return nil, err + } if _, err := approvalSvc.CreateApproval(ctx, utils.ApprovalWorkflowExpense, uint(detail.Id), utils.ExpenseStepFinance, &action, actorID, nil); err != nil { return nil, err } diff --git a/internal/utils/constant.go b/internal/utils/constant.go index 6ec50447..44c79e35 100644 --- a/internal/utils/constant.go +++ b/internal/utils/constant.go @@ -354,20 +354,22 @@ var MarketingApprovalSteps = map[approvalutils.ApprovalStep]string{ // ------------------------------------------------------------------- const ( - ApprovalWorkflowExpense approvalutils.ApprovalWorkflowKey = approvalutils.ApprovalWorkflowKey("EXPENSES") - ExpenseStepPengajuan approvalutils.ApprovalStep = 1 - ExpenseStepManager approvalutils.ApprovalStep = 2 - ExpenseStepFinance approvalutils.ApprovalStep = 3 - ExpenseStepRealisasi approvalutils.ApprovalStep = 4 - ExpenseStepSelesai approvalutils.ApprovalStep = 5 + ApprovalWorkflowExpense approvalutils.ApprovalWorkflowKey = approvalutils.ApprovalWorkflowKey("EXPENSES") + ExpenseStepPengajuan approvalutils.ApprovalStep = 1 + ExpenseStepManager approvalutils.ApprovalStep = 2 + ExpenseStepUnitVicePresident approvalutils.ApprovalStep = 3 + ExpenseStepFinance approvalutils.ApprovalStep = 4 + ExpenseStepRealisasi approvalutils.ApprovalStep = 5 + ExpenseStepSelesai approvalutils.ApprovalStep = 6 ) var ExpenseApprovalSteps = map[approvalutils.ApprovalStep]string{ - ExpenseStepPengajuan: "Pengajuan", - ExpenseStepManager: "Approval Manager", - ExpenseStepFinance: "Approval Finance", - ExpenseStepRealisasi: "Realisasi", - ExpenseStepSelesai: "Selesai", + ExpenseStepPengajuan: "Pengajuan", + ExpenseStepManager: "Approval Head Area", + ExpenseStepUnitVicePresident: "Approval Business Unit Vice President", + ExpenseStepFinance: "Approval Finance", + ExpenseStepRealisasi: "Realisasi", + ExpenseStepSelesai: "Selesai", } // ------------------------------------------------------------------- From 9515848d8ff86b438eb685c29a6c39d4e8141ca4 Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Mon, 12 Jan 2026 10:11:31 +0700 Subject: [PATCH 2/2] feat(BE): update approval flow to use head area instead of manager --- internal/middleware/permissions.go | 2 +- internal/modules/expenses/controllers/expense.controller.go | 4 ++-- internal/modules/expenses/route.go | 2 +- internal/modules/expenses/services/expense.service.go | 6 +++--- .../inventory/transfers/services/transfer_expense_bridge.go | 2 +- internal/modules/purchases/services/expense_bridge.go | 2 +- internal/utils/constant.go | 4 ++-- 7 files changed, 11 insertions(+), 11 deletions(-) diff --git a/internal/middleware/permissions.go b/internal/middleware/permissions.go index 5820db27..6e4fe6db 100644 --- a/internal/middleware/permissions.go +++ b/internal/middleware/permissions.go @@ -24,7 +24,7 @@ const ( P_ExpenseUpdateOne = "lti.expense.update" P_ExpenseGetOne = "lti.expense.detail" P_ExpenseDeleteOne = "lti.expense.delete" - P_ExpenseApprovalManager = "lti.expense.approve.manager" + P_ExpenseApprovalHeadArea = "lti.expense.approve.head_area" P_ExpenseApprovalFinance = "lti.expense.approve.finance" P_ExpenseApprovalUnitVicePresident = "lti.expense.approve.unit_vice_president" P_ExpenseCreateRealizations = "lti.expense.create.realization" diff --git a/internal/modules/expenses/controllers/expense.controller.go b/internal/modules/expenses/controllers/expense.controller.go index 125aeb0c..49c8f356 100644 --- a/internal/modules/expenses/controllers/expense.controller.go +++ b/internal/modules/expenses/controllers/expense.controller.go @@ -229,8 +229,8 @@ func (u *ExpenseController) Approval(c *fiber.Ctx) error { path := c.Path() approvalType := "" - if strings.Contains(path, "/approvals/manager") { - approvalType = "manager" + if strings.Contains(path, "/approvals/head-area") { + approvalType = "head-area" } else if strings.Contains(path, "/approvals/finance") { approvalType = "finance" } else if strings.Contains(path, "/approvals/unit-vice-president") { diff --git a/internal/modules/expenses/route.go b/internal/modules/expenses/route.go index cfb4dd23..6ddceb14 100644 --- a/internal/modules/expenses/route.go +++ b/internal/modules/expenses/route.go @@ -28,7 +28,7 @@ func ExpenseRoutes(v1 fiber.Router, u user.UserService, s expense.ExpenseService route.Patch("/:id", m.RequirePermissions(m.P_ExpenseUpdateOne), ctrl.UpdateOne) route.Delete("/:id", m.RequirePermissions(m.P_ExpenseDeleteOne), ctrl.DeleteOne) - route.Post("/approvals/manager", m.RequirePermissions(m.P_ExpenseApprovalManager), ctrl.Approval) + route.Post("/approvals/head-area", m.RequirePermissions(m.P_ExpenseApprovalHeadArea), ctrl.Approval) route.Post("/approvals/finance", m.RequirePermissions(m.P_ExpenseApprovalFinance), ctrl.Approval) route.Post("/approvals/unit-vice-president", m.RequirePermissions(m.P_ExpenseApprovalUnitVicePresident), ctrl.Approval) diff --git a/internal/modules/expenses/services/expense.service.go b/internal/modules/expenses/services/expense.service.go index 8afbac28..9a994bc9 100644 --- a/internal/modules/expenses/services/expense.service.go +++ b/internal/modules/expenses/services/expense.service.go @@ -1049,9 +1049,9 @@ func (s *expenseService) Approval(c *fiber.Ctx, req *validation.ApprovalRequest, } var stepNumber approvalutils.ApprovalStep - if approvalType == "manager" { + if approvalType == "head-area" { - stepNumber = utils.ExpenseStepManager + stepNumber = utils.ExpenseStepHeadArea if latestApproval.StepNumber != uint16(utils.ExpenseStepPengajuan) { currentStepName := utils.ExpenseApprovalSteps[approvalutils.ApprovalStep(latestApproval.StepNumber)] return fiber.NewError(fiber.StatusBadRequest, @@ -1060,7 +1060,7 @@ func (s *expenseService) Approval(c *fiber.Ctx, req *validation.ApprovalRequest, } else if approvalType == "unit-vice-president" { stepNumber = utils.ExpenseStepUnitVicePresident - if latestApproval.StepNumber != uint16(utils.ExpenseStepManager) { + if latestApproval.StepNumber != uint16(utils.ExpenseStepHeadArea) { currentStepName := utils.ExpenseApprovalSteps[approvalutils.ApprovalStep(latestApproval.StepNumber)] return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Cannot process at Unit Vice President step. Latest approval is at %s step. Expected previous step: Head Area", currentStepName)) diff --git a/internal/modules/inventory/transfers/services/transfer_expense_bridge.go b/internal/modules/inventory/transfers/services/transfer_expense_bridge.go index d4322be6..c4f28354 100644 --- a/internal/modules/inventory/transfers/services/transfer_expense_bridge.go +++ b/internal/modules/inventory/transfers/services/transfer_expense_bridge.go @@ -252,7 +252,7 @@ func (b *transferExpenseBridge) createExpenseViaService( actorID = 1 } approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(b.db)) - if _, err := approvalSvc.CreateApproval(ctx, utils.ApprovalWorkflowExpense, uint(detail.Id), utils.ExpenseStepManager, &action, actorID, nil); err != nil { + if _, err := approvalSvc.CreateApproval(ctx, utils.ApprovalWorkflowExpense, uint(detail.Id), utils.ExpenseStepHeadArea, &action, actorID, nil); err != nil { return nil, err } if _, err := approvalSvc.CreateApproval(ctx, utils.ApprovalWorkflowExpense, uint(detail.Id), utils.ExpenseStepUnitVicePresident, &action, actorID, nil); err != nil { diff --git a/internal/modules/purchases/services/expense_bridge.go b/internal/modules/purchases/services/expense_bridge.go index 094b99c1..7e5cbd91 100644 --- a/internal/modules/purchases/services/expense_bridge.go +++ b/internal/modules/purchases/services/expense_bridge.go @@ -618,7 +618,7 @@ func (b *expenseBridge) createExpenseViaService( actorID = 1 } approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(b.db)) - if _, err := approvalSvc.CreateApproval(ctx, utils.ApprovalWorkflowExpense, uint(detail.Id), utils.ExpenseStepManager, &action, actorID, nil); err != nil { + if _, err := approvalSvc.CreateApproval(ctx, utils.ApprovalWorkflowExpense, uint(detail.Id), utils.ExpenseStepHeadArea, &action, actorID, nil); err != nil { return nil, err } if _, err := approvalSvc.CreateApproval(ctx, utils.ApprovalWorkflowExpense, uint(detail.Id), utils.ExpenseStepUnitVicePresident, &action, actorID, nil); err != nil { diff --git a/internal/utils/constant.go b/internal/utils/constant.go index 44c79e35..ba0f51f1 100644 --- a/internal/utils/constant.go +++ b/internal/utils/constant.go @@ -356,7 +356,7 @@ var MarketingApprovalSteps = map[approvalutils.ApprovalStep]string{ const ( ApprovalWorkflowExpense approvalutils.ApprovalWorkflowKey = approvalutils.ApprovalWorkflowKey("EXPENSES") ExpenseStepPengajuan approvalutils.ApprovalStep = 1 - ExpenseStepManager approvalutils.ApprovalStep = 2 + ExpenseStepHeadArea approvalutils.ApprovalStep = 2 ExpenseStepUnitVicePresident approvalutils.ApprovalStep = 3 ExpenseStepFinance approvalutils.ApprovalStep = 4 ExpenseStepRealisasi approvalutils.ApprovalStep = 5 @@ -365,7 +365,7 @@ const ( var ExpenseApprovalSteps = map[approvalutils.ApprovalStep]string{ ExpenseStepPengajuan: "Pengajuan", - ExpenseStepManager: "Approval Head Area", + ExpenseStepHeadArea: "Approval Head Area", ExpenseStepUnitVicePresident: "Approval Business Unit Vice President", ExpenseStepFinance: "Approval Finance", ExpenseStepRealisasi: "Realisasi",