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" expenseRealizationRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories" marketingDeliveryProductRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories" marketingRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories" chickinRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/repositories" projectflockRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories" recordingRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories" purchaseRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/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.ClosingListItemDTO, 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) GetOverhead(ctx *fiber.Ctx, projectFlockID uint) (*dto.OverheadListDTO, error) GetClosingSapronak(ctx *fiber.Ctx, projectFlockID uint, params *validation.ClosingSapronakQuery) ([]dto.ClosingSapronakItemDTO, int64, error) GetClosingKeuangan(ctx *fiber.Ctx, projectFlockID uint) (*dto.ReportResponse, 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 ExpenseRealizationRepo expenseRealizationRepository.ExpenseRealizationRepository ProjectBudgetRepo projectflockRepository.ProjectBudgetRepository ChickinRepo chickinRepository.ProjectChickinRepository PurchaseRepo purchaseRepository.PurchaseRepository RecordingRepo recordingRepository.RecordingRepository } func NewClosingService(repo repository.ClosingRepository, projectFlockRepo projectflockRepository.ProjectflockRepository, marketingRepo marketingRepository.MarketingRepository, marketingDeliveryProductRepo marketingDeliveryProductRepository.MarketingDeliveryProductRepository, approvalSvc commonSvc.ApprovalService, expenseRealizationRepo expenseRealizationRepository.ExpenseRealizationRepository, projectBudgetRepo projectflockRepository.ProjectBudgetRepository, chickinRepo chickinRepository.ProjectChickinRepository, purchaseRepo purchaseRepository.PurchaseRepository, recordingRepo recordingRepository.RecordingRepository, validate *validator.Validate) ClosingService { return &closingService{ Log: utils.Log, Validate: validate, Repository: repo, ProjectFlockRepo: projectFlockRepo, MarketingRepo: marketingRepo, MarketingDeliveryProductRepo: marketingDeliveryProductRepo, ApprovalSvc: approvalSvc, ExpenseRealizationRepo: expenseRealizationRepo, ProjectBudgetRepo: projectBudgetRepo, ChickinRepo: chickinRepo, PurchaseRepo: purchaseRepo, RecordingRepo: recordingRepo, } } 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("Location"). Preload("KandangHistory"). Preload("KandangHistory.Chickins") } func (s closingService) GetAll(c *fiber.Ctx, params *validation.Query) ([]dto.ClosingListItemDTO, 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.ClosingListItemDTO, 0, len(closings)) for _, closing := range closings { statusProject, _, 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.ToClosingListItemDTO(closing, statusProject)) } 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.ClosingSapronakQuery) ([]dto.ClosingSapronakItemDTO, int64, error) { if projectFlockID == 0 { return nil, 0, fiber.NewError(fiber.StatusBadRequest, "Invalid project flock id") } if params == nil { params = &validation.ClosingSapronakQuery{} } 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, }) } return items, 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.ProjectFlockStepAktif) { 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 } func (s closingService) GetOverhead(c *fiber.Ctx, projectFlockID uint) (*dto.OverheadListDTO, error) { budgets, err := s.ProjectBudgetRepo.GetByProjectFlockID(c.Context(), projectFlockID) if err != nil { return nil, err } realizations, err := s.ExpenseRealizationRepo.GetByProjectFlockID(c.Context(), projectFlockID) if err != nil { return nil, err } chickins, err := s.ChickinRepo.GetByProjectFlockID(c.Context(), projectFlockID) if err != nil { return nil, err } var totalChickinQty float64 for _, chickin := range chickins { totalChickinQty += chickin.UsageQty } totalDepletion, err := s.RecordingRepo.GetTotalDepletionByProjectFlockID(c.Context(), projectFlockID) if err != nil { s.Log.Warnf("GetTotalDepletionByProjectFlockID error: %v", err) } totalActualPopulation := totalChickinQty - totalDepletion result := dto.ToOverheadListDTOs(budgets, realizations, totalChickinQty, totalActualPopulation) return &result, nil } func (s closingService) GetClosingKeuangan(c *fiber.Ctx, projectFlockID uint) (*dto.ReportResponse, error) { if projectFlockID == 0 { return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid project flock id") } if err := commonSvc.EnsureRelations(c.Context(), commonSvc.RelationCheck{Name: "Project Flock", ID: &projectFlockID, Exists: func(ctx context.Context, id uint) (bool, error) { _, err := s.ProjectFlockRepo.GetByID(ctx, id, nil) if err != nil && errors.Is(err, gorm.ErrRecordNotFound) { return false, nil } return err == nil, err }}, ); err != nil { return nil, err } projectFlock, err := s.ProjectFlockRepo.GetByID(c.Context(), projectFlockID, nil) if err != nil { return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock") } budgets, err := s.ProjectBudgetRepo.GetByProjectFlockID(c.Context(), projectFlockID) if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch budgets") } purchaseItems, err := s.PurchaseRepo.GetItemsByProjectFlockID(c.Context(), projectFlockID) if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch purchase items") } realizations, err := s.ExpenseRealizationRepo.GetByProjectFlockID(c.Context(), projectFlockID) if err != nil { return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch realizations") } deliveryProducts, err := s.MarketingDeliveryProductRepo.GetDeliveryProductsByProjectFlockID(c.Context(), projectFlockID, func(db *gorm.DB) *gorm.DB { return db.Preload("MarketingProduct"). Preload("MarketingProduct.ProductWarehouse"). Preload("MarketingProduct.ProductWarehouse.Product") }) if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch delivery products") } chickins, err := s.ChickinRepo.GetByProjectFlockID(c.Context(), projectFlockID) if err != nil { return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch chickins") } totalWeightProduced, _, err := s.RecordingRepo.GetProductionWeightAndQtyByProjectFlockID(c.Context(), projectFlockID) if err != nil { s.Log.Warnf("GetProductionWeightAndQtyByProjectFlockID error: %v", err) } // Fetch depletion data to calculate actual population for cost allocation totalDepletion, err := s.RecordingRepo.GetTotalDepletionByProjectFlockID(c.Context(), projectFlockID) if err != nil { s.Log.Warnf("GetTotalDepletionByProjectFlockID error: %v", err) } report := dto.ToClosingKeuanganReport(projectFlock.Category, purchaseItems, budgets, realizations, deliveryProducts, chickins, totalWeightProduced, totalDepletion) return &report, nil }