mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-20 13:31:56 +00:00
codex/fix: store stocks on farm warehouse when recording egg
This commit is contained in:
+3
@@ -347,7 +347,10 @@ func (r *projectFlockKandangRepositoryImpl) GetByID(ctx context.Context, id uint
|
||||
func (r *projectFlockKandangRepositoryImpl) GetByIDLight(ctx context.Context, id uint) (*entity.ProjectFlockKandang, error) {
|
||||
record := new(entity.ProjectFlockKandang)
|
||||
if err := r.db.WithContext(ctx).
|
||||
Preload("Kandang").
|
||||
Preload("Kandang.Location").
|
||||
Preload("ProjectFlock").
|
||||
Preload("ProjectFlock.Location").
|
||||
First(record, id).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -355,6 +355,14 @@ func (s *recordingService) CreateOne(c *fiber.Ctx, req *validation.Create) (*ent
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Egg details permitted only for laying project flocks")
|
||||
}
|
||||
|
||||
actorID, err := m.ActorIDFromContext(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if req.Eggs, err = s.resolveEggRequestsToFarmWarehouses(ctx, pfk, actorID, req.Eggs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := s.ensureProductWarehousesExist(c, req.Stocks, req.Depletions, req.Eggs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -379,10 +387,6 @@ func (s *recordingService) CreateOne(c *fiber.Ctx, req *validation.Create) (*ent
|
||||
if err := s.ensureProductWarehousesByFlags(ctx, eggIDs, []string{"TELUR-UTUH", "TELUR-PECAH", "TELUR-PUTIH", "TELUR-RETAK", "TELUR"}, "egg"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
actorID, err := m.ActorIDFromContext(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var createdRecording entity.Recording
|
||||
transactionErr := s.Repository.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
if s.ProductionStandardSvc != nil {
|
||||
@@ -687,6 +691,15 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
|
||||
s.Log.Errorf("Failed to list existing eggs: %+v", err)
|
||||
return err
|
||||
}
|
||||
normalizeEggWarehouses, err := s.shouldNormalizeEggRequestsOnUpdate(ctx, existingEggs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if normalizeEggWarehouses {
|
||||
if req.Eggs, err = s.resolveEggRequestsToFarmWarehouses(ctx, pfkForRoute, actorID, req.Eggs); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
existingTotals := recordingutil.EggTotalsByWarehouse(existingEggs, func(egg entity.RecordingEgg) (uint, int, *float64) {
|
||||
return egg.ProductWarehouseId, egg.Qty, egg.Weight
|
||||
})
|
||||
@@ -1492,6 +1505,194 @@ func boolPtr(value bool) *bool {
|
||||
return &v
|
||||
}
|
||||
|
||||
func (s *recordingService) resolveEggRequestsToFarmWarehouses(
|
||||
ctx context.Context,
|
||||
pfk *entity.ProjectFlockKandang,
|
||||
actorID uint,
|
||||
eggs []validation.Egg,
|
||||
) ([]validation.Egg, error) {
|
||||
if len(eggs) == 0 {
|
||||
return eggs, nil
|
||||
}
|
||||
|
||||
locationID, farmName := farmContextFromProjectFlockKandang(pfk)
|
||||
if locationID == 0 {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Farm recording tidak valid")
|
||||
}
|
||||
|
||||
farmWarehouse, err := s.findFirstFarmWarehouse(ctx, locationID)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Farm %s belum memiliki gudang", farmName))
|
||||
}
|
||||
s.Log.Errorf("Failed to resolve farm warehouse for egg recording: %+v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
idSet := make(map[uint]struct{}, len(eggs))
|
||||
for _, egg := range eggs {
|
||||
if egg.ProductWarehouseId != 0 {
|
||||
idSet[egg.ProductWarehouseId] = struct{}{}
|
||||
}
|
||||
}
|
||||
if len(idSet) == 0 {
|
||||
return eggs, nil
|
||||
}
|
||||
|
||||
ids := make([]uint, 0, len(idSet))
|
||||
for id := range idSet {
|
||||
ids = append(ids, id)
|
||||
}
|
||||
|
||||
var sourceWarehouses []entity.ProductWarehouse
|
||||
if err := s.ProductWarehouseRepo.DB().WithContext(ctx).
|
||||
Preload("Warehouse").
|
||||
Where("id IN ?", ids).
|
||||
Find(&sourceWarehouses).Error; err != nil {
|
||||
s.Log.Errorf("Failed to load egg source product warehouses: %+v", err)
|
||||
return nil, err
|
||||
}
|
||||
if len(sourceWarehouses) != len(ids) {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Product warehouse telur tidak ditemukan")
|
||||
}
|
||||
|
||||
sourceByID := make(map[uint]entity.ProductWarehouse, len(sourceWarehouses))
|
||||
resolvedBySource := make(map[uint]uint, len(sourceWarehouses))
|
||||
for _, source := range sourceWarehouses {
|
||||
if err := ensureEggSourceMatchesRecordingScope(source, locationID, pfk.KandangId); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sourceByID[source.Id] = source
|
||||
}
|
||||
|
||||
normalized := make([]validation.Egg, len(eggs))
|
||||
copy(normalized, eggs)
|
||||
for i := range normalized {
|
||||
source := sourceByID[normalized[i].ProductWarehouseId]
|
||||
if resolvedID, ok := resolvedBySource[source.Id]; ok {
|
||||
normalized[i].ProductWarehouseId = resolvedID
|
||||
continue
|
||||
}
|
||||
|
||||
resolvedID, err := s.ProductWarehouseRepo.EnsureProductWarehouse(ctx, source.ProductId, farmWarehouse.Id, nil, actorID)
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed to ensure egg farm product warehouse: %+v", err)
|
||||
return nil, err
|
||||
}
|
||||
resolvedBySource[source.Id] = resolvedID
|
||||
normalized[i].ProductWarehouseId = resolvedID
|
||||
}
|
||||
|
||||
return normalized, nil
|
||||
}
|
||||
|
||||
func farmContextFromProjectFlockKandang(pfk *entity.ProjectFlockKandang) (uint, string) {
|
||||
if pfk == nil {
|
||||
return 0, "tidak diketahui"
|
||||
}
|
||||
|
||||
if pfk.ProjectFlock.LocationId != 0 {
|
||||
name := strings.TrimSpace(pfk.ProjectFlock.Location.Name)
|
||||
if name == "" {
|
||||
name = strings.TrimSpace(pfk.Kandang.Location.Name)
|
||||
}
|
||||
if name == "" {
|
||||
name = fmt.Sprintf("#%d", pfk.ProjectFlock.LocationId)
|
||||
}
|
||||
return pfk.ProjectFlock.LocationId, name
|
||||
}
|
||||
|
||||
if pfk.Kandang.LocationId != 0 {
|
||||
name := strings.TrimSpace(pfk.Kandang.Location.Name)
|
||||
if name == "" {
|
||||
name = fmt.Sprintf("#%d", pfk.Kandang.LocationId)
|
||||
}
|
||||
return pfk.Kandang.LocationId, name
|
||||
}
|
||||
|
||||
return 0, "tidak diketahui"
|
||||
}
|
||||
|
||||
func (s *recordingService) findFirstFarmWarehouse(ctx context.Context, locationID uint) (*entity.Warehouse, error) {
|
||||
var warehouse entity.Warehouse
|
||||
if err := s.Repository.DB().WithContext(ctx).
|
||||
Model(&entity.Warehouse{}).
|
||||
Where("location_id = ? AND type = ?", locationID, utils.WarehouseTypeLokasi).
|
||||
Order("id ASC").
|
||||
First(&warehouse).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &warehouse, nil
|
||||
}
|
||||
|
||||
func ensureEggSourceMatchesRecordingScope(source entity.ProductWarehouse, locationID uint, kandangID uint) error {
|
||||
if source.Id == 0 {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Product warehouse telur tidak ditemukan")
|
||||
}
|
||||
if source.ProductId == 0 {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Produk telur tidak valid")
|
||||
}
|
||||
if source.WarehouseId == 0 {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Gudang telur tidak valid")
|
||||
}
|
||||
if source.Warehouse.LocationId == nil || *source.Warehouse.LocationId != locationID {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Produk telur harus berasal dari farm yang sama")
|
||||
}
|
||||
|
||||
switch strings.ToUpper(strings.TrimSpace(source.Warehouse.Type)) {
|
||||
case string(utils.WarehouseTypeLokasi):
|
||||
return nil
|
||||
case string(utils.WarehouseTypeKandang):
|
||||
if source.Warehouse.KandangId == nil || *source.Warehouse.KandangId != kandangID {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Produk telur harus berasal dari kandang recording yang sama")
|
||||
}
|
||||
return nil
|
||||
default:
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Produk telur harus berasal dari gudang farm atau kandang recording")
|
||||
}
|
||||
}
|
||||
|
||||
func (s *recordingService) shouldNormalizeEggRequestsOnUpdate(ctx context.Context, existingEggs []entity.RecordingEgg) (bool, error) {
|
||||
if len(existingEggs) == 0 {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
idSet := make(map[uint]struct{}, len(existingEggs))
|
||||
for _, egg := range existingEggs {
|
||||
if egg.ProductWarehouseId != 0 {
|
||||
idSet[egg.ProductWarehouseId] = struct{}{}
|
||||
}
|
||||
}
|
||||
if len(idSet) == 0 {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
ids := make([]uint, 0, len(idSet))
|
||||
for id := range idSet {
|
||||
ids = append(ids, id)
|
||||
}
|
||||
|
||||
var productWarehouses []entity.ProductWarehouse
|
||||
if err := s.ProductWarehouseRepo.DB().WithContext(ctx).
|
||||
Preload("Warehouse").
|
||||
Where("id IN ?", ids).
|
||||
Find(&productWarehouses).Error; err != nil {
|
||||
s.Log.Errorf("Failed to load existing egg product warehouses: %+v", err)
|
||||
return false, err
|
||||
}
|
||||
if len(productWarehouses) != len(ids) {
|
||||
return false, fiber.NewError(fiber.StatusBadRequest, "Product warehouse telur tidak ditemukan")
|
||||
}
|
||||
|
||||
for _, productWarehouse := range productWarehouses {
|
||||
if strings.EqualFold(strings.TrimSpace(productWarehouse.Warehouse.Type), string(utils.WarehouseTypeLokasi)) {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (s *recordingService) ensureProductWarehousesExist(c *fiber.Ctx, stocks []validation.Stock, depletions []validation.Depletion, eggs []validation.Egg) error {
|
||||
idSet := make(map[uint]struct{})
|
||||
|
||||
|
||||
@@ -0,0 +1,195 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/glebarez/sqlite"
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
|
||||
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories"
|
||||
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/validations"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func TestResolveEggRequestsToFarmWarehousesChoosesFirstFarmWarehouse(t *testing.T) {
|
||||
db := setupRecordingServiceTestDB(t)
|
||||
repo := repository.NewRecordingRepository(db)
|
||||
productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db)
|
||||
svc := &recordingService{
|
||||
Log: nil,
|
||||
Repository: repo,
|
||||
ProductWarehouseRepo: productWarehouseRepo,
|
||||
}
|
||||
|
||||
pfk := &entity.ProjectFlockKandang{
|
||||
Id: 10,
|
||||
KandangId: 59,
|
||||
ProjectFlock: entity.ProjectFlock{
|
||||
LocationId: 16,
|
||||
Location: entity.Location{Name: "Jamali"},
|
||||
},
|
||||
Kandang: entity.Kandang{
|
||||
Id: 59,
|
||||
LocationId: 16,
|
||||
Location: entity.Location{Name: "Jamali"},
|
||||
},
|
||||
}
|
||||
|
||||
got, err := svc.resolveEggRequestsToFarmWarehouses(context.Background(), pfk, 9, []validation.Egg{
|
||||
{ProductWarehouseId: 101, Qty: 120},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
if len(got) != 1 {
|
||||
t.Fatalf("expected 1 egg row, got %d", len(got))
|
||||
}
|
||||
if got[0].ProductWarehouseId == 101 {
|
||||
t.Fatalf("expected egg warehouse to be remapped to farm warehouse")
|
||||
}
|
||||
|
||||
var resolved entity.ProductWarehouse
|
||||
if err := db.WithContext(context.Background()).
|
||||
Preload("Warehouse").
|
||||
First(&resolved, got[0].ProductWarehouseId).Error; err != nil {
|
||||
t.Fatalf("failed to load resolved product warehouse: %v", err)
|
||||
}
|
||||
|
||||
if resolved.ProductId != 8 {
|
||||
t.Fatalf("expected product_id 8, got %d", resolved.ProductId)
|
||||
}
|
||||
if resolved.WarehouseId != 21 {
|
||||
t.Fatalf("expected first farm warehouse id 21, got %d", resolved.WarehouseId)
|
||||
}
|
||||
if resolved.ProjectFlockKandangId != nil {
|
||||
t.Fatalf("expected farm-level product warehouse to remain shared, got pfk %+v", resolved.ProjectFlockKandangId)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveEggRequestsToFarmWarehousesFailsWhenFarmHasNoWarehouse(t *testing.T) {
|
||||
db := setupRecordingServiceTestDB(t)
|
||||
if err := db.Exec("DELETE FROM warehouses WHERE type = 'LOKASI'").Error; err != nil {
|
||||
t.Fatalf("failed to remove farm warehouses: %v", err)
|
||||
}
|
||||
|
||||
repo := repository.NewRecordingRepository(db)
|
||||
productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db)
|
||||
svc := &recordingService{
|
||||
Log: nil,
|
||||
Repository: repo,
|
||||
ProductWarehouseRepo: productWarehouseRepo,
|
||||
}
|
||||
|
||||
pfk := &entity.ProjectFlockKandang{
|
||||
Id: 10,
|
||||
KandangId: 59,
|
||||
ProjectFlock: entity.ProjectFlock{
|
||||
LocationId: 16,
|
||||
Location: entity.Location{Name: "Jamali"},
|
||||
},
|
||||
}
|
||||
|
||||
_, err := svc.resolveEggRequestsToFarmWarehouses(context.Background(), pfk, 9, []validation.Egg{
|
||||
{ProductWarehouseId: 101, Qty: 120},
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("expected validation error when farm warehouse is missing")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "Farm Jamali belum memiliki gudang") {
|
||||
t.Fatalf("expected missing farm warehouse error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestShouldNormalizeEggRequestsOnUpdatePreservesHistoricalKandangEggs(t *testing.T) {
|
||||
db := setupRecordingServiceTestDB(t)
|
||||
repo := repository.NewRecordingRepository(db)
|
||||
productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db)
|
||||
svc := &recordingService{
|
||||
Log: nil,
|
||||
Repository: repo,
|
||||
ProductWarehouseRepo: productWarehouseRepo,
|
||||
}
|
||||
|
||||
shouldNormalize, err := svc.shouldNormalizeEggRequestsOnUpdate(context.Background(), []entity.RecordingEgg{
|
||||
{ProductWarehouseId: 101, Qty: 120},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
if shouldNormalize {
|
||||
t.Fatal("expected historical kandang-level egg rows to remain kandang-level on update")
|
||||
}
|
||||
}
|
||||
|
||||
func TestShouldNormalizeEggRequestsOnUpdateNormalizesFarmLevelEggs(t *testing.T) {
|
||||
db := setupRecordingServiceTestDB(t)
|
||||
if err := db.Exec(`INSERT INTO product_warehouses (id, product_id, warehouse_id, project_flock_kandang_id, qty) VALUES (201, 8, 21, NULL, 300)`).Error; err != nil {
|
||||
t.Fatalf("failed to insert farm-level egg warehouse: %v", err)
|
||||
}
|
||||
|
||||
repo := repository.NewRecordingRepository(db)
|
||||
productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db)
|
||||
svc := &recordingService{
|
||||
Log: nil,
|
||||
Repository: repo,
|
||||
ProductWarehouseRepo: productWarehouseRepo,
|
||||
}
|
||||
|
||||
shouldNormalize, err := svc.shouldNormalizeEggRequestsOnUpdate(context.Background(), []entity.RecordingEgg{
|
||||
{ProductWarehouseId: 201, Qty: 120},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
if !shouldNormalize {
|
||||
t.Fatal("expected farm-level egg rows to keep using farm normalization on update")
|
||||
}
|
||||
}
|
||||
|
||||
func setupRecordingServiceTestDB(t *testing.T) *gorm.DB {
|
||||
t.Helper()
|
||||
|
||||
db, err := gorm.Open(sqlite.Open("file:"+t.Name()+"?mode=memory&cache=private"), &gorm.Config{})
|
||||
if err != nil {
|
||||
t.Fatalf("failed opening sqlite db: %v", err)
|
||||
}
|
||||
|
||||
statements := []string{
|
||||
`CREATE TABLE locations (
|
||||
id INTEGER PRIMARY KEY,
|
||||
name TEXT NOT NULL
|
||||
)`,
|
||||
`CREATE TABLE warehouses (
|
||||
id INTEGER PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
type TEXT NOT NULL,
|
||||
location_id INTEGER NULL,
|
||||
kandang_id INTEGER NULL,
|
||||
deleted_at TIMESTAMP NULL
|
||||
)`,
|
||||
`CREATE TABLE product_warehouses (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
product_id INTEGER NOT NULL,
|
||||
warehouse_id INTEGER NOT NULL,
|
||||
project_flock_kandang_id INTEGER NULL,
|
||||
qty NUMERIC NULL
|
||||
)`,
|
||||
`INSERT INTO locations (id, name) VALUES (16, 'Jamali')`,
|
||||
`INSERT INTO warehouses (id, name, type, location_id, kandang_id, deleted_at) VALUES
|
||||
(21, 'Gudang Farm Jamali A', 'LOKASI', 16, NULL, NULL),
|
||||
(25, 'Gudang Farm Jamali B', 'LOKASI', 16, NULL, NULL),
|
||||
(46, 'Gudang Jamali 1', 'KANDANG', 16, 59, NULL)`,
|
||||
`INSERT INTO product_warehouses (id, product_id, warehouse_id, project_flock_kandang_id, qty) VALUES
|
||||
(101, 8, 46, 10, 500)`,
|
||||
}
|
||||
|
||||
for _, stmt := range statements {
|
||||
if err := db.Exec(stmt).Error; err != nil {
|
||||
t.Fatalf("failed preparing schema: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
return db
|
||||
}
|
||||
Reference in New Issue
Block a user