package service import ( "context" "errors" "strconv" commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/dto" repository "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/repositories" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/validations" marketingDeliveryProductRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/repositories" marketingRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/repositories" projectflockRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/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 ClosingService interface { GetAll(ctx *fiber.Ctx, params *validation.Query) ([]dto.ClosingSummaryDTO, int64, error) GetProjectFlockByID(ctx *fiber.Ctx, id uint) (*entity.ProjectFlock, error) GetPenjualan(ctx *fiber.Ctx, projectFlockID uint) ([]entity.MarketingDeliveryProduct, error) GetClosingSummary(ctx *fiber.Ctx, projectFlockID uint) (*dto.ClosingSummaryDTO, error) GetClosingSapronak(ctx *fiber.Ctx, projectFlockID uint, params *validation.SapronakQuery) (*dto.ClosingSapronakDTO, int64, error) } type closingService struct { Log *logrus.Logger Validate *validator.Validate Repository repository.ClosingRepository ProjectFlockRepo projectflockRepository.ProjectflockRepository MarketingRepo marketingRepository.MarketingRepository MarketingDeliveryProductRepo marketingDeliveryProductRepository.MarketingDeliveryProductRepository ApprovalSvc commonSvc.ApprovalService } func NewClosingService(repo repository.ClosingRepository, projectFlockRepo projectflockRepository.ProjectflockRepository, marketingRepo marketingRepository.MarketingRepository, marketingDeliveryProductRepo marketingDeliveryProductRepository.MarketingDeliveryProductRepository, approvalSvc commonSvc.ApprovalService, validate *validator.Validate) ClosingService { return &closingService{ Log: utils.Log, Validate: validate, Repository: repo, ProjectFlockRepo: projectFlockRepo, MarketingRepo: marketingRepo, MarketingDeliveryProductRepo: marketingDeliveryProductRepo, ApprovalSvc: approvalSvc, } } func (s closingService) withRelations(db *gorm.DB) *gorm.DB { return db.Preload("CreatedUser") } func (s closingService) withClosingRelations(db *gorm.DB) *gorm.DB { return s.withRelations(db). Preload("KandangHistory"). Preload("KandangHistory.Chickins") } func (s closingService) GetAll(c *fiber.Ctx, params *validation.Query) ([]dto.ClosingSummaryDTO, int64, error) { if err := s.Validate.Struct(params); err != nil { return nil, 0, err } offset := (params.Page - 1) * params.Limit closings, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB { db = s.withClosingRelations(db) if params.Search != "" { return db.Where("flock_name LIKE ?", "%"+params.Search+"%") } return db.Order("created_at DESC").Order("updated_at DESC") }) if err != nil { s.Log.Errorf("Failed to get closings: %+v", err) return nil, 0, err } result := make([]dto.ClosingSummaryDTO, 0, len(closings)) for _, closing := range closings { statusProject, statusClosing, err := s.getApprovalStatuses(c.Context(), closing.Id) if err != nil { s.Log.Errorf("Failed to retrieve approval statuses for project flock %d: %+v", closing.Id, err) return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch approval status") } result = append(result, dto.ToClosingSummaryDTO(closing, statusProject, statusClosing)) } return result, total, nil } func (s closingService) GetProjectFlockByID(c *fiber.Ctx, id uint) (*entity.ProjectFlock, error) { projectFlock, err := s.ProjectFlockRepo.GetByID(c.Context(), id, s.withRelations) if errors.Is(err, gorm.ErrRecordNotFound) { return nil, fiber.NewError(fiber.StatusNotFound, "Project Flock not found") } if err != nil { return nil, err } return projectFlock, nil } func (s closingService) GetPenjualan(c *fiber.Ctx, projectFlockID uint) ([]entity.MarketingDeliveryProduct, error) { realisasi, err := s.MarketingDeliveryProductRepo.GetDeliveryProductsByProjectFlockID(c.Context(), projectFlockID, func(db *gorm.DB) *gorm.DB { return db. Preload("MarketingProduct"). Preload("MarketingProduct.ProductWarehouse"). Preload("MarketingProduct.ProductWarehouse.Product"). Preload("MarketingProduct.ProductWarehouse.Product.ProductCategory"). Preload("MarketingProduct.ProductWarehouse.Product.Uom"). Preload("MarketingProduct.ProductWarehouse.Product.Flags"). Preload("MarketingProduct.ProductWarehouse.Warehouse"). Preload("MarketingProduct.ProductWarehouse.ProjectFlockKandang"). Preload("MarketingProduct.ProductWarehouse.ProjectFlockKandang.Kandang"). Preload("MarketingProduct.Marketing"). Preload("MarketingProduct.Marketing.Customer"). Order("marketing_delivery_products.delivery_date DESC") }) if err != nil { return nil, err } if len(realisasi) == 0 { return nil, fiber.NewError(fiber.StatusNotFound, "Penjualan realisasi not found") } return realisasi, nil } func (s closingService) GetClosingSummary(c *fiber.Ctx, projectFlockID uint) (*dto.ClosingSummaryDTO, error) { if projectFlockID == 0 { return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid project flock id") } project, err := s.Repository.GetByID(c.Context(), projectFlockID, s.withClosingRelations) if errors.Is(err, gorm.ErrRecordNotFound) { return nil, fiber.NewError(fiber.StatusNotFound, "Project flock not found") } if err != nil { s.Log.Errorf("Failed get project flock %d for closing summary: %+v", projectFlockID, err) return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock") } statusProject, statusClosing, err := s.getApprovalStatuses(c.Context(), projectFlockID) if err != nil { s.Log.Errorf("Failed to retrieve approval statuses for project flock %d: %+v", projectFlockID, err) return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch approval status") } summary := dto.ToClosingSummaryDTO(*project, statusProject, statusClosing) return &summary, nil } func (s closingService) GetClosingSapronak(c *fiber.Ctx, projectFlockID uint, params *validation.SapronakQuery) (*dto.ClosingSapronakDTO, int64, error) { if projectFlockID == 0 { return nil, 0, fiber.NewError(fiber.StatusBadRequest, "Invalid project flock id") } if params == nil { params = &validation.SapronakQuery{} } if params.Page == 0 { params.Page = 1 } if params.Limit == 0 { params.Limit = 10 } if err := s.Validate.Struct(params); err != nil { return nil, 0, fiber.NewError(fiber.StatusBadRequest, err.Error()) } if params.Type != validation.SapronakTypeIncoming && params.Type != validation.SapronakTypeOutgoing { return nil, 0, fiber.NewError(fiber.StatusBadRequest, "type must be either incoming or outgoing") } if _, err := s.Repository.GetByID(c.Context(), projectFlockID, nil); err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, 0, fiber.NewError(fiber.StatusNotFound, "Project flock tidak ditemukan") } s.Log.Errorf("Failed get project flock %d for sapronak closing: %+v", projectFlockID, err) return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock") } warehouseIDs, err := s.getWarehouseIDsByProjectFlock(c.Context(), projectFlockID) if err != nil { s.Log.Errorf("Failed to fetch warehouses for project flock %d: %+v", projectFlockID, err) return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch warehouses for project flock") } var projectFlockKandangIDs []uint if params.Type == validation.SapronakTypeOutgoing { projectFlockKandangIDs, err = s.getProjectFlockKandangIDs(c.Context(), projectFlockID) if err != nil { s.Log.Errorf("Failed to fetch project flock kandang IDs for project flock %d: %+v", projectFlockID, err) return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock kandang") } } offset := (params.Page - 1) * params.Limit rows, totalResults, err := s.Repository.GetSapronak(c.Context(), repository.SapronakQueryParams{ Type: params.Type, WarehouseIDs: warehouseIDs, ProjectFlockKandangIDs: projectFlockKandangIDs, Limit: params.Limit, Offset: offset, }) if err != nil { s.Log.Errorf("Failed to fetch sapronak %s for project flock %d: %+v", params.Type, projectFlockID, err) return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch sapronak data") } items := make([]dto.ClosingSapronakItemDTO, 0, len(rows)) for _, row := range rows { dateStr := row.DateText if dateStr == "" && !row.SortDate.IsZero() { dateStr = row.SortDate.Format("02-Jan-2006") } items = append(items, dto.ClosingSapronakItemDTO{ Id: row.Id, Date: dateStr, ReferenceNumber: row.ReferenceNumber, TransactionType: row.TransactionType, ProductName: row.ProductName, ProductCategory: row.ProductCategory, ProductSubCategory: row.ProductSubCategory, SourceWarehouse: row.SourceWarehouse, DestinationWarehouse: row.DestinationWarehouse, Destination: row.Destination, Quantity: row.Quantity, Unit: row.Unit, FormattedQuantity: formatQuantity(row.Quantity, row.Unit), Notes: row.Notes, SortDate: row.SortDate, }) } result := dto.ClosingSapronakDTO{ IncomingSapronak: []dto.ClosingSapronakItemDTO{}, OutgoingSapronak: []dto.ClosingSapronakItemDTO{}, } if params.Type == validation.SapronakTypeIncoming { result.IncomingSapronak = items } else { result.OutgoingSapronak = items } return &result, totalResults, nil } func (s closingService) getWarehouseIDsByProjectFlock(ctx context.Context, projectFlockID uint) ([]uint, error) { var kandangIDs []uint db := s.Repository.DB().WithContext(ctx) if err := db.Model(&entity.ProjectFlockKandang{}). Where("project_flock_id = ?", projectFlockID). Pluck("kandang_id", &kandangIDs).Error; err != nil { return nil, err } if len(kandangIDs) == 0 { return []uint{}, nil } var warehouses []entity.Warehouse if err := db.Where("kandang_id IN ?", kandangIDs).Find(&warehouses).Error; err != nil { return nil, err } unique := make(map[uint]struct{}) for _, warehouse := range warehouses { unique[warehouse.Id] = struct{}{} } ids := make([]uint, 0, len(unique)) for id := range unique { ids = append(ids, id) } return ids, nil } func (s closingService) getProjectFlockKandangIDs(ctx context.Context, projectFlockID uint) ([]uint, error) { var ids []uint err := s.Repository.DB().WithContext(ctx). Model(&entity.ProjectFlockKandang{}). Where("project_flock_id = ?", projectFlockID). Pluck("id", &ids).Error if err != nil { return nil, err } return ids, nil } func formatQuantity(qty float64, uom string) string { qtyStr := strconv.FormatFloat(qty, 'f', -1, 64) if uom == "" { return qtyStr } return qtyStr + " " + uom } func (s closingService) getApprovalStatuses(ctx context.Context, projectFlockID uint) (string, string, error) { if s.ApprovalSvc == nil { return "", "Belum Selesai", nil } records, _, err := s.ApprovalSvc.List(ctx, utils.ApprovalWorkflowProjectFlock.String(), &projectFlockID, 1, 1000, "") if err != nil { return "", "", err } var ( minStep uint16 statusProject string completed int ) for _, rec := range records { if minStep == 0 || rec.StepNumber < minStep { minStep = rec.StepNumber statusProject = rec.StepName } if rec.StepNumber == uint16(utils.ProjectFlockStepSelesai) { completed++ } } if statusProject == "" && minStep > 0 { if label, ok := approvalutils.ApprovalStepName(utils.ApprovalWorkflowProjectFlock, approvalutils.ApprovalStep(minStep)); ok { statusProject = label } } statusClosing := "Belum Selesai" switch { case len(records) == 0 || completed == 0: statusClosing = "Belum Selesai" case completed < len(records): statusClosing = "Sebagian" default: statusClosing = "Selesai" } return statusProject, statusClosing, nil }