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

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

See merge request mbugroup/lti-api!22
This commit is contained in:
Hafizh A. Y.
2025-10-21 06:53:39 +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;
+46 -52
View File
@@ -50,7 +50,7 @@ func Run(db *gorm.DB) error {
return err
}
projectFlocks, err := seedProjectFlocks(tx, adminID, flocks, areas, productCategories, fcrs, locations)
projectFlocks, err := seedProjectFlocks(tx, adminID, flocks, areas, fcrs, locations)
if err != nil {
return err
}
@@ -239,33 +239,33 @@ func seedFlocks(tx *gorm.DB, createdBy uint) (map[string]uint, error) {
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 {
Key string
Flock string
Area string
ProductCategory string
Fcr string
Location string
Period int
Key string
Flock string
Area string
Category utils.ProjectFlockCategory
Fcr string
Location string
Period int
}{
{
Key: "Singaparna Period 1",
Flock: "Flock Priangan",
Area: "Priangan",
ProductCategory: "Day Old Chick",
Fcr: "FCR Layer",
Location: "Singaparna",
Period: 1,
Key: "Singaparna Period 1",
Flock: "Flock Priangan",
Area: "Priangan",
Category: utils.ProjectFlockCategoryGrowing,
Fcr: "FCR Layer",
Location: "Singaparna",
Period: 1,
},
{
Key: "Cikaum Period 1",
Flock: "Flock Banten",
Area: "Banten",
ProductCategory: "Day Old Chick",
Fcr: "FCR Layer",
Location: "Cikaum",
Period: 1,
Key: "Cikaum Period 1",
Flock: "Flock Banten",
Area: "Banten",
Category: utils.ProjectFlockCategoryGrowing,
Fcr: "FCR Layer",
Location: "Cikaum",
Period: 1,
},
}
@@ -280,10 +280,6 @@ func seedProjectFlocks(tx *gorm.DB, createdBy uint, flocks, areas, productCatego
if !ok {
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]
if !ok {
return nil, fmt.Errorf("fcr %s not seeded", seed.Fcr)
@@ -294,17 +290,17 @@ func seedProjectFlocks(tx *gorm.DB, createdBy uint, flocks, areas, productCatego
}
var projectFlock entity.ProjectFlock
err := tx.Where("flock_id = ? AND area_id = ? AND product_category_id = ? AND fcr_id = ? AND location_id = ? AND period = ?",
flockID, areaID, categoryID, 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,
AreaId: areaID,
ProductCategoryId: categoryID,
FcrId: fcrID,
LocationId: locationID,
Period: seed.Period,
CreatedBy: createdBy,
FlockId: flockID,
AreaId: areaID,
Category: string(seed.Category),
FcrId: fcrID,
LocationId: locationID,
Period: seed.Period,
CreatedBy: createdBy,
}
if err := tx.Create(&projectFlock).Error; err != nil {
return nil, err
@@ -313,12 +309,12 @@ func seedProjectFlocks(tx *gorm.DB, createdBy uint, flocks, areas, productCatego
return nil, err
} else {
if err := tx.Model(&entity.ProjectFlock{}).Where("id = ?", projectFlock.Id).Updates(map[string]any{
"flock_id": flockID,
"area_id": areaID,
"product_category_id": categoryID,
"fcr_id": fcrID,
"location_id": locationID,
"period": seed.Period,
"flock_id": flockID,
"area_id": areaID,
"category": string(seed.Category),
"fcr_id": fcrID,
"location_id": locationID,
"period": seed.Period,
}).Error; err != nil {
return nil, err
}
@@ -378,7 +374,7 @@ func seedKandangs(tx *gorm.DB, createdBy uint, locations map[string]uint, users
if err := tx.Create(&kandang).Error; err != nil {
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
}
} 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 {
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
}
}
@@ -407,25 +403,24 @@ func seedKandangs(tx *gorm.DB, createdBy uint, locations map[string]uint, users
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 {
return err
}
if projectFlockID == nil {
return nil
}
return ensureActivePivot(tx, *projectFlockID, kandangID, createdBy)
return ensureActivePivot(tx, *projectFlockID, kandangID)
}
func detachActivePivot(tx *gorm.DB, kandangID uint) error {
return tx.Model(&entity.ProjectFlockKandang{}).
Where("kandang_id = ? AND detached_at IS NULL", kandangID).
Updates(map[string]any{"detached_at": time.Now()}).Error
return tx.Where("kandang_id = ?", kandangID).
Delete(&entity.ProjectFlockKandang{}).Error
}
func ensureActivePivot(tx *gorm.DB, projectFlockID, kandangID, createdBy uint) error {
func ensureActivePivot(tx *gorm.DB, projectFlockID, kandangID uint) error {
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
if err == nil {
return nil
@@ -436,7 +431,6 @@ func ensureActivePivot(tx *gorm.DB, projectFlockID, kandangID, createdBy uint) e
newRecord := entity.ProjectFlockKandang{
ProjectFlockId: projectFlockID,
KandangId: kandangID,
CreatedBy: createdBy,
}
return tx.Create(&newRecord).Error
}
+18 -19
View File
@@ -7,23 +7,22 @@ import (
)
type ProjectFlock struct {
Id uint `gorm:"primaryKey"`
FlockId uint `gorm:"not null;uniqueIndex:idx_project_flocks_flock_period,priority:1"`
AreaId uint `gorm:"not null"`
ProductCategoryId uint `gorm:"not null"`
FcrId uint `gorm:"not null"`
LocationId uint `gorm:"not null"`
Period int `gorm:"not null;uniqueIndex:idx_project_flocks_flock_period,priority:2"`
CreatedBy uint `gorm:"not null"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
Flock Flock `gorm:"foreignKey:FlockId;references:Id"`
Area Area `gorm:"foreignKey:AreaId;references:Id"`
ProductCategory ProductCategory `gorm:"foreignKey:ProductCategoryId;references:Id"`
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"`
Id uint `gorm:"primaryKey"`
FlockId uint `gorm:"not null;uniqueIndex:project_flocks_flock_period_unique"`
AreaId uint `gorm:"not null"`
Category string `gorm:"type:varchar(20);not null"`
FcrId uint `gorm:"not null"`
LocationId uint `gorm:"not null"`
Period int `gorm:"not null;uniqueIndex:project_flocks_flock_period_unique"`
CreatedBy uint `gorm:"not null"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
Flock Flock `gorm:"foreignKey:FlockId;references:Id"`
Area Area `gorm:"foreignKey:AreaId;references:Id"`
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"`
}
+2 -7
View File
@@ -4,14 +4,9 @@ import "time"
type ProjectFlockKandang struct {
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"`
KandangId uint `gorm:"not null;index:idx_project_flock_kandangs_kandang;uniqueIndex:idx_project_flock_kandangs_active,priority:2,where:detached_at IS NULL"`
CreatedBy uint `gorm:"not null"`
AssignedAt time.Time `gorm:"autoCreateTime"`
DetachedAt *time.Time `gorm:"index"`
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"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
ProjectFlock ProjectFlock `gorm:"foreignKey:ProjectFlockId;references:Id"`
Kandang Kandang `gorm:"foreignKey:KandangId;references:Id"`
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
}
@@ -1,21 +1,30 @@
package repository
import (
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"context"
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gorm.io/gorm"
)
type FlockRepository interface {
repository.BaseRepository[entity.Flock]
NameExists(ctx context.Context, name string, excludeID *uint) (bool, error)
}
type FlockRepositoryImpl struct {
*repository.BaseRepositoryImpl[entity.Flock]
db *gorm.DB
}
func NewFlockRepository(db *gorm.DB) FlockRepository {
return &FlockRepositoryImpl{
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 (
"errors"
"fmt"
"strings"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
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
}
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{
Name: req.Name,
Name: name,
CreatedBy: 1,
}
@@ -100,7 +116,20 @@ func (s flockService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (
updateBody := make(map[string]any)
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 {
@@ -4,75 +4,66 @@ import (
"time"
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"
)
type ProjectFlockBaseDTO struct {
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"`
Id uint `json:"id"`
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 {
return ProjectFlockBaseDTO{
Id: e.Id,
// FlockId: e.FlockId,
// AreaId: e.AreaId,
// ProductCategoryId: e.ProductCategoryId,
// FcrId: e.FcrId,
// LocationId: e.LocationId,
Period: e.Period,
var flock *flockDTO.FlockBaseDTO
if e.Flock.Id != 0 {
mapped := flockDTO.ToFlockBaseDTO(e.Flock)
flock = &mapped
}
}
type FlockSummaryDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
}
var area *areaDTO.AreaBaseDTO
if e.Area.Id != 0 {
mapped := areaDTO.ToAreaBaseDTO(e.Area)
area = &mapped
}
type AreaSummaryDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
}
var fcr *fcrDTO.FcrBaseDTO
if e.Fcr.Id != 0 {
mapped := fcrDTO.ToFcrBaseDTO(e.Fcr)
fcr = &mapped
}
type ProductCategorySummaryDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
Code string `json:"code"`
}
var location *locationDTO.LocationBaseDTO
if e.Location.Id != 0 {
mapped := locationDTO.ToLocationBaseDTO(e.Location)
location = &mapped
}
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"`
return ProjectFlockBaseDTO{
Id: e.Id,
Period: e.Period,
Category: e.Category,
Flock: flock,
Area: area,
Fcr: fcr,
Location: location,
}
}
type ProjectFlockListDTO struct {
ProjectFlockBaseDTO
Flock *FlockSummaryDTO `json:"flock,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"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Kandangs []kandangDTO.KandangBaseDTO `json:"kandangs,omitempty"`
CreatedUser *userDTO.UserBaseDTO `json:"created_user"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type ProjectFlockDetailDTO struct {
@@ -80,8 +71,8 @@ type ProjectFlockDetailDTO struct {
}
type FlockPeriodSummaryDTO struct {
Flock FlockSummaryDTO `json:"flock"`
NextPeriod int `json:"next_period"`
Flock flockDTO.FlockBaseDTO `json:"flock"`
NextPeriod int `json:"next_period"`
}
func ToProjectFlockListDTO(e entity.ProjectFlock) ProjectFlockListDTO {
@@ -91,62 +82,16 @@ func ToProjectFlockListDTO(e entity.ProjectFlock) ProjectFlockListDTO {
createdUser = &mapped
}
var flockSummary *FlockSummaryDTO
if e.Flock.Id != 0 {
summary := ToFlockSummaryDTO(e.Flock)
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 {
kandangSummaries[i] = KandangSummaryDTO{
Id: kandang.Id,
Name: kandang.Name,
Status: kandang.Status,
var kandangSummaries []kandangDTO.KandangBaseDTO
if len(e.Kandangs) > 0 {
kandangSummaries = make([]kandangDTO.KandangBaseDTO, len(e.Kandangs))
for i, kandang := range e.Kandangs {
kandangSummaries[i] = kandangDTO.ToKandangBaseDTO(kandang)
}
}
return ProjectFlockListDTO{
ProjectFlockBaseDTO: ToProjectFlockBaseDTO(e),
Flock: flockSummary,
Area: areaSummary,
ProductCategory: categorySummary,
Fcr: fcrSummary,
Location: locationSummary,
Kandangs: kandangSummaries,
CreatedAt: e.CreatedAt,
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 {
return FlockPeriodSummaryDTO{
Flock: ToFlockSummaryDTO(flock),
Flock: flockDTO.ToFlockBaseDTO(flock),
NextPeriod: next,
}
}
@@ -2,7 +2,6 @@ package repository
import (
"context"
"time"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gorm.io/gorm"
@@ -10,7 +9,7 @@ import (
type ProjectFlockKandangRepository interface {
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)
WithTx(tx *gorm.DB) ProjectFlockKandangRepository
DB() *gorm.DB
@@ -31,14 +30,13 @@ func (r *projectFlockKandangRepositoryImpl) CreateMany(ctx context.Context, reco
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 {
return nil
}
return r.db.WithContext(ctx).
Model(&entity.ProjectFlockKandang{}).
Where("project_flock_id = ? AND kandang_id IN ? AND detached_at IS NULL", projectFlockID, kandangIDs).
Updates(map[string]any{"detached_at": detachedAt}).Error
Where("project_flock_id = ? AND kandang_id IN ?", projectFlockID, kandangIDs).
Delete(&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.Flock").
Preload("Kandang").
Preload("CreatedUser").
Order("project_flock_id ASC, assigned_at ASC").
Order("project_flock_id ASC, created_at ASC").
Find(&records).Error; err != nil {
return nil, err
}
@@ -5,7 +5,6 @@ import (
"errors"
"fmt"
"strings"
"time"
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
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("Flock").
Preload("Area").
Preload("ProductCategory").
Preload("Fcr").
Preload("Location").
Preload("Kandangs")
@@ -115,15 +113,13 @@ func (s projectflockService) GetAll(c *fiber.Ctx, params *validation.Query) ([]e
db = db.
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 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 locations ON locations.id = project_flocks.location_id").
Joins("LEFT JOIN users AS created_users ON created_users.id = project_flocks.created_by").
Where(`
LOWER(flocks.name) LIKE ?
OR LOWER(areas.name) LIKE ?
OR LOWER(product_categories.name) LIKE ?
OR LOWER(product_categories.code) LIKE ?
OR LOWER(project_flocks.category) LIKE ?
OR LOWER(fcrs.name) LIKE ?
OR LOWER(locations.name) 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,
)
}
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
}
category, ok := utils.NormalizeProjectFlockCategory(req.Category)
if !ok {
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid category")
}
if len(req.KandangIds) == 0 {
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(),
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: "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: "Location", ID: &req.LocationId, Exists: relationExistsChecker[entity.Location](s.Repository.DB())},
); err != nil {
@@ -224,13 +223,13 @@ func (s *projectflockService) CreateOne(c *fiber.Ctx, req *validation.Create) (*
}
createBody := &entity.ProjectFlock{
FlockId: req.FlockId,
AreaId: req.AreaId,
ProductCategoryId: req.ProductCategoryId,
FcrId: req.FcrId,
LocationId: req.LocationId,
Period: nextPeriod,
CreatedBy: 1,
FlockId: req.FlockId,
AreaId: req.AreaId,
Category: string(category),
FcrId: req.FcrId,
LocationId: req.LocationId,
Period: nextPeriod,
CreatedBy: 1,
}
if err := projectRepo.CreateOne(c.Context(), createBody, nil); err != nil {
@@ -242,7 +241,7 @@ func (s *projectflockService) CreateOne(c *fiber.Ctx, req *validation.Create) (*
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()
s.Log.Errorf("Failed to attach kandangs to projectflock %d: %+v", createBody.Id, 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()),
})
}
if req.ProductCategoryId != nil {
updateBody["product_category_id"] = *req.ProductCategoryId
relationChecks = append(relationChecks, common.RelationCheck{
Name: "Product category",
ID: req.ProductCategoryId,
Exists: relationExistsChecker[entity.ProductCategory](s.Repository.DB()),
})
if req.Category != nil {
if normalized, ok := utils.NormalizeProjectFlockCategory(*req.Category); ok {
updateBody["category"] = string(normalized)
} else {
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid category")
}
}
if req.FcrId != nil {
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 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()
s.Log.Errorf("Failed to attach kandangs to projectflock %d: %+v", id, 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 {
return nil
}
if err := tx.Model(&entity.Kandang{}).
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")
}
@@ -551,7 +552,6 @@ func (s projectflockService) attachKandangs(ctx context.Context, tx *gorm.DB, pr
records[i] = &entity.ProjectFlockKandang{
ProjectFlockId: projectFlockID,
KandangId: id,
CreatedBy: createdBy,
}
}
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")
}
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 nil
@@ -1,32 +1,32 @@
package validation
type Create struct {
FlockId uint `json:"flock_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"`
FcrId uint `json:"fcr_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"`
FlockId uint `json:"flock_id" validate:"required_strict,number,gt=0"`
AreaId uint `json:"area_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"`
LocationId uint `json:"location_id" validate:"required_strict,number,gt=0"`
KandangIds []uint `json:"kandang_ids" validate:"required,min=1,dive,gt=0"`
}
type Update struct {
FlockId *uint `json:"flock_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"`
FcrId *uint `json:"fcr_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"`
KandangIds []uint `json:"kandang_ids,omitempty" validate:"omitempty,min=1,dive,gt=0"`
FlockId *uint `json:"flock_id,omitempty" validate:"omitempty,number,gt=0"`
AreaId *uint `json:"area_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"`
LocationId *uint `json:"location_id,omitempty" validate:"omitempty,number,gt=0"`
Period *int `json:"period,omitempty" validate:"omitempty,number,gt=0"`
KandangIds []uint `json:"kandang_ids,omitempty" validate:"omitempty,min=1,dive,gt=0"`
}
type Query struct {
Page int `query:"page" validate:"omitempty,number,min=1"`
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"`
Search string `query:"search" validate:"omitempty,max=50"`
SortBy string `query:"sort_by" validate:"omitempty,oneof=area location kandangs period"`
SortOrder string `query:"sort_order" validate:"omitempty,oneof=asc desc"`
AreaId uint `query:"area_id" validate:"omitempty,number,gt=0"`
LocationId uint `query:"location_id" validate:"omitempty,number,gt=0"`
Period int `query:"period" validate:"omitempty,number,gt=0"`
Page int `query:"page" validate:"omitempty,number,min=1"`
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"`
Search string `query:"search" validate:"omitempty,max=50"`
SortBy string `query:"sort_by" validate:"omitempty,oneof=area location kandangs period"`
SortOrder string `query:"sort_order" validate:"omitempty,oneof=asc desc"`
AreaId uint `query:"area_id" validate:"omitempty,number,gt=0"`
LocationId uint `query:"location_id" validate:"omitempty,number,gt=0"`
Period int `query:"period" validate:"omitempty,number,gt=0"`
KandangIds []uint `query:"kandang_id" validate:"omitempty,dive,gt=0"`
}
+31 -9
View File
@@ -59,7 +59,6 @@ var allFlagTypes = func() map[FlagType]struct{} {
return m
}()
func AllFlagTypes() map[FlagType]struct{} {
return allFlagTypes
}
@@ -76,8 +75,6 @@ const (
WarehouseTypeKandang WarehouseType = "KANDANG"
)
// -------------------------------------------------------------------
// WarehouseType
// -------------------------------------------------------------------
@@ -100,19 +97,29 @@ const (
SupplierCategorySapronak SupplierCategory = "SAPRONAK"
)
// -------------------------------------------------------------------
// Kandang Status
// Kandang Status
// -------------------------------------------------------------------
type KandangStatus string
const (
KandangStatusNonActive KandangStatus = "NON_ACTIVE"
KandangStatusPengajuan KandangStatus = "PENGAJUAN"
KandangStatusActive KandangStatus = "ACTIVE"
KandangStatusNonActive KandangStatus = "NON_ACTIVE"
KandangStatusPengajuan KandangStatus = "PENGAJUAN"
KandangStatusActive KandangStatus = "ACTIVE"
)
// -------------------------------------------------------------------
// ProjectFlockCategory
// -------------------------------------------------------------------
type ProjectFlockCategory string
const (
ProjectFlockCategoryGrowing ProjectFlockCategory = "GROWING"
ProjectFlockCategoryLaying ProjectFlockCategory = "LAYING"
)
// -------------------------------------------------------------------
// Validators
// -------------------------------------------------------------------
@@ -223,6 +230,21 @@ func IsValidCustomerSupplierType(v string) bool {
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 {
switch SupplierCategory(v) {
case SupplierCategoryBOP, SupplierCategorySapronak:
+8 -8
View File
@@ -8,6 +8,7 @@ import (
"github.com/gofiber/fiber/v2"
"gitlab.com/mbugroup/lti-api.git/internal/entities"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
)
func TestKandangIntegration(t *testing.T) {
@@ -51,20 +52,19 @@ func TestKandangIntegration(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{
{"weight": 1.0, "fcr_number": 1.5, "mortality": 2.0},
})
flocID := createFlock(t, app, "Floc Test")
projectFloc := entities.ProjectFlock{
FlockId: flocID,
AreaId: areaID,
ProductCategoryId: categoryID,
FcrId: fcrID,
LocationId: locationID,
Period: 1,
CreatedBy: 1,
FlockId: flocID,
AreaId: areaID,
Category: string(utils.ProjectFlockCategoryGrowing),
FcrId: fcrID,
LocationId: locationID,
Period: 1,
CreatedBy: 1,
}
if err := db.Create(&projectFloc).Error; err != nil {
t.Fatalf("failed to seed project floc: %v", err)
@@ -19,19 +19,18 @@ func TestProjectFlockSummary(t *testing.T) {
areaID := createArea(t, app, "Area Project")
locationID := createLocation(t, app, "Location Project", "Address", areaID)
flockID := createFlock(t, app, "Flock Summary")
categoryID := createProductCategory(t, app, "DOC Summary", "DOCS")
fcrID := createFcr(t, app, "FCR Summary", []map[string]any{
{"weight": 1.0, "fcr_number": 1.5, "mortality": 2.0},
})
kandangID := createKandang(t, app, "Kandang Summary", locationID, 1)
createPayload := map[string]any{
"flock_id": flockID,
"area_id": areaID,
"product_category_id": categoryID,
"fcr_id": fcrID,
"location_id": locationID,
"kandang_ids": []uint{kandangID},
"flock_id": flockID,
"area_id": areaID,
"category": "growing",
"fcr_id": fcrID,
"location_id": locationID,
"kandang_ids": []uint{kandangID},
}
resp, body := doJSONRequest(t, app, http.MethodPost, "/api/production/project_flocks", createPayload)
if resp.StatusCode != fiber.StatusCreated {
@@ -40,9 +39,10 @@ func TestProjectFlockSummary(t *testing.T) {
var createResp struct {
Data struct {
Id uint `json:"id"`
Period int `json:"period"`
Flock struct {
Id uint `json:"id"`
Period int `json:"period"`
Category string `json:"category"`
Flock struct {
Id uint `json:"id"`
Name string `json:"name"`
} `json:"flock"`
@@ -50,11 +50,6 @@ func TestProjectFlockSummary(t *testing.T) {
Id uint `json:"id"`
Name string `json:"name"`
} `json:"area"`
ProductCategory struct {
Id uint `json:"id"`
Name string `json:"name"`
Code string `json:"code"`
} `json:"product_category"`
Fcr struct {
Id uint `json:"id"`
Name string `json:"name"`
@@ -86,19 +81,27 @@ func TestProjectFlockSummary(t *testing.T) {
if createResp.Data.Area.Id != areaID || createResp.Data.Area.Name == "" {
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 == "" {
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 {
t.Fatalf("expected kandang detail to be present, got %+v", createResp.Data.Kandangs)
}
if createResp.Data.Kandangs[0].Status == "" {
t.Fatalf("expected kandang status to be present, got %+v", createResp.Data.Kandangs[0])
if createResp.Data.Kandangs[0].Status != string(utils.KandangStatusPengajuan) {
t.Fatalf("expected kandang status to be PENGAJUAN, got %s", createResp.Data.Kandangs[0].Status)
}
if createResp.Data.Period != 1 {
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
if err := db.Where("project_flock_id = ?", createResp.Data.Id).Find(&pivotRecords).Error; err != nil {
t.Fatalf("failed to fetch pivot records: %v", err)
@@ -110,18 +113,15 @@ func TestProjectFlockSummary(t *testing.T) {
if firstPivotRecord.KandangId != 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)
secondPayload := map[string]any{
"flock_id": flockID,
"area_id": areaID,
"product_category_id": categoryID,
"fcr_id": fcrID,
"location_id": locationID,
"kandang_ids": []uint{secondKandangID},
"flock_id": flockID,
"area_id": areaID,
"category": "laying",
"fcr_id": fcrID,
"location_id": locationID,
"kandang_ids": []uint{secondKandangID},
}
resp, body = doJSONRequest(t, app, http.MethodPost, "/api/production/project_flocks", secondPayload)
if resp.StatusCode != fiber.StatusCreated {
@@ -129,8 +129,9 @@ func TestProjectFlockSummary(t *testing.T) {
}
var createRespSecond struct {
Data struct {
Id uint `json:"id"`
Period int `json:"period"`
Id uint `json:"id"`
Period int `json:"period"`
Category string `json:"category"`
} `json:"data"`
}
if err := json.Unmarshal(body, &createRespSecond); err != nil {
@@ -139,6 +140,9 @@ func TestProjectFlockSummary(t *testing.T) {
if createRespSecond.Data.Period != 2 {
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
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 {
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)
@@ -186,15 +192,14 @@ func TestProjectFlockSummary(t *testing.T) {
t.Fatalf("expected kandang status to revert to NON_ACTIVE, got %s", firstKandang.Status)
}
var firstPivot entities.ProjectFlockKandang
if err := db.First(&firstPivot, firstPivotRecord.Id).Error; err != nil {
t.Fatalf("failed to reload first pivot record: %v", err)
var remainingFirst int64
if err := db.Model(&entities.ProjectFlockKandang{}).
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 {
t.Fatalf("expected first pivot DetachedAt to be set after delete")
}
if firstPivot.ProjectFlockId != createResp.Data.Id {
t.Fatalf("expected first pivot project_flock_id %d, got %d", createResp.Data.Id, firstPivot.ProjectFlockId)
if remainingFirst != 0 {
t.Fatalf("expected no pivot records remaining after delete, found %d", remainingFirst)
}
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))
}
secondKandang := fetchKandang(t, db, secondKandangID)
secondKandang = fetchKandang(t, db, secondKandangID)
if secondKandang.ProjectFlockId != nil {
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)
}
var secondPivot entities.ProjectFlockKandang
if err := db.First(&secondPivot, secondPivotRecord.Id).Error; err != nil {
t.Fatalf("failed to reload second pivot record: %v", err)
var remainingSecond int64
if err := db.Model(&entities.ProjectFlockKandang{}).
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 {
t.Fatalf("expected second pivot DetachedAt to be set after delete")
}
if secondPivot.ProjectFlockId != createRespSecond.Data.Id {
t.Fatalf("expected second pivot project_flock_id %d, got %d", createRespSecond.Data.Id, secondPivot.ProjectFlockId)
if remainingSecond != 0 {
t.Fatalf("expected no second pivot records remaining after delete, found %d", remainingSecond)
}
resp, body = doJSONRequest(t, app, http.MethodGet, "/api/production/project_flocks/flocks/"+uintToString(flockID)+"/periods", nil)
@@ -245,19 +249,18 @@ func TestProjectFlockSearchByRelatedFields(t *testing.T) {
areaID := createArea(t, app, "Area Search Target")
locationID := createLocation(t, app, "Location Search Target", "Location Address Target", areaID)
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{
{"weight": 1.0, "fcr_number": 1.5, "mortality": 2.0},
})
kandangID := createKandang(t, app, "Kandang Search Target", locationID, 1)
createPayload := map[string]any{
"flock_id": flockID,
"area_id": areaID,
"product_category_id": categoryID,
"fcr_id": fcrID,
"location_id": locationID,
"kandang_ids": []uint{kandangID},
"flock_id": flockID,
"area_id": areaID,
"category": "growing",
"fcr_id": fcrID,
"location_id": locationID,
"kandang_ids": []uint{kandangID},
}
resp, body := doJSONRequest(t, app, http.MethodPost, "/api/production/project_flocks", createPayload)
@@ -277,8 +280,8 @@ func TestProjectFlockSearchByRelatedFields(t *testing.T) {
searchTerms := []string{
"Flock Search Target",
"Area Search Target",
"Category Search Target",
"CATGT",
string(utils.ProjectFlockCategoryGrowing),
"growing",
"FCR Search Target",
"Kandang Search Target",
"Location Search Target",
@@ -329,7 +332,6 @@ func TestProjectFlockSorting(t *testing.T) {
flockOne := createFlock(t, app, "Flock Sort One")
flockTwo := createFlock(t, app, "Flock Sort Two")
categoryID := createProductCategory(t, app, "Category Sort", "CSORT")
fcrID := createFcr(t, app, "FCR Sort", []map[string]any{
{"weight": 1.0, "fcr_number": 1.5, "mortality": 2.0},
})
@@ -339,12 +341,12 @@ func TestProjectFlockSorting(t *testing.T) {
kandangThree := createKandang(t, app, "Kandang Sort Three", locationB, 1)
projectOnePayload := map[string]any{
"flock_id": flockOne,
"area_id": areaA,
"product_category_id": categoryID,
"fcr_id": fcrID,
"location_id": locationA,
"kandang_ids": []uint{kandangOne},
"flock_id": flockOne,
"area_id": areaA,
"category": "growing",
"fcr_id": fcrID,
"location_id": locationA,
"kandang_ids": []uint{kandangOne},
}
resp, body := doJSONRequest(t, app, http.MethodPost, "/api/production/project_flocks", projectOnePayload)
if resp.StatusCode != fiber.StatusCreated {
@@ -353,12 +355,12 @@ func TestProjectFlockSorting(t *testing.T) {
projectOneID := parseProjectFlockID(t, body)
projectTwoPayload := map[string]any{
"flock_id": flockTwo,
"area_id": areaB,
"product_category_id": categoryID,
"fcr_id": fcrID,
"location_id": locationB,
"kandang_ids": []uint{kandangTwo, kandangThree},
"flock_id": flockTwo,
"area_id": areaB,
"category": "laying",
"fcr_id": fcrID,
"location_id": locationB,
"kandang_ids": []uint{kandangTwo, kandangThree},
}
resp, body = doJSONRequest(t, app, http.MethodPost, "/api/production/project_flocks", projectTwoPayload)
if resp.StatusCode != fiber.StatusCreated {