mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-20 21:41:55 +00:00
feat/BE/US-76/TASK-122,133,121,120 Recording add create delete edit
This commit is contained in:
@@ -2,6 +2,7 @@ package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
@@ -18,6 +19,8 @@ type KandangRepository interface {
|
||||
GetFirstByProjectFlockID(ctx context.Context, projectFlockID uint) (*entity.Kandang, error)
|
||||
HasActiveKandangForProjectFlock(ctx context.Context, projectFlockID uint, excludeID *uint) (bool, error)
|
||||
UpdateStatusByProjectFlockID(ctx context.Context, projectFlockID uint, status utils.KandangStatus) error
|
||||
UpsertProjectFlockKandang(ctx context.Context, projectFlockID, kandangID uint) error
|
||||
|
||||
}
|
||||
|
||||
type KandangRepositoryImpl struct {
|
||||
@@ -58,14 +61,15 @@ func (r *KandangRepositoryImpl) ProjectFlockExists(ctx context.Context, projectF
|
||||
|
||||
func (r *KandangRepositoryImpl) HasActiveKandangForProjectFlock(ctx context.Context, projectFlockID uint, excludeID *uint) (bool, error) {
|
||||
var count int64
|
||||
q := r.db.WithContext(ctx).
|
||||
Model(&entity.Kandang{}).
|
||||
Where("project_flock_id = ?", projectFlockID).
|
||||
Where("status = ?", utils.KandangStatusActive).
|
||||
Where("deleted_at IS NULL")
|
||||
if excludeID != nil {
|
||||
q = q.Where("id <> ?", *excludeID)
|
||||
}
|
||||
q := r.db.WithContext(ctx).
|
||||
Table("kandangs k").
|
||||
Joins("JOIN project_flock_kandangs pfk ON pfk.kandang_id = k.id").
|
||||
Where("pfk.project_flock_id = ?", projectFlockID).
|
||||
Where("k.status = ?", utils.KandangStatusActive).
|
||||
Where("k.deleted_at IS NULL")
|
||||
if excludeID != nil {
|
||||
q = q.Where("k.id <> ?", *excludeID)
|
||||
}
|
||||
if err := q.Count(&count).Error; err != nil {
|
||||
return false, err
|
||||
}
|
||||
@@ -74,18 +78,49 @@ func (r *KandangRepositoryImpl) HasActiveKandangForProjectFlock(ctx context.Cont
|
||||
|
||||
func (r *KandangRepositoryImpl) GetFirstByProjectFlockID(ctx context.Context, projectFlockID uint) (*entity.Kandang, error) {
|
||||
kandang := new(entity.Kandang)
|
||||
err := r.db.WithContext(ctx).
|
||||
Where("project_flock_id = ?", projectFlockID).
|
||||
First(kandang).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err := r.db.WithContext(ctx).
|
||||
Table("kandangs k").
|
||||
Joins("JOIN project_flock_kandangs pfk ON pfk.kandang_id = k.id").
|
||||
Where("pfk.project_flock_id = ?", projectFlockID).
|
||||
Where("k.deleted_at IS NULL").
|
||||
Order("k.id ASC").
|
||||
Limit(1).
|
||||
Find(kandang).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if kandang.Id == 0 {
|
||||
return nil, gorm.ErrRecordNotFound
|
||||
}
|
||||
|
||||
return kandang, nil
|
||||
}
|
||||
|
||||
func (r *KandangRepositoryImpl) UpdateStatusByProjectFlockID(ctx context.Context, projectFlockID uint, status utils.KandangStatus) error {
|
||||
return r.db.WithContext(ctx).
|
||||
Model(&entity.Kandang{}).
|
||||
Where("project_flock_id = ?", projectFlockID).
|
||||
Update("status", string(status)).Error
|
||||
sub := r.db.WithContext(ctx).
|
||||
Table("project_flock_kandangs").
|
||||
Select("kandang_id").
|
||||
Where("project_flock_id = ?", projectFlockID)
|
||||
|
||||
return r.db.WithContext(ctx).
|
||||
Model(&entity.Kandang{}).
|
||||
Where("id IN (?)", sub).
|
||||
Where("deleted_at IS NULL").
|
||||
Update("status", string(status)).Error
|
||||
}
|
||||
|
||||
func (r *KandangRepositoryImpl) UpsertProjectFlockKandang(ctx context.Context, projectFlockID, kandangID uint) error {
|
||||
var link entity.ProjectFlockKandang
|
||||
err := r.db.WithContext(ctx).
|
||||
Where("project_flock_id = ? AND kandang_id = ?", projectFlockID, kandangID).
|
||||
First(&link).Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
link = entity.ProjectFlockKandang{
|
||||
ProjectFlockId: projectFlockID,
|
||||
KandangId: kandangID,
|
||||
}
|
||||
return r.db.WithContext(ctx).Create(&link).Error
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -40,7 +40,8 @@ func NewKandangService(repo repository.KandangRepository, validate *validator.Va
|
||||
}
|
||||
|
||||
func (s kandangService) withRelations(db *gorm.DB) *gorm.DB {
|
||||
return db.Preload("CreatedUser").Preload("Location").Preload("Pic")
|
||||
return db.Preload("CreatedUser").Preload("Location").Preload("Pic").Preload("ProjectFlockKandangs.ProjectFlock")
|
||||
|
||||
}
|
||||
|
||||
func (s kandangService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.Kandang, int64, error) {
|
||||
@@ -110,7 +111,6 @@ func (s *kandangService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entit
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid kandang status")
|
||||
}
|
||||
|
||||
var projectFlockID *uint
|
||||
if req.ProjectFlockId != nil {
|
||||
if exists, err := s.Repository.ProjectFlockExists(c.Context(), *req.ProjectFlockId); err != nil {
|
||||
s.Log.Errorf("Failed to check project flock existence: %+v", err)
|
||||
@@ -128,8 +128,6 @@ func (s *kandangService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entit
|
||||
}
|
||||
}
|
||||
|
||||
idCopy := *req.ProjectFlockId
|
||||
projectFlockID = &idCopy
|
||||
}
|
||||
|
||||
//TODO: created by dummy
|
||||
@@ -138,7 +136,6 @@ func (s *kandangService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entit
|
||||
LocationId: req.LocationId,
|
||||
Status: status,
|
||||
PicId: req.PicId,
|
||||
ProjectFlockId: projectFlockID,
|
||||
CreatedBy: 1,
|
||||
}
|
||||
|
||||
@@ -147,6 +144,12 @@ func (s *kandangService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entit
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if req.ProjectFlockId != nil {
|
||||
if err := s.Repository.UpsertProjectFlockKandang(c.Context(), *req.ProjectFlockId, createBody.Id); err != nil {
|
||||
s.Log.Errorf("Failed to link kandang to project_flock via pivot: %+v", err)
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to link kandang to project flock")
|
||||
}
|
||||
}
|
||||
return s.GetOne(c, createBody.Id)
|
||||
}
|
||||
|
||||
@@ -201,7 +204,6 @@ func (s kandangService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
|
||||
finalStatus = status
|
||||
}
|
||||
|
||||
projectFlockIDToUse := existing.ProjectFlockId
|
||||
if req.ProjectFlockId != nil {
|
||||
if exists, err := s.Repository.ProjectFlockExists(c.Context(), *req.ProjectFlockId); err != nil {
|
||||
s.Log.Errorf("Failed to check project flock existence: %+v", err)
|
||||
@@ -209,30 +211,33 @@ func (s kandangService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
|
||||
} else if !exists {
|
||||
return nil, fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Project flock with id %d not found", *req.ProjectFlockId))
|
||||
}
|
||||
idCopy := *req.ProjectFlockId
|
||||
projectFlockIDToUse = &idCopy
|
||||
updateBody["project_flock_id"] = idCopy
|
||||
}
|
||||
|
||||
if projectFlockIDToUse != nil && finalStatus == string(utils.KandangStatusActive) {
|
||||
if active, err := s.Repository.HasActiveKandangForProjectFlock(c.Context(), *projectFlockIDToUse, &id); err != nil {
|
||||
s.Log.Errorf("Failed to check kandang activity for project flock %d: %+v", *projectFlockIDToUse, err)
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check active kandang for project flock")
|
||||
} else if active {
|
||||
return nil, fiber.NewError(fiber.StatusConflict, "Project flock already has an active kandang")
|
||||
// Kalau status jadi ACTIVE, pastikan tidak ada kandang aktif lain pada project flock tsb (hitung via pivot)
|
||||
if finalStatus == string(utils.KandangStatusActive) {
|
||||
if active, err := s.Repository.HasActiveKandangForProjectFlock(c.Context(), *req.ProjectFlockId, &id); err != nil {
|
||||
s.Log.Errorf("Failed to check kandang activity for project flock %d: %+v", *req.ProjectFlockId, err)
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check active kandang for project flock")
|
||||
} else if active {
|
||||
return nil, fiber.NewError(fiber.StatusConflict, "Project flock already has an active kandang")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(updateBody) == 0 {
|
||||
return s.GetOne(c, id)
|
||||
if len(updateBody) > 0 {
|
||||
if err := s.Repository.PatchOne(c.Context(), id, updateBody, nil); err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, fiber.NewError(fiber.StatusNotFound, "Kandang not found")
|
||||
}
|
||||
s.Log.Errorf("Failed to update kandang: %+v", err)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if err := s.Repository.PatchOne(c.Context(), id, updateBody, nil); err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, fiber.NewError(fiber.StatusNotFound, "Kandang not found")
|
||||
if req.ProjectFlockId != nil {
|
||||
if err := s.Repository.UpsertProjectFlockKandang(c.Context(), *req.ProjectFlockId, id); err != nil {
|
||||
s.Log.Errorf("Failed to upsert pivot kandang-project_flock: %+v", err)
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to link kandang to project flock")
|
||||
}
|
||||
s.Log.Errorf("Failed to update kandang: %+v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.GetOne(c, id)
|
||||
|
||||
@@ -28,4 +28,5 @@ func ProjectflockRoutes(v1 fiber.Router, u user.UserService, s projectflock.Proj
|
||||
route.Get("/kandangs/lookup", ctrl.LookupProjectFlockKandang)
|
||||
route.Post("/approvals", ctrl.Approval)
|
||||
route.Get("/flocks/:flock_id/periods", ctrl.GetFlockPeriodSummary)
|
||||
|
||||
}
|
||||
|
||||
@@ -107,9 +107,14 @@ func (s projectflockService) GetAll(c *fiber.Ctx, params *validation.Query) ([]e
|
||||
db = db.Where("project_flocks.period = ?", params.Period)
|
||||
}
|
||||
if len(params.KandangIds) > 0 {
|
||||
db = db.Where("EXISTS (SELECT 1 FROM kandangs WHERE kandangs.project_flock_id = project_flocks.id AND kandangs.id IN ?)", params.KandangIds)
|
||||
db = db.Where(`
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM project_flock_kandangs pfk
|
||||
WHERE pfk.project_flock_id = project_flocks.id
|
||||
AND pfk.kandang_id IN ?
|
||||
)`, params.KandangIds)
|
||||
}
|
||||
|
||||
if params.Search != "" {
|
||||
normalizedSearch := strings.ToLower(strings.TrimSpace(params.Search))
|
||||
if normalizedSearch == "" {
|
||||
@@ -250,10 +255,11 @@ func (s *projectflockService) CreateOne(c *fiber.Ctx, req *validation.Create) (*
|
||||
if len(kandangs) != len(kandangIDs) {
|
||||
return nil, fiber.NewError(fiber.StatusNotFound, "Some kandangs not found")
|
||||
}
|
||||
for _, kandang := range kandangs {
|
||||
if kandang.ProjectFlockId != nil {
|
||||
return nil, fiber.NewError(fiber.StatusConflict, fmt.Sprintf("Kandang %s sudah memiliki project flock", kandang.Name))
|
||||
}
|
||||
// larang kalau ada yg sudah terikat ke project lain
|
||||
if linked, err := s.anyKandangLinkedToOtherProject(c.Context(), s.Repository.DB(), kandangIDs, nil); err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate kandangs linkage")
|
||||
} else if linked {
|
||||
return nil, fiber.NewError(fiber.StatusConflict, "Beberapa kandang sudah terikat dengan project flock lain")
|
||||
}
|
||||
|
||||
createBody := &entity.ProjectFlock{
|
||||
@@ -394,11 +400,12 @@ func (s projectflockService) UpdateOne(c *fiber.Ctx, req *validation.Update, id
|
||||
if len(kandangs) != len(newKandangIDs) {
|
||||
return nil, fiber.NewError(fiber.StatusNotFound, "Some kandangs not found")
|
||||
}
|
||||
for _, k := range kandangs {
|
||||
if k.ProjectFlockId != nil && *k.ProjectFlockId != id {
|
||||
return nil, fiber.NewError(fiber.StatusConflict, fmt.Sprintf("Kandang %s sudah terikat dengan project flock lain", k.Name))
|
||||
}
|
||||
if linked, err := s.anyKandangLinkedToOtherProject(c.Context(), s.Repository.DB(), newKandangIDs, &id); err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate kandangs linkage")
|
||||
} else if linked {
|
||||
return nil, fiber.NewError(fiber.StatusConflict, "Beberapa kandang sudah terikat dengan project flock lain")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
hasChanges := hasBodyChanges || hasKandangChanges
|
||||
@@ -754,7 +761,7 @@ func (s projectflockService) buildOrderExpressions(sortBy, sortOrder string) []s
|
||||
}
|
||||
case "kandangs":
|
||||
return []string{
|
||||
fmt.Sprintf("(SELECT COUNT(*) FROM kandangs WHERE kandangs.project_flock_id = project_flocks.id) %s", direction),
|
||||
fmt.Sprintf("(SELECT COUNT(*) FROM project_flock_kandangs pfk WHERE pfk.project_flock_id = project_flocks.id) %s", direction),
|
||||
fmt.Sprintf("project_flocks.id %s", direction),
|
||||
}
|
||||
case "period":
|
||||
@@ -775,24 +782,50 @@ func (s projectflockService) attachKandangs(ctx context.Context, dbTransaction *
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := dbTransaction.Model(&entity.Kandang{}).
|
||||
if err := dbTransaction.
|
||||
Model(&entity.Kandang{}).
|
||||
Where("id IN ?", kandangIDs).
|
||||
Updates(map[string]any{
|
||||
"project_flock_id": projectFlockID,
|
||||
"status": string(utils.KandangStatusPengajuan),
|
||||
"status": string(utils.KandangStatusPengajuan),
|
||||
}).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update kandangs")
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update kandangs status")
|
||||
}
|
||||
|
||||
pivotRepo := s.pivotRepoWithTx(dbTransaction)
|
||||
records := make([]*entity.ProjectFlockKandang, len(kandangIDs))
|
||||
for i, id := range kandangIDs {
|
||||
records[i] = &entity.ProjectFlockKandang{
|
||||
ProjectFlockId: projectFlockID,
|
||||
KandangId: id,
|
||||
var already []uint
|
||||
if err := dbTransaction.
|
||||
Table("project_flock_kandangs").
|
||||
Where("project_flock_id = ? AND kandang_id IN ?", projectFlockID, kandangIDs).
|
||||
Pluck("kandang_id", &already).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to check existing pivot")
|
||||
}
|
||||
exists := make(map[uint]struct{}, len(already))
|
||||
for _, id := range already {
|
||||
exists[id] = struct{}{}
|
||||
}
|
||||
|
||||
var toAttach []uint
|
||||
seen := make(map[uint]struct{}, len(kandangIDs))
|
||||
for _, id := range kandangIDs {
|
||||
if _, ok := seen[id]; ok {
|
||||
continue
|
||||
}
|
||||
seen[id] = struct{}{}
|
||||
if _, ok := exists[id]; !ok {
|
||||
toAttach = append(toAttach, id)
|
||||
}
|
||||
}
|
||||
if err := pivotRepo.CreateMany(ctx, records); err != nil {
|
||||
if len(toAttach) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
records := make([]*entity.ProjectFlockKandang, 0, len(toAttach))
|
||||
for _, id := range toAttach {
|
||||
records = append(records, &entity.ProjectFlockKandang{
|
||||
ProjectFlockId: projectFlockID,
|
||||
KandangId: id,
|
||||
})
|
||||
}
|
||||
if err := s.pivotRepoWithTx(dbTransaction).CreateMany(ctx, records); err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to persist project flock history")
|
||||
}
|
||||
return nil
|
||||
@@ -803,15 +836,15 @@ func (s projectflockService) detachKandangs(ctx context.Context, dbTransaction *
|
||||
return nil
|
||||
}
|
||||
|
||||
updates := map[string]any{"project_flock_id": nil}
|
||||
if resetStatus {
|
||||
updates["status"] = string(utils.KandangStatusNonActive)
|
||||
}
|
||||
|
||||
if err := dbTransaction.Model(&entity.Kandang{}).
|
||||
Where("id IN ?", kandangIDs).
|
||||
Updates(updates).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update kandangs")
|
||||
if err := dbTransaction.
|
||||
Model(&entity.Kandang{}).
|
||||
Where("id IN ?", kandangIDs).
|
||||
Updates(map[string]any{
|
||||
"status": string(utils.KandangStatusNonActive),
|
||||
}).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update kandangs status")
|
||||
}
|
||||
}
|
||||
|
||||
if err := s.pivotRepoWithTx(dbTransaction).DeleteMany(ctx, projectFlockID, kandangIDs); err != nil {
|
||||
@@ -820,9 +853,24 @@ func (s projectflockService) detachKandangs(ctx context.Context, dbTransaction *
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
func (s projectflockService) pivotRepoWithTx(dbTransaction *gorm.DB) repository.ProjectFlockKandangRepository {
|
||||
if s.PivotRepo == nil {
|
||||
return repository.NewProjectFlockKandangRepository(dbTransaction)
|
||||
}
|
||||
return s.PivotRepo.WithTx(dbTransaction)
|
||||
}
|
||||
|
||||
func (s projectflockService) anyKandangLinkedToOtherProject(ctx context.Context, db *gorm.DB, kandangIDs []uint, exceptProjectID *uint) (bool, error) {
|
||||
q := db.WithContext(ctx).
|
||||
Table("project_flock_kandangs").
|
||||
Where("kandang_id IN ?", kandangIDs)
|
||||
if exceptProjectID != nil {
|
||||
q = q.Where("project_flock_id <> ?", *exceptProjectID)
|
||||
}
|
||||
var count int64
|
||||
if err := q.Count(&count).Error; err != nil {
|
||||
return false, err
|
||||
}
|
||||
return count > 0, nil
|
||||
}
|
||||
|
||||
@@ -1,13 +1,38 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
"errors"
|
||||
"math"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type RecordingRepository interface {
|
||||
repository.BaseRepository[entity.Recording]
|
||||
|
||||
WithRelations(db *gorm.DB) *gorm.DB
|
||||
GenerateNextDay(tx *gorm.DB, projectFlockKandangId uint) (int, error)
|
||||
|
||||
CreateBodyWeights(tx *gorm.DB, bodyWeights []entity.RecordingBW) error
|
||||
DeleteBodyWeights(tx *gorm.DB, recordingID uint) error
|
||||
|
||||
CreateStocks(tx *gorm.DB, stocks []entity.RecordingStock) error
|
||||
DeleteStocks(tx *gorm.DB, recordingID uint) error
|
||||
|
||||
CreateDepletions(tx *gorm.DB, depletions []entity.RecordingDepletion) error
|
||||
DeleteDepletions(tx *gorm.DB, recordingID uint) error
|
||||
|
||||
SumRecordingDepletions(tx *gorm.DB, recordingID uint) (int64, error)
|
||||
FindPreviousRecording(tx *gorm.DB, projectFlockKandangId uint, currentDay int) (*entity.Recording, error)
|
||||
GetTotalChick(tx *gorm.DB, projectFlockKandangId uint) (int64, error)
|
||||
GetAverageBodyWeight(tx *gorm.DB, recordingID uint) (float64, error)
|
||||
GetFeedUsageInGrams(tx *gorm.DB, recordingID uint) (float64, error)
|
||||
GetFcrID(tx *gorm.DB, projectFlockKandangId uint) (uint, error)
|
||||
GetFcrStandardWeightKg(tx *gorm.DB, fcrId uint, currentWeightKg float64) (float64, bool, error)
|
||||
}
|
||||
|
||||
type RecordingRepositoryImpl struct {
|
||||
@@ -19,3 +44,235 @@ func NewRecordingRepository(db *gorm.DB) RecordingRepository {
|
||||
BaseRepositoryImpl: repository.NewBaseRepository[entity.Recording](db),
|
||||
}
|
||||
}
|
||||
|
||||
func (r *RecordingRepositoryImpl) WithRelations(db *gorm.DB) *gorm.DB {
|
||||
return db.
|
||||
Preload("CreatedUser").
|
||||
Preload("ProjectFlockKandang").
|
||||
Preload("ProjectFlockKandang.ProjectFlock").
|
||||
Preload("BodyWeights").
|
||||
Preload("Depletions").
|
||||
Preload("Depletions.ProductWarehouse").
|
||||
Preload("Depletions.ProductWarehouse.Product").
|
||||
Preload("Depletions.ProductWarehouse.Warehouse").
|
||||
Preload("Stocks").
|
||||
Preload("Stocks.ProductWarehouse").
|
||||
Preload("Stocks.ProductWarehouse.Product").
|
||||
Preload("Stocks.ProductWarehouse.Warehouse")
|
||||
}
|
||||
|
||||
func (r *RecordingRepositoryImpl) GenerateNextDay(tx *gorm.DB, projectFlockKandangId uint) (int, error) {
|
||||
var days []int
|
||||
if err := tx.Model(&entity.Recording{}).
|
||||
Where("project_flock_id = ?", projectFlockKandangId).
|
||||
Where("day IS NOT NULL").
|
||||
Pluck("day", &days).Error; err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return nextRecordingDay(days), nil
|
||||
}
|
||||
|
||||
func (r *RecordingRepositoryImpl) CreateBodyWeights(tx *gorm.DB, bodyWeights []entity.RecordingBW) error {
|
||||
if len(bodyWeights) == 0 {
|
||||
return nil
|
||||
}
|
||||
return tx.Create(&bodyWeights).Error
|
||||
}
|
||||
|
||||
func (r *RecordingRepositoryImpl) DeleteBodyWeights(tx *gorm.DB, recordingID uint) error {
|
||||
return tx.Where("recording_id = ?", recordingID).Delete(&entity.RecordingBW{}).Error
|
||||
}
|
||||
|
||||
func (r *RecordingRepositoryImpl) CreateStocks(tx *gorm.DB, stocks []entity.RecordingStock) error {
|
||||
if len(stocks) == 0 {
|
||||
return nil
|
||||
}
|
||||
return tx.Create(&stocks).Error
|
||||
}
|
||||
|
||||
func (r *RecordingRepositoryImpl) DeleteStocks(tx *gorm.DB, recordingID uint) error {
|
||||
return tx.Where("recording_id = ?", recordingID).Delete(&entity.RecordingStock{}).Error
|
||||
}
|
||||
|
||||
func (r *RecordingRepositoryImpl) CreateDepletions(tx *gorm.DB, depletions []entity.RecordingDepletion) error {
|
||||
if len(depletions) == 0 {
|
||||
return nil
|
||||
}
|
||||
return tx.Create(&depletions).Error
|
||||
}
|
||||
|
||||
func (r *RecordingRepositoryImpl) DeleteDepletions(tx *gorm.DB, recordingID uint) error {
|
||||
return tx.Where("recording_id = ?", recordingID).Delete(&entity.RecordingDepletion{}).Error
|
||||
}
|
||||
|
||||
func (r *RecordingRepositoryImpl) SumRecordingDepletions(tx *gorm.DB, recordingID uint) (int64, error) {
|
||||
var result int64
|
||||
if err := tx.Model(&entity.RecordingDepletion{}).
|
||||
Where("recording_id = ?", recordingID).
|
||||
Select("COALESCE(SUM(total), 0)").
|
||||
Scan(&result).Error; err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (r *RecordingRepositoryImpl) FindPreviousRecording(tx *gorm.DB, projectFlockKandangId uint, currentDay int) (*entity.Recording, error) {
|
||||
if currentDay <= 1 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var prev entity.Recording
|
||||
err := tx.
|
||||
Where("project_flock_id = ? AND day < ?", projectFlockKandangId, currentDay).
|
||||
Where("day IS NOT NULL").
|
||||
Order("day DESC").
|
||||
Limit(1).
|
||||
Find(&prev).Error
|
||||
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) || prev.Id == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &prev, nil
|
||||
}
|
||||
|
||||
func (r *RecordingRepositoryImpl) GetTotalChick(tx *gorm.DB, projectFlockKandangId uint) (int64, error) {
|
||||
var population entity.ProjectFlockPopulation
|
||||
err := tx.
|
||||
Where("project_flock_kandang_id = ?", projectFlockKandangId).
|
||||
Order("created_at DESC").
|
||||
First(&population).Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return 0, nil
|
||||
}
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return int64(math.Round(population.InitialQuantity)), nil
|
||||
}
|
||||
|
||||
func (r *RecordingRepositoryImpl) GetAverageBodyWeight(tx *gorm.DB, recordingID uint) (float64, error) {
|
||||
var result struct {
|
||||
TotalWeight float64
|
||||
TotalQty float64
|
||||
}
|
||||
if err := tx.Model(&entity.RecordingBW{}).
|
||||
Select("COALESCE(SUM(weight * qty), 0) AS total_weight, COALESCE(SUM(qty), 0) AS total_qty").
|
||||
Where("recording_id = ?", recordingID).
|
||||
Scan(&result).Error; err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if result.TotalQty == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
return result.TotalWeight / result.TotalQty, nil
|
||||
}
|
||||
|
||||
func (r *RecordingRepositoryImpl) GetFeedUsageInGrams(tx *gorm.DB, recordingID uint) (float64, error) {
|
||||
var rows []struct {
|
||||
UsageAmount float64
|
||||
UomName string
|
||||
}
|
||||
|
||||
if err := tx.
|
||||
Table("recording_stocks").
|
||||
Select("COALESCE(recording_stocks.usage_amount, 0) AS usage_amount, LOWER(uoms.name) AS uom_name").
|
||||
Joins("JOIN product_warehouses ON product_warehouses.id = recording_stocks.product_warehouse_id").
|
||||
Joins("JOIN products ON products.id = product_warehouses.product_id").
|
||||
Joins("JOIN uoms ON uoms.id = products.uom_id").
|
||||
Where("recording_stocks.recording_id = ?", recordingID).
|
||||
Scan(&rows).Error; err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
var total float64
|
||||
for _, row := range rows {
|
||||
if row.UsageAmount <= 0 {
|
||||
continue
|
||||
}
|
||||
switch strings.TrimSpace(row.UomName) {
|
||||
case "kilogram", "kg", "kilograms", "kilo":
|
||||
total += row.UsageAmount * 1000
|
||||
case "gram", "g", "grams":
|
||||
total += row.UsageAmount
|
||||
default:
|
||||
total += row.UsageAmount
|
||||
}
|
||||
}
|
||||
return total, nil
|
||||
}
|
||||
|
||||
func (r *RecordingRepositoryImpl) GetFcrID(tx *gorm.DB, projectFlockKandangId uint) (uint, error) {
|
||||
var result struct {
|
||||
FcrID uint
|
||||
}
|
||||
if err := tx.Table("project_flock_kandangs").
|
||||
Select("project_flocks.fcr_id AS fcr_id").
|
||||
Joins("JOIN project_flocks ON project_flocks.id = project_flock_kandangs.project_flock_id").
|
||||
Where("project_flock_kandangs.id = ?", projectFlockKandangId).
|
||||
Scan(&result).Error; err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return result.FcrID, nil
|
||||
}
|
||||
|
||||
func (r *RecordingRepositoryImpl) GetFcrStandardWeightKg(tx *gorm.DB, fcrId uint, currentWeightKg float64) (float64, bool, error) {
|
||||
if fcrId == 0 {
|
||||
return 0, false, nil
|
||||
}
|
||||
|
||||
var standard entity.FcrStandard
|
||||
err := tx.
|
||||
Where("fcr_id = ? AND weight >= ?", fcrId, currentWeightKg).
|
||||
Order("weight ASC").
|
||||
First(&standard).Error
|
||||
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
err = tx.
|
||||
Where("fcr_id = ?", fcrId).
|
||||
Order("weight DESC").
|
||||
First(&standard).Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return 0, false, nil
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return 0, false, err
|
||||
}
|
||||
|
||||
weight := standard.Weight
|
||||
if weight > 10 {
|
||||
return weight / 1000, true, nil
|
||||
}
|
||||
return weight, true, nil
|
||||
}
|
||||
|
||||
func nextRecordingDay(days []int) int {
|
||||
if len(days) == 0 {
|
||||
return 1
|
||||
}
|
||||
|
||||
unique := make(map[int]struct{}, len(days))
|
||||
for _, day := range days {
|
||||
if day > 0 {
|
||||
unique[day] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
normalized := make([]int, 0, len(unique))
|
||||
for day := range unique {
|
||||
normalized = append(normalized, day)
|
||||
}
|
||||
sort.Ints(normalized)
|
||||
|
||||
for idx, day := range normalized {
|
||||
expected := idx + 1
|
||||
if day != expected {
|
||||
return expected
|
||||
}
|
||||
}
|
||||
|
||||
return len(normalized) + 1
|
||||
}
|
||||
|
||||
@@ -4,8 +4,6 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
@@ -53,22 +51,6 @@ func NewRecordingService(
|
||||
}
|
||||
}
|
||||
|
||||
func (s recordingService) withRelations(db *gorm.DB) *gorm.DB {
|
||||
return db.
|
||||
Preload("CreatedUser").
|
||||
Preload("ProjectFlockKandang").
|
||||
Preload("ProjectFlockKandang.ProjectFlock").
|
||||
Preload("BodyWeights").
|
||||
Preload("Depletions").
|
||||
Preload("Depletions.ProductWarehouse").
|
||||
Preload("Depletions.ProductWarehouse.Product").
|
||||
Preload("Depletions.ProductWarehouse.Warehouse").
|
||||
Preload("Stocks").
|
||||
Preload("Stocks.ProductWarehouse").
|
||||
Preload("Stocks.ProductWarehouse.Product").
|
||||
Preload("Stocks.ProductWarehouse.Warehouse")
|
||||
}
|
||||
|
||||
func (s recordingService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.Recording, int64, error) {
|
||||
if err := s.Validate.Struct(params); err != nil {
|
||||
return nil, 0, err
|
||||
@@ -85,7 +67,7 @@ func (s recordingService) GetAll(c *fiber.Ctx, params *validation.Query) ([]enti
|
||||
offset := (page - 1) * limit
|
||||
|
||||
recordings, total, err := s.Repository.GetAll(c.Context(), offset, limit, func(db *gorm.DB) *gorm.DB {
|
||||
db = s.withRelations(db)
|
||||
db = s.Repository.WithRelations(db)
|
||||
if params.ProjectFlockKandangId != 0 {
|
||||
db = db.Where("project_flock_id = ?", params.ProjectFlockKandangId)
|
||||
}
|
||||
@@ -100,7 +82,9 @@ func (s recordingService) GetAll(c *fiber.Ctx, params *validation.Query) ([]enti
|
||||
}
|
||||
|
||||
func (s recordingService) GetOne(c *fiber.Ctx, id uint) (*entity.Recording, error) {
|
||||
recording, err := s.Repository.GetByID(c.Context(), id, s.withRelations)
|
||||
recording, err := s.Repository.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB {
|
||||
return s.Repository.WithRelations(db)
|
||||
})
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, fiber.NewError(fiber.StatusNotFound, "Recording not found")
|
||||
}
|
||||
@@ -117,7 +101,7 @@ func (s recordingService) GetNextDay(c *fiber.Ctx, projectFlockKandangId uint) (
|
||||
}
|
||||
|
||||
db := s.Repository.DB().WithContext(c.Context())
|
||||
next, err := s.generateNextDay(db, projectFlockKandangId)
|
||||
next, err := s.Repository.GenerateNextDay(db, projectFlockKandangId)
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed to compute next recording day for project_flock_kandang_id=%d: %+v", projectFlockKandangId, err)
|
||||
return 0, err
|
||||
@@ -155,7 +139,7 @@ func (s *recordingService) CreateOne(c *fiber.Ctx, req *validation.Create) (*ent
|
||||
}
|
||||
}()
|
||||
|
||||
nextDay, err := s.generateNextDay(tx, req.ProjectFlockKandangId)
|
||||
nextDay, err := s.Repository.GenerateNextDay(tx, req.ProjectFlockKandangId)
|
||||
if err != nil {
|
||||
_ = tx.Rollback()
|
||||
s.Log.Errorf("Failed to determine recording day: %+v", err)
|
||||
@@ -184,21 +168,25 @@ func (s *recordingService) CreateOne(c *fiber.Ctx, req *validation.Create) (*ent
|
||||
|
||||
if err := tx.Create(recording).Error; err != nil {
|
||||
_ = tx.Rollback()
|
||||
if errors.Is(err, gorm.ErrDuplicatedKey) {
|
||||
dateStr := recordDate.Format("2006-01-02")
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Recording for project flock %d on %s already exists", req.ProjectFlockKandangId, dateStr))
|
||||
}
|
||||
s.Log.Errorf("Failed to create recording: %+v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := s.persistBodyWeights(tx, recording.Id, req.BodyWeights); err != nil {
|
||||
if err := s.Repository.CreateBodyWeights(tx, mapBodyWeights(recording.Id, req.BodyWeights)); err != nil {
|
||||
_ = tx.Rollback()
|
||||
s.Log.Errorf("Failed to persist body weights: %+v", err)
|
||||
return nil, err
|
||||
}
|
||||
if err := s.persistStocks(tx, recording.Id, req.Stocks); err != nil {
|
||||
if err := s.Repository.CreateStocks(tx, mapStocks(recording.Id, req.Stocks)); err != nil {
|
||||
_ = tx.Rollback()
|
||||
s.Log.Errorf("Failed to persist stocks: %+v", err)
|
||||
return nil, err
|
||||
}
|
||||
if err := s.persistDepletions(tx, recording.Id, req.Depletions); err != nil {
|
||||
if err := s.Repository.CreateDepletions(tx, mapDepletions(recording.Id, req.Depletions)); err != nil {
|
||||
_ = tx.Rollback()
|
||||
s.Log.Errorf("Failed to persist depletions: %+v", err)
|
||||
return nil, err
|
||||
@@ -254,7 +242,12 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
|
||||
recording.Ontime = ontimeValue
|
||||
|
||||
if req.BodyWeights != nil {
|
||||
if err := s.replaceBodyWeights(tx, recording.Id, req.BodyWeights); err != nil {
|
||||
if err := s.Repository.DeleteBodyWeights(tx, recording.Id); err != nil {
|
||||
_ = tx.Rollback()
|
||||
s.Log.Errorf("Failed to clear body weights: %+v", err)
|
||||
return nil, err
|
||||
}
|
||||
if err := s.Repository.CreateBodyWeights(tx, mapBodyWeights(recording.Id, req.BodyWeights)); err != nil {
|
||||
_ = tx.Rollback()
|
||||
s.Log.Errorf("Failed to update body weights: %+v", err)
|
||||
return nil, err
|
||||
@@ -265,7 +258,12 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
|
||||
_ = tx.Rollback()
|
||||
return nil, err
|
||||
}
|
||||
if err := s.replaceStocks(tx, recording.Id, req.Stocks); err != nil {
|
||||
if err := s.Repository.DeleteStocks(tx, recording.Id); err != nil {
|
||||
_ = tx.Rollback()
|
||||
s.Log.Errorf("Failed to clear stocks: %+v", err)
|
||||
return nil, err
|
||||
}
|
||||
if err := s.Repository.CreateStocks(tx, mapStocks(recording.Id, req.Stocks)); err != nil {
|
||||
_ = tx.Rollback()
|
||||
s.Log.Errorf("Failed to update stocks: %+v", err)
|
||||
return nil, err
|
||||
@@ -276,7 +274,12 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
|
||||
_ = tx.Rollback()
|
||||
return nil, err
|
||||
}
|
||||
if err := s.replaceDepletions(tx, recording.Id, req.Depletions); err != nil {
|
||||
if err := s.Repository.DeleteDepletions(tx, recording.Id); err != nil {
|
||||
_ = tx.Rollback()
|
||||
s.Log.Errorf("Failed to clear depletions: %+v", err)
|
||||
return nil, err
|
||||
}
|
||||
if err := s.Repository.CreateDepletions(tx, mapDepletions(recording.Id, req.Depletions)); err != nil {
|
||||
_ = tx.Rollback()
|
||||
s.Log.Errorf("Failed to update depletions: %+v", err)
|
||||
return nil, err
|
||||
@@ -342,45 +345,6 @@ func (s *recordingService) ensureProductWarehousesExist(c *fiber.Ctx, stocks []v
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *recordingService) generateNextDay(tx *gorm.DB, projectFlockKandangId uint) (int, error) {
|
||||
var days []int
|
||||
if err := tx.Model(&entity.Recording{}).
|
||||
Where("project_flock_id = ?", projectFlockKandangId).
|
||||
Where("day IS NOT NULL").
|
||||
Pluck("day", &days).Error; err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return nextRecordingDay(days), nil
|
||||
}
|
||||
|
||||
func nextRecordingDay(days []int) int {
|
||||
if len(days) == 0 {
|
||||
return 1
|
||||
}
|
||||
|
||||
unique := make(map[int]struct{}, len(days))
|
||||
for _, day := range days {
|
||||
if day > 0 {
|
||||
unique[day] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
normalized := make([]int, 0, len(unique))
|
||||
for day := range unique {
|
||||
normalized = append(normalized, day)
|
||||
}
|
||||
sort.Ints(normalized)
|
||||
|
||||
for idx, day := range normalized {
|
||||
expected := idx + 1
|
||||
if day != expected {
|
||||
return expected
|
||||
}
|
||||
}
|
||||
|
||||
return len(normalized) + 1
|
||||
}
|
||||
|
||||
func computeOntime(recordDatetime, reference time.Time) bool {
|
||||
return !recordDatetime.Before(reference)
|
||||
}
|
||||
@@ -392,107 +356,81 @@ func boolToInt(v bool) int {
|
||||
return 0
|
||||
}
|
||||
|
||||
func (s *recordingService) persistBodyWeights(tx *gorm.DB, recordingID uint, payload []validation.BodyWeight) error {
|
||||
func mapBodyWeights(recordingID uint, payload []validation.BodyWeight) []entity.RecordingBW {
|
||||
if len(payload) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
bodyWeights := make([]entity.RecordingBW, len(payload))
|
||||
items := make([]entity.RecordingBW, len(payload))
|
||||
for i, bw := range payload {
|
||||
bodyWeights[i] = entity.RecordingBW{
|
||||
items[i] = entity.RecordingBW{
|
||||
RecordingId: recordingID,
|
||||
Weight: bw.Weight,
|
||||
Qty: bw.Qty,
|
||||
Notes: bw.Notes,
|
||||
}
|
||||
}
|
||||
|
||||
return tx.Create(&bodyWeights).Error
|
||||
return items
|
||||
}
|
||||
|
||||
func (s *recordingService) persistStocks(tx *gorm.DB, recordingID uint, payload []validation.Stock) error {
|
||||
func mapStocks(recordingID uint, payload []validation.Stock) []entity.RecordingStock {
|
||||
if len(payload) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
stocks := make([]entity.RecordingStock, len(payload))
|
||||
items := make([]entity.RecordingStock, len(payload))
|
||||
for i, stock := range payload {
|
||||
stocks[i] = entity.RecordingStock{
|
||||
items[i] = entity.RecordingStock{
|
||||
RecordingId: recordingID,
|
||||
ProductWarehouseId: stock.ProductWarehouseId,
|
||||
Notes: stock.Notes,
|
||||
}
|
||||
if stock.Increase != nil {
|
||||
val := *stock.Increase
|
||||
stocks[i].Increase = &val
|
||||
items[i].Increase = &val
|
||||
}
|
||||
if stock.Decrease != nil {
|
||||
val := *stock.Decrease
|
||||
stocks[i].Decrease = &val
|
||||
items[i].Decrease = &val
|
||||
}
|
||||
if stock.UsageAmount != nil {
|
||||
val := *stock.UsageAmount
|
||||
stocks[i].UsageAmount = &val
|
||||
items[i].UsageAmount = &val
|
||||
}
|
||||
}
|
||||
|
||||
return tx.Create(&stocks).Error
|
||||
return items
|
||||
}
|
||||
|
||||
func (s *recordingService) persistDepletions(tx *gorm.DB, recordingID uint, payload []validation.Depletion) error {
|
||||
func mapDepletions(recordingID uint, payload []validation.Depletion) []entity.RecordingDepletion {
|
||||
if len(payload) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
depletions := make([]entity.RecordingDepletion, len(payload))
|
||||
for i, depl := range payload {
|
||||
total := depl.Total
|
||||
depletions[i] = entity.RecordingDepletion{
|
||||
items := make([]entity.RecordingDepletion, len(payload))
|
||||
for i, dep := range payload {
|
||||
total := dep.Total
|
||||
items[i] = entity.RecordingDepletion{
|
||||
RecordingId: recordingID,
|
||||
ProductWarehouseId: depl.ProductWarehouseId,
|
||||
ProductWarehouseId: dep.ProductWarehouseId,
|
||||
Total: total,
|
||||
Notes: depl.Notes,
|
||||
Notes: dep.Notes,
|
||||
}
|
||||
}
|
||||
|
||||
return tx.Create(&depletions).Error
|
||||
return items
|
||||
}
|
||||
|
||||
func (s *recordingService) replaceBodyWeights(tx *gorm.DB, recordingID uint, payload []validation.BodyWeight) error {
|
||||
if err := tx.Where("recording_id = ?", recordingID).Delete(&entity.RecordingBW{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return s.persistBodyWeights(tx, recordingID, payload)
|
||||
}
|
||||
|
||||
func (s *recordingService) replaceStocks(tx *gorm.DB, recordingID uint, payload []validation.Stock) error {
|
||||
if err := tx.Where("recording_id = ?", recordingID).Delete(&entity.RecordingStock{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return s.persistStocks(tx, recordingID, payload)
|
||||
}
|
||||
|
||||
func (s *recordingService) replaceDepletions(tx *gorm.DB, recordingID uint, payload []validation.Depletion) error {
|
||||
if err := tx.Where("recording_id = ?", recordingID).Delete(&entity.RecordingDepletion{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return s.persistDepletions(tx, recordingID, payload)
|
||||
}
|
||||
|
||||
// === Metrics Calculation ===
|
||||
|
||||
func (s *recordingService) computeAndUpdateMetrics(tx *gorm.DB, recording *entity.Recording) error {
|
||||
day := 0
|
||||
if recording.Day != nil {
|
||||
day = *recording.Day
|
||||
}
|
||||
|
||||
totalDepletion, err := s.sumRecordingDepletions(tx, recording.Id)
|
||||
totalDepletion, err := s.Repository.SumRecordingDepletions(tx, recording.Id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("sumRecordingDepletions: %w", err)
|
||||
}
|
||||
|
||||
prevRecording, err := s.getPreviousRecording(tx, recording.ProjectFlockKandangId, day)
|
||||
prevRecording, err := s.Repository.FindPreviousRecording(tx, recording.ProjectFlockKandangId, day)
|
||||
if err != nil {
|
||||
return fmt.Errorf("getPreviousRecording: %w", err)
|
||||
}
|
||||
@@ -507,28 +445,28 @@ func (s *recordingService) computeAndUpdateMetrics(tx *gorm.DB, recording *entit
|
||||
if prevRecording.CumIntake != nil {
|
||||
prevCumIntake = float64(*prevRecording.CumIntake)
|
||||
}
|
||||
prevAvgWeight, err = s.getAverageBodyWeight(tx, prevRecording.Id)
|
||||
prevAvgWeight, err = s.Repository.GetAverageBodyWeight(tx, prevRecording.Id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("getAverageBodyWeight(prev): %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
totalChick, err := s.getTotalChick(tx, recording.ProjectFlockKandangId)
|
||||
totalChick, err := s.Repository.GetTotalChick(tx, recording.ProjectFlockKandangId)
|
||||
if err != nil {
|
||||
return fmt.Errorf("getTotalChick: %w", err)
|
||||
}
|
||||
|
||||
currentAvgWeight, err := s.getAverageBodyWeight(tx, recording.Id)
|
||||
currentAvgWeight, err := s.Repository.GetAverageBodyWeight(tx, recording.Id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("getAverageBodyWeight(current): %w", err)
|
||||
}
|
||||
|
||||
usageInGrams, err := s.getFeedUsageInGrams(tx, recording.Id)
|
||||
usageInGrams, err := s.Repository.GetFeedUsageInGrams(tx, recording.Id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("getFeedUsageInGrams: %w", err)
|
||||
}
|
||||
|
||||
fcrId, err := s.getFcrID(tx, recording.ProjectFlockKandangId)
|
||||
fcrId, err := s.Repository.GetFcrID(tx, recording.ProjectFlockKandangId)
|
||||
if err != nil {
|
||||
return fmt.Errorf("getFcrID: %w", err)
|
||||
}
|
||||
@@ -551,11 +489,11 @@ func (s *recordingService) computeAndUpdateMetrics(tx *gorm.DB, recording *entit
|
||||
|
||||
if totalChick > 0 {
|
||||
remainingChick := totalChick - cumDepletion
|
||||
if remainingChick < 0 {
|
||||
remainingChick = 0
|
||||
}
|
||||
updates["total_chick"] = remainingChick
|
||||
recording.TotalChick = &remainingChick
|
||||
if remainingChick < 0 {
|
||||
remainingChick = 0
|
||||
}
|
||||
updates["total_chick"] = remainingChick
|
||||
recording.TotalChick = &remainingChick
|
||||
|
||||
cumRate := (float64(cumDepletion) / float64(totalChick)) * 100
|
||||
updates["cum_depletion_rate"] = cumRate
|
||||
@@ -587,7 +525,7 @@ func (s *recordingService) computeAndUpdateMetrics(tx *gorm.DB, recording *entit
|
||||
}
|
||||
|
||||
if fcrId != 0 && currentAvgKg > 0 && day > 0 {
|
||||
if fcrWeightKg, ok, err := s.getFcrStandardWeightKg(tx, fcrId, currentAvgKg); err != nil {
|
||||
if fcrWeightKg, ok, err := s.Repository.GetFcrStandardWeightKg(tx, fcrId, currentAvgKg); err != nil {
|
||||
return fmt.Errorf("getFcrStandardWeightKg: %w", err)
|
||||
} else if ok {
|
||||
avgDailyGain := (currentAvgKg - fcrWeightKg) / float64(day)
|
||||
@@ -644,153 +582,6 @@ func (s *recordingService) computeAndUpdateMetrics(tx *gorm.DB, recording *entit
|
||||
return nil
|
||||
}
|
||||
|
||||
// === Query Helpers ===
|
||||
|
||||
func (s *recordingService) sumRecordingDepletions(tx *gorm.DB, recordingID uint) (int64, error) {
|
||||
var result int64
|
||||
if err := tx.Model(&entity.RecordingDepletion{}).
|
||||
Where("recording_id = ?", recordingID).
|
||||
Select("COALESCE(SUM(total), 0)").
|
||||
Scan(&result).Error; err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s *recordingService) getPreviousRecording(tx *gorm.DB, projectFlockKandangId uint, currentDay int) (*entity.Recording, error) {
|
||||
if currentDay <= 1 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var prev entity.Recording
|
||||
err := tx.
|
||||
Where("project_flock_id = ? AND day < ?", projectFlockKandangId, currentDay).
|
||||
Where("day IS NOT NULL").
|
||||
Order("day DESC").
|
||||
Limit(1).
|
||||
Find(&prev).Error
|
||||
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) || prev.Id == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &prev, nil
|
||||
}
|
||||
|
||||
func (s *recordingService) getTotalChick(tx *gorm.DB, projectFlockKandangId uint) (int64, error) {
|
||||
var population entity.ProjectFlockPopulation
|
||||
err := tx.
|
||||
Where("project_flock_kandang_id = ?", projectFlockKandangId).
|
||||
Order("created_at DESC").
|
||||
First(&population).Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return 0, nil
|
||||
}
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return int64(math.Round(population.InitialQuantity)), nil
|
||||
}
|
||||
|
||||
func (s *recordingService) getAverageBodyWeight(tx *gorm.DB, recordingID uint) (float64, error) {
|
||||
var result struct {
|
||||
TotalWeight float64
|
||||
TotalQty float64
|
||||
}
|
||||
if err := tx.Model(&entity.RecordingBW{}).
|
||||
Select("COALESCE(SUM(weight * qty), 0) AS total_weight, COALESCE(SUM(qty), 0) AS total_qty").
|
||||
Where("recording_id = ?", recordingID).
|
||||
Scan(&result).Error; err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if result.TotalQty == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
return result.TotalWeight / result.TotalQty, nil
|
||||
}
|
||||
|
||||
func (s *recordingService) getFeedUsageInGrams(tx *gorm.DB, recordingID uint) (float64, error) {
|
||||
var rows []struct {
|
||||
UsageAmount float64
|
||||
UomName string
|
||||
}
|
||||
|
||||
if err := tx.
|
||||
Table("recording_stocks").
|
||||
Select("COALESCE(recording_stocks.usage_amount, 0) AS usage_amount, LOWER(uoms.name) AS uom_name").
|
||||
Joins("JOIN product_warehouses ON product_warehouses.id = recording_stocks.product_warehouse_id").
|
||||
Joins("JOIN products ON products.id = product_warehouses.product_id").
|
||||
Joins("JOIN uoms ON uoms.id = products.uom_id").
|
||||
Where("recording_stocks.recording_id = ?", recordingID).
|
||||
Scan(&rows).Error; err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
var total float64
|
||||
for _, row := range rows {
|
||||
if row.UsageAmount <= 0 {
|
||||
continue
|
||||
}
|
||||
switch strings.TrimSpace(row.UomName) {
|
||||
case "kilogram", "kg", "kilograms", "kilo":
|
||||
total += row.UsageAmount * 1000
|
||||
case "gram", "g", "grams":
|
||||
total += row.UsageAmount
|
||||
default:
|
||||
total += row.UsageAmount
|
||||
}
|
||||
}
|
||||
return total, nil
|
||||
}
|
||||
|
||||
func (s *recordingService) getFcrID(tx *gorm.DB, projectFlockKandangId uint) (uint, error) {
|
||||
var result struct {
|
||||
FcrID uint
|
||||
}
|
||||
if err := tx.Table("project_flock_kandangs").
|
||||
Select("project_flocks.fcr_id AS fcr_id").
|
||||
Joins("JOIN project_flocks ON project_flocks.id = project_flock_kandangs.project_flock_id").
|
||||
Where("project_flock_kandangs.id = ?", projectFlockKandangId).
|
||||
Scan(&result).Error; err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return result.FcrID, nil
|
||||
}
|
||||
|
||||
func (s *recordingService) getFcrStandardWeightKg(tx *gorm.DB, fcrId uint, currentWeightKg float64) (float64, bool, error) {
|
||||
if fcrId == 0 {
|
||||
return 0, false, nil
|
||||
}
|
||||
|
||||
var standard entity.FcrStandard
|
||||
err := tx.
|
||||
Where("fcr_id = ? AND weight >= ?", fcrId, currentWeightKg).
|
||||
Order("weight ASC").
|
||||
First(&standard).Error
|
||||
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
err = tx.
|
||||
Where("fcr_id = ?", fcrId).
|
||||
Order("weight DESC").
|
||||
First(&standard).Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return 0, false, nil
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return 0, false, err
|
||||
}
|
||||
|
||||
weight := standard.Weight
|
||||
if weight > 10 {
|
||||
// assume already in grams
|
||||
return weight / 1000, true, nil
|
||||
}
|
||||
return weight, true, nil
|
||||
}
|
||||
|
||||
// === Unit Helpers ===
|
||||
|
||||
func toGrams(weight float64) float64 {
|
||||
|
||||
Reference in New Issue
Block a user