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

FIX[BE]: period and adjustment helper to function

See merge request mbugroup/lti-api!17
This commit is contained in:
Hafizh A. Y.
2025-10-17 03:32:01 +00:00
7 changed files with 102 additions and 13 deletions
@@ -13,6 +13,7 @@ import (
type KandangBaseDTO struct { type KandangBaseDTO struct {
Id uint `json:"id"` Id uint `json:"id"`
Name string `json:"name"` Name string `json:"name"`
Status string `json:"status"`
Location *locationDTO.LocationBaseDTO `json:"location"` Location *locationDTO.LocationBaseDTO `json:"location"`
Pic *userDTO.UserBaseDTO `json:"pic"` Pic *userDTO.UserBaseDTO `json:"pic"`
} }
@@ -46,6 +47,7 @@ func ToKandangBaseDTO(e entity.Kandang) KandangBaseDTO {
return KandangBaseDTO{ return KandangBaseDTO{
Id: e.Id, Id: e.Id,
Name: e.Name, Name: e.Name,
Status: e.Status,
Location: location, Location: location,
Pic: pic, Pic: pic,
} }
@@ -101,7 +101,11 @@ func (s *kandangService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entit
); err != nil { ); err != nil {
return nil, err return nil, err
} }
status := strings.ToUpper(req.Status)
status := strings.ToUpper(strings.TrimSpace(req.Status))
if status == "" {
status = string(utils.KandangStatusNonActive)
}
if !utils.IsValidKandangStatus(status) { if !utils.IsValidKandangStatus(status) {
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid kandang status") return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid kandang status")
} }
@@ -2,7 +2,7 @@ package validation
type Create struct { type Create struct {
Name string `json:"name" validate:"required_strict,min=3"` Name string `json:"name" validate:"required_strict,min=3"`
Status string `json:"status" validate:"required_strict,min=3"` Status string `json:"status,omitempty" validate:"omitempty,min=3"`
LocationId uint `json:"location_id" validate:"required_strict,number,gt=0"` LocationId uint `json:"location_id" validate:"required_strict,number,gt=0"`
PicId uint `json:"pic_id" validate:"required_strict,number,gt=0"` PicId uint `json:"pic_id" validate:"required_strict,number,gt=0"`
ProjectFlockId *uint `json:"project_flock_id" validate:"omitempty,number,gt=0"` ProjectFlockId *uint `json:"project_flock_id" validate:"omitempty,number,gt=0"`
@@ -7,6 +7,7 @@ import (
"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" entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gorm.io/gorm" "gorm.io/gorm"
"gorm.io/gorm/clause"
) )
type ProjectflockRepository interface { type ProjectflockRepository interface {
@@ -14,6 +15,7 @@ type ProjectflockRepository interface {
GetAllByFlock(ctx context.Context, flockID uint) ([]entity.ProjectFlock, error) GetAllByFlock(ctx context.Context, flockID uint) ([]entity.ProjectFlock, error)
GetActiveByFlock(ctx context.Context, flockID uint) (*entity.ProjectFlock, error) GetActiveByFlock(ctx context.Context, flockID uint) (*entity.ProjectFlock, error)
GetMaxPeriodByFlock(ctx context.Context, flockID uint) (int, error) GetMaxPeriodByFlock(ctx context.Context, flockID uint) (int, error)
GetNextPeriodForFlock(ctx context.Context, flockID uint) (int, error)
} }
type ProjectflockRepositoryImpl struct { type ProjectflockRepositoryImpl struct {
@@ -64,3 +66,23 @@ func (r *ProjectflockRepositoryImpl) GetMaxPeriodByFlock(ctx context.Context, fl
} }
return max, nil return max, nil
} }
func (r *ProjectflockRepositoryImpl) GetNextPeriodForFlock(ctx context.Context, flockID uint) (int, error) {
var payload struct {
Period int
}
if err := r.DB().WithContext(ctx).
Model(&entity.ProjectFlock{}).
Where("flock_id = ?", flockID).
Clauses(clause.Locking{Strength: "UPDATE"}).
Order("period DESC").
Limit(1).
Select("period").
Scan(&payload).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return 1, nil
}
return 0, err
}
return payload.Period + 1, nil
}
@@ -1,9 +1,12 @@
package service package service
import ( import (
"context"
"errors" "errors"
"fmt" "fmt"
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
common "gitlab.com/mbugroup/lti-api.git/internal/common/service"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities" entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
flockRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/repositories" flockRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/repositories"
kandangRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories" kandangRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories"
@@ -15,7 +18,6 @@ import (
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"gorm.io/gorm" "gorm.io/gorm"
"gorm.io/gorm/clause"
) )
type ProjectflockService interface { type ProjectflockService interface {
@@ -106,6 +108,16 @@ func (s *projectflockService) CreateOne(c *fiber.Ctx, req *validation.Create) (*
return nil, fiber.NewError(fiber.StatusBadRequest, "kandang_ids is required") return nil, fiber.NewError(fiber.StatusBadRequest, "kandang_ids is required")
} }
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 {
return nil, err
}
kandangIDs := uniqueUintSlice(req.KandangIds) kandangIDs := uniqueUintSlice(req.KandangIds)
kandangs, err := s.KandangRepo.GetByIDs(c.Context(), kandangIDs, nil) kandangs, err := s.KandangRepo.GetByIDs(c.Context(), kandangIDs, nil)
if err != nil { if err != nil {
@@ -128,18 +140,14 @@ func (s *projectflockService) CreateOne(c *fiber.Ctx, req *validation.Create) (*
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to start transaction") return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to start transaction")
} }
var nextPeriod int projectRepo := repository.NewProjectflockRepository(tx)
periodQuery := tx.Model(&entity.ProjectFlock{}). nextPeriod, err := projectRepo.GetNextPeriodForFlock(c.Context(), req.FlockId)
Where("flock_id = ?", req.FlockId). if err != nil {
Clauses(clause.Locking{Strength: "UPDATE"})
if err := periodQuery.Select("COALESCE(MAX(period), 0)").Scan(&nextPeriod).Error; err != nil {
tx.Rollback() tx.Rollback()
s.Log.Errorf("Failed to determine next period for flock %d: %+v", req.FlockId, err) s.Log.Errorf("Failed to determine next period for flock %d: %+v", req.FlockId, err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to determine next period") return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to determine next period")
} }
nextPeriod++
projectRepo := s.Repository.WithTx(tx)
createBody := &entity.ProjectFlock{ createBody := &entity.ProjectFlock{
FlockId: req.FlockId, FlockId: req.FlockId,
AreaId: req.AreaId, AreaId: req.AreaId,
@@ -190,26 +198,58 @@ func (s projectflockService) UpdateOne(c *fiber.Ctx, req *validation.Update, id
} }
updateBody := make(map[string]any) updateBody := make(map[string]any)
var relationChecks []common.RelationCheck
if req.FlockId != nil { if req.FlockId != nil {
updateBody["flock_id"] = *req.FlockId updateBody["flock_id"] = *req.FlockId
relationChecks = append(relationChecks, common.RelationCheck{
Name: "Flock",
ID: req.FlockId,
Exists: relationExistsChecker[entity.Flock](s.Repository.DB()),
})
} }
if req.AreaId != nil { if req.AreaId != nil {
updateBody["area_id"] = *req.AreaId updateBody["area_id"] = *req.AreaId
relationChecks = append(relationChecks, common.RelationCheck{
Name: "Area",
ID: req.AreaId,
Exists: relationExistsChecker[entity.Area](s.Repository.DB()),
})
} }
if req.ProductCategoryId != nil { if req.ProductCategoryId != nil {
updateBody["product_category_id"] = *req.ProductCategoryId 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.FcrId != nil { if req.FcrId != nil {
updateBody["fcr_id"] = *req.FcrId updateBody["fcr_id"] = *req.FcrId
relationChecks = append(relationChecks, common.RelationCheck{
Name: "FCR",
ID: req.FcrId,
Exists: relationExistsChecker[entity.Fcr](s.Repository.DB()),
})
} }
if req.LocationId != nil { if req.LocationId != nil {
updateBody["location_id"] = *req.LocationId updateBody["location_id"] = *req.LocationId
relationChecks = append(relationChecks, common.RelationCheck{
Name: "Location",
ID: req.LocationId,
Exists: relationExistsChecker[entity.Location](s.Repository.DB()),
})
} }
if req.Period != nil { if req.Period != nil {
updateBody["period"] = *req.Period updateBody["period"] = *req.Period
} }
if len(relationChecks) > 0 {
if err := common.EnsureRelations(c.Context(), relationChecks...); err != nil {
return nil, err
}
}
var newKandangIDs []uint var newKandangIDs []uint
if req.KandangIds != nil { if req.KandangIds != nil {
if len(req.KandangIds) == 0 { if len(req.KandangIds) == 0 {
@@ -238,7 +278,7 @@ func (s projectflockService) UpdateOne(c *fiber.Ctx, req *validation.Update, id
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to start transaction") return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to start transaction")
} }
projectRepo := s.Repository.WithTx(tx) projectRepo := repository.NewProjectflockRepository(tx)
if len(updateBody) > 0 { if len(updateBody) > 0 {
if err := projectRepo.PatchOne(c.Context(), id, updateBody, nil); err != nil { if err := projectRepo.PatchOne(c.Context(), id, updateBody, nil); err != nil {
tx.Rollback() tx.Rollback()
@@ -332,7 +372,7 @@ func (s projectflockService) DeleteOne(c *fiber.Ctx, id uint) error {
} }
} }
if err := s.Repository.WithTx(tx).DeleteOne(c.Context(), id); err != nil { if err := repository.NewProjectflockRepository(tx).DeleteOne(c.Context(), id); err != nil {
tx.Rollback() tx.Rollback()
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusNotFound, "Projectflock not found") return fiber.NewError(fiber.StatusNotFound, "Projectflock not found")
@@ -385,3 +425,9 @@ func uniqueUintSlice(values []uint) []uint {
} }
return result return result
} }
func relationExistsChecker[T any](db *gorm.DB) func(context.Context, uint) (bool, error) {
return func(ctx context.Context, id uint) (bool, error) {
return commonRepo.Exists[T](ctx, db, id)
}
}
+13 -1
View File
@@ -1,6 +1,7 @@
package test package test
import ( import (
"encoding/json"
"net/http" "net/http"
"testing" "testing"
@@ -17,13 +18,24 @@ func TestKandangIntegration(t *testing.T) {
t.Run("create kandang success", func(t *testing.T) { t.Run("create kandang success", func(t *testing.T) {
resp, body := doJSONRequest(t, app, http.MethodPost, "/api/master-data/kandangs", map[string]any{ resp, body := doJSONRequest(t, app, http.MethodPost, "/api/master-data/kandangs", map[string]any{
"name": "Kandang OK", "name": "Kandang OK",
"status": "ACTIVE",
"location_id": locationID, "location_id": locationID,
"pic_id": 1, "pic_id": 1,
}) })
if resp.StatusCode != fiber.StatusCreated { if resp.StatusCode != fiber.StatusCreated {
t.Fatalf("expected 201, got %d: %s", resp.StatusCode, string(body)) t.Fatalf("expected 201, got %d: %s", resp.StatusCode, string(body))
} }
var createResp struct {
Data struct {
Status string `json:"status"`
} `json:"data"`
}
if err := json.Unmarshal(body, &createResp); err != nil {
t.Fatalf("failed to parse create response: %v", err)
}
if createResp.Data.Status == "" {
t.Fatalf("expected default status to be returned, got empty")
}
}) })
t.Run("create kandang with unknown location fails", func(t *testing.T) { t.Run("create kandang with unknown location fails", func(t *testing.T) {
@@ -88,6 +88,9 @@ func TestProjectFlockSummary(t *testing.T) {
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 == "" {
t.Fatalf("expected kandang status to be present, got %+v", createResp.Data.Kandangs[0])
}
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)
} }