package service import ( "context" "errors" "fmt" "strings" 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" repository "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/validations" customerRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/repositories" warehouseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories" projectFlockKandangRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories" userRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories" "gitlab.com/mbugroup/lti-api.git/internal/utils" approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals" "github.com/go-playground/validator/v10" "github.com/gofiber/fiber/v2" "github.com/sirupsen/logrus" "gorm.io/gorm" ) type SalesOrdersService interface { CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.Marketing, error) UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.Marketing, error) DeleteOne(ctx *fiber.Ctx, id uint) error Approval(ctx *fiber.Ctx, req *validation.Approve) ([]entity.Marketing, error) } type salesOrdersService struct { Log *logrus.Logger Validate *validator.Validate MarketingRepo repository.MarketingRepository CustomerRepo customerRepo.CustomerRepository ProductWarehouseRepo productWarehouseRepo.ProductWarehouseRepository UserRepo userRepo.UserRepository ApprovalSvc commonSvc.ApprovalService WarehouseRepo warehouseRepo.WarehouseRepository ProjectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository } func NewSalesOrdersService(marketingRepo repository.MarketingRepository, customerRepo customerRepo.CustomerRepository, productWarehouseRepo productWarehouseRepo.ProductWarehouseRepository, userRepo userRepo.UserRepository, approvalSvc commonSvc.ApprovalService, warehouseRepo warehouseRepo.WarehouseRepository, projectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository, validate *validator.Validate) SalesOrdersService { return &salesOrdersService{ Log: utils.Log, Validate: validate, MarketingRepo: marketingRepo, CustomerRepo: customerRepo, ProductWarehouseRepo: productWarehouseRepo, UserRepo: userRepo, ApprovalSvc: approvalSvc, WarehouseRepo: warehouseRepo, ProjectFlockKandangRepo: projectFlockKandangRepo, } } func (s salesOrdersService) withRelations(db *gorm.DB) *gorm.DB { return db. Preload("CreatedUser"). Preload("Customer"). Preload("SalesPerson"). Preload("Products.ProductWarehouse.Product.Flags"). Preload("Products.ProductWarehouse.Warehouse") } func (s salesOrdersService) getOne(c *fiber.Ctx, id uint) (*entity.Marketing, error) { marketing, err := s.MarketingRepo.GetByID(c.Context(), id, s.withRelations) if errors.Is(err, gorm.ErrRecordNotFound) { return nil, fiber.NewError(fiber.StatusNotFound, "SalesOrders not found") } if err != nil { s.Log.Errorf("Failed get marketing by id: %+v", err) return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch sales order") } if s.ApprovalSvc != nil { approvals, err := s.ApprovalSvc.ListByTarget(c.Context(), utils.ApprovalWorkflowMarketing, id, func(db *gorm.DB) *gorm.DB { return db.Preload("ActionUser") }) if err == nil && len(approvals) > 0 { latest := approvals[len(approvals)-1] marketing.LatestApproval = &latest } } return marketing, nil } func (s *salesOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.Marketing, error) { if err := s.Validate.Struct(req); err != nil { return nil, err } actorID, err := m.ActorIDFromContext(c) if err != nil { return nil, err } if err := commonSvc.EnsureRelations(c.Context(), commonSvc.RelationCheck{Name: "Customer", ID: &req.CustomerId, Exists: s.CustomerRepo.IdExists}, ); err != nil { return nil, err } for _, item := range req.MarketingProducts { if err := commonSvc.EnsureRelations(c.Context(), commonSvc.RelationCheck{Name: "ProductWarehouse", ID: &item.ProductWarehouseId, Exists: s.ProductWarehouseRepo.IdExists}, ); err != nil { return nil, err } } soDate, err := utils.ParseDateString(req.Date) if err != nil { return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid date format") } soNumber, err := s.MarketingRepo.NextSoNumber(context.Background(), nil) if err != nil { return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to generate SO number") } var marketing *entity.Marketing err = s.MarketingRepo.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { marketingRepoTx := repository.NewMarketingRepository(dbTransaction) marketingProductRepoTx := repository.NewMarketingProductRepository(dbTransaction) invDeliveryRepoTx := repository.NewMarketingDeliveryProductRepository(dbTransaction) approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction)) marketing = &entity.Marketing{ CustomerId: req.CustomerId, SoNumber: soNumber, SoDate: soDate, SalesPersonId: req.SalesPersonId, Notes: req.Notes, CreatedBy: actorID, } if err := marketingRepoTx.CreateOne(c.Context(), marketing, nil); err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to create salesOrders") } if len(req.MarketingProducts) > 0 { pwIDs := make([]uint, 0, len(req.MarketingProducts)) for _, product := range req.MarketingProducts { if product.ProductWarehouseId != 0 { pwIDs = append(pwIDs, product.ProductWarehouseId) } if err := s.createMarketingProductWithDelivery(c.Context(), marketing.Id, product, marketingProductRepoTx, invDeliveryRepoTx); err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to create marketing product") } } if err := commonSvc.EnsureProjectFlockNotClosedForProductWarehouses(c.Context(), s.MarketingRepo.DB(), pwIDs); err != nil { return err } } approvalAction := entity.ApprovalActionCreated if _, err := approvalSvcTx.CreateApproval( c.Context(), utils.ApprovalWorkflowMarketing, marketing.Id, utils.MarketingStepPengajuan, &approvalAction, actorID, nil); err != nil { if !errors.Is(err, gorm.ErrDuplicatedKey) { 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 create salesOrders") } marketing, err = s.MarketingRepo.GetByID(c.Context(), marketing.Id, s.withRelations) if err != nil { return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch created sales order") } return marketing, nil } func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.Marketing, error) { if err := s.Validate.Struct(req); err != nil { return nil, err } actorID, err := m.ActorIDFromContext(c) if err != nil { return nil, err } if err := commonSvc.EnsureRelations(c.Context(), commonSvc.RelationCheck{Name: "Marketing", ID: &id, Exists: s.MarketingRepo.IdExists}, commonSvc.RelationCheck{Name: "Customer", ID: &req.CustomerId, Exists: s.CustomerRepo.IdExists}, commonSvc.RelationCheck{Name: "SalesPerson", ID: &req.SalesPersonId, Exists: s.UserRepo.IdExists}, ); err != nil { return nil, err } latestApproval, err := s.ApprovalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowMarketing, id, nil) if err != nil { return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check approval status") } if latestApproval != nil && latestApproval.StepNumber >= 3 { return nil, fiber.NewError(fiber.StatusBadRequest, "Cannot update sales order after delivery order approval") } if len(req.MarketingProducts) > 0 { for _, item := range req.MarketingProducts { if err := commonSvc.EnsureRelations(c.Context(), commonSvc.RelationCheck{Name: "ProductWarehouse", ID: &item.ProductWarehouseId, Exists: s.ProductWarehouseRepo.IdExists}, ); err != nil { return nil, err } } } if len(req.MarketingProducts) > 0 { pwIDs := make([]uint, 0, len(req.MarketingProducts)) for _, item := range req.MarketingProducts { if item.ProductWarehouseId != 0 { pwIDs = append(pwIDs, item.ProductWarehouseId) } } if err := commonSvc.EnsureProjectFlockNotClosedForProductWarehouses(c.Context(), s.MarketingRepo.DB(), pwIDs); err != nil { return nil, err } } err = s.MarketingRepo.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { marketingRepoTx := repository.NewMarketingRepository(dbTransaction) marketingProductRepoTx := repository.NewMarketingProductRepository(dbTransaction) approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction)) invDeliveryRepoTx := repository.NewMarketingDeliveryProductRepository(dbTransaction) updateBody := make(map[string]any) if req.CustomerId != 0 { updateBody["customer_id"] = req.CustomerId } if req.SalesPersonId != 0 { updateBody["sales_person_id"] = req.SalesPersonId } if req.Date != "" { soDate, err := utils.ParseDateString(req.Date) if err != nil { return fiber.NewError(fiber.StatusBadRequest, "Invalid date format") } updateBody["so_date"] = soDate } if req.Notes != "" { updateBody["notes"] = req.Notes } if len(updateBody) > 0 { if err := marketingRepoTx.PatchOne(c.Context(), id, updateBody, nil); err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to update sales order") } } if len(req.MarketingProducts) > 0 { oldProducts, err := marketingProductRepoTx.GetByMarketingID(c.Context(), id) if err != nil && err != gorm.ErrRecordNotFound { return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch existing products") } oldByPW := make(map[uint]*entity.MarketingProduct) for i := range oldProducts { p := oldProducts[i] oldByPW[p.ProductWarehouseId] = &p } reqByPW := make(map[uint]validation.CreateMarketingProduct) for _, rp := range req.MarketingProducts { reqByPW[rp.ProductWarehouseId] = rp } for _, rp := range req.MarketingProducts { if old, ok := oldByPW[rp.ProductWarehouseId]; ok { updateBody := map[string]any{ "product_warehouse_id": rp.ProductWarehouseId, "qty": rp.Qty, "unit_price": rp.UnitPrice, "avg_weight": rp.AvgWeight, "total_weight": rp.TotalWeight, "total_price": rp.TotalPrice, } if err := marketingProductRepoTx.PatchOne(c.Context(), old.Id, updateBody, nil); err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to update marketing product") } if _, err := invDeliveryRepoTx.GetByMarketingProductID(c.Context(), old.Id); err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { mdp := &entity.MarketingDeliveryProduct{ MarketingProductId: old.Id, Qty: 0, UnitPrice: 0, TotalWeight: 0, AvgWeight: 0, TotalPrice: 0, DeliveryDate: nil, VehicleNumber: rp.VehicleNumber, } if err := invDeliveryRepoTx.CreateOne(c.Context(), mdp, nil); err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to create marketing delivery product") } } else { return fiber.NewError(fiber.StatusInternalServerError, "Failed to check delivery product") } } } else { if err := s.createMarketingProductWithDelivery(c.Context(), id, rp, marketingProductRepoTx, invDeliveryRepoTx); err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to create marketing product") } } } for _, old := range oldProducts { if _, ok := reqByPW[old.ProductWarehouseId]; !ok { deliveryProduct, err := invDeliveryRepoTx.GetByMarketingProductID(c.Context(), old.Id) if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { return fiber.NewError(fiber.StatusInternalServerError, "Failed to check existing delivery product") } if err == nil { if deliveryProduct.DeliveryDate != nil || deliveryProduct.Qty > 0 { return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Cannot delete marketing product %d because it has delivery records", old.Id)) } if err := invDeliveryRepoTx.DeleteOne(c.Context(), deliveryProduct.Id); err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to delete marketing delivery product") } } if err := marketingProductRepoTx.DeleteOne(c.Context(), old.Id); err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to delete marketing product") } } } } if latestApproval != nil { action := entity.ApprovalActionUpdated _, err := approvalSvcTx.CreateApproval( c.Context(), utils.ApprovalWorkflowMarketing, id, approvalutils.ApprovalStep(latestApproval.StepNumber), &action, actorID, nil) if err != nil { if !errors.Is(err, gorm.ErrDuplicatedKey) { return fiber.NewError(fiber.StatusInternalServerError, "Failed to create update approval") } } } return nil }) if err != nil { if fiberErr, ok := err.(*fiber.Error); ok { return nil, fiberErr } return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to update sales order") } return s.getOne(c, id) } func (s salesOrdersService) DeleteOne(c *fiber.Ctx, id uint) error { marketing, err := s.MarketingRepo.GetByID(c.Context(), id, s.withRelations) if errors.Is(err, gorm.ErrRecordNotFound) { return fiber.NewError(fiber.StatusNotFound, "SalesOrders not found") } if err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch sales order") } if len(marketing.Products) > 0 { pwIDs := make([]uint, 0, len(marketing.Products)) for _, p := range marketing.Products { if p.ProductWarehouseId != 0 { pwIDs = append(pwIDs, p.ProductWarehouseId) } } if err := commonSvc.EnsureProjectFlockNotClosedForProductWarehouses(c.Context(), s.MarketingRepo.DB(), pwIDs); err != nil { return err } } err = s.MarketingRepo.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { marketingProductRepoTx := repository.NewMarketingProductRepository(dbTransaction) marketingDeliveryProductRepoTx := repository.NewMarketingDeliveryProductRepository(dbTransaction) marketingRepoTx := repository.NewMarketingRepository(dbTransaction) if len(marketing.Products) > 0 { for _, product := range marketing.Products { if err := marketingDeliveryProductRepoTx.DeleteMany(c.Context(), func(db *gorm.DB) *gorm.DB { return db.Where("marketing_product_id = ?", product.Id).Unscoped() }); err != nil && err != gorm.ErrRecordNotFound { return fiber.NewError(fiber.StatusInternalServerError, "Failed to delete sales order products") } } } if err := marketingProductRepoTx.DeleteMany(c.Context(), func(db *gorm.DB) *gorm.DB { return db.Where("marketing_id = ?", id).Unscoped() }); err != nil && err != gorm.ErrRecordNotFound { return fiber.NewError(fiber.StatusInternalServerError, "Failed to delete sales order products") } if err := marketingRepoTx.DeleteOne(c.Context(), id); err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to delete sales order") } return nil }) if err != nil { if fiberErr, ok := err.(*fiber.Error); ok { return fiberErr } return fiber.NewError(fiber.StatusInternalServerError, "Failed to delete sales order") } return nil } func (s salesOrdersService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entity.Marketing, error) { if err := s.Validate.Struct(req); err != nil { return nil, err } actorID, err := m.ActorIDFromContext(c) if err != nil { return nil, err } approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(s.MarketingRepo.DB())) var action entity.ApprovalAction switch strings.ToUpper(strings.TrimSpace(req.Action)) { case string(entity.ApprovalActionRejected): action = entity.ApprovalActionRejected case string(entity.ApprovalActionApproved): action = entity.ApprovalActionApproved default: return nil, fiber.NewError(fiber.StatusBadRequest, "action must be APPROVED or REJECTED") } 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 := commonSvc.EnsureRelations(c.Context(), commonSvc.RelationCheck{Name: "Marketing", ID: &id, Exists: s.MarketingRepo.IdExists}, ); err != nil { return nil, err } latestApproval, err := approvalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowMarketing, id, nil) if err != nil { return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check approval status") } if latestApproval == nil { return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("No approval found for Marketing %d - sales orders must be created first", id)) } if action == entity.ApprovalActionApproved { switch latestApproval.StepNumber { case uint16(utils.MarketingStepPengajuan): case uint16(utils.MarketingStepSalesOrder): default: return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Marketing %d cannot be approved - current step is %d", id, latestApproval.StepNumber)) } } marketing, mErr := s.MarketingRepo.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB { return db.Preload("Products") }) if mErr != nil { if errors.Is(mErr, gorm.ErrRecordNotFound) { return nil, fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("SalesOrders %d not found", id)) } return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch sales order for project validation") } if len(marketing.Products) > 0 { pwIDs := make([]uint, 0, len(marketing.Products)) for _, p := range marketing.Products { if p.ProductWarehouseId != 0 { pwIDs = append(pwIDs, p.ProductWarehouseId) } } if err := commonSvc.EnsureProjectFlockNotClosedForProductWarehouses(c.Context(), s.MarketingRepo.DB(), pwIDs); err != nil { return nil, err } } } err = s.MarketingRepo.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction)) for _, approvableID := range approvableIDs { latestApproval, err := approvalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowMarketing, approvableID, nil) if err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to check current approval step") } if latestApproval == nil { return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("No approval found for Marketing %d", approvableID)) } var nextStep approvalutils.ApprovalStep currentStep := latestApproval.StepNumber if action == entity.ApprovalActionApproved { if currentStep == uint16(utils.MarketingStepPengajuan) { nextStep = utils.MarketingStepSalesOrder } else if currentStep == uint16(utils.MarketingStepSalesOrder) { nextStep = utils.MarketingDeliveryOrder } else { return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Marketing %d already completed all approval steps", approvableID)) } } else { nextStep = approvalutils.ApprovalStep(currentStep) } if _, err := approvalSvc.CreateApproval( c.Context(), utils.ApprovalWorkflowMarketing, approvableID, nextStep, &action, actorID, req.Notes, ); err != nil { s.Log.Errorf("Failed to create approval for %d: %+v", approvableID, err) 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 record approval") } updated := make([]entity.Marketing, 0, len(approvableIDs)) for _, id := range approvableIDs { marketing, err := s.getOne(c, id) if err != nil { return nil, err } updated = append(updated, *marketing) } return updated, nil } func (s *salesOrdersService) createMarketingProductWithDelivery(ctx context.Context, marketingId uint, rp validation.CreateMarketingProduct, marketingProductRepo repository.MarketingProductRepository, invDeliveryRepo repository.MarketingDeliveryProductRepository) error { marketingProduct := &entity.MarketingProduct{ MarketingId: marketingId, ProductWarehouseId: rp.ProductWarehouseId, Qty: rp.Qty, UnitPrice: rp.UnitPrice, AvgWeight: rp.AvgWeight, TotalWeight: rp.TotalWeight, TotalPrice: rp.TotalPrice, } if err := marketingProductRepo.CreateOne(ctx, marketingProduct, nil); err != nil { return err } marketingDeliveryProduct := &entity.MarketingDeliveryProduct{ MarketingProductId: marketingProduct.Id, Qty: 0, UnitPrice: 0, TotalWeight: 0, AvgWeight: 0, TotalPrice: 0, DeliveryDate: nil, VehicleNumber: rp.VehicleNumber, } if err := invDeliveryRepo.CreateOne(ctx, marketingDeliveryProduct, nil); err != nil { return err } return nil }