Merge branch 'feat/BE/US-74/pengajuan-flock' into 'development-before-sso'

FIX[BE]: name duplicate flock,projectflock category change,menerapkan dto...

See merge request mbugroup/lti-api!24
This commit is contained in:
Hafizh A. Y.
2025-10-21 06:54:27 +00:00
16 changed files with 390 additions and 341 deletions
@@ -0,0 +1,25 @@
BEGIN;
-- Recreate legacy columns on project_flock_kandangs
DROP INDEX IF EXISTS idx_project_flock_kandangs_unique;
ALTER TABLE project_flock_kandangs
ADD COLUMN IF NOT EXISTS created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE,
ADD COLUMN IF NOT EXISTS assigned_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
ADD COLUMN IF NOT EXISTS detached_at TIMESTAMPTZ,
ADD COLUMN IF NOT EXISTS updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW();
CREATE UNIQUE INDEX IF NOT EXISTS idx_project_flock_kandangs_active
ON project_flock_kandangs (project_flock_id, kandang_id)
WHERE detached_at IS NULL;
-- Restore product_category_id reference and drop category column
ALTER TABLE project_flocks
ADD COLUMN IF NOT EXISTS product_category_id BIGINT REFERENCES product_categories (id) ON DELETE RESTRICT ON UPDATE CASCADE;
ALTER TABLE project_flocks
DROP COLUMN IF EXISTS category;
COMMIT;
DROP INDEX IF EXISTS project_flocks_flock_period_unique;
@@ -0,0 +1,43 @@
BEGIN;
-- Add category column to project_flocks and backfill existing rows
ALTER TABLE project_flocks
ADD COLUMN IF NOT EXISTS category VARCHAR(20);
UPDATE project_flocks
SET category = 'GROWING'
WHERE category IS NULL;
ALTER TABLE project_flocks
ALTER COLUMN category SET NOT NULL;
ALTER TABLE project_flocks
ALTER COLUMN category SET DEFAULT 'GROWING';
-- Drop legacy foreign key reference and column
ALTER TABLE project_flocks
DROP CONSTRAINT IF EXISTS project_flocks_product_category_id_fkey;
ALTER TABLE project_flocks
DROP COLUMN IF EXISTS product_category_id;
-- Simplify project_flock_kandangs structure
DROP INDEX IF EXISTS idx_project_flock_kandangs_active;
ALTER TABLE project_flock_kandangs
DROP COLUMN IF EXISTS created_by,
DROP COLUMN IF EXISTS assigned_at,
DROP COLUMN IF EXISTS detached_at,
DROP COLUMN IF EXISTS updated_at;
ALTER TABLE project_flock_kandangs
ALTER COLUMN created_at SET DEFAULT NOW();
CREATE UNIQUE INDEX IF NOT EXISTS idx_project_flock_kandangs_unique
ON project_flock_kandangs (project_flock_id, kandang_id);
COMMIT;
CREATE UNIQUE INDEX project_flocks_flock_period_unique
ON project_flocks (flock_id, period)
WHERE deleted_at IS NULL;
@@ -1 +0,0 @@
DROP INDEX IF EXISTS project_flocks_flock_period_unique;
@@ -1,3 +0,0 @@
CREATE UNIQUE INDEX project_flocks_flock_period_unique
ON project_flocks (flock_id, period)
WHERE deleted_at IS NULL;
+17 -23
View File
@@ -50,7 +50,7 @@ func Run(db *gorm.DB) error {
return err return err
} }
projectFlocks, err := seedProjectFlocks(tx, adminID, flocks, areas, productCategories, fcrs, locations) projectFlocks, err := seedProjectFlocks(tx, adminID, flocks, areas, fcrs, locations)
if err != nil { if err != nil {
return err return err
} }
@@ -239,12 +239,12 @@ func seedFlocks(tx *gorm.DB, createdBy uint) (map[string]uint, error) {
return result, nil return result, nil
} }
func seedProjectFlocks(tx *gorm.DB, createdBy uint, flocks, areas, productCategories, fcrs, locations map[string]uint) (map[string]uint, error) { func seedProjectFlocks(tx *gorm.DB, createdBy uint, flocks, areas, fcrs, locations map[string]uint) (map[string]uint, error) {
seeds := []struct { seeds := []struct {
Key string Key string
Flock string Flock string
Area string Area string
ProductCategory string Category utils.ProjectFlockCategory
Fcr string Fcr string
Location string Location string
Period int Period int
@@ -253,7 +253,7 @@ func seedProjectFlocks(tx *gorm.DB, createdBy uint, flocks, areas, productCatego
Key: "Singaparna Period 1", Key: "Singaparna Period 1",
Flock: "Flock Priangan", Flock: "Flock Priangan",
Area: "Priangan", Area: "Priangan",
ProductCategory: "Day Old Chick", Category: utils.ProjectFlockCategoryGrowing,
Fcr: "FCR Layer", Fcr: "FCR Layer",
Location: "Singaparna", Location: "Singaparna",
Period: 1, Period: 1,
@@ -262,7 +262,7 @@ func seedProjectFlocks(tx *gorm.DB, createdBy uint, flocks, areas, productCatego
Key: "Cikaum Period 1", Key: "Cikaum Period 1",
Flock: "Flock Banten", Flock: "Flock Banten",
Area: "Banten", Area: "Banten",
ProductCategory: "Day Old Chick", Category: utils.ProjectFlockCategoryGrowing,
Fcr: "FCR Layer", Fcr: "FCR Layer",
Location: "Cikaum", Location: "Cikaum",
Period: 1, Period: 1,
@@ -280,10 +280,6 @@ func seedProjectFlocks(tx *gorm.DB, createdBy uint, flocks, areas, productCatego
if !ok { if !ok {
return nil, fmt.Errorf("area %s not seeded", seed.Area) return nil, fmt.Errorf("area %s not seeded", seed.Area)
} }
categoryID, ok := productCategories[seed.ProductCategory]
if !ok {
return nil, fmt.Errorf("product category %s not seeded", seed.ProductCategory)
}
fcrID, ok := fcrs[seed.Fcr] fcrID, ok := fcrs[seed.Fcr]
if !ok { if !ok {
return nil, fmt.Errorf("fcr %s not seeded", seed.Fcr) return nil, fmt.Errorf("fcr %s not seeded", seed.Fcr)
@@ -294,13 +290,13 @@ func seedProjectFlocks(tx *gorm.DB, createdBy uint, flocks, areas, productCatego
} }
var projectFlock entity.ProjectFlock var projectFlock entity.ProjectFlock
err := tx.Where("flock_id = ? AND area_id = ? AND product_category_id = ? AND fcr_id = ? AND location_id = ? AND period = ?", err := tx.Where("flock_id = ? AND area_id = ? AND category = ? AND fcr_id = ? AND location_id = ? AND period = ?",
flockID, areaID, categoryID, fcrID, locationID, seed.Period).First(&projectFlock).Error flockID, areaID, seed.Category, fcrID, locationID, seed.Period).First(&projectFlock).Error
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
projectFlock = entity.ProjectFlock{ projectFlock = entity.ProjectFlock{
FlockId: flockID, FlockId: flockID,
AreaId: areaID, AreaId: areaID,
ProductCategoryId: categoryID, Category: string(seed.Category),
FcrId: fcrID, FcrId: fcrID,
LocationId: locationID, LocationId: locationID,
Period: seed.Period, Period: seed.Period,
@@ -315,7 +311,7 @@ func seedProjectFlocks(tx *gorm.DB, createdBy uint, flocks, areas, productCatego
if err := tx.Model(&entity.ProjectFlock{}).Where("id = ?", projectFlock.Id).Updates(map[string]any{ if err := tx.Model(&entity.ProjectFlock{}).Where("id = ?", projectFlock.Id).Updates(map[string]any{
"flock_id": flockID, "flock_id": flockID,
"area_id": areaID, "area_id": areaID,
"product_category_id": categoryID, "category": string(seed.Category),
"fcr_id": fcrID, "fcr_id": fcrID,
"location_id": locationID, "location_id": locationID,
"period": seed.Period, "period": seed.Period,
@@ -378,7 +374,7 @@ func seedKandangs(tx *gorm.DB, createdBy uint, locations map[string]uint, users
if err := tx.Create(&kandang).Error; err != nil { if err := tx.Create(&kandang).Error; err != nil {
return nil, err return nil, err
} }
if err := syncPivotRelation(tx, projectFlockID, kandang.Id, createdBy); err != nil { if err := syncPivotRelation(tx, projectFlockID, kandang.Id); err != nil {
return nil, err return nil, err
} }
} else if err != nil { } else if err != nil {
@@ -397,7 +393,7 @@ func seedKandangs(tx *gorm.DB, createdBy uint, locations map[string]uint, users
if err := tx.Model(&entity.Kandang{}).Where("id = ?", kandang.Id).Updates(updates).Error; err != nil { if err := tx.Model(&entity.Kandang{}).Where("id = ?", kandang.Id).Updates(updates).Error; err != nil {
return nil, err return nil, err
} }
if err := syncPivotRelation(tx, projectFlockID, kandang.Id, createdBy); err != nil { if err := syncPivotRelation(tx, projectFlockID, kandang.Id); err != nil {
return nil, err return nil, err
} }
} }
@@ -407,25 +403,24 @@ func seedKandangs(tx *gorm.DB, createdBy uint, locations map[string]uint, users
return result, nil return result, nil
} }
func syncPivotRelation(tx *gorm.DB, projectFlockID *uint, kandangID uint, createdBy uint) error { func syncPivotRelation(tx *gorm.DB, projectFlockID *uint, kandangID uint) error {
if err := detachActivePivot(tx, kandangID); err != nil { if err := detachActivePivot(tx, kandangID); err != nil {
return err return err
} }
if projectFlockID == nil { if projectFlockID == nil {
return nil return nil
} }
return ensureActivePivot(tx, *projectFlockID, kandangID, createdBy) return ensureActivePivot(tx, *projectFlockID, kandangID)
} }
func detachActivePivot(tx *gorm.DB, kandangID uint) error { func detachActivePivot(tx *gorm.DB, kandangID uint) error {
return tx.Model(&entity.ProjectFlockKandang{}). return tx.Where("kandang_id = ?", kandangID).
Where("kandang_id = ? AND detached_at IS NULL", kandangID). Delete(&entity.ProjectFlockKandang{}).Error
Updates(map[string]any{"detached_at": time.Now()}).Error
} }
func ensureActivePivot(tx *gorm.DB, projectFlockID, kandangID, createdBy uint) error { func ensureActivePivot(tx *gorm.DB, projectFlockID, kandangID uint) error {
var pivot entity.ProjectFlockKandang var pivot entity.ProjectFlockKandang
err := tx.Where("project_flock_id = ? AND kandang_id = ? AND detached_at IS NULL", projectFlockID, kandangID). err := tx.Where("project_flock_id = ? AND kandang_id = ?", projectFlockID, kandangID).
First(&pivot).Error First(&pivot).Error
if err == nil { if err == nil {
return nil return nil
@@ -436,7 +431,6 @@ func ensureActivePivot(tx *gorm.DB, projectFlockID, kandangID, createdBy uint) e
newRecord := entity.ProjectFlockKandang{ newRecord := entity.ProjectFlockKandang{
ProjectFlockId: projectFlockID, ProjectFlockId: projectFlockID,
KandangId: kandangID, KandangId: kandangID,
CreatedBy: createdBy,
} }
return tx.Create(&newRecord).Error return tx.Create(&newRecord).Error
} }
+3 -4
View File
@@ -8,19 +8,18 @@ import (
type ProjectFlock struct { type ProjectFlock struct {
Id uint `gorm:"primaryKey"` Id uint `gorm:"primaryKey"`
FlockId uint `gorm:"not null;uniqueIndex:idx_project_flocks_flock_period,priority:1"` FlockId uint `gorm:"not null;uniqueIndex:project_flocks_flock_period_unique"`
AreaId uint `gorm:"not null"` AreaId uint `gorm:"not null"`
ProductCategoryId uint `gorm:"not null"` Category string `gorm:"type:varchar(20);not null"`
FcrId uint `gorm:"not null"` FcrId uint `gorm:"not null"`
LocationId uint `gorm:"not null"` LocationId uint `gorm:"not null"`
Period int `gorm:"not null;uniqueIndex:idx_project_flocks_flock_period,priority:2"` Period int `gorm:"not null;uniqueIndex:project_flocks_flock_period_unique"`
CreatedBy uint `gorm:"not null"` CreatedBy uint `gorm:"not null"`
CreatedAt time.Time `gorm:"autoCreateTime"` CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"` UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
Flock Flock `gorm:"foreignKey:FlockId;references:Id"` Flock Flock `gorm:"foreignKey:FlockId;references:Id"`
Area Area `gorm:"foreignKey:AreaId;references:Id"` Area Area `gorm:"foreignKey:AreaId;references:Id"`
ProductCategory ProductCategory `gorm:"foreignKey:ProductCategoryId;references:Id"`
Fcr Fcr `gorm:"foreignKey:FcrId;references:Id"` Fcr Fcr `gorm:"foreignKey:FcrId;references:Id"`
Location Location `gorm:"foreignKey:LocationId;references:Id"` Location Location `gorm:"foreignKey:LocationId;references:Id"`
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"` CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
+2 -7
View File
@@ -4,14 +4,9 @@ import "time"
type ProjectFlockKandang struct { type ProjectFlockKandang struct {
Id uint `gorm:"primaryKey"` Id uint `gorm:"primaryKey"`
ProjectFlockId uint `gorm:"not null;index:idx_project_flock_kandangs_project;uniqueIndex:idx_project_flock_kandangs_active,priority:1,where:detached_at IS NULL"` 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_active,priority:2,where:detached_at IS NULL"` KandangId uint `gorm:"not null;index:idx_project_flock_kandangs_kandang;uniqueIndex:idx_project_flock_kandangs_unique"`
CreatedBy uint `gorm:"not null"`
AssignedAt time.Time `gorm:"autoCreateTime"`
DetachedAt *time.Time `gorm:"index"`
CreatedAt time.Time `gorm:"autoCreateTime"` CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
ProjectFlock ProjectFlock `gorm:"foreignKey:ProjectFlockId;references:Id"` ProjectFlock ProjectFlock `gorm:"foreignKey:ProjectFlockId;references:Id"`
Kandang Kandang `gorm:"foreignKey:KandangId;references:Id"` Kandang Kandang `gorm:"foreignKey:KandangId;references:Id"`
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
} }
@@ -1,21 +1,30 @@
package repository package repository
import ( import (
entity "gitlab.com/mbugroup/lti-api.git/internal/entities" "context"
"gitlab.com/mbugroup/lti-api.git/internal/common/repository" "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gorm.io/gorm" "gorm.io/gorm"
) )
type FlockRepository interface { type FlockRepository interface {
repository.BaseRepository[entity.Flock] repository.BaseRepository[entity.Flock]
NameExists(ctx context.Context, name string, excludeID *uint) (bool, error)
} }
type FlockRepositoryImpl struct { type FlockRepositoryImpl struct {
*repository.BaseRepositoryImpl[entity.Flock] *repository.BaseRepositoryImpl[entity.Flock]
db *gorm.DB
} }
func NewFlockRepository(db *gorm.DB) FlockRepository { func NewFlockRepository(db *gorm.DB) FlockRepository {
return &FlockRepositoryImpl{ return &FlockRepositoryImpl{
BaseRepositoryImpl: repository.NewBaseRepository[entity.Flock](db), BaseRepositoryImpl: repository.NewBaseRepository[entity.Flock](db),
db: db,
} }
} }
func (r *FlockRepositoryImpl) NameExists(ctx context.Context, name string, excludeID *uint) (bool, error) {
return repository.ExistsByName[entity.Flock](ctx, r.db, name, excludeID)
}
@@ -2,6 +2,8 @@ package service
import ( import (
"errors" "errors"
"fmt"
"strings"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities" entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/repositories" repository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/repositories"
@@ -79,8 +81,22 @@ func (s *flockService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.
return nil, err return nil, err
} }
name := strings.TrimSpace(req.Name)
if name == "" {
return nil, fiber.NewError(fiber.StatusBadRequest, "Name is required")
}
exists, err := s.Repository.NameExists(c.Context(), name, nil)
if err != nil {
s.Log.Errorf("Failed to check flock name: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check flock name")
}
if exists {
return nil, fiber.NewError(fiber.StatusConflict, fmt.Sprintf("Flock with name %s already exists", name))
}
createBody := &entity.Flock{ createBody := &entity.Flock{
Name: req.Name, Name: name,
CreatedBy: 1, CreatedBy: 1,
} }
@@ -100,7 +116,20 @@ func (s flockService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (
updateBody := make(map[string]any) updateBody := make(map[string]any)
if req.Name != nil { if req.Name != nil {
updateBody["name"] = *req.Name name := strings.TrimSpace(*req.Name)
if name == "" {
return nil, fiber.NewError(fiber.StatusBadRequest, "Name cannot be empty")
}
exists, err := s.Repository.NameExists(c.Context(), name, &id)
if err != nil {
s.Log.Errorf("Failed to check flock name: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check flock name")
}
if exists {
return nil, fiber.NewError(fiber.StatusConflict, fmt.Sprintf("Flock with name %s already exists", name))
}
updateBody["name"] = name
} }
if len(updateBody) == 0 { if len(updateBody) == 0 {
@@ -4,72 +4,63 @@ import (
"time" "time"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities" entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
areaDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/areas/dto"
fcrDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/fcrs/dto"
flockDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/dto"
kandangDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/dto"
locationDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/locations/dto"
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto" userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
) )
type ProjectFlockBaseDTO struct { type ProjectFlockBaseDTO struct {
Id uint `json:"id"` Id uint `json:"id"`
// FlockId uint `json:"flock_id"`
// AreaId uint `json:"area_id"`
// ProductCategoryId uint `json:"product_category_id"`
// FcrId uint `json:"fcr_id"`
// LocationId uint `json:"location_id"`
Period int `json:"period"` Period int `json:"period"`
Category string `json:"category"`
Flock *flockDTO.FlockBaseDTO `json:"flock"`
Area *areaDTO.AreaBaseDTO `json:"area"`
Fcr *fcrDTO.FcrBaseDTO `json:"fcr"`
Location *locationDTO.LocationBaseDTO `json:"location"`
} }
func ToProjectFlockBaseDTO(e entity.ProjectFlock) ProjectFlockBaseDTO { func ToProjectFlockBaseDTO(e entity.ProjectFlock) ProjectFlockBaseDTO {
var flock *flockDTO.FlockBaseDTO
if e.Flock.Id != 0 {
mapped := flockDTO.ToFlockBaseDTO(e.Flock)
flock = &mapped
}
var area *areaDTO.AreaBaseDTO
if e.Area.Id != 0 {
mapped := areaDTO.ToAreaBaseDTO(e.Area)
area = &mapped
}
var fcr *fcrDTO.FcrBaseDTO
if e.Fcr.Id != 0 {
mapped := fcrDTO.ToFcrBaseDTO(e.Fcr)
fcr = &mapped
}
var location *locationDTO.LocationBaseDTO
if e.Location.Id != 0 {
mapped := locationDTO.ToLocationBaseDTO(e.Location)
location = &mapped
}
return ProjectFlockBaseDTO{ return ProjectFlockBaseDTO{
Id: e.Id, Id: e.Id,
// FlockId: e.FlockId,
// AreaId: e.AreaId,
// ProductCategoryId: e.ProductCategoryId,
// FcrId: e.FcrId,
// LocationId: e.LocationId,
Period: e.Period, Period: e.Period,
Category: e.Category,
Flock: flock,
Area: area,
Fcr: fcr,
Location: location,
} }
} }
type FlockSummaryDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
}
type AreaSummaryDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
}
type ProductCategorySummaryDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
Code string `json:"code"`
}
type FcrSummaryDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
}
type LocationSummaryDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
Address string `json:"address"`
}
type KandangSummaryDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
Status string `json:"status"`
}
type ProjectFlockListDTO struct { type ProjectFlockListDTO struct {
ProjectFlockBaseDTO ProjectFlockBaseDTO
Flock *FlockSummaryDTO `json:"flock,omitempty"` Kandangs []kandangDTO.KandangBaseDTO `json:"kandangs,omitempty"`
Area *AreaSummaryDTO `json:"area,omitempty"`
ProductCategory *ProductCategorySummaryDTO `json:"product_category,omitempty"`
Fcr *FcrSummaryDTO `json:"fcr,omitempty"`
Location *LocationSummaryDTO `json:"location,omitempty"`
Kandangs []KandangSummaryDTO `json:"kandangs,omitempty"`
CreatedUser *userDTO.UserBaseDTO `json:"created_user"` CreatedUser *userDTO.UserBaseDTO `json:"created_user"`
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"` UpdatedAt time.Time `json:"updated_at"`
@@ -80,7 +71,7 @@ type ProjectFlockDetailDTO struct {
} }
type FlockPeriodSummaryDTO struct { type FlockPeriodSummaryDTO struct {
Flock FlockSummaryDTO `json:"flock"` Flock flockDTO.FlockBaseDTO `json:"flock"`
NextPeriod int `json:"next_period"` NextPeriod int `json:"next_period"`
} }
@@ -91,62 +82,16 @@ func ToProjectFlockListDTO(e entity.ProjectFlock) ProjectFlockListDTO {
createdUser = &mapped createdUser = &mapped
} }
var flockSummary *FlockSummaryDTO var kandangSummaries []kandangDTO.KandangBaseDTO
if e.Flock.Id != 0 { if len(e.Kandangs) > 0 {
summary := ToFlockSummaryDTO(e.Flock) kandangSummaries = make([]kandangDTO.KandangBaseDTO, len(e.Kandangs))
flockSummary = &summary
}
var areaSummary *AreaSummaryDTO
if e.Area.Id != 0 {
areaSummary = &AreaSummaryDTO{
Id: e.Area.Id,
Name: e.Area.Name,
}
}
var categorySummary *ProductCategorySummaryDTO
if e.ProductCategory.Id != 0 {
categorySummary = &ProductCategorySummaryDTO{
Id: e.ProductCategory.Id,
Name: e.ProductCategory.Name,
Code: e.ProductCategory.Code,
}
}
var fcrSummary *FcrSummaryDTO
if e.Fcr.Id != 0 {
fcrSummary = &FcrSummaryDTO{
Id: e.Fcr.Id,
Name: e.Fcr.Name,
}
}
var locationSummary *LocationSummaryDTO
if e.Location.Id != 0 {
locationSummary = &LocationSummaryDTO{
Id: e.Location.Id,
Name: e.Location.Name,
Address: e.Location.Address,
}
}
kandangSummaries := make([]KandangSummaryDTO, len(e.Kandangs))
for i, kandang := range e.Kandangs { for i, kandang := range e.Kandangs {
kandangSummaries[i] = KandangSummaryDTO{ kandangSummaries[i] = kandangDTO.ToKandangBaseDTO(kandang)
Id: kandang.Id,
Name: kandang.Name,
Status: kandang.Status,
} }
} }
return ProjectFlockListDTO{ return ProjectFlockListDTO{
ProjectFlockBaseDTO: ToProjectFlockBaseDTO(e), ProjectFlockBaseDTO: ToProjectFlockBaseDTO(e),
Flock: flockSummary,
Area: areaSummary,
ProductCategory: categorySummary,
Fcr: fcrSummary,
Location: locationSummary,
Kandangs: kandangSummaries, Kandangs: kandangSummaries,
CreatedAt: e.CreatedAt, CreatedAt: e.CreatedAt,
UpdatedAt: e.UpdatedAt, UpdatedAt: e.UpdatedAt,
@@ -168,16 +113,9 @@ func ToProjectFlockDetailDTO(e entity.ProjectFlock) ProjectFlockDetailDTO {
} }
} }
func ToFlockSummaryDTO(e entity.Flock) FlockSummaryDTO {
return FlockSummaryDTO{
Id: e.Id,
Name: e.Name,
}
}
func ToFlockPeriodSummaryDTO(flock entity.Flock, next int) FlockPeriodSummaryDTO { func ToFlockPeriodSummaryDTO(flock entity.Flock, next int) FlockPeriodSummaryDTO {
return FlockPeriodSummaryDTO{ return FlockPeriodSummaryDTO{
Flock: ToFlockSummaryDTO(flock), Flock: flockDTO.ToFlockBaseDTO(flock),
NextPeriod: next, NextPeriod: next,
} }
} }
@@ -2,7 +2,6 @@ package repository
import ( import (
"context" "context"
"time"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities" entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gorm.io/gorm" "gorm.io/gorm"
@@ -10,7 +9,7 @@ import (
type ProjectFlockKandangRepository interface { type ProjectFlockKandangRepository interface {
CreateMany(ctx context.Context, records []*entity.ProjectFlockKandang) error CreateMany(ctx context.Context, records []*entity.ProjectFlockKandang) error
MarkDetached(ctx context.Context, projectFlockID uint, kandangIDs []uint, detachedAt time.Time) error DeleteMany(ctx context.Context, projectFlockID uint, kandangIDs []uint) error
GetAll(ctx context.Context) ([]entity.ProjectFlockKandang, error) GetAll(ctx context.Context) ([]entity.ProjectFlockKandang, error)
WithTx(tx *gorm.DB) ProjectFlockKandangRepository WithTx(tx *gorm.DB) ProjectFlockKandangRepository
DB() *gorm.DB DB() *gorm.DB
@@ -31,14 +30,13 @@ func (r *projectFlockKandangRepositoryImpl) CreateMany(ctx context.Context, reco
return r.db.WithContext(ctx).Create(&records).Error return r.db.WithContext(ctx).Create(&records).Error
} }
func (r *projectFlockKandangRepositoryImpl) MarkDetached(ctx context.Context, projectFlockID uint, kandangIDs []uint, detachedAt time.Time) error { func (r *projectFlockKandangRepositoryImpl) DeleteMany(ctx context.Context, projectFlockID uint, kandangIDs []uint) error {
if len(kandangIDs) == 0 { if len(kandangIDs) == 0 {
return nil return nil
} }
return r.db.WithContext(ctx). return r.db.WithContext(ctx).
Model(&entity.ProjectFlockKandang{}). Where("project_flock_id = ? AND kandang_id IN ?", projectFlockID, kandangIDs).
Where("project_flock_id = ? AND kandang_id IN ? AND detached_at IS NULL", projectFlockID, kandangIDs). Delete(&entity.ProjectFlockKandang{}).Error
Updates(map[string]any{"detached_at": detachedAt}).Error
} }
func (r *projectFlockKandangRepositoryImpl) GetAll(ctx context.Context) ([]entity.ProjectFlockKandang, error) { func (r *projectFlockKandangRepositoryImpl) GetAll(ctx context.Context) ([]entity.ProjectFlockKandang, error) {
@@ -47,8 +45,7 @@ func (r *projectFlockKandangRepositoryImpl) GetAll(ctx context.Context) ([]entit
Preload("ProjectFlock"). Preload("ProjectFlock").
Preload("ProjectFlock.Flock"). Preload("ProjectFlock.Flock").
Preload("Kandang"). Preload("Kandang").
Preload("CreatedUser"). Order("project_flock_id ASC, created_at ASC").
Order("project_flock_id ASC, assigned_at ASC").
Find(&records).Error; err != nil { Find(&records).Error; err != nil {
return nil, err return nil, err
} }
@@ -5,7 +5,6 @@ import (
"errors" "errors"
"fmt" "fmt"
"strings" "strings"
"time"
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository" commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
common "gitlab.com/mbugroup/lti-api.git/internal/common/service" common "gitlab.com/mbugroup/lti-api.git/internal/common/service"
@@ -67,7 +66,6 @@ func (s projectflockService) withRelations(db *gorm.DB) *gorm.DB {
Preload("CreatedUser"). Preload("CreatedUser").
Preload("Flock"). Preload("Flock").
Preload("Area"). Preload("Area").
Preload("ProductCategory").
Preload("Fcr"). Preload("Fcr").
Preload("Location"). Preload("Location").
Preload("Kandangs") Preload("Kandangs")
@@ -115,15 +113,13 @@ func (s projectflockService) GetAll(c *fiber.Ctx, params *validation.Query) ([]e
db = db. db = db.
Joins("LEFT JOIN flocks ON flocks.id = project_flocks.flock_id"). Joins("LEFT JOIN flocks ON flocks.id = project_flocks.flock_id").
Joins("LEFT JOIN areas ON areas.id = project_flocks.area_id"). Joins("LEFT JOIN areas ON areas.id = project_flocks.area_id").
Joins("LEFT JOIN product_categories ON product_categories.id = project_flocks.product_category_id").
Joins("LEFT JOIN fcrs ON fcrs.id = project_flocks.fcr_id"). Joins("LEFT JOIN fcrs ON fcrs.id = project_flocks.fcr_id").
Joins("LEFT JOIN locations ON locations.id = project_flocks.location_id"). Joins("LEFT JOIN locations ON locations.id = project_flocks.location_id").
Joins("LEFT JOIN users AS created_users ON created_users.id = project_flocks.created_by"). Joins("LEFT JOIN users AS created_users ON created_users.id = project_flocks.created_by").
Where(` Where(`
LOWER(flocks.name) LIKE ? LOWER(flocks.name) LIKE ?
OR LOWER(areas.name) LIKE ? OR LOWER(areas.name) LIKE ?
OR LOWER(product_categories.name) LIKE ? OR LOWER(project_flocks.category) LIKE ?
OR LOWER(product_categories.code) LIKE ?
OR LOWER(fcrs.name) LIKE ? OR LOWER(fcrs.name) LIKE ?
OR LOWER(locations.name) LIKE ? OR LOWER(locations.name) LIKE ?
OR LOWER(locations.address) LIKE ? OR LOWER(locations.address) LIKE ?
@@ -146,7 +142,6 @@ func (s projectflockService) GetAll(c *fiber.Ctx, params *validation.Query) ([]e
likeQuery, likeQuery,
likeQuery, likeQuery,
likeQuery, likeQuery,
likeQuery,
) )
} }
for _, expr := range s.buildOrderExpressions(params.SortBy, params.SortOrder) { for _, expr := range s.buildOrderExpressions(params.SortBy, params.SortOrder) {
@@ -179,6 +174,11 @@ func (s *projectflockService) CreateOne(c *fiber.Ctx, req *validation.Create) (*
return nil, err return nil, err
} }
category, ok := utils.NormalizeProjectFlockCategory(req.Category)
if !ok {
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid category")
}
if len(req.KandangIds) == 0 { if len(req.KandangIds) == 0 {
return nil, fiber.NewError(fiber.StatusBadRequest, "kandang_ids is required") return nil, fiber.NewError(fiber.StatusBadRequest, "kandang_ids is required")
} }
@@ -186,7 +186,6 @@ func (s *projectflockService) CreateOne(c *fiber.Ctx, req *validation.Create) (*
if err := common.EnsureRelations(c.Context(), if err := common.EnsureRelations(c.Context(),
common.RelationCheck{Name: "Flock", ID: &req.FlockId, Exists: relationExistsChecker[entity.Flock](s.Repository.DB())}, common.RelationCheck{Name: "Flock", ID: &req.FlockId, Exists: relationExistsChecker[entity.Flock](s.Repository.DB())},
common.RelationCheck{Name: "Area", ID: &req.AreaId, Exists: relationExistsChecker[entity.Area](s.Repository.DB())}, common.RelationCheck{Name: "Area", ID: &req.AreaId, Exists: relationExistsChecker[entity.Area](s.Repository.DB())},
common.RelationCheck{Name: "Product category", ID: &req.ProductCategoryId, Exists: relationExistsChecker[entity.ProductCategory](s.Repository.DB())},
common.RelationCheck{Name: "FCR", ID: &req.FcrId, Exists: relationExistsChecker[entity.Fcr](s.Repository.DB())}, common.RelationCheck{Name: "FCR", ID: &req.FcrId, Exists: relationExistsChecker[entity.Fcr](s.Repository.DB())},
common.RelationCheck{Name: "Location", ID: &req.LocationId, Exists: relationExistsChecker[entity.Location](s.Repository.DB())}, common.RelationCheck{Name: "Location", ID: &req.LocationId, Exists: relationExistsChecker[entity.Location](s.Repository.DB())},
); err != nil { ); err != nil {
@@ -226,7 +225,7 @@ func (s *projectflockService) CreateOne(c *fiber.Ctx, req *validation.Create) (*
createBody := &entity.ProjectFlock{ createBody := &entity.ProjectFlock{
FlockId: req.FlockId, FlockId: req.FlockId,
AreaId: req.AreaId, AreaId: req.AreaId,
ProductCategoryId: req.ProductCategoryId, Category: string(category),
FcrId: req.FcrId, FcrId: req.FcrId,
LocationId: req.LocationId, LocationId: req.LocationId,
Period: nextPeriod, Period: nextPeriod,
@@ -242,7 +241,7 @@ func (s *projectflockService) CreateOne(c *fiber.Ctx, req *validation.Create) (*
return nil, err return nil, err
} }
if err := s.attachKandangs(c.Context(), tx, createBody.Id, kandangIDs, createBody.CreatedBy); err != nil { if err := s.attachKandangs(c.Context(), tx, createBody.Id, kandangIDs); err != nil {
tx.Rollback() tx.Rollback()
s.Log.Errorf("Failed to attach kandangs to projectflock %d: %+v", createBody.Id, err) s.Log.Errorf("Failed to attach kandangs to projectflock %d: %+v", createBody.Id, err)
return nil, err return nil, err
@@ -289,13 +288,12 @@ func (s projectflockService) UpdateOne(c *fiber.Ctx, req *validation.Update, id
Exists: relationExistsChecker[entity.Area](s.Repository.DB()), Exists: relationExistsChecker[entity.Area](s.Repository.DB()),
}) })
} }
if req.ProductCategoryId != nil { if req.Category != nil {
updateBody["product_category_id"] = *req.ProductCategoryId if normalized, ok := utils.NormalizeProjectFlockCategory(*req.Category); ok {
relationChecks = append(relationChecks, common.RelationCheck{ updateBody["category"] = string(normalized)
Name: "Product category", } else {
ID: req.ProductCategoryId, return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid category")
Exists: relationExistsChecker[entity.ProductCategory](s.Repository.DB()), }
})
} }
if req.FcrId != nil { if req.FcrId != nil {
updateBody["fcr_id"] = *req.FcrId updateBody["fcr_id"] = *req.FcrId
@@ -396,7 +394,7 @@ func (s projectflockService) UpdateOne(c *fiber.Ctx, req *validation.Update, id
} }
if len(toAttach) > 0 { if len(toAttach) > 0 {
if err := s.attachKandangs(c.Context(), tx, id, toAttach, existing.CreatedBy); err != nil { if err := s.attachKandangs(c.Context(), tx, id, toAttach); err != nil {
tx.Rollback() tx.Rollback()
s.Log.Errorf("Failed to attach kandangs to projectflock %d: %+v", id, err) s.Log.Errorf("Failed to attach kandangs to projectflock %d: %+v", id, err)
return nil, err return nil, err
@@ -534,14 +532,17 @@ func (s projectflockService) buildOrderExpressions(sortBy, sortOrder string) []s
} }
} }
func (s projectflockService) attachKandangs(ctx context.Context, tx *gorm.DB, projectFlockID uint, kandangIDs []uint, createdBy uint) error { func (s projectflockService) attachKandangs(ctx context.Context, tx *gorm.DB, projectFlockID uint, kandangIDs []uint) error {
if len(kandangIDs) == 0 { if len(kandangIDs) == 0 {
return nil return nil
} }
if err := tx.Model(&entity.Kandang{}). if err := tx.Model(&entity.Kandang{}).
Where("id IN ?", kandangIDs). Where("id IN ?", kandangIDs).
Updates(map[string]any{"project_flock_id": projectFlockID}).Error; err != nil { Updates(map[string]any{
"project_flock_id": projectFlockID,
"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")
} }
@@ -551,7 +552,6 @@ func (s projectflockService) attachKandangs(ctx context.Context, tx *gorm.DB, pr
records[i] = &entity.ProjectFlockKandang{ records[i] = &entity.ProjectFlockKandang{
ProjectFlockId: projectFlockID, ProjectFlockId: projectFlockID,
KandangId: id, KandangId: id,
CreatedBy: createdBy,
} }
} }
if err := pivotRepo.CreateMany(ctx, records); err != nil { if err := pivotRepo.CreateMany(ctx, records); err != nil {
@@ -576,7 +576,7 @@ func (s projectflockService) detachKandangs(ctx context.Context, tx *gorm.DB, pr
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update kandangs") return fiber.NewError(fiber.StatusInternalServerError, "Failed to update kandangs")
} }
if err := s.pivotRepoWithTx(tx).MarkDetached(ctx, projectFlockID, kandangIDs, time.Now()); err != nil { if err := s.pivotRepoWithTx(tx).DeleteMany(ctx, projectFlockID, kandangIDs); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to persist project flock history") return fiber.NewError(fiber.StatusInternalServerError, "Failed to persist project flock history")
} }
return nil return nil
@@ -3,7 +3,7 @@ package validation
type Create struct { type Create struct {
FlockId uint `json:"flock_id" validate:"required_strict,number,gt=0"` FlockId uint `json:"flock_id" validate:"required_strict,number,gt=0"`
AreaId uint `json:"area_id" validate:"required_strict,number,gt=0"` AreaId uint `json:"area_id" validate:"required_strict,number,gt=0"`
ProductCategoryId uint `json:"product_category_id" validate:"required_strict,number,gt=0"` Category string `json:"category" validate:"required_strict,oneof=growing laying GROWING LAYING"`
FcrId uint `json:"fcr_id" validate:"required_strict,number,gt=0"` FcrId uint `json:"fcr_id" validate:"required_strict,number,gt=0"`
LocationId uint `json:"location_id" validate:"required_strict,number,gt=0"` LocationId uint `json:"location_id" validate:"required_strict,number,gt=0"`
KandangIds []uint `json:"kandang_ids" validate:"required,min=1,dive,gt=0"` KandangIds []uint `json:"kandang_ids" validate:"required,min=1,dive,gt=0"`
@@ -12,7 +12,7 @@ type Create struct {
type Update struct { type Update struct {
FlockId *uint `json:"flock_id,omitempty" validate:"omitempty,number,gt=0"` FlockId *uint `json:"flock_id,omitempty" validate:"omitempty,number,gt=0"`
AreaId *uint `json:"area_id,omitempty" validate:"omitempty,number,gt=0"` AreaId *uint `json:"area_id,omitempty" validate:"omitempty,number,gt=0"`
ProductCategoryId *uint `json:"product_category_id,omitempty" validate:"omitempty,number,gt=0"` Category *string `json:"category,omitempty" validate:"omitempty,oneof=growing laying GROWING LAYING"`
FcrId *uint `json:"fcr_id,omitempty" validate:"omitempty,number,gt=0"` FcrId *uint `json:"fcr_id,omitempty" validate:"omitempty,number,gt=0"`
LocationId *uint `json:"location_id,omitempty" validate:"omitempty,number,gt=0"` LocationId *uint `json:"location_id,omitempty" validate:"omitempty,number,gt=0"`
Period *int `json:"period,omitempty" validate:"omitempty,number,gt=0"` Period *int `json:"period,omitempty" validate:"omitempty,number,gt=0"`
+27 -5
View File
@@ -59,7 +59,6 @@ var allFlagTypes = func() map[FlagType]struct{} {
return m return m
}() }()
func AllFlagTypes() map[FlagType]struct{} { func AllFlagTypes() map[FlagType]struct{} {
return allFlagTypes return allFlagTypes
} }
@@ -76,8 +75,6 @@ const (
WarehouseTypeKandang WarehouseType = "KANDANG" WarehouseTypeKandang WarehouseType = "KANDANG"
) )
// ------------------------------------------------------------------- // -------------------------------------------------------------------
// WarehouseType // WarehouseType
// ------------------------------------------------------------------- // -------------------------------------------------------------------
@@ -100,8 +97,6 @@ const (
SupplierCategorySapronak SupplierCategory = "SAPRONAK" SupplierCategorySapronak SupplierCategory = "SAPRONAK"
) )
// ------------------------------------------------------------------- // -------------------------------------------------------------------
// Kandang Status // Kandang Status
// ------------------------------------------------------------------- // -------------------------------------------------------------------
@@ -113,6 +108,18 @@ const (
KandangStatusPengajuan KandangStatus = "PENGAJUAN" KandangStatusPengajuan KandangStatus = "PENGAJUAN"
KandangStatusActive KandangStatus = "ACTIVE" KandangStatusActive KandangStatus = "ACTIVE"
) )
// -------------------------------------------------------------------
// ProjectFlockCategory
// -------------------------------------------------------------------
type ProjectFlockCategory string
const (
ProjectFlockCategoryGrowing ProjectFlockCategory = "GROWING"
ProjectFlockCategoryLaying ProjectFlockCategory = "LAYING"
)
// ------------------------------------------------------------------- // -------------------------------------------------------------------
// Validators // Validators
// ------------------------------------------------------------------- // -------------------------------------------------------------------
@@ -223,6 +230,21 @@ func IsValidCustomerSupplierType(v string) bool {
return false return false
} }
func NormalizeProjectFlockCategory(v string) (ProjectFlockCategory, bool) {
normalized := ProjectFlockCategory(strings.ToUpper(strings.TrimSpace(v)))
switch normalized {
case ProjectFlockCategoryGrowing, ProjectFlockCategoryLaying:
return normalized, true
default:
return "", false
}
}
func IsValidProjectFlockCategory(v string) bool {
_, ok := NormalizeProjectFlockCategory(v)
return ok
}
func IsValidSupplierCategory(v string) bool { func IsValidSupplierCategory(v string) bool {
switch SupplierCategory(v) { switch SupplierCategory(v) {
case SupplierCategoryBOP, SupplierCategorySapronak: case SupplierCategoryBOP, SupplierCategorySapronak:
+2 -2
View File
@@ -8,6 +8,7 @@ import (
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
"gitlab.com/mbugroup/lti-api.git/internal/entities" "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
) )
func TestKandangIntegration(t *testing.T) { func TestKandangIntegration(t *testing.T) {
@@ -51,7 +52,6 @@ func TestKandangIntegration(t *testing.T) {
}) })
t.Run("cannot assign project floc with existing active kandang", func(t *testing.T) { t.Run("cannot assign project floc with existing active kandang", func(t *testing.T) {
categoryID := createProductCategory(t, app, "DOC Category", "DOC1")
fcrID := createFcr(t, app, "FCR For Floc", []map[string]any{ fcrID := createFcr(t, app, "FCR For Floc", []map[string]any{
{"weight": 1.0, "fcr_number": 1.5, "mortality": 2.0}, {"weight": 1.0, "fcr_number": 1.5, "mortality": 2.0},
}) })
@@ -60,7 +60,7 @@ func TestKandangIntegration(t *testing.T) {
projectFloc := entities.ProjectFlock{ projectFloc := entities.ProjectFlock{
FlockId: flocID, FlockId: flocID,
AreaId: areaID, AreaId: areaID,
ProductCategoryId: categoryID, Category: string(utils.ProjectFlockCategoryGrowing),
FcrId: fcrID, FcrId: fcrID,
LocationId: locationID, LocationId: locationID,
Period: 1, Period: 1,
@@ -19,7 +19,6 @@ func TestProjectFlockSummary(t *testing.T) {
areaID := createArea(t, app, "Area Project") areaID := createArea(t, app, "Area Project")
locationID := createLocation(t, app, "Location Project", "Address", areaID) locationID := createLocation(t, app, "Location Project", "Address", areaID)
flockID := createFlock(t, app, "Flock Summary") flockID := createFlock(t, app, "Flock Summary")
categoryID := createProductCategory(t, app, "DOC Summary", "DOCS")
fcrID := createFcr(t, app, "FCR Summary", []map[string]any{ fcrID := createFcr(t, app, "FCR Summary", []map[string]any{
{"weight": 1.0, "fcr_number": 1.5, "mortality": 2.0}, {"weight": 1.0, "fcr_number": 1.5, "mortality": 2.0},
}) })
@@ -28,7 +27,7 @@ func TestProjectFlockSummary(t *testing.T) {
createPayload := map[string]any{ createPayload := map[string]any{
"flock_id": flockID, "flock_id": flockID,
"area_id": areaID, "area_id": areaID,
"product_category_id": categoryID, "category": "growing",
"fcr_id": fcrID, "fcr_id": fcrID,
"location_id": locationID, "location_id": locationID,
"kandang_ids": []uint{kandangID}, "kandang_ids": []uint{kandangID},
@@ -42,6 +41,7 @@ func TestProjectFlockSummary(t *testing.T) {
Data struct { Data struct {
Id uint `json:"id"` Id uint `json:"id"`
Period int `json:"period"` Period int `json:"period"`
Category string `json:"category"`
Flock struct { Flock struct {
Id uint `json:"id"` Id uint `json:"id"`
Name string `json:"name"` Name string `json:"name"`
@@ -50,11 +50,6 @@ func TestProjectFlockSummary(t *testing.T) {
Id uint `json:"id"` Id uint `json:"id"`
Name string `json:"name"` Name string `json:"name"`
} `json:"area"` } `json:"area"`
ProductCategory struct {
Id uint `json:"id"`
Name string `json:"name"`
Code string `json:"code"`
} `json:"product_category"`
Fcr struct { Fcr struct {
Id uint `json:"id"` Id uint `json:"id"`
Name string `json:"name"` Name string `json:"name"`
@@ -86,19 +81,27 @@ func TestProjectFlockSummary(t *testing.T) {
if createResp.Data.Area.Id != areaID || createResp.Data.Area.Name == "" { if createResp.Data.Area.Id != areaID || createResp.Data.Area.Name == "" {
t.Fatalf("expected area detail to be present, got %+v", createResp.Data.Area) t.Fatalf("expected area detail to be present, got %+v", createResp.Data.Area)
} }
if createResp.Data.Category != string(utils.ProjectFlockCategoryGrowing) {
t.Fatalf("expected category to be %s, got %s", utils.ProjectFlockCategoryGrowing, createResp.Data.Category)
}
if createResp.Data.Location.Id != locationID || createResp.Data.Location.Name == "" { if createResp.Data.Location.Id != locationID || createResp.Data.Location.Name == "" {
t.Fatalf("expected location detail to be present, got %+v", createResp.Data.Location) t.Fatalf("expected location detail to be present, got %+v", createResp.Data.Location)
} }
if len(createResp.Data.Kandangs) != 1 || createResp.Data.Kandangs[0].Id != kandangID { if len(createResp.Data.Kandangs) != 1 || createResp.Data.Kandangs[0].Id != kandangID {
t.Fatalf("expected kandang detail to be present, got %+v", createResp.Data.Kandangs) t.Fatalf("expected kandang detail to be present, got %+v", createResp.Data.Kandangs)
} }
if createResp.Data.Kandangs[0].Status == "" { if createResp.Data.Kandangs[0].Status != string(utils.KandangStatusPengajuan) {
t.Fatalf("expected kandang status to be present, got %+v", createResp.Data.Kandangs[0]) t.Fatalf("expected kandang status to be PENGAJUAN, got %s", createResp.Data.Kandangs[0].Status)
} }
if createResp.Data.Period != 1 { if createResp.Data.Period != 1 {
t.Fatalf("expected period 1 to be assigned automatically, got %d", createResp.Data.Period) t.Fatalf("expected period 1 to be assigned automatically, got %d", createResp.Data.Period)
} }
createdKandang := fetchKandang(t, db, kandangID)
if createdKandang.Status != string(utils.KandangStatusPengajuan) {
t.Fatalf("expected kandang status in DB to be PENGAJUAN, got %s", createdKandang.Status)
}
var pivotRecords []entities.ProjectFlockKandang var pivotRecords []entities.ProjectFlockKandang
if err := db.Where("project_flock_id = ?", createResp.Data.Id).Find(&pivotRecords).Error; err != nil { if err := db.Where("project_flock_id = ?", createResp.Data.Id).Find(&pivotRecords).Error; err != nil {
t.Fatalf("failed to fetch pivot records: %v", err) t.Fatalf("failed to fetch pivot records: %v", err)
@@ -110,15 +113,12 @@ func TestProjectFlockSummary(t *testing.T) {
if firstPivotRecord.KandangId != kandangID { if firstPivotRecord.KandangId != kandangID {
t.Fatalf("expected pivot kandang id %d, got %d", kandangID, firstPivotRecord.KandangId) t.Fatalf("expected pivot kandang id %d, got %d", kandangID, firstPivotRecord.KandangId)
} }
if firstPivotRecord.DetachedAt != nil {
t.Fatalf("expected pivot DetachedAt to be nil for active assignment, got %v", firstPivotRecord.DetachedAt)
}
secondKandangID := createKandang(t, app, "Kandang Summary 2", locationID, 1) secondKandangID := createKandang(t, app, "Kandang Summary 2", locationID, 1)
secondPayload := map[string]any{ secondPayload := map[string]any{
"flock_id": flockID, "flock_id": flockID,
"area_id": areaID, "area_id": areaID,
"product_category_id": categoryID, "category": "laying",
"fcr_id": fcrID, "fcr_id": fcrID,
"location_id": locationID, "location_id": locationID,
"kandang_ids": []uint{secondKandangID}, "kandang_ids": []uint{secondKandangID},
@@ -131,6 +131,7 @@ func TestProjectFlockSummary(t *testing.T) {
Data struct { Data struct {
Id uint `json:"id"` Id uint `json:"id"`
Period int `json:"period"` Period int `json:"period"`
Category string `json:"category"`
} `json:"data"` } `json:"data"`
} }
if err := json.Unmarshal(body, &createRespSecond); err != nil { if err := json.Unmarshal(body, &createRespSecond); err != nil {
@@ -139,6 +140,9 @@ func TestProjectFlockSummary(t *testing.T) {
if createRespSecond.Data.Period != 2 { if createRespSecond.Data.Period != 2 {
t.Fatalf("expected second period to be 2, got %d", createRespSecond.Data.Period) t.Fatalf("expected second period to be 2, got %d", createRespSecond.Data.Period)
} }
if createRespSecond.Data.Category != string(utils.ProjectFlockCategoryLaying) {
t.Fatalf("expected category to be %s, got %s", utils.ProjectFlockCategoryLaying, createRespSecond.Data.Category)
}
pivotRecords = nil pivotRecords = nil
if err := db.Where("project_flock_id = ?", createRespSecond.Data.Id).Find(&pivotRecords).Error; err != nil { if err := db.Where("project_flock_id = ?", createRespSecond.Data.Id).Find(&pivotRecords).Error; err != nil {
@@ -151,8 +155,10 @@ func TestProjectFlockSummary(t *testing.T) {
if secondPivotRecord.KandangId != secondKandangID { if secondPivotRecord.KandangId != secondKandangID {
t.Fatalf("expected second pivot kandang id %d, got %d", secondKandangID, secondPivotRecord.KandangId) t.Fatalf("expected second pivot kandang id %d, got %d", secondKandangID, secondPivotRecord.KandangId)
} }
if secondPivotRecord.DetachedAt != nil {
t.Fatalf("expected second pivot DetachedAt to be nil, got %v", secondPivotRecord.DetachedAt) secondKandang := fetchKandang(t, db, secondKandangID)
if secondKandang.Status != string(utils.KandangStatusPengajuan) {
t.Fatalf("expected second kandang status in DB to be PENGAJUAN, got %s", secondKandang.Status)
} }
resp, body = doJSONRequest(t, app, http.MethodGet, "/api/production/project_flocks/flocks/"+uintToString(flockID)+"/periods", nil) resp, body = doJSONRequest(t, app, http.MethodGet, "/api/production/project_flocks/flocks/"+uintToString(flockID)+"/periods", nil)
@@ -186,15 +192,14 @@ func TestProjectFlockSummary(t *testing.T) {
t.Fatalf("expected kandang status to revert to NON_ACTIVE, got %s", firstKandang.Status) t.Fatalf("expected kandang status to revert to NON_ACTIVE, got %s", firstKandang.Status)
} }
var firstPivot entities.ProjectFlockKandang var remainingFirst int64
if err := db.First(&firstPivot, firstPivotRecord.Id).Error; err != nil { if err := db.Model(&entities.ProjectFlockKandang{}).
t.Fatalf("failed to reload first pivot record: %v", err) Where("project_flock_id = ? AND kandang_id = ?", createResp.Data.Id, kandangID).
Count(&remainingFirst).Error; err != nil {
t.Fatalf("failed to count first pivot records after delete: %v", err)
} }
if firstPivot.DetachedAt == nil { if remainingFirst != 0 {
t.Fatalf("expected first pivot DetachedAt to be set after delete") t.Fatalf("expected no pivot records remaining after delete, found %d", remainingFirst)
}
if firstPivot.ProjectFlockId != createResp.Data.Id {
t.Fatalf("expected first pivot project_flock_id %d, got %d", createResp.Data.Id, firstPivot.ProjectFlockId)
} }
resp, body = doJSONRequest(t, app, http.MethodDelete, "/api/production/project_flocks/"+uintToString(createRespSecond.Data.Id), nil) resp, body = doJSONRequest(t, app, http.MethodDelete, "/api/production/project_flocks/"+uintToString(createRespSecond.Data.Id), nil)
@@ -202,7 +207,7 @@ func TestProjectFlockSummary(t *testing.T) {
t.Fatalf("expected 200 when deleting second project flock, got %d: %s", resp.StatusCode, string(body)) t.Fatalf("expected 200 when deleting second project flock, got %d: %s", resp.StatusCode, string(body))
} }
secondKandang := fetchKandang(t, db, secondKandangID) secondKandang = fetchKandang(t, db, secondKandangID)
if secondKandang.ProjectFlockId != nil { if secondKandang.ProjectFlockId != nil {
t.Fatalf("expected second project_flock_id to be nil after delete, got %v", *secondKandang.ProjectFlockId) t.Fatalf("expected second project_flock_id to be nil after delete, got %v", *secondKandang.ProjectFlockId)
} }
@@ -210,15 +215,14 @@ func TestProjectFlockSummary(t *testing.T) {
t.Fatalf("expected second kandang status to revert to NON_ACTIVE, got %s", secondKandang.Status) t.Fatalf("expected second kandang status to revert to NON_ACTIVE, got %s", secondKandang.Status)
} }
var secondPivot entities.ProjectFlockKandang var remainingSecond int64
if err := db.First(&secondPivot, secondPivotRecord.Id).Error; err != nil { if err := db.Model(&entities.ProjectFlockKandang{}).
t.Fatalf("failed to reload second pivot record: %v", err) Where("project_flock_id = ? AND kandang_id = ?", createRespSecond.Data.Id, secondKandangID).
Count(&remainingSecond).Error; err != nil {
t.Fatalf("failed to count second pivot records after delete: %v", err)
} }
if secondPivot.DetachedAt == nil { if remainingSecond != 0 {
t.Fatalf("expected second pivot DetachedAt to be set after delete") t.Fatalf("expected no second pivot records remaining after delete, found %d", remainingSecond)
}
if secondPivot.ProjectFlockId != createRespSecond.Data.Id {
t.Fatalf("expected second pivot project_flock_id %d, got %d", createRespSecond.Data.Id, secondPivot.ProjectFlockId)
} }
resp, body = doJSONRequest(t, app, http.MethodGet, "/api/production/project_flocks/flocks/"+uintToString(flockID)+"/periods", nil) resp, body = doJSONRequest(t, app, http.MethodGet, "/api/production/project_flocks/flocks/"+uintToString(flockID)+"/periods", nil)
@@ -245,7 +249,6 @@ func TestProjectFlockSearchByRelatedFields(t *testing.T) {
areaID := createArea(t, app, "Area Search Target") areaID := createArea(t, app, "Area Search Target")
locationID := createLocation(t, app, "Location Search Target", "Location Address Target", areaID) locationID := createLocation(t, app, "Location Search Target", "Location Address Target", areaID)
flockID := createFlock(t, app, "Flock Search Target") flockID := createFlock(t, app, "Flock Search Target")
categoryID := createProductCategory(t, app, "Category Search Target", "CATGT")
fcrID := createFcr(t, app, "FCR Search Target", []map[string]any{ fcrID := createFcr(t, app, "FCR Search Target", []map[string]any{
{"weight": 1.0, "fcr_number": 1.5, "mortality": 2.0}, {"weight": 1.0, "fcr_number": 1.5, "mortality": 2.0},
}) })
@@ -254,7 +257,7 @@ func TestProjectFlockSearchByRelatedFields(t *testing.T) {
createPayload := map[string]any{ createPayload := map[string]any{
"flock_id": flockID, "flock_id": flockID,
"area_id": areaID, "area_id": areaID,
"product_category_id": categoryID, "category": "growing",
"fcr_id": fcrID, "fcr_id": fcrID,
"location_id": locationID, "location_id": locationID,
"kandang_ids": []uint{kandangID}, "kandang_ids": []uint{kandangID},
@@ -277,8 +280,8 @@ func TestProjectFlockSearchByRelatedFields(t *testing.T) {
searchTerms := []string{ searchTerms := []string{
"Flock Search Target", "Flock Search Target",
"Area Search Target", "Area Search Target",
"Category Search Target", string(utils.ProjectFlockCategoryGrowing),
"CATGT", "growing",
"FCR Search Target", "FCR Search Target",
"Kandang Search Target", "Kandang Search Target",
"Location Search Target", "Location Search Target",
@@ -329,7 +332,6 @@ func TestProjectFlockSorting(t *testing.T) {
flockOne := createFlock(t, app, "Flock Sort One") flockOne := createFlock(t, app, "Flock Sort One")
flockTwo := createFlock(t, app, "Flock Sort Two") flockTwo := createFlock(t, app, "Flock Sort Two")
categoryID := createProductCategory(t, app, "Category Sort", "CSORT")
fcrID := createFcr(t, app, "FCR Sort", []map[string]any{ fcrID := createFcr(t, app, "FCR Sort", []map[string]any{
{"weight": 1.0, "fcr_number": 1.5, "mortality": 2.0}, {"weight": 1.0, "fcr_number": 1.5, "mortality": 2.0},
}) })
@@ -341,7 +343,7 @@ func TestProjectFlockSorting(t *testing.T) {
projectOnePayload := map[string]any{ projectOnePayload := map[string]any{
"flock_id": flockOne, "flock_id": flockOne,
"area_id": areaA, "area_id": areaA,
"product_category_id": categoryID, "category": "growing",
"fcr_id": fcrID, "fcr_id": fcrID,
"location_id": locationA, "location_id": locationA,
"kandang_ids": []uint{kandangOne}, "kandang_ids": []uint{kandangOne},
@@ -355,7 +357,7 @@ func TestProjectFlockSorting(t *testing.T) {
projectTwoPayload := map[string]any{ projectTwoPayload := map[string]any{
"flock_id": flockTwo, "flock_id": flockTwo,
"area_id": areaB, "area_id": areaB,
"product_category_id": categoryID, "category": "laying",
"fcr_id": fcrID, "fcr_id": fcrID,
"location_id": locationB, "location_id": locationB,
"kandang_ids": []uint{kandangTwo, kandangThree}, "kandang_ids": []uint{kandangTwo, kandangThree},