feat: bulk approve endpoint for marketings and expenses

This commit is contained in:
Adnan Zahir
2026-04-21 20:06:37 +07:00
parent 1e34a0e7b2
commit 0d04397bd5
9 changed files with 607 additions and 11 deletions
@@ -5,10 +5,13 @@ import (
"strconv"
"strings"
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
"gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/dto"
service "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/services"
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/utils"
approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals"
"github.com/gofiber/fiber/v2"
)
@@ -152,3 +155,64 @@ func (u *DeliveryOrdersController) UpdateOne(c *fiber.Ctx) error {
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
}
+1
View File
@@ -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.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("/approvals/bulk", deliveryOrdersCtrl.BulkApproveToStatus)
route.Post("/delivery-orders", m.RequirePermissions(m.P_DeliveryCreateOne), deliveryOrdersCtrl.CreateOne)
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"
rShared "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/repositories"
"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"
"github.com/go-playground/validator/v10"
@@ -32,6 +33,7 @@ type DeliveryOrdersService interface {
GetOne(ctx *fiber.Ctx, id uint) (*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)
BulkApproveToStatus(ctx *fiber.Ctx, req *validation.BulkApprovalRequest, target approvalutils.ApprovalStep) ([]dto.MarketingDetailDTO, error)
}
type deliveryOrdersService struct {
@@ -544,6 +546,192 @@ func (s deliveryOrdersService) UpdateOne(c *fiber.Ctx, req *validation.DeliveryO
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) {
if marketingType == string(utils.MarketingTypeTrading) {
totalWeight = 0
@@ -1,5 +1,13 @@
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 {
CustomerId uint `json:"customer_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"`
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
}