Merge branch 'feat/BE/Sprint-6' of https://gitlab.com/mbugroup/lti-api into feat/BE/US-284/Report-counting-sapronak

This commit is contained in:
ragilap
2025-12-11 09:05:20 +07:00
106 changed files with 4110 additions and 1824 deletions
@@ -3,14 +3,17 @@ 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"
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"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals"
@@ -22,10 +25,12 @@ import (
)
type ClosingService interface {
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.ProjectFlock, int64, error)
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)
}
type closingService struct {
@@ -36,9 +41,12 @@ type closingService struct {
MarketingRepo marketingRepository.MarketingRepository
MarketingDeliveryProductRepo marketingDeliveryProductRepository.MarketingDeliveryProductRepository
ApprovalSvc commonSvc.ApprovalService
ExpenseRealizationRepo expenseRealizationRepository.ExpenseRealizationRepository
ProjectBudgetRepo projectflockRepository.ProjectBudgetRepository
ChickinRepo chickinRepository.ProjectChickinRepository
}
func NewClosingService(repo repository.ClosingRepository, projectFlockRepo projectflockRepository.ProjectflockRepository, marketingRepo marketingRepository.MarketingRepository, marketingDeliveryProductRepo marketingDeliveryProductRepository.MarketingDeliveryProductRepository, approvalSvc commonSvc.ApprovalService, validate *validator.Validate) ClosingService {
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, validate *validator.Validate) ClosingService {
return &closingService{
Log: utils.Log,
Validate: validate,
@@ -47,6 +55,9 @@ func NewClosingService(repo repository.ClosingRepository, projectFlockRepo proje
MarketingRepo: marketingRepo,
MarketingDeliveryProductRepo: marketingDeliveryProductRepo,
ApprovalSvc: approvalSvc,
ExpenseRealizationRepo: expenseRealizationRepo,
ProjectBudgetRepo: projectBudgetRepo,
ChickinRepo: chickinRepo,
}
}
@@ -56,11 +67,12 @@ func (s closingService) withRelations(db *gorm.DB) *gorm.DB {
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) ([]entity.ProjectFlock, int64, error) {
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
}
@@ -68,9 +80,9 @@ func (s closingService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity
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.withRelations(db)
db = s.withClosingRelations(db)
if params.Search != "" {
return db.Where("name LIKE ?", "%"+params.Search+"%")
return db.Where("flock_name LIKE ?", "%"+params.Search+"%")
}
return db.Order("created_at DESC").Order("updated_at DESC")
})
@@ -79,7 +91,19 @@ func (s closingService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity
s.Log.Errorf("Failed to get closings: %+v", err)
return nil, 0, err
}
return closings, total, nil
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) {
@@ -144,6 +168,147 @@ func (s closingService) GetClosingSummary(c *fiber.Ctx, projectFlockID uint) (*d
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
@@ -188,3 +353,29 @@ func (s closingService) getApprovalStatuses(ctx context.Context, projectFlockID
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
}
result := dto.ToOverheadListDTOs(budgets, realizations, totalChickinQty)
return &result, nil
}
@@ -19,7 +19,7 @@ import (
type SapronakService interface {
GetSapronakByProject(ctx *fiber.Ctx, projectFlockID uint, flag string) ([]dto.SapronakReportDTO, error)
GetSapronakByKandang(ctx *fiber.Ctx, projectFlockID uint, pfkID uint, flag string) (*dto.SapronakReportDTO, error)
GetSapronakReport(ctx *fiber.Ctx, params *validation.SapronakQuery) ([]dto.SapronakReportDTO, error)
GetSapronakReport(ctx *fiber.Ctx, params *validation.CountSapronakQuery) ([]dto.SapronakReportDTO, error)
}
type sapronakService struct {
@@ -36,7 +36,7 @@ func NewSapronakService(repo repository.ClosingRepository, validate *validator.V
}
}
func (s sapronakService) GetSapronakReport(c *fiber.Ctx, params *validation.SapronakQuery) ([]dto.SapronakReportDTO, error) {
func (s sapronakService) GetSapronakReport(c *fiber.Ctx, params *validation.CountSapronakQuery) ([]dto.SapronakReportDTO, error) {
if err := s.Validate.Struct(params); err != nil {
return nil, err
}
@@ -47,7 +47,7 @@ func (s sapronakService) GetSapronakByProject(c *fiber.Ctx, projectFlockID uint,
if projectFlockID == 0 {
return nil, fiber.NewError(fiber.StatusBadRequest, "project_flock_id is required")
}
reports, err := s.computeSapronakReports(c.Context(), &validation.SapronakQuery{
reports, err := s.computeSapronakReports(c.Context(), &validation.CountSapronakQuery{
ProjectFlockID: projectFlockID,
Status: "all",
Flag: flag,
@@ -68,7 +68,7 @@ func (s sapronakService) GetSapronakByKandang(c *fiber.Ctx, projectFlockID uint,
return nil, fiber.NewError(fiber.StatusBadRequest, "project_flock_id and project_flock_kandang_id are required")
}
results, err := s.computeSapronakReports(c.Context(), &validation.SapronakQuery{
results, err := s.computeSapronakReports(c.Context(), &validation.CountSapronakQuery{
ProjectFlockID: projectFlockID,
ProjectFlockKandangID: pfkID,
Status: "all",
@@ -87,7 +87,7 @@ func (s sapronakService) GetSapronakByKandang(c *fiber.Ctx, projectFlockID uint,
return nil, fiber.NewError(fiber.StatusNotFound, "Sapronak for kandang not found")
}
func (s sapronakService) computeSapronakReports(ctx context.Context, params *validation.SapronakQuery) ([]dto.SapronakReportDTO, error) {
func (s sapronakService) computeSapronakReports(ctx context.Context, params *validation.CountSapronakQuery) ([]dto.SapronakReportDTO, error) {
pfks, err := s.loadProjectFlockKandangs(ctx, params)
if err != nil {
return nil, err
@@ -158,7 +158,7 @@ func (s sapronakService) computeSapronakReports(ctx context.Context, params *val
return results, nil
}
func (s sapronakService) loadProjectFlockKandangs(ctx context.Context, params *validation.SapronakQuery) ([]entity.ProjectFlockKandang, error) {
func (s sapronakService) loadProjectFlockKandangs(ctx context.Context, params *validation.CountSapronakQuery) ([]entity.ProjectFlockKandang, error) {
pfks, err := s.Repository.ListProjectFlockKandangsForSapronak(ctx, params)
if err != nil {
s.Log.Errorf("Failed to load project flock kandangs for sapronak report: %+v", err)
@@ -37,22 +37,22 @@ func (f *sapronakFormatter) mapFromReport(report *dto.SapronakReportDTO, flag st
result := dto.SapronakProjectAggregatedDTO{}
if report == nil {
return result
report = &dto.SapronakReportDTO{}
}
filter := strings.ToUpper(strings.TrimSpace(flag))
byFlag := map[string]**dto.SapronakCategoryDTO{}
if filter == "" || filter == "DOC" {
result.Doc = &dto.SapronakCategoryDTO{}
result.Doc = &dto.SapronakCategoryDTO{Rows: make([]dto.SapronakCategoryRowDTO, 0)}
byFlag["DOC"] = &result.Doc
}
if filter == "" || filter == "OVK" {
result.Ovk = &dto.SapronakCategoryDTO{}
result.Ovk = &dto.SapronakCategoryDTO{Rows: make([]dto.SapronakCategoryRowDTO, 0),}
byFlag["OVK"] = &result.Ovk
}
if filter == "" || filter == "PAKAN" {
result.Pakan = &dto.SapronakCategoryDTO{}
result.Pakan = &dto.SapronakCategoryDTO{Rows: make([]dto.SapronakCategoryRowDTO, 0),}
byFlag["PAKAN"] = &result.Pakan
}