ini api per farm

This commit is contained in:
giovanni
2026-06-03 00:30:41 +07:00
parent ef2f9568ad
commit 93ed89b4ef
8 changed files with 918 additions and 0 deletions
@@ -0,0 +1,233 @@
package repositories
import (
"context"
"time"
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
"gitlab.com/mbugroup/lti-api.git/internal/utils/fifo"
"gorm.io/gorm"
)
// HppPerFarmFlockMetaRow describes a LAYING project flock and the farm
// (location) it belongs to. Farm identity is project_flocks.location_id.
type HppPerFarmFlockMetaRow struct {
ProjectFlockID uint
FlockName string
LocationID uint
LocationName string
AreaID uint
}
// HppPerFarmDocRow holds the DOC/pullet acquisition cost trace per flock.
// Used only as an informational field (average_doc_price_rp); it is NOT part
// of total_cost because the pullet cost is expensed through depreciation.
type HppPerFarmDocRow struct {
ProjectFlockID uint
DocCost float64
DocQty float64
}
type HppPerFarmRepository interface {
GetCandidateFlocks(ctx context.Context, start time.Time, areaIDs, locationIDs []int64) ([]HppPerFarmFlockMetaRow, error)
SumRecordingEggWeightByFlock(ctx context.Context, start, endExclusive time.Time, projectFlockIDs []uint) (map[uint]float64, error)
SumMarketingDoTelurWeightByFlock(ctx context.Context, start, endExclusive time.Time, projectFlockIDs []uint) (map[uint]float64, error)
GetDocCostByFlock(ctx context.Context, projectFlockIDs []uint) (map[uint]HppPerFarmDocRow, error)
DB() *gorm.DB
}
type hppPerFarmRepository struct {
db *gorm.DB
}
func NewHppPerFarmRepository(db *gorm.DB) HppPerFarmRepository {
return &hppPerFarmRepository{db: db}
}
func (r *hppPerFarmRepository) DB() *gorm.DB {
return r.db
}
// GetCandidateFlocks returns the LAYING project flocks (with their farm/location
// metadata) that are still active on or after the range start, scoped by area
// and location. Mirrors ExpenseDepreciationRepository.GetCandidateFarms but adds
// location info so flocks can be grouped per farm.
func (r *hppPerFarmRepository) GetCandidateFlocks(ctx context.Context, start time.Time, areaIDs, locationIDs []int64) ([]HppPerFarmFlockMetaRow, error) {
rows := make([]HppPerFarmFlockMetaRow, 0)
query := r.db.WithContext(ctx).
Table("project_flocks AS pf").
Select(`
DISTINCT pf.id AS project_flock_id,
pf.flock_name AS flock_name,
pf.location_id AS location_id,
loc.name AS location_name,
pf.area_id AS area_id`).
Joins("JOIN project_flock_kandangs AS pfk ON pfk.project_flock_id = pf.id").
Joins("JOIN locations AS loc ON loc.id = pf.location_id").
Where("pf.deleted_at IS NULL").
Where("pf.category = ?", utils.ProjectFlockCategoryLaying).
Where("(pfk.closed_at IS NULL OR DATE(pfk.closed_at) >= DATE(?))", start)
if len(areaIDs) > 0 {
query = query.Where("pf.area_id IN ?", areaIDs)
}
if len(locationIDs) > 0 {
query = query.Where("pf.location_id IN ?", locationIDs)
}
if err := query.Order("pf.location_id ASC, pf.id ASC").Scan(&rows).Error; err != nil {
return nil, err
}
return rows, nil
}
// SumRecordingEggWeightByFlock sums recording_eggs.weight (kg) per project flock
// for non-rejected recordings whose record_datetime falls inside [start, endExclusive).
func (r *hppPerFarmRepository) SumRecordingEggWeightByFlock(ctx context.Context, start, endExclusive time.Time, projectFlockIDs []uint) (map[uint]float64, error) {
result := make(map[uint]float64)
if len(projectFlockIDs) == 0 {
return result, nil
}
latestApproval := r.db.WithContext(ctx).
Table("approvals AS a").
Select("a.approvable_id, a.action").
Joins(`
JOIN (
SELECT approvable_id, MAX(action_at) AS latest_action_at
FROM approvals
WHERE approvable_type = ?
GROUP BY approvable_id
) AS la ON la.approvable_id = a.approvable_id AND la.latest_action_at = a.action_at`,
string(utils.ApprovalWorkflowRecording),
)
type eggRow struct {
ProjectFlockID uint
Weight float64
}
rows := make([]eggRow, 0)
query := r.db.WithContext(ctx).
Table("recordings AS r").
Select(`
pfk.project_flock_id AS project_flock_id,
COALESCE(SUM(re.weight), 0) AS weight`).
Joins("JOIN project_flock_kandangs AS pfk ON pfk.id = r.project_flock_kandangs_id").
Joins("LEFT JOIN (?) AS la ON la.approvable_id = r.id", latestApproval).
Joins("JOIN recording_eggs AS re ON re.recording_id = r.id").
Where("pfk.project_flock_id IN ?", projectFlockIDs).
Where("r.record_datetime >= ? AND r.record_datetime < ?", start, endExclusive).
Where("r.deleted_at IS NULL").
Where("(la.action IS NULL OR la.action != ?)", string(entity.ApprovalActionRejected)).
Group("pfk.project_flock_id")
if err := query.Scan(&rows).Error; err != nil {
return nil, err
}
for _, row := range rows {
result[row.ProjectFlockID] = row.Weight
}
return result, nil
}
// SumMarketingDoTelurWeightByFlock sums delivered TELUR weight (marketing_delivery_products.total_weight)
// per project flock, for delivery_date inside [start, endExclusive). A delivery product that is
// attributed to multiple flocks is prorated by each flock's allocated qty share, so that
// the farm total equals the sum of its flocks.
func (r *hppPerFarmRepository) SumMarketingDoTelurWeightByFlock(ctx context.Context, start, endExclusive time.Time, projectFlockIDs []uint) (map[uint]float64, error) {
result := make(map[uint]float64)
if len(projectFlockIDs) == 0 {
return result, nil
}
telurFlags := []string{
string(utils.FlagTelur),
string(utils.FlagTelurUtuh),
string(utils.FlagTelurPecah),
string(utils.FlagTelurPutih),
string(utils.FlagTelurRetak),
}
// allocated qty per (marketing_delivery_product, project_flock)
attrByFlock := r.db.WithContext(ctx).
Table("(?) AS mda", commonRepo.MarketingDeliveryAttributionRowsQuery(r.db.WithContext(ctx))).
Select(`
mda.marketing_delivery_product_id AS mdp_id,
mda.project_flock_id AS project_flock_id,
SUM(mda.allocated_qty) AS flock_qty`).
Group("mda.marketing_delivery_product_id, mda.project_flock_id")
// prorate each delivery product's total_weight across its attributed flocks.
// Use EXISTS for the TELUR flag filter (not a JOIN) so a product carrying
// multiple egg flags does not fan out and double-count the weight share.
shareQuery := r.db.WithContext(ctx).
Table("(?) AS a", attrByFlock).
Select(`
a.project_flock_id AS project_flock_id,
mdp.total_weight * a.flock_qty / NULLIF(SUM(a.flock_qty) OVER (PARTITION BY a.mdp_id), 0) AS weight_share`).
Joins("JOIN marketing_delivery_products AS mdp ON mdp.id = a.mdp_id").
Joins("JOIN marketing_products AS mp ON mp.id = mdp.marketing_product_id").
Joins("JOIN product_warehouses AS pw ON pw.id = mp.product_warehouse_id").
Where("EXISTS (SELECT 1 FROM flags f WHERE f.flagable_id = pw.product_id AND f.flagable_type = ? AND f.name IN ?)", entity.FlagableTypeProduct, telurFlags).
Where("mdp.delivery_date >= ? AND mdp.delivery_date < ?", start, endExclusive)
type doRow struct {
ProjectFlockID uint
Weight float64
}
rows := make([]doRow, 0)
query := r.db.WithContext(ctx).
Table("(?) AS s", shareQuery).
Select(`
s.project_flock_id AS project_flock_id,
COALESCE(SUM(s.weight_share), 0) AS weight`).
Where("s.project_flock_id IN ?", projectFlockIDs).
Group("s.project_flock_id")
if err := query.Scan(&rows).Error; err != nil {
return nil, err
}
for _, row := range rows {
result[row.ProjectFlockID] = row.Weight
}
return result, nil
}
// GetDocCostByFlock returns the DOC acquisition cost (qty * purchase price) and qty
// traced to chick-in per project flock. Informational only.
func (r *hppPerFarmRepository) GetDocCostByFlock(ctx context.Context, projectFlockIDs []uint) (map[uint]HppPerFarmDocRow, error) {
result := make(map[uint]HppPerFarmDocRow)
if len(projectFlockIDs) == 0 {
return result, nil
}
rows := make([]HppPerFarmDocRow, 0)
query := r.db.WithContext(ctx).
Table("project_chickins AS pc").
Select(`
pfk.project_flock_id AS project_flock_id,
COALESCE(SUM(sa.qty * COALESCE(pi.price, 0)), 0) AS doc_cost,
COALESCE(SUM(sa.qty), 0) AS doc_qty`).
Joins("JOIN project_flock_kandangs AS pfk ON pfk.id = pc.project_flock_kandang_id").
Joins("LEFT JOIN stock_allocations AS sa ON sa.usable_type = ? AND sa.usable_id = pc.id AND sa.stockable_type = ? AND sa.status = ? AND sa.allocation_purpose = ?", fifo.UsableKeyProjectChickin.String(), fifo.StockableKeyPurchaseItems.String(), entity.StockAllocationStatusActive, entity.StockAllocationPurposeTraceChickin).
Joins("LEFT JOIN purchase_items AS pi ON pi.id = sa.stockable_id").
Where("pfk.project_flock_id IN ?", projectFlockIDs).
Group("pfk.project_flock_id")
if err := query.Scan(&rows).Error; err != nil {
return nil, err
}
for _, row := range rows {
result[row.ProjectFlockID] = row
}
return result, nil
}