feat[BE-375]: get api closing data produksi

This commit is contained in:
MacBook Air M1
2025-12-18 13:32:48 +07:00
parent 21d22c20a3
commit d675b1e826
5 changed files with 451 additions and 0 deletions
@@ -188,3 +188,25 @@ func (u *ClosingController) GetClosingSapronak(c *fiber.Ctx) error {
Data: result,
})
}
func (u *ClosingController) GetClosingDataProduksi(c *fiber.Ctx) error {
param := c.Params("projectFlockId")
id, err := strconv.Atoi(param)
if err != nil || id <= 0 {
return fiber.NewError(fiber.StatusBadRequest, "Invalid projectFlockId")
}
result, err := u.ClosingService.GetClosingDataProduksi(c, uint(id))
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Retrieved production data successfully",
Data: result,
})
}
@@ -58,6 +58,52 @@ type ClosingSummaryDTO struct {
StatusClosing string `json:"closing_status"`
}
type ClosingPurchaseDTO struct {
InitialPopulation int `json:"initial_population"`
ClaimCulling int `json:"claim_culling"`
FinalPopulation int `json:"final_population"`
FeedIn float64 `json:"feed_in"`
FeedUsed float64 `json:"feed_used"`
FeedUsedPerHead float64 `json:"feed_used_per_head"`
}
type ClosingSalesDTO struct {
SalesPopulation int `json:"sales_population"`
SalesWeight float64 `json:"sales_weight"`
AverageWeight float64 `json:"average_weight"`
AverageSellingPrice float64 `json:"average_selling_price"`
}
type ClosingEggSalesDTO struct {
EggPieces int `json:"egg_pieces"`
EggMassKg float64 `json:"egg_mass_kg"`
AverageEggWeightKg float64 `json:"average_egg_weight_kg"`
AverageSellingPrice float64 `json:"average_selling_price"`
}
type ClosingPerformanceDTO struct {
Depletion float64 `json:"depletion"`
Age float64 `json:"age"`
MortalityStd float64 `json:"mortality_std"`
MortalityAct float64 `json:"mortality_act"`
DeffMortality float64 `json:"deff_mortality"`
FcrStd float64 `json:"fcr_std"`
FcrAct float64 `json:"fcr_act"`
DeffFcr float64 `json:"deff_fcr"`
Adg float64 `json:"adg"`
}
type ClosingSalesGroupDTO struct {
ChickenProduction ClosingSalesDTO `json:"chicken_production"`
EggProduction ClosingEggSalesDTO `json:"egg_production"`
}
type ClosingProductionReportDTO struct {
Purchase ClosingPurchaseDTO `json:"purchase"`
Sales ClosingSalesGroupDTO `json:"sales"`
Performance ClosingPerformanceDTO `json:"performance"`
}
func ToClosingSummaryDTO(project entity.ProjectFlock, statusProject, statusClosing string) ClosingSummaryDTO {
history := project.KandangHistory
@@ -9,12 +9,19 @@ import (
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/validations"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
"gorm.io/gorm"
)
type ClosingRepository interface {
repository.BaseRepository[entity.ProjectFlock]
GetSapronak(ctx context.Context, params SapronakQueryParams) ([]SapronakRow, int64, error)
SumFeedPurchaseAndUsedByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, float64, error)
SumClaimCullingByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, error)
SumMarketingWeightAndQtyByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, float64, float64, error)
SumMarketingWeightAndQtyByProjectFlockKandangIDsAndFlagNames(ctx context.Context, projectFlockKandangIDs []uint, flagNames []string) (float64, float64, float64, error)
SumRecordingEggQtyByProjectFlockKandangIDsAndFlagNames(ctx context.Context, projectFlockKandangIDs []uint, flagNames []string) (float64, error)
GetFcrStandardsByFcrID(ctx context.Context, fcrID uint) ([]entity.FcrStandard, error)
}
type ClosingRepositoryImpl struct {
@@ -102,6 +109,163 @@ func (r *ClosingRepositoryImpl) GetSapronak(ctx context.Context, params Sapronak
return rows, totalResults, nil
}
func (r *ClosingRepositoryImpl) SumFeedPurchaseAndUsedByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, float64, error) {
if len(projectFlockKandangIDs) == 0 {
return 0, 0, nil
}
var purchaseAgg struct {
TotalIn float64 `gorm:"column:total_in"`
}
err := r.DB().WithContext(ctx).
Table("purchase_items pi").
Joins("JOIN flags f ON f.flagable_id = pi.product_id AND f.flagable_type = 'products'").
Where("f.name = ?", "PAKAN").
Where("pi.project_flock_kandang_id IN ?", projectFlockKandangIDs).
Select("COALESCE(SUM(pi.total_qty), 0) AS total_in").
Scan(&purchaseAgg).Error
if err != nil {
return 0, 0, err
}
var usageAgg struct {
TotalUsed float64 `gorm:"column:total_used"`
}
err = r.DB().WithContext(ctx).
Table("recording_stocks rs").
Joins("JOIN product_warehouses pw ON pw.id = rs.product_warehouse_id").
Joins("JOIN products prod ON prod.id = pw.product_id").
Joins("JOIN flags f ON f.flagable_id = prod.id AND f.flagable_type = ?", "products").
Where("pw.project_flock_kandang_id IN ?", projectFlockKandangIDs).
Where("f.name = ?", "PAKAN").
Select("COALESCE(SUM(COALESCE(rs.usage_qty, 0) + COALESCE(rs.pending_qty, 0)), 0) AS total_used").
Scan(&usageAgg).Error
if err != nil {
return 0, 0, err
}
return purchaseAgg.TotalIn, usageAgg.TotalUsed, nil
}
func (r *ClosingRepositoryImpl) SumClaimCullingByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, error) {
if len(projectFlockKandangIDs) == 0 {
return 0, nil
}
var agg struct {
Total float64 `gorm:"column:total_culling"`
}
err := r.DB().WithContext(ctx).
Table("recording_depletions rd").
Joins("JOIN product_warehouses pw ON pw.id = rd.product_warehouse_id").
Joins("JOIN products prod ON prod.id = pw.product_id").
Joins("JOIN flags f ON f.flagable_id = prod.id AND f.flagable_type = ?", "products").
Where("pw.project_flock_kandang_id IN ?", projectFlockKandangIDs).
Where("f.name = ?", utils.FlagAyamCulling).
Select("COALESCE(SUM(rd.qty), 0) AS total_culling").
Scan(&agg).Error
if err != nil {
return 0, err
}
return agg.Total, nil
}
func (r *ClosingRepositoryImpl) SumMarketingWeightAndQtyByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, float64, float64, error) {
if len(projectFlockKandangIDs) == 0 {
return 0, 0, 0, nil
}
var agg struct {
TotalWeight float64 `gorm:"column:total_weight"`
TotalQty float64 `gorm:"column:total_qty"`
TotalPrice float64 `gorm:"column:total_price"`
}
err := r.DB().WithContext(ctx).
Table("marketing_products mp").
Joins("JOIN product_warehouses pw ON pw.id = mp.product_warehouse_id").
Where("pw.project_flock_kandang_id IN ?", projectFlockKandangIDs).
Select("COALESCE(SUM(mp.total_weight), 0) AS total_weight, COALESCE(SUM(mp.qty), 0) AS total_qty, COALESCE(SUM(mp.total_price), 0) AS total_price").
Scan(&agg).Error
if err != nil {
return 0, 0, 0, err
}
return agg.TotalWeight, agg.TotalQty, agg.TotalPrice, nil
}
func (r *ClosingRepositoryImpl) SumMarketingWeightAndQtyByProjectFlockKandangIDsAndFlagNames(ctx context.Context, projectFlockKandangIDs []uint, flagNames []string) (float64, float64, float64, error) {
if len(projectFlockKandangIDs) == 0 || len(flagNames) == 0 {
return 0, 0, 0, nil
}
var agg struct {
TotalWeight float64 `gorm:"column:total_weight"`
TotalQty float64 `gorm:"column:total_qty"`
TotalPrice float64 `gorm:"column:total_price"`
}
err := r.DB().WithContext(ctx).
Table("marketing_products mp").
Joins("JOIN product_warehouses pw ON pw.id = mp.product_warehouse_id").
Joins("JOIN products prod ON prod.id = pw.product_id").
Joins("JOIN flags f ON f.flagable_id = prod.id AND f.flagable_type = ?", "products").
Where("pw.project_flock_kandang_id IN ?", projectFlockKandangIDs).
Where("f.name IN ?", flagNames).
Select("COALESCE(SUM(mp.total_weight), 0) AS total_weight, COALESCE(SUM(mp.qty), 0) AS total_qty, COALESCE(SUM(mp.total_price), 0) AS total_price").
Scan(&agg).Error
if err != nil {
return 0, 0, 0, err
}
return agg.TotalWeight, agg.TotalQty, agg.TotalPrice, nil
}
func (r *ClosingRepositoryImpl) SumRecordingEggQtyByProjectFlockKandangIDsAndFlagNames(ctx context.Context, projectFlockKandangIDs []uint, flagNames []string) (float64, error) {
if len(projectFlockKandangIDs) == 0 || len(flagNames) == 0 {
return 0, nil
}
var agg struct {
TotalQty float64 `gorm:"column:total_qty"`
}
err := r.DB().WithContext(ctx).
Table("recording_eggs re").
Joins("JOIN product_warehouses pw ON pw.id = re.product_warehouse_id").
Joins("JOIN products prod ON prod.id = pw.product_id").
Joins("JOIN flags f ON f.flagable_id = prod.id AND f.flagable_type = ?", "products").
Where("pw.project_flock_kandang_id IN ?", projectFlockKandangIDs).
Where("f.name IN ?", flagNames).
Select("COALESCE(SUM(re.qty), 0) AS total_qty").
Scan(&agg).Error
if err != nil {
return 0, err
}
return agg.TotalQty, nil
}
func (r *ClosingRepositoryImpl) GetFcrStandardsByFcrID(ctx context.Context, fcrID uint) ([]entity.FcrStandard, error) {
if fcrID == 0 {
return []entity.FcrStandard{}, nil
}
var standards []entity.FcrStandard
if err := r.DB().WithContext(ctx).
Where("fcr_id = ?", fcrID).
Order("weight ASC").
Find(&standards).Error; err != nil {
return nil, err
}
return standards, nil
}
const (
sapronakIncomingPurchasesSQL = `
SELECT
+1
View File
@@ -25,4 +25,5 @@ func ClosingRoutes(v1 fiber.Router, u user.UserService, s closing.ClosingService
route.Get("/:project_flock_id/overhead", ctrl.GetOverhead)
route.Get("/:projectFlockId", ctrl.GetClosingSummary)
route.Get("/:projectFlockId/sapronak", ctrl.GetClosingSapronak)
route.Get("/:projectFlockId/data-produksi", ctrl.GetClosingDataProduksi)
}
@@ -3,6 +3,7 @@ package service
import (
"context"
"errors"
"math"
"strconv"
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
@@ -30,6 +31,7 @@ type ClosingService interface {
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)
GetClosingDataProduksi(ctx *fiber.Ctx, projectFlockID uint) (*dto.ClosingProductionReportDTO, error)
GetClosingSapronak(ctx *fiber.Ctx, projectFlockID uint, params *validation.SapronakQuery) ([]dto.ClosingSapronakItemDTO, int64, error)
}
@@ -379,3 +381,219 @@ func (s closingService) GetOverhead(c *fiber.Ctx, projectFlockID uint) (*dto.Ove
return &result, nil
}
func (s closingService) GetClosingDataProduksi(c *fiber.Ctx, projectFlockID uint) (*dto.ClosingProductionReportDTO, 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 data produksi: %+v", projectFlockID, err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock")
}
var population float64
for _, history := range project.KandangHistory {
for _, chickin := range history.Chickins {
population += chickin.UsageQty + chickin.PendingUsageQty
}
}
projectFlockKandangIDs, err := s.getProjectFlockKandangIDs(c.Context(), projectFlockID)
if err != nil {
s.Log.Errorf("Failed to fetch project flock kandangs for %d: %+v", projectFlockID, err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock kandangs")
}
feedIn, feedUsed, err := s.Repository.SumFeedPurchaseAndUsedByProjectFlockKandangIDs(c.Context(), projectFlockKandangIDs)
if err != nil {
s.Log.Errorf("Failed to sum feed purchase/used qty for project flock %d: %+v", projectFlockID, err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch feed purchase data")
}
claimCulling, err := s.Repository.SumClaimCullingByProjectFlockKandangIDs(c.Context(), projectFlockKandangIDs)
if err != nil {
s.Log.Errorf("Failed to sum claim culling for project flock %d: %+v", projectFlockID, err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch claim culling data")
}
finalPopulation := population - claimCulling
var standards []entity.FcrStandard
if project.FcrId > 0 {
standards, err = s.Repository.GetFcrStandardsByFcrID(c.Context(), project.FcrId)
if err != nil {
s.Log.Errorf("Failed to fetch FCR standards for project flock %d: %+v", projectFlockID, err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch FCR standard data")
}
}
// masih dummy, karena tab penjualan agenya masih dummy juga
age := 1.0
feedUsedPerHead := 0.0
if population > 0 {
feedUsedPerHead = feedUsed / population
}
purchase := dto.ClosingPurchaseDTO{
InitialPopulation: int(population),
ClaimCulling: int(claimCulling),
FinalPopulation: int(finalPopulation),
FeedIn: feedIn,
FeedUsed: feedUsed,
FeedUsedPerHead: feedUsedPerHead,
}
chickenFlagNames := []string{string(utils.FlagPullet)}
chickenSalesWeight, chickenSalesQty, chickenSalesPrice, err := s.Repository.SumMarketingWeightAndQtyByProjectFlockKandangIDsAndFlagNames(c.Context(), projectFlockKandangIDs, chickenFlagNames)
if err != nil {
s.Log.Errorf("Failed to fetch chicken sales data for project flock %d: %+v", projectFlockID, err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch chicken sales data")
}
var chickenAverageWeight float64
if chickenSalesQty > 0 {
chickenAverageWeight = chickenSalesWeight / chickenSalesQty
}
var chickenAverageSellingPrice float64
if chickenSalesWeight > 0 {
chickenAverageSellingPrice = chickenSalesPrice / chickenSalesWeight
}
eggFlagNames := []string{
string(utils.FlagTelur),
string(utils.FlagTelurUtuh),
string(utils.FlagTelurPecah),
string(utils.FlagTelurPutih),
string(utils.FlagTelurRetak),
}
eggSalesWeight, eggSalesQty, eggSalesPrice, err := s.Repository.SumMarketingWeightAndQtyByProjectFlockKandangIDsAndFlagNames(c.Context(), projectFlockKandangIDs, eggFlagNames)
if err != nil {
s.Log.Errorf("Failed to fetch egg sales data for project flock %d: %+v", projectFlockID, err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch egg sales data")
}
var averageEggWeight float64
if eggSalesQty > 0 {
averageEggWeight = eggSalesWeight / eggSalesQty
}
var averageEggSellingPrice float64
if eggSalesWeight > 0 {
averageEggSellingPrice = eggSalesPrice / eggSalesWeight
}
chickenSales := dto.ClosingSalesDTO{
SalesPopulation: int(chickenSalesQty),
SalesWeight: chickenSalesWeight,
AverageWeight: chickenAverageWeight,
AverageSellingPrice: chickenAverageSellingPrice,
}
eggSales := dto.ClosingEggSalesDTO{
EggPieces: int(eggSalesQty),
EggMassKg: eggSalesWeight,
AverageEggWeightKg: averageEggWeight,
AverageSellingPrice: averageEggSellingPrice,
}
harvestEggQty, err := s.Repository.SumRecordingEggQtyByProjectFlockKandangIDsAndFlagNames(c.Context(), projectFlockKandangIDs, eggFlagNames)
if err != nil {
s.Log.Errorf("Failed to fetch recording egg qty for project flock %d: %+v", projectFlockID, err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch egg harvest data")
}
chickenDepletion := population - chickenSalesQty
if chickenDepletion < 0 {
chickenDepletion = 0
}
eggDepletion := harvestEggQty - eggSalesQty
if eggDepletion < 0 {
eggDepletion = 0
}
chickenPerformance := calculatePerformanceMetrics(chickenAverageWeight, chickenSalesWeight, feedUsed, population, chickenDepletion, age, standards)
eggPerformance := calculatePerformanceMetrics(averageEggWeight, eggSalesWeight, feedUsed, harvestEggQty, eggDepletion, age, standards)
sales := dto.ClosingSalesGroupDTO{
ChickenProduction: chickenSales,
EggProduction: eggSales,
}
performance := dto.ClosingPerformanceDTO{
Depletion: chickenPerformance.Depletion,
Age: age,
MortalityStd: chickenPerformance.MortalityStd,
MortalityAct: chickenPerformance.MortalityAct,
DeffMortality: chickenPerformance.DeffMortality,
FcrStd: eggPerformance.FcrStd,
FcrAct: eggPerformance.FcrAct,
DeffFcr: eggPerformance.DeffFcr,
Adg: eggPerformance.Adg,
}
result := dto.ClosingProductionReportDTO{
Purchase: purchase,
Sales: sales,
Performance: performance,
}
return &result, nil
}
func calculatePerformanceMetrics(averageWeight, totalWeight, feedUsed, basePopulation, depletion, age float64, standards []entity.FcrStandard) dto.ClosingPerformanceDTO {
mortalityStd, fcrStd := closestFcrValues(standards, averageWeight)
fcrAct := 0.0
if totalWeight > 0 {
fcrAct = feedUsed / totalWeight
}
mortalityAct := 0.0
if basePopulation > 0 {
mortalityAct = (depletion / basePopulation) * 100
}
deffMortality := mortalityStd - mortalityAct
deffFcr := fcrStd - fcrAct
adg := 0.0
if age > 0 {
adg = averageWeight / age
}
return dto.ClosingPerformanceDTO{
Depletion: depletion,
Age: age,
MortalityStd: mortalityStd,
MortalityAct: mortalityAct,
DeffMortality: deffMortality,
FcrStd: fcrStd,
FcrAct: fcrAct,
DeffFcr: deffFcr,
Adg: adg,
}
}
func closestFcrValues(standards []entity.FcrStandard, averageWeight float64) (float64, float64) {
if len(standards) == 0 || averageWeight <= 0 {
return 0, 0
}
closest := standards[0]
minDiff := math.Abs(closest.Weight - averageWeight)
for _, std := range standards[1:] {
diff := math.Abs(std.Weight - averageWeight)
if diff < minDiff {
minDiff = diff
closest = std
}
}
return closest.Mortality, closest.FcrNumber
}