package service import ( "context" "errors" "fmt" "time" commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository" 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" productWarehouseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories" "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/dto" marketingRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/validations" "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 DeliveryOrdersService interface { GetAll(ctx *fiber.Ctx, params *validation.DeliveryOrderQuery) ([]dto.MarketingListDTO, int64, error) 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) } type deliveryOrdersService struct { Log *logrus.Logger Validate *validator.Validate MarketingRepo marketingRepo.MarketingRepository MarketingProductRepo marketingRepo.MarketingProductRepository MarketingDeliveryProductRepo marketingRepo.MarketingDeliveryProductRepository ApprovalSvc commonSvc.ApprovalService } func NewDeliveryOrdersService( marketingRepo marketingRepo.MarketingRepository, marketingProductRepo marketingRepo.MarketingProductRepository, marketingDeliveryProductRepo marketingRepo.MarketingDeliveryProductRepository, approvalSvc commonSvc.ApprovalService, validate *validator.Validate, ) DeliveryOrdersService { return &deliveryOrdersService{ Log: utils.Log, Validate: validate, MarketingRepo: marketingRepo, MarketingProductRepo: marketingProductRepo, MarketingDeliveryProductRepo: marketingDeliveryProductRepo, ApprovalSvc: approvalSvc, } } func (s deliveryOrdersService) withRelations(db *gorm.DB) *gorm.DB { return db. Preload("CreatedUser"). Preload("Customer"). Preload("SalesPerson"). Preload("Products.ProductWarehouse.Product"). Preload("Products.ProductWarehouse.Warehouse"). Preload("Products.DeliveryProduct") } func (s deliveryOrdersService) getMarketingWithDeliveries(c *fiber.Ctx, marketingId uint) (*dto.MarketingDetailDTO, error) { marketing, err := s.MarketingRepo.GetByID(c.Context(), marketingId, s.withRelations) if err != nil { return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch marketing") } latestApproval, err := s.ApprovalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowMarketing, marketingId, nil) if err != nil { } marketing.LatestApproval = latestApproval allDeliveryProducts, err := s.MarketingDeliveryProductRepo.GetByMarketingId(c.Context(), marketingId) if err != nil { allDeliveryProducts = []entity.MarketingDeliveryProduct{} } responseDTO := dto.ToMarketingDetailDTO(marketing, allDeliveryProducts) return &responseDTO, nil } func (s deliveryOrdersService) GetAll(c *fiber.Ctx, params *validation.DeliveryOrderQuery) ([]dto.MarketingListDTO, int64, error) { if err := s.Validate.Struct(params); err != nil { return nil, 0, err } offset := (params.Page - 1) * params.Limit marketings, total, err := s.MarketingRepo.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB { db = db. Preload("CreatedUser"). Preload("Customer"). Preload("SalesPerson"). Preload("Products.ProductWarehouse.Product"). Preload("Products.ProductWarehouse.Warehouse"). Preload("Products.DeliveryProduct") if params.MarketingId != 0 { return db.Where("id = ?", params.MarketingId) } return db.Order("created_at DESC").Order("updated_at DESC") }) if err != nil { s.Log.Errorf("Failed to get marketings: %+v", err) return nil, 0, err } for i := range marketings { latestApproval, err := s.ApprovalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowMarketing, marketings[i].Id, func(db *gorm.DB) *gorm.DB { return db.Preload("ActionUser") }) if err != nil { s.Log.Warnf("Failed to load approval for marketing %d: %+v", marketings[i].Id, err) } marketings[i].LatestApproval = latestApproval } result := make([]dto.MarketingListDTO, len(marketings)) for i, marketing := range marketings { result[i] = dto.ToMarketingListDTO(&marketing, []entity.MarketingDeliveryProduct{}) } return result, total, nil } func (s deliveryOrdersService) GetOne(c *fiber.Ctx, id uint) (*dto.MarketingDetailDTO, error) { marketing, err := s.MarketingRepo.GetByID(c.Context(), id, s.withRelations) if errors.Is(err, gorm.ErrRecordNotFound) { return nil, fiber.NewError(fiber.StatusNotFound, "Marketing not found") } if err != nil { return nil, err } allDeliveryProducts, err := s.MarketingDeliveryProductRepo.GetByMarketingId(c.Context(), id) if err != nil { allDeliveryProducts = []entity.MarketingDeliveryProduct{} } if s.ApprovalSvc != nil { approvals, err := s.ApprovalSvc.ListByTarget(c.Context(), utils.ApprovalWorkflowMarketing, marketing.Id, func(db *gorm.DB) *gorm.DB { return db.Preload("ActionUser") }) if err != nil { } else if len(approvals) > 0 { if marketing.LatestApproval == nil { latest := approvals[len(approvals)-1] marketing.LatestApproval = &latest } } else { marketing.LatestApproval = nil } } responseDTO := dto.ToMarketingDetailDTO(marketing, allDeliveryProducts) return &responseDTO, nil } func (s *deliveryOrdersService) CreateOne(c *fiber.Ctx, req *validation.DeliveryOrderCreate) (*dto.MarketingDetailDTO, error) { if err := s.Validate.Struct(req); err != nil { return nil, err } if err := commonSvc.EnsureRelations(c.Context(), commonSvc.RelationCheck{Name: "Marketing", ID: &req.MarketingId, Exists: s.MarketingRepo.IdExists}, ); err != nil { return nil, err } actorID, err := m.ActorIDFromContext(c) if err != nil { return nil, err } approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(s.MarketingRepo.DB())) latestApproval, err := approvalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowMarketing, req.MarketingId, nil) if err != nil { return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check approval status") } if latestApproval == nil { return nil, fiber.NewError(fiber.StatusBadRequest, "Marketing has not been submitted for approval") } if latestApproval.StepNumber < uint16(utils.MarketingStepSalesOrder) { return nil, fiber.NewError(fiber.StatusBadRequest, "Marketing must be approved to Sales Order step before creating delivery order") } if latestApproval.StepNumber >= uint16(utils.MarketingDeliveryOrder) { return nil, fiber.NewError(fiber.StatusBadRequest, "Delivery order already exists for this marketing") } err = s.MarketingRepo.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { marketingProductRepositoryTx := marketingRepo.NewMarketingProductRepository(dbTransaction) marketingDeliveryProductRepositoryTx := marketingRepo.NewMarketingDeliveryProductRepository(dbTransaction) approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction)) allMarketingProducts, err := marketingProductRepositoryTx.GetByMarketingID(c.Context(), req.MarketingId) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("No marketing products found for marketing %d", req.MarketingId)) } return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch marketing products") } for _, requestedProduct := range req.DeliveryProducts { var foundMarketingProduct *entity.MarketingProduct for i := range allMarketingProducts { if allMarketingProducts[i].Id == requestedProduct.MarketingProductId { foundMarketingProduct = &allMarketingProducts[i] break } } if foundMarketingProduct == nil { return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Marketing product %d not found for this marketing", requestedProduct.MarketingProductId)) } if err := commonSvc.EnsureProjectFlockNotClosedForProductWarehouses( c.Context(), dbTransaction, []uint{foundMarketingProduct.ProductWarehouseId}, ); err != nil { return err } deliveryProduct, err := marketingDeliveryProductRepositoryTx.GetByMarketingProductID(c.Context(), foundMarketingProduct.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", requestedProduct.MarketingProductId)) } return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch delivery product") } var itemDeliveryDate *time.Time if requestedProduct.DeliveryDate != "" { parsedDate, err := utils.ParseDateString(requestedProduct.DeliveryDate) if err != nil { return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid delivery date format for product %d: %v", requestedProduct.MarketingProductId, err)) } itemDeliveryDate = &parsedDate } deliveryProduct.Qty = requestedProduct.Qty deliveryProduct.UnitPrice = requestedProduct.UnitPrice deliveryProduct.AvgWeight = requestedProduct.AvgWeight deliveryProduct.TotalWeight = requestedProduct.TotalWeight deliveryProduct.TotalPrice = requestedProduct.TotalPrice deliveryProduct.DeliveryDate = itemDeliveryDate deliveryProduct.VehicleNumber = requestedProduct.VehicleNumber if requestedProduct.Qty > 0 { if err := s.validateAndReduceProductWarehouse(c.Context(), dbTransaction, foundMarketingProduct, requestedProduct.Qty); err != nil { return err } } if err := marketingDeliveryProductRepositoryTx.UpdateOne(c.Context(), deliveryProduct.Id, deliveryProduct, nil); err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to update delivery product") } } approvalAction := entity.ApprovalActionApproved if _, err := approvalSvcTx.CreateApproval( c.Context(), utils.ApprovalWorkflowMarketing, req.MarketingId, utils.MarketingDeliveryOrder, &approvalAction, actorID, nil); err != nil { if !errors.Is(err, gorm.ErrDuplicatedKey) { return fiber.NewError(fiber.StatusInternalServerError, "Failed to create delivery order approval") } } return nil }) if err != nil { if fiberErr, ok := err.(*fiber.Error); ok { return nil, fiberErr } return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to create delivery order") } return s.getMarketingWithDeliveries(c, req.MarketingId) } func (s deliveryOrdersService) UpdateOne(c *fiber.Ctx, req *validation.DeliveryOrderUpdate, id uint) (*dto.MarketingDetailDTO, error) { if err := s.Validate.Struct(req); err != nil { return nil, err } if err := commonSvc.EnsureRelations(c.Context(), commonSvc.RelationCheck{Name: "Marketing", ID: &id, Exists: s.MarketingRepo.IdExists}, ); err != nil { return nil, err } err := s.MarketingRepo.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { marketingProductRepositoryTx := marketingRepo.NewMarketingProductRepository(dbTransaction) marketingDeliveryProductRepositoryTx := marketingRepo.NewMarketingDeliveryProductRepository(dbTransaction) allMarketingProducts, err := marketingProductRepositoryTx.GetByMarketingID(c.Context(), id) if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch marketing products") } if len(req.DeliveryProducts) > 0 { for _, requestedProduct := range req.DeliveryProducts { var foundMarketingProduct *entity.MarketingProduct for i := range allMarketingProducts { if allMarketingProducts[i].Id == requestedProduct.MarketingProductId { foundMarketingProduct = &allMarketingProducts[i] break } } if foundMarketingProduct == nil { return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Marketing product %d not found for this marketing", requestedProduct.MarketingProductId)) } if err := commonSvc.EnsureProjectFlockNotClosedForProductWarehouses( c.Context(), dbTransaction, []uint{foundMarketingProduct.ProductWarehouseId}, ); err != nil { return err } deliveryProduct, err := marketingDeliveryProductRepositoryTx.GetByMarketingProductID(c.Context(), foundMarketingProduct.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", requestedProduct.MarketingProductId)) } return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch delivery product") } var itemDeliveryDate *time.Time if requestedProduct.DeliveryDate != "" { parsedDate, err := utils.ParseDateString(requestedProduct.DeliveryDate) if err != nil { return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid delivery date format for product %d: %v", requestedProduct.MarketingProductId, err)) } itemDeliveryDate = &parsedDate } else if deliveryProduct.DeliveryDate != nil { itemDeliveryDate = deliveryProduct.DeliveryDate } oldQty := deliveryProduct.Qty deliveryProduct.Qty = requestedProduct.Qty deliveryProduct.UnitPrice = requestedProduct.UnitPrice deliveryProduct.AvgWeight = requestedProduct.AvgWeight deliveryProduct.TotalWeight = requestedProduct.TotalWeight deliveryProduct.TotalPrice = requestedProduct.TotalPrice deliveryProduct.DeliveryDate = itemDeliveryDate deliveryProduct.VehicleNumber = requestedProduct.VehicleNumber qtyChange := requestedProduct.Qty - oldQty if qtyChange > 0 { if err := s.validateAndReduceProductWarehouse(c.Context(), dbTransaction, foundMarketingProduct, qtyChange); err != nil { return err } } else if qtyChange < 0 { if err := s.restoreProductWarehouseStock(c.Context(), dbTransaction, foundMarketingProduct, -qtyChange); err != nil { return err } } if err := marketingDeliveryProductRepositoryTx.UpdateOne(c.Context(), deliveryProduct.Id, deliveryProduct, nil); err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to update delivery product") } } } return nil }) if err != nil { if fiberErr, ok := err.(*fiber.Error); ok { return nil, fiberErr } return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to update delivery order") } return s.getMarketingWithDeliveries(c, id) } func (s deliveryOrdersService) validateAndReduceProductWarehouse(ctx context.Context, tx *gorm.DB, marketingProduct *entity.MarketingProduct, qtyDeliver float64) error { if marketingProduct == nil || marketingProduct.ProductWarehouseId == 0 { return fiber.NewError(fiber.StatusInternalServerError, "Product warehouse not found") } pwRepo := productWarehouseRepo.NewProductWarehouseRepository(tx) pw, err := pwRepo.GetByID(ctx, marketingProduct.ProductWarehouseId, nil) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return fiber.NewError(fiber.StatusNotFound, "Product warehouse not found") } return fiber.NewError(fiber.StatusInternalServerError, "Failed to check stock") } if pw.Quantity < qtyDeliver { return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Insufficient stock for warehouse - available: %.2f, requested: %.2f", pw.Quantity, qtyDeliver)) } pw.Quantity = pw.Quantity - qtyDeliver if err := pwRepo.UpdateOne(ctx, pw.Id, pw, nil); err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to update stock") } return nil } func (s deliveryOrdersService) restoreProductWarehouseStock(ctx context.Context, tx *gorm.DB, marketingProduct *entity.MarketingProduct, qtyRestore float64) error { if marketingProduct == nil || marketingProduct.ProductWarehouseId == 0 { return fiber.NewError(fiber.StatusInternalServerError, "Product warehouse not found") } pwRepo := productWarehouseRepo.NewProductWarehouseRepository(tx) pw, err := pwRepo.GetByID(ctx, marketingProduct.ProductWarehouseId, nil) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return fiber.NewError(fiber.StatusNotFound, "Product warehouse not found") } return fiber.NewError(fiber.StatusInternalServerError, "Failed to check stock") } pw.Quantity = pw.Quantity + qtyRestore if err := pwRepo.UpdateOne(ctx, pw.Id, pw, nil); err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to update stock") } return nil }