feat/BE/US-76/TASK-122,133,121,120 Recording add create delete edit

This commit is contained in:
ragilap
2025-10-26 15:27:19 +07:00
parent cedd5365d8
commit 8ae614540f
13 changed files with 639 additions and 579 deletions
@@ -0,0 +1,22 @@
ALTER TABLE kandangs
DROP CONSTRAINT IF EXISTS kandangs_project_flock_id_fkey;
ALTER TABLE kandangs
DROP COLUMN IF EXISTS project_flock_id;
ALTER TABLE project_chickins
DROP CONSTRAINT fk_project_flock_kandang_id,
ADD CONSTRAINT fk_project_flock_kandang_id
FOREIGN KEY (project_flock_kandang_id)
REFERENCES project_flock_kandangs(id)
ON UPDATE CASCADE
ON DELETE CASCADE;
ALTER TABLE project_flock_populations
DROP CONSTRAINT fk_project_flock_kandang_id,
ADD CONSTRAINT fk_project_flock_kandang_id
FOREIGN KEY (project_flock_kandang_id)
REFERENCES project_flock_kandangs(id)
ON UPDATE CASCADE
ON DELETE CASCADE;
+111 -214
View File
@@ -51,12 +51,12 @@ func Run(db *gorm.DB) error {
return err
}
projectFlocks, err := seedProjectFlocks(tx, adminID, flocks, areas, fcrs, locations)
if err != nil {
if err := seedProjectFlocks(tx, adminID, flocks, areas, fcrs, locations); err != nil {
return err
}
kandangs, err := seedKandangs(tx, adminID, locations, users, projectFlocks)
kandangs, err := seedKandangs(tx, adminID, locations, users)
if err != nil {
return err
}
@@ -243,7 +243,11 @@ func seedFlocks(tx *gorm.DB, createdBy uint) (map[string]uint, error) {
return result, nil
}
func seedProjectFlocks(tx *gorm.DB, createdBy uint, flocks, areas, fcrs, locations map[string]uint) (map[string]uint, error) {
func seedProjectFlocks(
tx *gorm.DB,
createdBy uint,
flocks, areas, fcrs, locations map[string]uint,
) error {
seeds := []struct {
Key string
Flock string
@@ -273,29 +277,30 @@ func seedProjectFlocks(tx *gorm.DB, createdBy uint, flocks, areas, fcrs, locatio
},
}
result := make(map[string]uint, len(seeds))
for _, seed := range seeds {
flockID, ok := flocks[seed.Flock]
if !ok {
return nil, fmt.Errorf("floc %s not seeded", seed.Flock)
return fmt.Errorf("floc %s not seeded", seed.Flock)
}
areaID, ok := areas[seed.Area]
if !ok {
return nil, fmt.Errorf("area %s not seeded", seed.Area)
return fmt.Errorf("area %s not seeded", seed.Area)
}
fcrID, ok := fcrs[seed.Fcr]
if !ok {
return nil, fmt.Errorf("fcr %s not seeded", seed.Fcr)
return fmt.Errorf("fcr %s not seeded", seed.Fcr)
}
locationID, ok := locations[seed.Location]
if !ok {
return nil, fmt.Errorf("location %s not seeded", seed.Location)
return fmt.Errorf("location %s not seeded", seed.Location)
}
var projectFlock entity.ProjectFlock
err := tx.Where("flock_id = ? AND area_id = ? AND category = ? AND fcr_id = ? AND location_id = ? AND period = ?",
flockID, areaID, seed.Category, fcrID, locationID, seed.Period).First(&projectFlock).Error
err := tx.Where(
"flock_id = ? AND area_id = ? AND category = ? AND fcr_id = ? AND location_id = ? AND period = ?",
flockID, areaID, seed.Category, fcrID, locationID, seed.Period,
).First(&projectFlock).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
projectFlock = entity.ProjectFlock{
FlockId: flockID,
@@ -307,10 +312,10 @@ func seedProjectFlocks(tx *gorm.DB, createdBy uint, flocks, areas, fcrs, locatio
CreatedBy: createdBy,
}
if err := tx.Create(&projectFlock).Error; err != nil {
return nil, err
return err
}
} else if err != nil {
return nil, err
return err
} else {
if err := tx.Model(&entity.ProjectFlock{}).Where("id = ?", projectFlock.Id).Updates(map[string]any{
"flock_id": flockID,
@@ -320,17 +325,16 @@ func seedProjectFlocks(tx *gorm.DB, createdBy uint, flocks, areas, fcrs, locatio
"location_id": locationID,
"period": seed.Period,
}).Error; err != nil {
return nil, err
return err
}
}
if err := ensureProjectFlockApprovals(tx, projectFlock.Id, createdBy); err != nil {
return nil, err
return err
}
result[seed.Key] = projectFlock.Id
}
return result, nil
return nil
}
func ensureProjectFlockApprovals(tx *gorm.DB, projectFlockID uint, actorID uint) error {
@@ -385,17 +389,16 @@ func ensureProjectFlockApprovals(tx *gorm.DB, projectFlockID uint, actorID uint)
return nil
}
func seedKandangs(tx *gorm.DB, createdBy uint, locations map[string]uint, users map[string]uint, projectFlocks map[string]uint) (map[string]uint, error) {
func seedKandangs(tx *gorm.DB, createdBy uint, locations map[string]uint, users map[string]uint) (map[string]uint, error) {
seeds := []struct {
Name string
Status utils.KandangStatus
Location string
PicKey string
ProjectFlockKey *string
}{
{Name: "Singaparna 1", Status: utils.KandangStatusActive, Location: "Singaparna", PicKey: "admin", ProjectFlockKey: strPtr("Singaparna Period 1")},
{Name: "Singaparna 1", Status: utils.KandangStatusNonActive, Location: "Singaparna", PicKey: "admin"},
{Name: "Singaparna 2", Status: utils.KandangStatusNonActive, Location: "Singaparna", PicKey: "admin"},
{Name: "Cikaum 1", Status: utils.KandangStatusActive, Location: "Cikaum", PicKey: "admin", ProjectFlockKey: strPtr("Cikaum Period 1")},
{Name: "Cikaum 1", Status: utils.KandangStatusNonActive, Location: "Cikaum", PicKey: "admin"},
{Name: "Cikaum 2", Status: utils.KandangStatusNonActive, Location: "Cikaum", PicKey: "admin"},
}
@@ -411,14 +414,6 @@ func seedKandangs(tx *gorm.DB, createdBy uint, locations map[string]uint, users
return nil, fmt.Errorf("user %s not seeded", seed.PicKey)
}
var projectFlockID *uint
if seed.ProjectFlockKey != nil {
pfID, ok := projectFlocks[*seed.ProjectFlockKey]
if !ok {
return nil, fmt.Errorf("project flock %s not seeded", *seed.ProjectFlockKey)
}
projectFlockID = uintPtr(pfID)
}
var kandang entity.Kandang
err := tx.Where("name = ?", seed.Name).First(&kandang).Error
@@ -428,15 +423,11 @@ func seedKandangs(tx *gorm.DB, createdBy uint, locations map[string]uint, users
Status: string(seed.Status),
LocationId: locID,
PicId: picID,
ProjectFlockId: projectFlockID,
CreatedBy: createdBy,
}
if err := tx.Create(&kandang).Error; err != nil {
return nil, err
}
if err := syncPivotRelation(tx, projectFlockID, kandang.Id); err != nil {
return nil, err
}
} else if err != nil {
return nil, err
} else {
@@ -445,17 +436,9 @@ func seedKandangs(tx *gorm.DB, createdBy uint, locations map[string]uint, users
"pic_id": picID,
"status": string(seed.Status),
}
if projectFlockID != nil {
updates["project_flock_id"] = *projectFlockID
} else {
updates["project_flock_id"] = nil
}
if err := tx.Model(&entity.Kandang{}).Where("id = ?", kandang.Id).Updates(updates).Error; err != nil {
return nil, err
}
if err := syncPivotRelation(tx, projectFlockID, kandang.Id); err != nil {
return nil, err
}
}
result[seed.Name] = kandang.Id
}
@@ -463,37 +446,6 @@ func seedKandangs(tx *gorm.DB, createdBy uint, locations map[string]uint, users
return result, nil
}
func syncPivotRelation(tx *gorm.DB, projectFlockID *uint, kandangID uint) error {
if err := detachActivePivot(tx, kandangID); err != nil {
return err
}
if projectFlockID == nil {
return nil
}
return ensureActivePivot(tx, *projectFlockID, kandangID)
}
func detachActivePivot(tx *gorm.DB, kandangID uint) error {
return tx.Where("kandang_id = ?", kandangID).
Delete(&entity.ProjectFlockKandang{}).Error
}
func ensureActivePivot(tx *gorm.DB, projectFlockID, kandangID uint) error {
var pivot entity.ProjectFlockKandang
err := tx.Where("project_flock_id = ? AND kandang_id = ?", projectFlockID, kandangID).
First(&pivot).Error
if err == nil {
return nil
}
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return err
}
newRecord := entity.ProjectFlockKandang{
ProjectFlockId: projectFlockID,
KandangId: kandangID,
}
return tx.Create(&newRecord).Error
}
func seedWarehouses(tx *gorm.DB, createdBy uint, areas map[string]uint, locations map[string]uint, kandangs map[string]uint) error {
seeds := []struct {
@@ -1133,153 +1085,71 @@ func seedTransferStock(tx *gorm.DB, createdBy uint) error {
return nil
}
func seedChickin(tx *gorm.DB, createdBy uint) error {
seeds := []struct {
ProjectFlockKandangId uint
ChickInDate string
Quantity float64
Note string
}{
{ProjectFlockKandangId: 1, ChickInDate: "2025-10-20", Quantity: 100, Note: "Seeder chickin 1"},
{ProjectFlockKandangId: 2, ChickInDate: "2025-10-21", Quantity: 200, Note: "Seeder chickin 2"},
}
// gunakan identitas yang stabil, bukan ID pivot
seeds := []struct {
KandangName string
LocationName string
Period int
ChickInDate string
Quantity float64
Note string
}{
{"Singaparna 1", "Singaparna", 1, "2025-10-20", 100, "Seeder chickin 1"},
{"Cikaum 1", "Cikaum", 1, "2025-10-21", 200, "Seeder chickin 2"},
}
for _, seed := range seeds {
chickinDate, err := time.Parse("2006-01-02", seed.ChickInDate)
if err != nil {
return err
}
for _, s := range seeds {
pfkID, err := ensurePFK(tx, s.KandangName, s.LocationName, s.Period)
if err != nil { return err }
// Insert ProjectChickin jika belum ada
var chickin entity.ProjectChickin
err = tx.Where("project_flock_kandang_id = ? AND chick_in_date = ?", seed.ProjectFlockKandangId, chickinDate).
First(&chickin).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
chickin = entity.ProjectChickin{
ProjectFlockKandangId: seed.ProjectFlockKandangId,
ChickInDate: chickinDate,
Quantity: seed.Quantity,
Note: seed.Note,
CreatedBy: createdBy,
}
if err := tx.Create(&chickin).Error; err != nil {
return err
}
} else if err != nil {
return err
}
date, err := time.Parse("2006-01-02", s.ChickInDate)
if err != nil { return err }
var population entity.ProjectFlockPopulation
err = tx.Where("project_flock_kandang_id = ?", seed.ProjectFlockKandangId).First(&population).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
population = entity.ProjectFlockPopulation{
ProjectFlockKandangId: seed.ProjectFlockKandangId,
InitialQuantity: seed.Quantity,
CurrentQuantity: seed.Quantity,
ReservedQuantity: 0,
CreatedBy: createdBy,
}
if err := tx.Create(&population).Error; err != nil {
return err
}
} else if err != nil {
return err
} else {
// Update population quantities
if err := tx.Model(&entity.ProjectFlockPopulation{}).
Where("id = ?", population.Id).
Updates(map[string]any{
"initial_quantity": population.InitialQuantity + seed.Quantity,
"current_quantity": population.CurrentQuantity + seed.Quantity,
"reserved_quantity": 0,
}).Error; err != nil {
return err
}
}
// upsert project_chickin (idempotent)
var chickin entity.ProjectChickin
err = tx.Where("project_flock_kandang_id = ? AND chick_in_date = ?", pfkID, date).First(&chickin).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
chickin = entity.ProjectChickin{
ProjectFlockKandangId: pfkID,
ChickInDate: date,
Quantity: s.Quantity,
Note: s.Note,
CreatedBy: createdBy,
}
if err := tx.Create(&chickin).Error; err != nil { return err }
} else if err != nil {
return err
}
var pfk entity.ProjectFlockKandang
if err := tx.Where("id = ?", seed.ProjectFlockKandangId).First(&pfk).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
// no pivot found; skip creating details
continue
}
return err
}
var warehouse entity.Warehouse
if err := tx.Where("kandang_id = ?", pfk.KandangId).First(&warehouse).Error; err != nil {
// if warehouse not found, cannot create details
if errors.Is(err, gorm.ErrRecordNotFound) {
continue
}
return err
}
var productWarehouses []entity.ProductWarehouse
err = tx.Table("product_warehouses").
Select("product_warehouses.*").
Joins("JOIN products ON products.id = product_warehouses.product_id").
Joins("JOIN product_categories ON product_categories.id = products.product_category_id").
Where("product_categories.code = ? AND product_warehouses.warehouse_id = ?", "DOC", warehouse.Id).
Order("product_warehouses.created_at DESC").
Find(&productWarehouses).Error
if err != nil {
return err
}
// If no product warehouses found, keep existing chickin.Quantity and skip details
if len(productWarehouses) == 0 {
continue
}
// sum all pw quantities and set chickin.Quantity to that total (mimic CreateOne)
totalQty := 0.0
for _, pw := range productWarehouses {
totalQty += pw.Quantity
}
if chickin.Quantity != totalQty {
if err := tx.Model(&entity.ProjectChickin{}).Where("id = ?", chickin.Id).Update("quantity", totalQty).Error; err != nil {
return err
}
chickin.Quantity = totalQty
}
for _, pw := range productWarehouses {
// ensure detail exists or create it with full pw.Quantity
var detail entity.ProjectChickinDetail
err = tx.Where("project_chickin_id = ? AND product_warehouse_id = ?", chickin.Id, pw.Id).First(&detail).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
detail = entity.ProjectChickinDetail{
ProjectChickinId: chickin.Id,
ProductWarehouseId: pw.Id,
Quantity: pw.Quantity,
CreatedBy: createdBy,
}
if err := tx.Create(&detail).Error; err != nil {
return err
}
} else if err != nil {
return err
} else {
if detail.Quantity != pw.Quantity {
if err := tx.Model(&entity.ProjectChickinDetail{}).Where("id = ?", detail.Id).Update("quantity", pw.Quantity).Error; err != nil {
return err
}
}
}
// zero out pw quantity
if err := tx.Model(&entity.ProductWarehouse{}).Where("id = ?", pw.Id).Update("quantity", 0).Error; err != nil {
return err
}
}
}
return nil
// upsert population
var pop entity.ProjectFlockPopulation
err = tx.Where("project_flock_kandang_id = ?", pfkID).First(&pop).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
pop = entity.ProjectFlockPopulation{
ProjectFlockKandangId: pfkID,
InitialQuantity: s.Quantity,
CurrentQuantity: s.Quantity,
ReservedQuantity: 0,
CreatedBy: createdBy,
}
if err := tx.Create(&pop).Error; err != nil { return err }
} else if err != nil {
return err
} else {
if err := tx.Model(&entity.ProjectFlockPopulation{}).
Where("id = ?", pop.Id).
Updates(map[string]any{
"initial_quantity": pop.InitialQuantity + s.Quantity,
"current_quantity": pop.CurrentQuantity + s.Quantity,
"reserved_quantity": 0,
}).Error; err != nil { return err }
}
}
return nil
}
func ptr[T any](v T) *T {
return &v
}
@@ -1295,3 +1165,30 @@ func intPtr(v int) *int {
func uintPtr(v uint) *uint {
return &v
}
func ensurePFK(tx *gorm.DB, kandangName, locationName string, period int) (uint, error) {
var kandang entity.Kandang
if err := tx.Where("name = ?", kandangName).First(&kandang).Error; err != nil {
return 0, fmt.Errorf("kandang %q not found: %w", kandangName, err)
}
var loc entity.Location
if err := tx.Where("name = ?", locationName).First(&loc).Error; err != nil {
return 0, fmt.Errorf("location %q not found: %w", locationName, err)
}
var pf entity.ProjectFlock
if err := tx.Where("location_id = ? AND period = ?", loc.Id, period).First(&pf).Error; err != nil {
return 0, fmt.Errorf("project_flock for %s period %d not found: %w", locationName, period, err)
}
var pfk entity.ProjectFlockKandang
if err := tx.Where("project_flock_id = ? AND kandang_id = ?", pf.Id, kandang.Id).First(&pfk).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
pfk = entity.ProjectFlockKandang{ ProjectFlockId: pf.Id, KandangId: kandang.Id }
if err := tx.Create(&pfk).Error; err != nil {
return 0, fmt.Errorf("create pivot pfk(%d,%d) failed: %w", pf.Id, kandang.Id, err)
}
} else {
return 0, err
}
}
return pfk.Id, nil
}
+13 -14
View File
@@ -7,18 +7,17 @@ import (
)
type Kandang struct {
Id uint `gorm:"primaryKey"`
Name string `gorm:"not null;uniqueIndex:kandangs_name_unique,where:deleted_at IS NULL"`
Status string `gorm:"type:varchar(50);not null"`
LocationId uint `gorm:"not null"`
PicId uint `gorm:"not null"`
ProjectFlockId *uint `gorm:"column:project_flock_id"`
CreatedBy uint `gorm:"not null"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
Location Location `gorm:"foreignKey:LocationId;references:Id"`
Pic User `gorm:"foreignKey:PicId;references:Id"`
ProjectFlock *ProjectFlock `gorm:"foreignKey:ProjectFlockId;references:Id"`
Id uint `gorm:"primaryKey"`
Name string `gorm:"not null;uniqueIndex:kandangs_name_unique,where:deleted_at IS NULL"`
Status string `gorm:"type:varchar(50);not null"`
LocationId uint `gorm:"not null"`
PicId uint `gorm:"not null"`
CreatedBy uint `gorm:"not null"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
Location Location `gorm:"foreignKey:LocationId;references:Id"`
Pic User `gorm:"foreignKey:PicId;references:Id"`
ProjectFlockKandangs []ProjectFlockKandang `gorm:"foreignKey:KandangId;references:Id" json:"-"`
}
+1 -1
View File
@@ -10,7 +10,7 @@ const ()
type ProjectChickin struct {
Id uint `gorm:"primaryKey"`
ProjectFlockKandangId uint `gorm:"not null"`
ProjectFlockKandangId uint `gorm:"not null;index;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
ChickInDate time.Time `gorm:"not null"`
Quantity float64 `gorm:"not null"`
Note string `gorm:"type:text"`
@@ -8,7 +8,7 @@ import (
type ProjectFlockPopulation struct {
Id uint `gorm:"primaryKey"`
ProjectFlockKandangId uint `gorm:"not null"`
ProjectFlockKandangId uint `gorm:"not null;index;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
InitialQuantity float64 `gorm:"type:numeric(15,3);not null"`
CurrentQuantity float64 `gorm:"type:numeric(15,3);not null"`
ReservedQuantity float64 `gorm:"type:numeric(15,3)"`
@@ -18,5 +18,6 @@ type ProjectFlockPopulation struct {
DeletedAt gorm.DeletedAt `gorm:"index"`
ProjectFlockKandang *ProjectFlockKandang `gorm:"foreignKey:ProjectFlockKandangId;references:Id"`
CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"`
}
+4 -3
View File
@@ -24,7 +24,8 @@ type ProjectFlock struct {
Fcr Fcr `gorm:"foreignKey:FcrId;references:Id"`
Location Location `gorm:"foreignKey:LocationId;references:Id"`
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
Kandangs []Kandang `gorm:"foreignKey:ProjectFlockId;references:Id"`
KandangHistory []ProjectFlockKandang `gorm:"foreignKey:ProjectFlockId;references:Id"`
LatestApproval *Approval `gorm:"-" json:"-"`
Kandangs []Kandang `gorm:"many2many:project_flock_kandangs;joinTableForeignKey:project_flock_id;joinTableReferences:kandang_id" json:"kandangs,omitempty"`
KandangHistory []ProjectFlockKandang `gorm:"foreignKey:ProjectFlockId;references:Id" json:"-"`
LatestApproval *Approval `gorm:"-" json:"-"`
}
+5 -2
View File
@@ -7,6 +7,9 @@ type ProjectFlockKandang struct {
ProjectFlockId uint `gorm:"not null;index:idx_project_flock_kandangs_project;uniqueIndex:idx_project_flock_kandangs_unique"`
KandangId uint `gorm:"not null;index:idx_project_flock_kandangs_kandang;uniqueIndex:idx_project_flock_kandangs_unique"`
CreatedAt time.Time `gorm:"autoCreateTime"`
ProjectFlock ProjectFlock `gorm:"foreignKey:ProjectFlockId;references:Id"`
Kandang Kandang `gorm:"foreignKey:KandangId;references:Id"`
ProjectFlock ProjectFlock `gorm:"foreignKey:ProjectFlockId;references:Id"`
Kandang Kandang `gorm:"foreignKey:KandangId;references:Id"`
}
@@ -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 {