Merge branch 'dev/ragil-before-sso' of https://gitlab.com/mbugroup/lti-api into dev/teguh

This commit is contained in:
aguhh18
2025-10-20 06:06:22 +07:00
13 changed files with 663 additions and 51 deletions
@@ -0,0 +1 @@
DROP TABLE IF EXISTS project_flock_kandangs;
@@ -0,0 +1,17 @@
CREATE TABLE project_flock_kandangs (
id BIGSERIAL PRIMARY KEY,
project_flock_id BIGINT NOT NULL REFERENCES project_flocks (id) ON DELETE CASCADE ON UPDATE CASCADE,
kandang_id BIGINT NOT NULL REFERENCES kandangs (id) ON DELETE CASCADE ON UPDATE CASCADE,
created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE,
assigned_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
detached_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_project_flock_kandangs_project ON project_flock_kandangs (project_flock_id);
CREATE INDEX idx_project_flock_kandangs_kandang ON project_flock_kandangs (kandang_id);
CREATE UNIQUE INDEX idx_project_flock_kandangs_active ON project_flock_kandangs (project_flock_id, kandang_id)
WHERE
detached_at IS NULL;
+40
View File
@@ -378,6 +378,9 @@ 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 {
return nil, err
}
} else if err != nil {
return nil, err
} else {
@@ -394,6 +397,9 @@ 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 {
return nil, err
}
}
result[seed.Name] = kandang.Id
}
@@ -401,6 +407,40 @@ 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 {
if err := detachActivePivot(tx, kandangID); err != nil {
return err
}
if projectFlockID == nil {
return nil
}
return ensureActivePivot(tx, *projectFlockID, kandangID, createdBy)
}
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
}
func ensureActivePivot(tx *gorm.DB, projectFlockID, kandangID, createdBy uint) error {
var pivot entity.ProjectFlockKandang
err := tx.Where("project_flock_id = ? AND kandang_id = ? AND detached_at IS NULL", projectFlockID, kandangID).
First(&pivot).Error
if err == nil {
return nil
}
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return err
}
newRecord := entity.ProjectFlockKandang{
ProjectFlockId: projectFlockID,
KandangId: kandangID,
CreatedBy: createdBy,
}
return tx.Create(&newRecord).Error
}
func seedWarehouses(tx *gorm.DB, createdBy uint, areas map[string]uint, locations map[string]uint, kandangs map[string]uint) error {
seeds := []struct {
Name string
+19 -18
View File
@@ -7,22 +7,23 @@ 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"`
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"`
}
+17
View File
@@ -0,0 +1,17 @@
package entities
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"`
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,8 +1,11 @@
package controller
import (
"encoding/json"
"fmt"
"math"
"strconv"
"strings"
"gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/dto"
service "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/services"
@@ -23,10 +26,58 @@ func NewProjectflockController(projectflockService service.ProjectflockService)
}
func (u *ProjectflockController) GetAll(c *fiber.Ctx) error {
parseUintList := func(raw string) ([]uint, error) {
raw = strings.TrimSpace(raw)
if raw == "" {
return nil, nil
}
var ids []uint
if strings.HasPrefix(raw, "[") {
if err := json.Unmarshal([]byte(raw), &ids); err == nil {
return ids, nil
}
}
parts := strings.Split(raw, ",")
for _, part := range parts {
part = strings.Trim(part, " \"[]")
if part == "" {
continue
}
v, err := strconv.Atoi(part)
if err != nil || v <= 0 {
return nil, fmt.Errorf("invalid kandang id: %s", part)
}
ids = append(ids, uint(v))
}
return ids, nil
}
query := &validation.Query{
Page: c.QueryInt("page", 1),
Limit: c.QueryInt("limit", 10),
Search: c.Query("search", ""),
Page: c.QueryInt("page", 1),
Limit: c.QueryInt("limit", 10),
Search: c.Query("search", ""),
SortBy: c.Query("sort_by", ""),
SortOrder: c.Query("sort_order", ""),
}
if area := c.QueryInt("area_id", 0); area > 0 {
query.AreaId = uint(area)
}
if location := c.QueryInt("location_id", 0); location > 0 {
query.LocationId = uint(location)
}
if period := c.QueryInt("period", 0); period > 0 {
query.Period = period
}
if kandangRaw := c.Query("kandang_id", c.Query("kandang_ids", "")); kandangRaw != "" {
ids, err := parseUintList(kandangRaw)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
}
query.KandangIds = ids
}
result, totalResults, err := u.ProjectflockService.GetAll(c, query)
@@ -8,24 +8,24 @@ import (
)
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"`
}
func ToProjectFlockBaseDTO(e entity.ProjectFlock) 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,
}
}
@@ -20,9 +20,10 @@ func (ProjectflockModule) RegisterRoutes(router fiber.Router, db *gorm.DB, valid
flockRepo := rFlock.NewFlockRepository(db)
kandangRepo := rKandang.NewKandangRepository(db)
projectflockRepo := rProjectflock.NewProjectflockRepository(db)
projectflockKandangRepo := rProjectflock.NewProjectFlockKandangRepository(db)
userRepo := rUser.NewUserRepository(db)
projectflockService := sProjectflock.NewProjectflockService(projectflockRepo, flockRepo, kandangRepo, validate)
projectflockService := sProjectflock.NewProjectflockService(projectflockRepo, flockRepo, kandangRepo, projectflockKandangRepo, validate)
userService := sUser.NewUserService(userRepo, validate)
ProjectflockRoutes(router, userService, projectflockService)
@@ -0,0 +1,64 @@
package repository
import (
"context"
"time"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gorm.io/gorm"
)
type ProjectFlockKandangRepository interface {
CreateMany(ctx context.Context, records []*entity.ProjectFlockKandang) error
MarkDetached(ctx context.Context, projectFlockID uint, kandangIDs []uint, detachedAt time.Time) error
GetAll(ctx context.Context) ([]entity.ProjectFlockKandang, error)
WithTx(tx *gorm.DB) ProjectFlockKandangRepository
DB() *gorm.DB
}
type projectFlockKandangRepositoryImpl struct {
db *gorm.DB
}
func NewProjectFlockKandangRepository(db *gorm.DB) ProjectFlockKandangRepository {
return &projectFlockKandangRepositoryImpl{db: db}
}
func (r *projectFlockKandangRepositoryImpl) CreateMany(ctx context.Context, records []*entity.ProjectFlockKandang) error {
if len(records) == 0 {
return nil
}
return r.db.WithContext(ctx).Create(&records).Error
}
func (r *projectFlockKandangRepositoryImpl) MarkDetached(ctx context.Context, projectFlockID uint, kandangIDs []uint, detachedAt time.Time) 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
}
func (r *projectFlockKandangRepositoryImpl) GetAll(ctx context.Context) ([]entity.ProjectFlockKandang, error) {
var records []entity.ProjectFlockKandang
if err := r.db.WithContext(ctx).
Preload("ProjectFlock").
Preload("ProjectFlock.Flock").
Preload("Kandang").
Preload("CreatedUser").
Order("project_flock_id ASC, assigned_at ASC").
Find(&records).Error; err != nil {
return nil, err
}
return records, nil
}
func (r *projectFlockKandangRepositoryImpl) WithTx(tx *gorm.DB) ProjectFlockKandangRepository {
return &projectFlockKandangRepositoryImpl{db: tx}
}
func (r *projectFlockKandangRepositoryImpl) DB() *gorm.DB {
return r.db
}
@@ -4,6 +4,8 @@ import (
"context"
"errors"
"fmt"
"strings"
"time"
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
common "gitlab.com/mbugroup/lti-api.git/internal/common/service"
@@ -35,6 +37,7 @@ type projectflockService struct {
Repository repository.ProjectflockRepository
FlockRepo flockRepository.FlockRepository
KandangRepo kandangRepository.KandangRepository
PivotRepo repository.ProjectFlockKandangRepository
}
type FlockPeriodSummary struct {
@@ -46,6 +49,7 @@ func NewProjectflockService(
repo repository.ProjectflockRepository,
flockRepo flockRepository.FlockRepository,
kandangRepo kandangRepository.KandangRepository,
pivotRepo repository.ProjectFlockKandangRepository,
validate *validator.Validate,
) ProjectflockService {
return &projectflockService{
@@ -54,6 +58,7 @@ func NewProjectflockService(
Repository: repo,
FlockRepo: flockRepo,
KandangRepo: kandangRepo,
PivotRepo: pivotRepo,
}
}
@@ -73,11 +78,81 @@ func (s projectflockService) GetAll(c *fiber.Ctx, params *validation.Query) ([]e
return nil, 0, err
}
if params.Page <= 0 {
params.Page = 1
}
if params.Limit <= 0 {
params.Limit = 10
}
offset := (params.Page - 1) * params.Limit
projectflocks, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
db = s.withRelations(db)
return db.Order("created_at DESC").Order("updated_at DESC")
if params.AreaId > 0 {
db = db.Where("project_flocks.area_id = ?", params.AreaId)
}
if params.LocationId > 0 {
db = db.Where("project_flocks.location_id = ?", params.LocationId)
}
if params.Period > 0 {
db = db.Where("project_flocks.period = ?", params.Period)
}
if len(params.KandangIds) > 0 {
db = db.Where("EXISTS (SELECT 1 FROM kandangs WHERE kandangs.project_flock_id = project_flocks.id AND kandangs.id IN ?)", params.KandangIds)
}
if params.Search != "" {
normalizedSearch := strings.ToLower(strings.TrimSpace(params.Search))
if normalizedSearch == "" {
for _, expr := range s.buildOrderExpressions(params.SortBy, params.SortOrder) {
db = db.Order(expr)
}
return db
}
likeQuery := "%" + normalizedSearch + "%"
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(fcrs.name) LIKE ?
OR LOWER(locations.name) LIKE ?
OR LOWER(locations.address) LIKE ?
OR LOWER(created_users.name) LIKE ?
OR LOWER(created_users.email) LIKE ?
OR LOWER(CAST(project_flocks.period AS TEXT)) LIKE ?
OR EXISTS (
SELECT 1 FROM kandangs
WHERE kandangs.project_flock_id = project_flocks.id
AND LOWER(kandangs.name) LIKE ?
)
`,
likeQuery,
likeQuery,
likeQuery,
likeQuery,
likeQuery,
likeQuery,
likeQuery,
likeQuery,
likeQuery,
likeQuery,
likeQuery,
)
}
for _, expr := range s.buildOrderExpressions(params.SortBy, params.SortOrder) {
db = db.Order(expr)
}
return db
})
if err != nil {
@@ -167,12 +242,10 @@ func (s *projectflockService) CreateOne(c *fiber.Ctx, req *validation.Create) (*
return nil, err
}
if err := tx.Model(&entity.Kandang{}).
Where("id IN ?", kandangIDs).
Updates(map[string]any{"project_flock_id": createBody.Id}).Error; err != nil {
if err := s.attachKandangs(c.Context(), tx, createBody.Id, kandangIDs, createBody.CreatedBy); err != nil {
tx.Rollback()
s.Log.Errorf("Failed to assign kandangs to projectflock: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to assign kandangs")
s.Log.Errorf("Failed to attach kandangs to projectflock %d: %+v", createBody.Id, err)
return nil, err
}
if err := tx.Commit().Error; err != nil {
@@ -315,22 +388,18 @@ func (s projectflockService) UpdateOne(c *fiber.Ctx, req *validation.Update, id
}
if len(toDetach) > 0 {
if err := tx.Model(&entity.Kandang{}).
Where("id IN ?", toDetach).
Updates(map[string]any{"project_flock_id": nil}).Error; err != nil {
if err := s.detachKandangs(c.Context(), tx, id, toDetach, false); err != nil {
tx.Rollback()
s.Log.Errorf("Failed to detach kandangs: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to update kandangs")
s.Log.Errorf("Failed to detach kandangs from projectflock %d: %+v", id, err)
return nil, err
}
}
if len(toAttach) > 0 {
if err := tx.Model(&entity.Kandang{}).
Where("id IN ?", toAttach).
Updates(map[string]any{"project_flock_id": id}).Error; err != nil {
if err := s.attachKandangs(c.Context(), tx, id, toAttach, existing.CreatedBy); err != nil {
tx.Rollback()
s.Log.Errorf("Failed to attach kandangs: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to update kandangs")
s.Log.Errorf("Failed to attach kandangs to projectflock %d: %+v", id, err)
return nil, err
}
}
}
@@ -363,12 +432,10 @@ func (s projectflockService) DeleteOne(c *fiber.Ctx, id uint) error {
for i, k := range existing.Kandangs {
ids[i] = k.Id
}
if err := tx.Model(&entity.Kandang{}).
Where("id IN ?", ids).
Updates(map[string]any{"project_flock_id": nil}).Error; err != nil {
if err := s.detachKandangs(c.Context(), tx, id, ids, true); err != nil {
tx.Rollback()
s.Log.Errorf("Failed to detach kandangs before delete: %+v", err)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update kandangs")
s.Log.Errorf("Failed to detach kandangs before deleting projectflock %d: %+v", id, err)
return err
}
}
@@ -431,3 +498,93 @@ func relationExistsChecker[T any](db *gorm.DB) func(context.Context, uint) (bool
return commonRepo.Exists[T](ctx, db, id)
}
}
func (s projectflockService) buildOrderExpressions(sortBy, sortOrder string) []string {
direction := "ASC"
if strings.ToLower(sortOrder) == "desc" {
direction = "DESC"
}
switch sortBy {
case "area":
return []string{
fmt.Sprintf("(SELECT name FROM areas WHERE areas.id = project_flocks.area_id) %s", direction),
fmt.Sprintf("project_flocks.id %s", direction),
}
case "location":
return []string{
fmt.Sprintf("(SELECT name FROM locations WHERE locations.id = project_flocks.location_id) %s", direction),
fmt.Sprintf("project_flocks.id %s", direction),
}
case "kandangs":
return []string{
fmt.Sprintf("(SELECT COUNT(*) FROM kandangs WHERE kandangs.project_flock_id = project_flocks.id) %s", direction),
fmt.Sprintf("project_flocks.id %s", direction),
}
case "period":
return []string{
fmt.Sprintf("project_flocks.period %s", direction),
fmt.Sprintf("project_flocks.id %s", direction),
}
default:
return []string{
"project_flocks.created_at DESC",
"project_flocks.updated_at DESC",
}
}
}
func (s projectflockService) attachKandangs(ctx context.Context, tx *gorm.DB, projectFlockID uint, kandangIDs []uint, createdBy 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 {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update kandangs")
}
pivotRepo := s.pivotRepoWithTx(tx)
records := make([]*entity.ProjectFlockKandang, len(kandangIDs))
for i, id := range kandangIDs {
records[i] = &entity.ProjectFlockKandang{
ProjectFlockId: projectFlockID,
KandangId: id,
CreatedBy: createdBy,
}
}
if err := pivotRepo.CreateMany(ctx, records); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to persist project flock history")
}
return nil
}
func (s projectflockService) detachKandangs(ctx context.Context, tx *gorm.DB, projectFlockID uint, kandangIDs []uint, resetStatus bool) error {
if len(kandangIDs) == 0 {
return nil
}
updates := map[string]any{"project_flock_id": nil}
if resetStatus {
updates["status"] = string(utils.KandangStatusNonActive)
}
if err := tx.Model(&entity.Kandang{}).
Where("id IN ?", kandangIDs).
Updates(updates).Error; err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update kandangs")
}
if err := s.pivotRepoWithTx(tx).MarkDetached(ctx, projectFlockID, kandangIDs, time.Now()); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to persist project flock history")
}
return nil
}
func (s projectflockService) pivotRepoWithTx(tx *gorm.DB) repository.ProjectFlockKandangRepository {
if s.PivotRepo == nil {
return repository.NewProjectFlockKandangRepository(tx)
}
return s.PivotRepo.WithTx(tx)
}
@@ -20,7 +20,13 @@ type Update struct {
}
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"`
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"`
}
@@ -42,6 +42,7 @@ func setupIntegrationApp(t *testing.T) (*fiber.App, *gorm.DB) {
&entities.Location{},
&entities.Flock{},
&entities.ProjectFlock{},
&entities.ProjectFlockKandang{},
&entities.Kandang{},
&entities.Warehouse{},
&entities.Uom{},
@@ -191,6 +192,15 @@ func fetchCustomer(t *testing.T, db *gorm.DB, id uint) entities.Customer {
return customer
}
func fetchKandang(t *testing.T, db *gorm.DB, id uint) entities.Kandang {
t.Helper()
var kandang entities.Kandang
if err := db.Preload("ProjectFlock").First(&kandang, id).Error; err != nil {
t.Fatalf("failed to fetch kandang: %v", err)
}
return kandang
}
func createSupplier(t *testing.T, app *fiber.App, name, alias, category string) uint {
t.Helper()
identifier := strings.ToLower(strings.ReplaceAll(name, " ", "_"))
@@ -4,13 +4,17 @@ import (
"encoding/json"
"fmt"
"net/http"
"net/url"
"testing"
"github.com/gofiber/fiber/v2"
"gitlab.com/mbugroup/lti-api.git/internal/entities"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
)
func TestProjectFlockSummary(t *testing.T) {
app, _ := setupIntegrationApp(t)
app, db := setupIntegrationApp(t)
areaID := createArea(t, app, "Area Project")
locationID := createLocation(t, app, "Location Project", "Address", areaID)
@@ -95,6 +99,21 @@ func TestProjectFlockSummary(t *testing.T) {
t.Fatalf("expected period 1 to be assigned automatically, got %d", createResp.Data.Period)
}
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)
}
if len(pivotRecords) != 1 {
t.Fatalf("expected 1 pivot record, got %d", len(pivotRecords))
}
firstPivotRecord := pivotRecords[0]
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,
@@ -121,6 +140,21 @@ func TestProjectFlockSummary(t *testing.T) {
t.Fatalf("expected second period to be 2, got %d", createRespSecond.Data.Period)
}
pivotRecords = nil
if err := db.Where("project_flock_id = ?", createRespSecond.Data.Id).Find(&pivotRecords).Error; err != nil {
t.Fatalf("failed to fetch second pivot records: %v", err)
}
if len(pivotRecords) != 1 {
t.Fatalf("expected 1 pivot record for second project, got %d", len(pivotRecords))
}
secondPivotRecord := pivotRecords[0]
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)
}
resp, body = doJSONRequest(t, app, http.MethodGet, "/api/production/project_flocks/flocks/"+uintToString(flockID)+"/periods", nil)
if resp.StatusCode != fiber.StatusOK {
t.Fatalf("expected 200 when fetching summary, got %d: %s", resp.StatusCode, string(body))
@@ -144,11 +178,49 @@ func TestProjectFlockSummary(t *testing.T) {
t.Fatalf("expected 200 when deleting first project flock, got %d: %s", resp.StatusCode, string(body))
}
firstKandang := fetchKandang(t, db, kandangID)
if firstKandang.ProjectFlockId != nil {
t.Fatalf("expected project_flock_id to be nil after delete, got %v", *firstKandang.ProjectFlockId)
}
if firstKandang.Status != string(utils.KandangStatusNonActive) {
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)
}
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)
}
resp, body = doJSONRequest(t, app, http.MethodDelete, "/api/production/project_flocks/"+uintToString(createRespSecond.Data.Id), nil)
if resp.StatusCode != fiber.StatusOK {
t.Fatalf("expected 200 when deleting second project flock, got %d: %s", resp.StatusCode, string(body))
}
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)
}
if secondKandang.Status != string(utils.KandangStatusNonActive) {
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)
}
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)
}
resp, body = doJSONRequest(t, app, http.MethodGet, "/api/production/project_flocks/flocks/"+uintToString(flockID)+"/periods", nil)
if resp.StatusCode != fiber.StatusOK {
t.Fatalf("expected 200 when fetching summary after delete, got %d: %s", resp.StatusCode, string(body))
@@ -166,3 +238,178 @@ func TestProjectFlockSummary(t *testing.T) {
func uintToString(v uint) string {
return fmt.Sprintf("%d", v)
}
func TestProjectFlockSearchByRelatedFields(t *testing.T) {
app, _ := setupIntegrationApp(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},
}
resp, body := doJSONRequest(t, app, http.MethodPost, "/api/production/project_flocks", createPayload)
if resp.StatusCode != fiber.StatusCreated {
t.Fatalf("expected 201 when creating project flock, got %d: %s", resp.StatusCode, string(body))
}
var createResp struct {
Data struct {
Id uint `json:"id"`
} `json:"data"`
}
if err := json.Unmarshal(body, &createResp); err != nil {
t.Fatalf("failed to parse create response: %v", err)
}
searchTerms := []string{
"Flock Search Target",
"Area Search Target",
"Category Search Target",
"CATGT",
"FCR Search Target",
"Kandang Search Target",
"Location Search Target",
"Location Address Target",
"Tester",
"1",
}
for _, term := range searchTerms {
path := "/api/production/project_flocks?search=" + url.QueryEscape(term)
resp, body := doJSONRequest(t, app, http.MethodGet, path, nil)
if resp.StatusCode != fiber.StatusOK {
t.Fatalf("expected 200 when searching for %q, got %d: %s", term, resp.StatusCode, string(body))
}
var listResp struct {
Data []struct {
Id uint `json:"id"`
} `json:"data"`
Meta struct {
TotalResults int64 `json:"total_results"`
} `json:"meta"`
}
if err := json.Unmarshal(body, &listResp); err != nil {
t.Fatalf("failed to parse list response for %q: %v", term, err)
}
if listResp.Meta.TotalResults == 0 {
t.Fatalf("expected at least one result when searching for %q", term)
}
if len(listResp.Data) == 0 {
t.Fatalf("expected data when searching for %q", term)
}
if listResp.Data[0].Id != createResp.Data.Id {
t.Fatalf("expected project flock id %d for search term %q, got %d", createResp.Data.Id, term, listResp.Data[0].Id)
}
}
}
func TestProjectFlockSorting(t *testing.T) {
app, _ := setupIntegrationApp(t)
areaA := createArea(t, app, "Area Alpha")
areaB := createArea(t, app, "Area Beta")
locationA := createLocation(t, app, "Location Alpha", "Address Alpha", areaA)
locationB := createLocation(t, app, "Location Beta", "Address Beta", areaB)
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},
})
kandangOne := createKandang(t, app, "Kandang Sort One", locationA, 1)
kandangTwo := createKandang(t, app, "Kandang Sort Two", locationB, 1)
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},
}
resp, body := doJSONRequest(t, app, http.MethodPost, "/api/production/project_flocks", projectOnePayload)
if resp.StatusCode != fiber.StatusCreated {
t.Fatalf("expected 201 for project one, got %d: %s", resp.StatusCode, string(body))
}
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},
}
resp, body = doJSONRequest(t, app, http.MethodPost, "/api/production/project_flocks", projectTwoPayload)
if resp.StatusCode != fiber.StatusCreated {
t.Fatalf("expected 201 for project two, got %d: %s", resp.StatusCode, string(body))
}
projectTwoID := parseProjectFlockID(t, body)
updatePeriodPayload := map[string]any{"period": 5}
resp, body = doJSONRequest(t, app, http.MethodPatch, "/api/production/project_flocks/"+uintToString(projectTwoID), updatePeriodPayload)
if resp.StatusCode != fiber.StatusOK {
t.Fatalf("expected 200 when updating period, got %d: %s", resp.StatusCode, string(body))
}
assertOrder := func(t *testing.T, app *fiber.App, query string, expectedFirst uint) {
t.Helper()
resp, body := doJSONRequest(t, app, http.MethodGet, "/api/production/project_flocks?"+query, nil)
if resp.StatusCode != fiber.StatusOK {
t.Fatalf("expected 200 for query %q, got %d: %s", query, resp.StatusCode, string(body))
}
var listResp struct {
Data []struct {
Id uint `json:"id"`
} `json:"data"`
}
if err := json.Unmarshal(body, &listResp); err != nil {
t.Fatalf("failed to parse list response for %q: %v", query, err)
}
if len(listResp.Data) == 0 {
t.Fatalf("expected data for query %q", query)
}
if listResp.Data[0].Id != expectedFirst {
t.Fatalf("expected first id %d for query %q, got %d", expectedFirst, query, listResp.Data[0].Id)
}
}
assertOrder(t, app, "sort_by=area&sort_order=asc", projectOneID)
assertOrder(t, app, "sort_by=location&sort_order=desc", projectTwoID)
assertOrder(t, app, "sort_by=period&sort_order=desc", projectTwoID)
assertOrder(t, app, "sort_by=kandangs&sort_order=desc", projectTwoID)
assertOrder(t, app, "sort_by=kandangs&sort_order=asc", projectOneID)
}
func parseProjectFlockID(t *testing.T, body []byte) uint {
t.Helper()
var resp struct {
Data struct {
Id uint `json:"id"`
} `json:"data"`
}
if err := json.Unmarshal(body, &resp); err != nil {
t.Fatalf("failed to parse project flock response: %v", err)
}
return resp.Data.Id
}