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" "gitlab.com/mbugroup/lti-api.git/internal/utils/fifo" "github.com/go-playground/validator/v10" "github.com/gofiber/fiber/v2" "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 { Validate *validator.Validate MarketingRepo marketingRepo.MarketingRepository MarketingProductRepo marketingRepo.MarketingProductRepository MarketingDeliveryProductRepo marketingRepo.MarketingDeliveryProductRepository ApprovalSvc commonSvc.ApprovalService FifoSvc commonSvc.FifoService } func NewDeliveryOrdersService( marketingRepo marketingRepo.MarketingRepository, marketingProductRepo marketingRepo.MarketingProductRepository, marketingDeliveryProductRepo marketingRepo.MarketingDeliveryProductRepository, approvalSvc commonSvc.ApprovalService, fifoSvc commonSvc.FifoService, validate *validator.Validate, ) DeliveryOrdersService { return &deliveryOrdersService{ Validate: validate, MarketingRepo: marketingRepo, MarketingProductRepo: marketingProductRepo, MarketingDeliveryProductRepo: marketingDeliveryProductRepo, ApprovalSvc: approvalSvc, FifoSvc: fifoSvc, } } 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) { if err := m.EnsureMarketingAccess(c, s.MarketingRepo.DB(), marketingId); err != nil { return nil, err } 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 } scope, err := m.ResolveLocationScope(c, s.MarketingRepo.DB()) if 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 scope.Restrict { if len(scope.IDs) == 0 { return db.Where("1 = 0") } db = db. Joins("JOIN marketing_products mp ON mp.marketing_id = marketings.id"). Joins("JOIN product_warehouses pw ON pw.id = mp.product_warehouse_id"). Joins("JOIN warehouses w ON w.id = pw.warehouse_id"). Where("w.location_id IN ?", scope.IDs). Distinct("marketings.*") } if params.MarketingId != 0 { return db.Where("id = ?", params.MarketingId) } return db.Order("created_at DESC").Order("updated_at DESC") }) if err != nil { 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 { continue } 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) { if err := m.EnsureMarketingAccess(c, s.MarketingRepo.DB(), id); err != nil { return nil, err } 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 := m.EnsureMarketingAccess(c, s.MarketingRepo.DB(), req.MarketingId); 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 } // Hitung total_weight dan total_price otomatis totalWeight := requestedProduct.Qty * requestedProduct.AvgWeight totalPrice := requestedProduct.UnitPrice * requestedProduct.Qty deliveryProduct.ProductWarehouseId = foundMarketingProduct.ProductWarehouseId deliveryProduct.UnitPrice = requestedProduct.UnitPrice deliveryProduct.AvgWeight = requestedProduct.AvgWeight deliveryProduct.TotalWeight = totalWeight deliveryProduct.TotalPrice = totalPrice deliveryProduct.DeliveryDate = itemDeliveryDate deliveryProduct.VehicleNumber = requestedProduct.VehicleNumber if requestedProduct.Qty > 0 { if err := s.consumeDeliveryStock(c.Context(), dbTransaction, deliveryProduct, 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 := m.EnsureMarketingAccess(c, s.MarketingRepo.DB(), id); 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 } oldRequestedQty := deliveryProduct.UsageQty + deliveryProduct.PendingQty // Hitung total_weight dan total_price otomatis totalWeight := requestedProduct.Qty * requestedProduct.AvgWeight totalPrice := requestedProduct.UnitPrice * requestedProduct.Qty deliveryProduct.ProductWarehouseId = foundMarketingProduct.ProductWarehouseId deliveryProduct.UnitPrice = requestedProduct.UnitPrice deliveryProduct.AvgWeight = requestedProduct.AvgWeight deliveryProduct.TotalWeight = totalWeight deliveryProduct.TotalPrice = totalPrice deliveryProduct.DeliveryDate = itemDeliveryDate deliveryProduct.VehicleNumber = requestedProduct.VehicleNumber if requestedProduct.Qty != oldRequestedQty { if oldRequestedQty > 0 { if err := s.releaseDeliveryStock(c.Context(), dbTransaction, deliveryProduct, foundMarketingProduct); err != nil { return err } } if requestedProduct.Qty > 0 { if err := s.consumeDeliveryStock(c.Context(), dbTransaction, deliveryProduct, 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") } } } 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) consumeDeliveryStock(ctx context.Context, tx *gorm.DB, deliveryProduct *entity.MarketingDeliveryProduct, marketingProduct *entity.MarketingProduct, requestedQty float64) error { if marketingProduct == nil || marketingProduct.ProductWarehouseId == 0 { return fiber.NewError(fiber.StatusInternalServerError, "Product warehouse not found") } if deliveryProduct == nil || deliveryProduct.Id == 0 { return fiber.NewError(fiber.StatusInternalServerError, "Delivery product not found") } result, err := s.FifoSvc.Consume(ctx, commonSvc.StockConsumeRequest{ UsableKey: fifo.UsableKeyMarketingDelivery, UsableID: deliveryProduct.Id, ProductWarehouseID: marketingProduct.ProductWarehouseId, Quantity: requestedQty, AllowPending: false, Tx: tx, }) deliveryProductRepo := marketingRepo.NewMarketingDeliveryProductRepository(tx) if err != nil { pwRepo := productWarehouseRepo.NewProductWarehouseRepository(tx) pw, err2 := pwRepo.GetByID(ctx, marketingProduct.ProductWarehouseId, nil) if err2 != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to check product warehouse stock") } if pw == nil || pw.Quantity < requestedQty { return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Insufficient stock. Available: %.2f, Requested: %.2f", func() float64 { if pw != nil { return pw.Quantity } else { return 0 } }(), requestedQty)) } if err := deliveryProductRepo.UpdateFifoFields(ctx, deliveryProduct.Id, requestedQty, 0); err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to update delivery product") } return nil } if err := deliveryProductRepo.UpdateFifoFields(ctx, deliveryProduct.Id, result.UsageQuantity, result.PendingQuantity); err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to update delivery product") } return nil } func (s deliveryOrdersService) releaseDeliveryStock(ctx context.Context, tx *gorm.DB, deliveryProduct *entity.MarketingDeliveryProduct, marketingProduct *entity.MarketingProduct) error { if deliveryProduct == nil || deliveryProduct.Id == 0 { return nil } if marketingProduct == nil || marketingProduct.ProductWarehouseId == 0 { return fiber.NewError(fiber.StatusInternalServerError, "Product warehouse not found") } deliveryProductRepo := marketingRepo.NewMarketingDeliveryProductRepository(tx) currentUsage, err := deliveryProductRepo.GetUsageQty(ctx, deliveryProduct.Id) if err != nil { currentUsage = 0 } if currentUsage == 0 { return nil } if err := s.FifoSvc.ReleaseUsage(ctx, commonSvc.StockReleaseRequest{ UsableKey: fifo.UsableKeyMarketingDelivery, UsableID: deliveryProduct.Id, Tx: tx, }); err != nil { return err } if err := deliveryProductRepo.ResetFifoFields(ctx, deliveryProduct.Id); err != nil { return err } return nil }