mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-20 05:21:57 +00:00
feat: bulk approve endpoint for marketings and expenses
This commit is contained in:
@@ -30,3 +30,4 @@ coverage/
|
|||||||
.idea/
|
.idea/
|
||||||
*.swp
|
*.swp
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
.gemini/
|
||||||
|
|||||||
@@ -7,10 +7,13 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/dto"
|
"gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/dto"
|
||||||
service "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/services"
|
service "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/services"
|
||||||
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/validations"
|
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/validations"
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/response"
|
"gitlab.com/mbugroup/lti-api.git/internal/response"
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||||
|
approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals"
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
)
|
)
|
||||||
@@ -264,6 +267,51 @@ func (u *ExpenseController) Approval(c *fiber.Ctx) error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (u *ExpenseController) BulkApproveToStatus(c *fiber.Ctx) error {
|
||||||
|
req := new(validation.BulkApprovalRequest)
|
||||||
|
|
||||||
|
if err := c.BodyParser(req); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
|
||||||
|
}
|
||||||
|
|
||||||
|
targetStep, err := req.ResolveTarget()
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.RequiresDate(targetStep) && strings.TrimSpace(req.Date) == "" {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "date is required for REALISASI bulk approval")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := ensureExpenseBulkApprovalPermission(c, targetStep); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
results, err := u.ExpenseService.BulkApproveToStatus(c, req, targetStep)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
data interface{}
|
||||||
|
message = "Bulk approve expense successfully"
|
||||||
|
)
|
||||||
|
if len(results) == 1 {
|
||||||
|
data = results[0]
|
||||||
|
} else {
|
||||||
|
message = "Bulk approve expenses successfully"
|
||||||
|
data = results
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusOK).
|
||||||
|
JSON(response.Success{
|
||||||
|
Code: fiber.StatusOK,
|
||||||
|
Status: "success",
|
||||||
|
Message: message,
|
||||||
|
Data: data,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func (u *ExpenseController) CreateRealization(c *fiber.Ctx) error {
|
func (u *ExpenseController) CreateRealization(c *fiber.Ctx) error {
|
||||||
expenseID := c.Params("id")
|
expenseID := c.Params("id")
|
||||||
id, err := strconv.Atoi(expenseID)
|
id, err := strconv.Atoi(expenseID)
|
||||||
@@ -366,6 +414,31 @@ func (u *ExpenseController) CompleteExpense(c *fiber.Ctx) error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ensureExpenseBulkApprovalPermission(c *fiber.Ctx, targetStep approvalutils.ApprovalStep) error {
|
||||||
|
requiredPerms := []string{}
|
||||||
|
|
||||||
|
switch targetStep {
|
||||||
|
case utils.ExpenseStepHeadArea:
|
||||||
|
requiredPerms = []string{m.P_ExpenseApprovalHeadArea}
|
||||||
|
case utils.ExpenseStepUnitVicePresident:
|
||||||
|
requiredPerms = []string{m.P_ExpenseApprovalUnitVicePresident}
|
||||||
|
case utils.ExpenseStepFinance:
|
||||||
|
requiredPerms = []string{m.P_ExpenseApprovalFinance}
|
||||||
|
case utils.ExpenseStepRealisasi:
|
||||||
|
requiredPerms = []string{m.P_ExpenseApprovalFinance, m.P_ExpenseCreateRealizations}
|
||||||
|
default:
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid approval target")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, perm := range requiredPerms {
|
||||||
|
if !m.HasPermission(c, perm) {
|
||||||
|
return fiber.NewError(fiber.StatusForbidden, "Insufficient permission")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (u *ExpenseController) DeleteDocument(c *fiber.Ctx) error {
|
func (u *ExpenseController) DeleteDocument(c *fiber.Ctx) error {
|
||||||
expenseID, err := strconv.Atoi(c.Params("id"))
|
expenseID, err := strconv.Atoi(c.Params("id"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ func ExpenseRoutes(v1 fiber.Router, u user.UserService, s expense.ExpenseService
|
|||||||
route.Post("/approvals/head-area", m.RequirePermissions(m.P_ExpenseApprovalHeadArea), 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/finance", m.RequirePermissions(m.P_ExpenseApprovalFinance), ctrl.Approval)
|
||||||
route.Post("/approvals/unit-vice-president", m.RequirePermissions(m.P_ExpenseApprovalUnitVicePresident), ctrl.Approval)
|
route.Post("/approvals/unit-vice-president", m.RequirePermissions(m.P_ExpenseApprovalUnitVicePresident), ctrl.Approval)
|
||||||
|
route.Post("/approvals/bulk", ctrl.BulkApproveToStatus)
|
||||||
|
|
||||||
route.Post("/:id/realizations", m.RequirePermissions(m.P_ExpenseCreateRealizations), ctrl.CreateRealization)
|
route.Post("/:id/realizations", m.RequirePermissions(m.P_ExpenseCreateRealizations), ctrl.CreateRealization)
|
||||||
route.Patch("/:id/realizations", m.RequirePermissions(m.P_ExpenseUpdateRealizations), ctrl.UpdateRealization)
|
route.Patch("/:id/realizations", m.RequirePermissions(m.P_ExpenseUpdateRealizations), ctrl.UpdateRealization)
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ type ExpenseService interface {
|
|||||||
UpdateRealization(ctx *fiber.Ctx, expenseID uint, req *validation.UpdateRealization) (*expenseDto.ExpenseDetailDTO, error)
|
UpdateRealization(ctx *fiber.Ctx, expenseID uint, req *validation.UpdateRealization) (*expenseDto.ExpenseDetailDTO, error)
|
||||||
DeleteDocument(ctx *fiber.Ctx, expenseID uint, documentID uint64, isRealization bool) error
|
DeleteDocument(ctx *fiber.Ctx, expenseID uint, documentID uint64, isRealization bool) error
|
||||||
Approval(ctx *fiber.Ctx, req *validation.ApprovalRequest, approvalType string) ([]expenseDto.ExpenseDetailDTO, error)
|
Approval(ctx *fiber.Ctx, req *validation.ApprovalRequest, approvalType string) ([]expenseDto.ExpenseDetailDTO, error)
|
||||||
|
BulkApproveToStatus(ctx *fiber.Ctx, req *validation.BulkApprovalRequest, target approvalutils.ApprovalStep) ([]expenseDto.ExpenseDetailDTO, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type expenseService struct {
|
type expenseService struct {
|
||||||
@@ -742,8 +743,12 @@ func (s *expenseService) CreateRealization(c *fiber.Ctx, expenseID uint, req *va
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.Repository.DB().WithContext(c.Context()).Transaction(func(tx *gorm.DB) error {
|
actorID, err := middleware.ActorIDFromContext(c)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fiber.NewError(fiber.StatusUnauthorized, "Failed to get actor ID from context")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.Repository.DB().WithContext(c.Context()).Transaction(func(tx *gorm.DB) error {
|
||||||
approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(tx))
|
approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(tx))
|
||||||
realizationRepoTx := repository.NewExpenseRealizationRepository(tx)
|
realizationRepoTx := repository.NewExpenseRealizationRepository(tx)
|
||||||
expenseNonstockRepoTx := repository.NewExpenseNonstockRepository(tx)
|
expenseNonstockRepoTx := repository.NewExpenseNonstockRepository(tx)
|
||||||
@@ -780,12 +785,6 @@ func (s *expenseService) CreateRealization(c *fiber.Ctx, expenseID uint, req *va
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := expenseRepoTx.PatchOne(c.Context(), expenseID, map[string]interface{}{
|
|
||||||
"realization_date": realizationDate,
|
|
||||||
}, nil); err != nil {
|
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update realization date")
|
|
||||||
}
|
|
||||||
|
|
||||||
if s.DocumentSvc != nil && len(req.Documents) > 0 {
|
if s.DocumentSvc != nil && len(req.Documents) > 0 {
|
||||||
documentFiles := make([]commonSvc.DocumentFile, 0, len(req.Documents))
|
documentFiles := make([]commonSvc.DocumentFile, 0, len(req.Documents))
|
||||||
for idx, file := range req.Documents {
|
for idx, file := range req.Documents {
|
||||||
@@ -795,7 +794,6 @@ func (s *expenseService) CreateRealization(c *fiber.Ctx, expenseID uint, req *va
|
|||||||
Index: &idx,
|
Index: &idx,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
actorID := uint(1) // TODO: replace with authenticated user id
|
|
||||||
_, err := s.DocumentSvc.UploadDocuments(c.Context(), commonSvc.DocumentUploadRequest{
|
_, err := s.DocumentSvc.UploadDocuments(c.Context(), commonSvc.DocumentUploadRequest{
|
||||||
DocumentableType: string(utils.DocumentableTypeExpenseRealization),
|
DocumentableType: string(utils.DocumentableTypeExpenseRealization),
|
||||||
DocumentableID: uint64(expenseID),
|
DocumentableID: uint64(expenseID),
|
||||||
@@ -807,6 +805,12 @@ func (s *expenseService) CreateRealization(c *fiber.Ctx, expenseID uint, req *va
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := expenseRepoTx.PatchOne(c.Context(), expenseID, map[string]interface{}{
|
||||||
|
"realization_date": realizationDate,
|
||||||
|
}, nil); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update realization date")
|
||||||
|
}
|
||||||
|
|
||||||
approvalAction := entity.ApprovalActionCreated
|
approvalAction := entity.ApprovalActionCreated
|
||||||
if _, err := approvalSvc.CreateApproval(
|
if _, err := approvalSvc.CreateApproval(
|
||||||
c.Context(),
|
c.Context(),
|
||||||
@@ -814,9 +818,9 @@ func (s *expenseService) CreateRealization(c *fiber.Ctx, expenseID uint, req *va
|
|||||||
expenseID,
|
expenseID,
|
||||||
utils.ExpenseStepRealisasi,
|
utils.ExpenseStepRealisasi,
|
||||||
&approvalAction,
|
&approvalAction,
|
||||||
uint(1), // TODO: replace with authenticated user id
|
actorID,
|
||||||
nil); err != nil {
|
nil,
|
||||||
|
); err != nil {
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create realization approval")
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create realization approval")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -834,6 +838,205 @@ func (s *expenseService) CreateRealization(c *fiber.Ctx, expenseID uint, req *va
|
|||||||
return responseDTO, nil
|
return responseDTO, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *expenseService) BulkApproveToStatus(c *fiber.Ctx, req *validation.BulkApprovalRequest, target approvalutils.ApprovalStep) ([]expenseDto.ExpenseDetailDTO, error) {
|
||||||
|
if err := s.Validate.Struct(req); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
approvableIDs := utils.UniqueUintSlice(req.ApprovableIds)
|
||||||
|
if len(approvableIDs) == 0 {
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, "approvable_ids must contain at least one id")
|
||||||
|
}
|
||||||
|
|
||||||
|
actorID, err := middleware.ActorIDFromContext(c)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fiber.NewError(fiber.StatusUnauthorized, "Failed to get actor ID from context")
|
||||||
|
}
|
||||||
|
|
||||||
|
var realizationDate time.Time
|
||||||
|
if req.RequiresDate(target) {
|
||||||
|
realizationDate, err = utils.ParseDateString(req.Date)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid date format")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
invalidateFromDateByExpenseID := make(map[uint]time.Time, len(approvableIDs))
|
||||||
|
|
||||||
|
err = s.Repository.DB().WithContext(c.Context()).Transaction(func(tx *gorm.DB) error {
|
||||||
|
approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(tx))
|
||||||
|
expenseRepoTx := repository.NewExpenseRepository(tx)
|
||||||
|
|
||||||
|
for _, id := range approvableIDs {
|
||||||
|
if err := commonSvc.EnsureRelations(c.Context(),
|
||||||
|
commonSvc.RelationCheck{Name: "Expense", ID: &id, Exists: expenseRepoTx.IdExists},
|
||||||
|
); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
expense, err := expenseRepoTx.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB {
|
||||||
|
return db.Preload("Nonstocks")
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return fiber.NewError(fiber.StatusNotFound, "Expense not found")
|
||||||
|
}
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to load expense")
|
||||||
|
}
|
||||||
|
|
||||||
|
latestApproval, err := approvalSvcTx.LatestByTarget(c.Context(), utils.ApprovalWorkflowExpense, id, nil)
|
||||||
|
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to validate workflow")
|
||||||
|
}
|
||||||
|
if latestApproval == nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("No approval found for Expense %d", id))
|
||||||
|
}
|
||||||
|
if latestApproval.Action != nil && *latestApproval.Action == entity.ApprovalActionRejected {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Expense %d is rejected and cannot be bulk approved", id))
|
||||||
|
}
|
||||||
|
|
||||||
|
currentStep := approvalutils.ApprovalStep(latestApproval.StepNumber)
|
||||||
|
if currentStep >= target {
|
||||||
|
currentStepName := utils.ExpenseApprovalSteps[currentStep]
|
||||||
|
targetStepName := utils.ExpenseApprovalSteps[target]
|
||||||
|
if currentStep == target {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Expense %d is already at %s step", id, targetStepName))
|
||||||
|
}
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Expense %d is already beyond %s step (current step: %s)", id, targetStepName, currentStepName))
|
||||||
|
}
|
||||||
|
|
||||||
|
invalidateFromDate := commonSvc.MinNonZeroDateOnlyUTC(expense.TransactionDate, expense.RealizationDate)
|
||||||
|
|
||||||
|
for step := currentStep + 1; step <= target; step++ {
|
||||||
|
if step == utils.ExpenseStepRealisasi {
|
||||||
|
if err := s.createRealizationFromExpenseLines(c.Context(), tx, expense, realizationDate, actorID, req.Notes); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
invalidateFromDate = commonSvc.MinNonZeroDateOnlyUTC(expense.TransactionDate, expense.RealizationDate, realizationDate)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
approvalAction := entity.ApprovalActionApproved
|
||||||
|
if _, err := approvalSvcTx.CreateApproval(
|
||||||
|
c.Context(),
|
||||||
|
utils.ApprovalWorkflowExpense,
|
||||||
|
id,
|
||||||
|
step,
|
||||||
|
&approvalAction,
|
||||||
|
actorID,
|
||||||
|
req.Notes,
|
||||||
|
); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create approval")
|
||||||
|
}
|
||||||
|
|
||||||
|
if step == utils.ExpenseStepFinance && expense.PoNumber == "" {
|
||||||
|
poNumber, err := s.generatePoNumber(tx, id)
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to generate PO number")
|
||||||
|
}
|
||||||
|
if err := expenseRepoTx.PatchOne(c.Context(), id, map[string]interface{}{"po_number": poNumber}, nil); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update PO number")
|
||||||
|
}
|
||||||
|
expense.PoNumber = poNumber
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
invalidateFromDateByExpenseID[id] = invalidateFromDate
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
if fiberErr, ok := err.(*fiber.Error); ok {
|
||||||
|
return nil, fiberErr
|
||||||
|
}
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to bulk approve expenses")
|
||||||
|
}
|
||||||
|
|
||||||
|
results := make([]expenseDto.ExpenseDetailDTO, 0, len(approvableIDs))
|
||||||
|
for _, id := range approvableIDs {
|
||||||
|
responseDTO, err := s.GetOne(c, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
results = append(results, *responseDTO)
|
||||||
|
}
|
||||||
|
|
||||||
|
for expenseID, invalidateFromDate := range invalidateFromDateByExpenseID {
|
||||||
|
s.invalidateDepreciationSnapshotsByExpense(c.Context(), nil, expenseID, invalidateFromDate, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
return results, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *expenseService) createRealizationFromExpenseLines(
|
||||||
|
ctx context.Context,
|
||||||
|
tx *gorm.DB,
|
||||||
|
expense *entity.Expense,
|
||||||
|
realizationDate time.Time,
|
||||||
|
actorID uint,
|
||||||
|
notes *string,
|
||||||
|
) error {
|
||||||
|
if expense == nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Expense not found")
|
||||||
|
}
|
||||||
|
if tx == nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Transaction is required")
|
||||||
|
}
|
||||||
|
if err := s.ensureProjectFlockNotClosedForExpense(ctx, expense); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
realizationRepoTx := repository.NewExpenseRealizationRepository(tx)
|
||||||
|
expenseRepoTx := repository.NewExpenseRepository(tx)
|
||||||
|
approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(tx))
|
||||||
|
|
||||||
|
for _, expenseNonstock := range expense.Nonstocks {
|
||||||
|
expenseNonstockID := expenseNonstock.Id
|
||||||
|
|
||||||
|
_, err := realizationRepoTx.GetByExpenseNonstockID(ctx, expenseNonstockID)
|
||||||
|
if err == nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Realization already exists for expense nonstock %d", expenseNonstockID))
|
||||||
|
}
|
||||||
|
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to check existing realization")
|
||||||
|
}
|
||||||
|
|
||||||
|
realization := &entity.ExpenseRealization{
|
||||||
|
ExpenseNonstockId: &expenseNonstockID,
|
||||||
|
Qty: expenseNonstock.Qty,
|
||||||
|
Price: expenseNonstock.Price,
|
||||||
|
Notes: expenseNonstock.Notes,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := realizationRepoTx.CreateOne(ctx, realization, nil); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create realization")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := expenseRepoTx.PatchOne(ctx, uint(expense.Id), map[string]interface{}{
|
||||||
|
"realization_date": realizationDate,
|
||||||
|
}, nil); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update realization date")
|
||||||
|
}
|
||||||
|
|
||||||
|
approvalAction := entity.ApprovalActionCreated
|
||||||
|
if _, err := approvalSvc.CreateApproval(
|
||||||
|
ctx,
|
||||||
|
utils.ApprovalWorkflowExpense,
|
||||||
|
uint(expense.Id),
|
||||||
|
utils.ExpenseStepRealisasi,
|
||||||
|
&approvalAction,
|
||||||
|
actorID,
|
||||||
|
notes,
|
||||||
|
); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create realization approval")
|
||||||
|
}
|
||||||
|
|
||||||
|
expense.RealizationDate = realizationDate
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *expenseService) CompleteExpense(c *fiber.Ctx, id uint, notes *string) (*expenseDto.ExpenseDetailDTO, error) {
|
func (s *expenseService) CompleteExpense(c *fiber.Ctx, id uint, notes *string) (*expenseDto.ExpenseDetailDTO, error) {
|
||||||
|
|
||||||
if err := commonSvc.EnsureRelations(c.Context(),
|
if err := commonSvc.EnsureRelations(c.Context(),
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
package validation
|
package validation
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"mime/multipart"
|
"mime/multipart"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||||
|
approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Create struct {
|
type Create struct {
|
||||||
@@ -66,3 +71,31 @@ type ApprovalRequest struct {
|
|||||||
ApprovableIds []uint `json:"approvable_ids" validate:"required,min=1,dive,gt=0"`
|
ApprovableIds []uint `json:"approvable_ids" validate:"required,min=1,dive,gt=0"`
|
||||||
Notes *string `json:"notes" form:"notes"`
|
Notes *string `json:"notes" form:"notes"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type BulkApprovalRequest struct {
|
||||||
|
ApprovableIds []uint `json:"approvable_ids" validate:"required,min=1,dive,gt=0"`
|
||||||
|
Status string `json:"status" validate:"required,max=100"`
|
||||||
|
Date string `json:"date,omitempty" validate:"omitempty,datetime=2006-01-02"`
|
||||||
|
Notes *string `json:"notes,omitempty" validate:"omitempty,max=500"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *BulkApprovalRequest) ResolveTarget() (approvalutils.ApprovalStep, error) {
|
||||||
|
status := strings.ToUpper(strings.ReplaceAll(strings.TrimSpace(r.Status), " ", "_"))
|
||||||
|
|
||||||
|
switch status {
|
||||||
|
case "HEAD_AREA", "APPROVAL_HEAD_AREA":
|
||||||
|
return utils.ExpenseStepHeadArea, nil
|
||||||
|
case "UNIT_VICE_PRESIDENT", "APPROVAL_UNIT_VICE_PRESIDENT", "BUSINESS_UNIT_VICE_PRESIDENT", "APPROVAL_BUSINESS_UNIT_VICE_PRESIDENT":
|
||||||
|
return utils.ExpenseStepUnitVicePresident, nil
|
||||||
|
case "FINANCE", "APPROVAL_FINANCE":
|
||||||
|
return utils.ExpenseStepFinance, nil
|
||||||
|
case "REALISASI":
|
||||||
|
return utils.ExpenseStepRealisasi, nil
|
||||||
|
default:
|
||||||
|
return 0, errors.New("status must be one of HEAD_AREA, UNIT_VICE_PRESIDENT, FINANCE, or REALISASI")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *BulkApprovalRequest) RequiresDate(target approvalutils.ApprovalStep) bool {
|
||||||
|
return target == utils.ExpenseStepRealisasi
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,10 +5,13 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/dto"
|
"gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/dto"
|
||||||
service "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/services"
|
service "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/services"
|
||||||
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/validations"
|
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/validations"
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/response"
|
"gitlab.com/mbugroup/lti-api.git/internal/response"
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||||
|
approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals"
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
)
|
)
|
||||||
@@ -152,3 +155,64 @@ func (u *DeliveryOrdersController) UpdateOne(c *fiber.Ctx) error {
|
|||||||
Data: result,
|
Data: result,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (u *DeliveryOrdersController) BulkApproveToStatus(c *fiber.Ctx) error {
|
||||||
|
req := new(validation.BulkApprovalRequest)
|
||||||
|
|
||||||
|
if err := c.BodyParser(req); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
|
||||||
|
}
|
||||||
|
|
||||||
|
targetStep, err := req.ResolveTarget()
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.RequiresDate(targetStep) && strings.TrimSpace(req.Date) == "" {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "date is required for DELIVERY bulk approval")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := ensureMarketingBulkApprovalPermission(c, targetStep); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
results, err := u.DeliveryOrdersService.BulkApproveToStatus(c, req, targetStep)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
data interface{}
|
||||||
|
message = "Bulk approve marketing successfully"
|
||||||
|
)
|
||||||
|
if len(results) == 1 {
|
||||||
|
data = results[0]
|
||||||
|
} else {
|
||||||
|
message = "Bulk approve marketings successfully"
|
||||||
|
data = results
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusOK).
|
||||||
|
JSON(response.Success{
|
||||||
|
Code: fiber.StatusOK,
|
||||||
|
Status: "success",
|
||||||
|
Message: message,
|
||||||
|
Data: data,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func ensureMarketingBulkApprovalPermission(c *fiber.Ctx, targetStep approvalutils.ApprovalStep) error {
|
||||||
|
requiredPerms := []string{m.P_SalesOrderApproval}
|
||||||
|
|
||||||
|
if targetStep == utils.MarketingDeliveryOrder {
|
||||||
|
requiredPerms = append(requiredPerms, m.P_DeliveryUpdateOne)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, perm := range requiredPerms {
|
||||||
|
if !m.HasPermission(c, perm) {
|
||||||
|
return fiber.NewError(fiber.StatusForbidden, "Insufficient permission")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ func RegisterRoutes(router fiber.Router, userService user.UserService, salesOrde
|
|||||||
route.Post("/sales-orders", m.RequirePermissions(m.P_SalesOrderCreateOne), salesOrdersCtrl.CreateOne)
|
route.Post("/sales-orders", m.RequirePermissions(m.P_SalesOrderCreateOne), salesOrdersCtrl.CreateOne)
|
||||||
route.Patch("/sales-orders/:id", m.RequirePermissions(m.P_SalesOrderUpdateOne), salesOrdersCtrl.UpdateOne)
|
route.Patch("/sales-orders/:id", m.RequirePermissions(m.P_SalesOrderUpdateOne), salesOrdersCtrl.UpdateOne)
|
||||||
route.Post("/sales-orders/approvals", m.RequirePermissions(m.P_SalesOrderApproval), salesOrdersCtrl.Approval)
|
route.Post("/sales-orders/approvals", m.RequirePermissions(m.P_SalesOrderApproval), salesOrdersCtrl.Approval)
|
||||||
|
route.Post("/approvals/bulk", deliveryOrdersCtrl.BulkApproveToStatus)
|
||||||
|
|
||||||
route.Post("/delivery-orders", m.RequirePermissions(m.P_DeliveryCreateOne), deliveryOrdersCtrl.CreateOne)
|
route.Post("/delivery-orders", m.RequirePermissions(m.P_DeliveryCreateOne), deliveryOrdersCtrl.CreateOne)
|
||||||
route.Patch("/delivery-orders/:id", m.RequirePermissions(m.P_DeliveryUpdateOne), deliveryOrdersCtrl.UpdateOne)
|
route.Patch("/delivery-orders/:id", m.RequirePermissions(m.P_DeliveryUpdateOne), deliveryOrdersCtrl.UpdateOne)
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import (
|
|||||||
rProjectFlock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
rProjectFlock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
||||||
rShared "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/repositories"
|
rShared "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/repositories"
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||||
|
approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals"
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/utils/fifo"
|
"gitlab.com/mbugroup/lti-api.git/internal/utils/fifo"
|
||||||
|
|
||||||
"github.com/go-playground/validator/v10"
|
"github.com/go-playground/validator/v10"
|
||||||
@@ -32,6 +33,7 @@ type DeliveryOrdersService interface {
|
|||||||
GetOne(ctx *fiber.Ctx, id uint) (*dto.MarketingDetailDTO, error)
|
GetOne(ctx *fiber.Ctx, id uint) (*dto.MarketingDetailDTO, error)
|
||||||
CreateOne(ctx *fiber.Ctx, req *validation.DeliveryOrderCreate) (*dto.MarketingDetailDTO, error)
|
CreateOne(ctx *fiber.Ctx, req *validation.DeliveryOrderCreate) (*dto.MarketingDetailDTO, error)
|
||||||
UpdateOne(ctx *fiber.Ctx, req *validation.DeliveryOrderUpdate, id uint) (*dto.MarketingDetailDTO, error)
|
UpdateOne(ctx *fiber.Ctx, req *validation.DeliveryOrderUpdate, id uint) (*dto.MarketingDetailDTO, error)
|
||||||
|
BulkApproveToStatus(ctx *fiber.Ctx, req *validation.BulkApprovalRequest, target approvalutils.ApprovalStep) ([]dto.MarketingDetailDTO, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type deliveryOrdersService struct {
|
type deliveryOrdersService struct {
|
||||||
@@ -544,6 +546,192 @@ func (s deliveryOrdersService) UpdateOne(c *fiber.Ctx, req *validation.DeliveryO
|
|||||||
return s.getMarketingWithDeliveries(c, id)
|
return s.getMarketingWithDeliveries(c, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s deliveryOrdersService) BulkApproveToStatus(c *fiber.Ctx, req *validation.BulkApprovalRequest, target approvalutils.ApprovalStep) ([]dto.MarketingDetailDTO, error) {
|
||||||
|
if err := s.Validate.Struct(req); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
approvableIDs := utils.UniqueUintSlice(req.ApprovableIds)
|
||||||
|
if len(approvableIDs) == 0 {
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, "approvable_ids must contain at least one id")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, id := range approvableIDs {
|
||||||
|
if err := m.EnsureMarketingAccess(c, s.MarketingRepo.DB(), id); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
actorID, err := m.ActorIDFromContext(c)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var deliveryDate time.Time
|
||||||
|
if req.RequiresDate(target) {
|
||||||
|
deliveryDate, err = utils.ParseDateString(req.Date)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid date format")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = s.MarketingRepo.DB().WithContext(c.Context()).Transaction(func(tx *gorm.DB) error {
|
||||||
|
approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(tx))
|
||||||
|
marketingRepoTx := marketingRepo.NewMarketingRepository(tx)
|
||||||
|
|
||||||
|
for _, id := range approvableIDs {
|
||||||
|
if err := commonSvc.EnsureRelations(c.Context(),
|
||||||
|
commonSvc.RelationCheck{Name: "Marketing", ID: &id, Exists: marketingRepoTx.IdExists},
|
||||||
|
); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
marketing, err := marketingRepoTx.GetByID(c.Context(), id, s.withRelations)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Marketing %d not found", id))
|
||||||
|
}
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch marketing")
|
||||||
|
}
|
||||||
|
|
||||||
|
latestApproval, err := approvalSvcTx.LatestByTarget(c.Context(), utils.ApprovalWorkflowMarketing, id, nil)
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to check approval status")
|
||||||
|
}
|
||||||
|
if latestApproval == nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("No approval found for Marketing %d", id))
|
||||||
|
}
|
||||||
|
if latestApproval.Action != nil && *latestApproval.Action == entity.ApprovalActionRejected {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Marketing %d is rejected and cannot be bulk approved", id))
|
||||||
|
}
|
||||||
|
|
||||||
|
currentStep := approvalutils.ApprovalStep(latestApproval.StepNumber)
|
||||||
|
if currentStep >= target {
|
||||||
|
currentStepName := utils.MarketingApprovalSteps[currentStep]
|
||||||
|
targetStepName := utils.MarketingApprovalSteps[target]
|
||||||
|
if currentStep == target {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Marketing %d is already at %s step", id, targetStepName))
|
||||||
|
}
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Marketing %d is already beyond %s step (current step: %s)", id, targetStepName, currentStepName))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(marketing.Products) > 0 {
|
||||||
|
pwIDs := make([]uint, 0, len(marketing.Products))
|
||||||
|
for _, product := range marketing.Products {
|
||||||
|
if product.ProductWarehouseId != 0 {
|
||||||
|
pwIDs = append(pwIDs, product.ProductWarehouseId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := commonSvc.EnsureProjectFlockNotClosedForProductWarehouses(c.Context(), tx, pwIDs); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for step := currentStep + 1; step <= target; step++ {
|
||||||
|
if step == utils.MarketingDeliveryOrder {
|
||||||
|
if err := s.createDeliveryFromMarketingProducts(c.Context(), tx, marketing, deliveryDate, actorID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
approvalAction := entity.ApprovalActionApproved
|
||||||
|
if _, err := approvalSvcTx.CreateApproval(
|
||||||
|
c.Context(),
|
||||||
|
utils.ApprovalWorkflowMarketing,
|
||||||
|
id,
|
||||||
|
step,
|
||||||
|
&approvalAction,
|
||||||
|
actorID,
|
||||||
|
req.Notes,
|
||||||
|
); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create approval")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
if fiberErr, ok := err.(*fiber.Error); ok {
|
||||||
|
return nil, fiberErr
|
||||||
|
}
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to bulk approve marketings")
|
||||||
|
}
|
||||||
|
|
||||||
|
results := make([]dto.MarketingDetailDTO, 0, len(approvableIDs))
|
||||||
|
for _, id := range approvableIDs {
|
||||||
|
result, err := s.getMarketingWithDeliveries(c, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
results = append(results, *result)
|
||||||
|
}
|
||||||
|
|
||||||
|
return results, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s deliveryOrdersService) createDeliveryFromMarketingProducts(
|
||||||
|
ctx context.Context,
|
||||||
|
tx *gorm.DB,
|
||||||
|
marketing *entity.Marketing,
|
||||||
|
deliveryDate time.Time,
|
||||||
|
actorID uint,
|
||||||
|
) error {
|
||||||
|
if marketing == nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Marketing not found")
|
||||||
|
}
|
||||||
|
if tx == nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Transaction is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
marketingDeliveryProductRepositoryTx := marketingRepo.NewMarketingDeliveryProductRepository(tx)
|
||||||
|
|
||||||
|
for _, marketingProduct := range marketing.Products {
|
||||||
|
deliveryProduct := marketingProduct.DeliveryProduct
|
||||||
|
if deliveryProduct == nil {
|
||||||
|
record, err := marketingDeliveryProductRepositoryTx.GetByMarketingProductID(ctx, marketingProduct.Id)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Delivery product for marketing product %d not found", marketingProduct.Id))
|
||||||
|
}
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch delivery product")
|
||||||
|
}
|
||||||
|
deliveryProduct = record
|
||||||
|
}
|
||||||
|
|
||||||
|
oldRequestedQty := deliveryProduct.UsageQty + deliveryProduct.PendingQty
|
||||||
|
deliveryDateCopy := deliveryDate
|
||||||
|
|
||||||
|
deliveryProduct.ProductWarehouseId = marketingProduct.ProductWarehouseId
|
||||||
|
deliveryProduct.UnitPrice = marketingProduct.UnitPrice
|
||||||
|
deliveryProduct.AvgWeight = marketingProduct.AvgWeight
|
||||||
|
deliveryProduct.WeightPerConvertion = marketingProduct.WeightPerConvertion
|
||||||
|
deliveryProduct.TotalWeight = marketingProduct.TotalWeight
|
||||||
|
deliveryProduct.TotalPrice = marketingProduct.TotalPrice
|
||||||
|
deliveryProduct.DeliveryDate = &deliveryDateCopy
|
||||||
|
|
||||||
|
requestedQty := marketingProduct.Qty
|
||||||
|
if requestedQty != oldRequestedQty {
|
||||||
|
if oldRequestedQty > 0 {
|
||||||
|
if err := s.releaseDeliveryStock(ctx, tx, deliveryProduct, &marketingProduct, actorID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if requestedQty > 0 {
|
||||||
|
if err := s.consumeDeliveryStock(ctx, tx, deliveryProduct, &marketingProduct, requestedQty, actorID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := marketingDeliveryProductRepositoryTx.UpdateOne(ctx, deliveryProduct.Id, deliveryProduct, nil); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update delivery product")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *deliveryOrdersService) calculatePriceByMarketingType(marketingType string, qty, avgWeight, unitPrice float64, week *int, convertionUnit *string, _ *float64) (totalWeight, totalPrice float64) {
|
func (s *deliveryOrdersService) calculatePriceByMarketingType(marketingType string, qty, avgWeight, unitPrice float64, week *int, convertionUnit *string, _ *float64) (totalWeight, totalPrice float64) {
|
||||||
if marketingType == string(utils.MarketingTypeTrading) {
|
if marketingType == string(utils.MarketingTypeTrading) {
|
||||||
totalWeight = 0
|
totalWeight = 0
|
||||||
|
|||||||
@@ -1,5 +1,13 @@
|
|||||||
package validation
|
package validation
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||||
|
approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals"
|
||||||
|
)
|
||||||
|
|
||||||
type Create struct {
|
type Create struct {
|
||||||
CustomerId uint `json:"customer_id" validate:"required,gt=0"`
|
CustomerId uint `json:"customer_id" validate:"required,gt=0"`
|
||||||
SalesPersonId uint `json:"sales_person_id" validate:"required,gt=0"`
|
SalesPersonId uint `json:"sales_person_id" validate:"required,gt=0"`
|
||||||
@@ -33,3 +41,27 @@ type Approve struct {
|
|||||||
ApprovableIds []uint `json:"approvable_ids" validate:"required_strict,min=1,dive,gt=0"`
|
ApprovableIds []uint `json:"approvable_ids" validate:"required_strict,min=1,dive,gt=0"`
|
||||||
Notes *string `json:"notes,omitempty" validate:"omitempty,max=500"`
|
Notes *string `json:"notes,omitempty" validate:"omitempty,max=500"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type BulkApprovalRequest struct {
|
||||||
|
ApprovableIds []uint `json:"approvable_ids" validate:"required,min=1,dive,gt=0"`
|
||||||
|
Status string `json:"status" validate:"required,max=100"`
|
||||||
|
Date string `json:"date,omitempty" validate:"omitempty,datetime=2006-01-02"`
|
||||||
|
Notes *string `json:"notes,omitempty" validate:"omitempty,max=500"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *BulkApprovalRequest) ResolveTarget() (approvalutils.ApprovalStep, error) {
|
||||||
|
status := strings.ToUpper(strings.ReplaceAll(strings.TrimSpace(r.Status), " ", "_"))
|
||||||
|
|
||||||
|
switch status {
|
||||||
|
case "SALES_ORDER":
|
||||||
|
return utils.MarketingStepSalesOrder, nil
|
||||||
|
case "DELIVERY", "DELIVERY_ORDER":
|
||||||
|
return utils.MarketingDeliveryOrder, nil
|
||||||
|
default:
|
||||||
|
return 0, errors.New("status must be one of SALES_ORDER or DELIVERY")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *BulkApprovalRequest) RequiresDate(target approvalutils.ApprovalStep) bool {
|
||||||
|
return target == utils.MarketingDeliveryOrder
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user