From a390d1d23a5ebec7c7d47b95d945ed2cd8aa7ee9 Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Wed, 29 Oct 2025 14:19:08 +0700 Subject: [PATCH 01/12] FIX[BE]: Fix Delete one on chickin match with BE standard --- .../product_warehouse.repository.go | 29 ++ .../project_chickin_detail.repository.go | 25 ++ .../chickins/services/chickin.service.go | 258 ++++++++---------- .../project_flock_population_repository.go | 20 +- 4 files changed, 194 insertions(+), 138 deletions(-) diff --git a/internal/modules/inventory/product-warehouses/repositories/product_warehouse.repository.go b/internal/modules/inventory/product-warehouses/repositories/product_warehouse.repository.go index f1f1fa57..0017fe6c 100644 --- a/internal/modules/inventory/product-warehouses/repositories/product_warehouse.repository.go +++ b/internal/modules/inventory/product-warehouses/repositories/product_warehouse.repository.go @@ -17,6 +17,9 @@ type ProductWarehouseRepository interface { ExistsByID(ctx context.Context, id uint) (bool, error) GetProductWarehouseByProductAndWarehouseID(ctx context.Context, productId, warehouseId uint) (*entity.ProductWarehouse, error) GetByCategoryCodeAndWarehouseID(ctx context.Context, categoryCode string, warehouseId uint) ([]entity.ProductWarehouse, error) + GetLatestByCategoryCodeAndWarehouseID(ctx context.Context, categoryCode string, warehouseId uint) (*entity.ProductWarehouse, error) + WithTxRepo(tx *gorm.DB) ProductWarehouseRepository + DB() *gorm.DB } type ProductWarehouseRepositoryImpl struct { @@ -31,6 +34,15 @@ func NewProductWarehouseRepository(db *gorm.DB) ProductWarehouseRepository { } } +func (r *ProductWarehouseRepositoryImpl) WithTxRepo(tx *gorm.DB) ProductWarehouseRepository { + return &ProductWarehouseRepositoryImpl{ + BaseRepositoryImpl: repository.NewBaseRepository[entity.ProductWarehouse](tx), + db: tx, + } +} +func (r *ProductWarehouseRepositoryImpl) DB() *gorm.DB { + return r.db +} func (r *ProductWarehouseRepositoryImpl) IsProductExist(ctx context.Context, productId uint) (bool, error) { return repository.Exists[entity.Product](ctx, r.db, productId) } @@ -89,3 +101,20 @@ func (r *ProductWarehouseRepositoryImpl) GetByCategoryCodeAndWarehouseID(ctx con } return productWarehouses, nil } + +func (r *ProductWarehouseRepositoryImpl) GetLatestByCategoryCodeAndWarehouseID(ctx context.Context, categoryCode string, warehouseId uint) (*entity.ProductWarehouse, error) { + var productWarehouse entity.ProductWarehouse + err := r.db.WithContext(ctx). + Table("product_warehouses"). + Select("product_warehouses.*"). + Joins("JOIN products ON products.id = product_warehouses.product_id"). + Joins("JOIN product_categories ON product_categories.id = products.product_category_id"). + Where("product_categories.code = ? AND product_warehouses.warehouse_id = ?", categoryCode, warehouseId). + Order("product_warehouses.created_at DESC"). + Limit(1). + First(&productWarehouse).Error + if err != nil { + return nil, err + } + return &productWarehouse, nil +} diff --git a/internal/modules/production/chickins/repositories/project_chickin_detail.repository.go b/internal/modules/production/chickins/repositories/project_chickin_detail.repository.go index 42c267ec..f5be8770 100644 --- a/internal/modules/production/chickins/repositories/project_chickin_detail.repository.go +++ b/internal/modules/production/chickins/repositories/project_chickin_detail.repository.go @@ -1,6 +1,8 @@ package repository import ( + "context" + "gitlab.com/mbugroup/lti-api.git/internal/common/repository" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" "gorm.io/gorm" @@ -8,6 +10,10 @@ import ( type ProjectChickinDetailRepository interface { repository.BaseRepository[entity.ProjectChickinDetail] + CreateOne(ctx context.Context, entity *entity.ProjectChickinDetail, modifier func(*gorm.DB) *gorm.DB) error + DeleteMany(ctx context.Context, modifier func(*gorm.DB) *gorm.DB) error + GetByProjectChickinID(ctx context.Context, projectChickinID uint) ([]entity.ProjectChickinDetail, error) + WithTxRepo(tx *gorm.DB) ProjectChickinDetailRepository } type ChickinDetailRepositoryImpl struct { @@ -19,3 +25,22 @@ func NewChickinDetailRepository(db *gorm.DB) ProjectChickinDetailRepository { BaseRepositoryImpl: repository.NewBaseRepository[entity.ProjectChickinDetail](db), } } + +func (r *ChickinDetailRepositoryImpl) WithTxRepo(tx *gorm.DB) ProjectChickinDetailRepository { + return &ChickinDetailRepositoryImpl{BaseRepositoryImpl: repository.NewBaseRepository[entity.ProjectChickinDetail](tx)} +} + +func (r *ChickinDetailRepositoryImpl) DB() *gorm.DB { + return r.BaseRepositoryImpl.DB() +} + +func (r *ChickinDetailRepositoryImpl) GetByProjectChickinID(ctx context.Context, projectChickinID uint) ([]entity.ProjectChickinDetail, error) { + var records []entity.ProjectChickinDetail + if err := r.DB().WithContext(ctx).Where("project_chickin_id = ?", projectChickinID).Find(&records).Error; err != nil { + return nil, err + } + if len(records) == 0 { + return nil, gorm.ErrRecordNotFound + } + return records, nil +} diff --git a/internal/modules/production/chickins/services/chickin.service.go b/internal/modules/production/chickins/services/chickin.service.go index f422666f..83d94ba5 100644 --- a/internal/modules/production/chickins/services/chickin.service.go +++ b/internal/modules/production/chickins/services/chickin.service.go @@ -77,6 +77,7 @@ func (s chickinService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity } offset := (params.Page - 1) * params.Limit + chickins, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB { db = s.withRelations(db) if params.ProjectFlockKandangId != 0 { @@ -241,143 +242,129 @@ func (s chickinService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) } func (s chickinService) DeleteOne(c *fiber.Ctx, id uint) error { - db := s.Repository.DB() - tx := db.WithContext(c.Context()).Begin() - if tx.Error != nil { - s.Log.Errorf("Failed to begin transaction: %+v", tx.Error) - return tx.Error - } - rollback := func(err error) error { - if rerr := tx.Rollback().Error; rerr != nil { - s.Log.Errorf("Rollback failed: %+v", rerr) + err := s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { + + chickinRepo := repository.NewChickinRepository(dbTransaction) + projectFlockKandangRepo := s.ProjectflockKandangRepo.WithTx(dbTransaction) + productWarehouseRepo := s.ProductWarehouseRepo.WithTxRepo(dbTransaction) + projectFlockPopulationRepo := s.ProjectflockPopulationRepo.WithTx(dbTransaction) + projectChickinDetailRepo := s.ProjectChickinDetailRepo.WithTxRepo(dbTransaction) + warehouseRepoTx := rWarehouse.NewWarehouseRepository(dbTransaction) + + chickin, err := chickinRepo.GetByID(c.Context(), id, nil) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fiber.NewError(fiber.StatusNotFound, "Chickin not found") + } + return err + } + + population, err := projectFlockPopulationRepo.GetByProjectFlockKandangID(c.Context(), chickin.ProjectFlockKandangId) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fiber.NewError(fiber.StatusNotFound, "Project flock population not found") + } + return err + } + + newReserved := population.ReservedQuantity - chickin.Quantity + if newReserved < 0 { + newReserved = 0 + } + + err = projectFlockPopulationRepo.PatchOne(c.Context(), population.Id, map[string]any{ + "reserved_quantity": newReserved, + }, nil) + if err != nil { + return err + } + + restoreFromDetails := func() (bool, error) { + + details, err := projectChickinDetailRepo.GetByProjectChickinID(c.Context(), chickin.Id) + if err != nil { + return false, err + } + + for _, d := range details { + productWarehouse, err := productWarehouseRepo.GetByID(c.Context(), d.ProductWarehouseId, nil) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + continue + } + return false, err + } + + updatedQuantity := productWarehouse.Quantity + d.Quantity + if err := productWarehouseRepo.PatchOne(c.Context(), productWarehouse.Id, map[string]any{"quantity": updatedQuantity}, nil); err != nil { + return false, err + } + } + + if err := projectChickinDetailRepo.DeleteMany(c.Context(), func(db *gorm.DB) *gorm.DB { + return db.Where("project_chickin_id = ?", chickin.Id) + }); err != nil { + return false, err + } + + return true, nil + } + + restored, err := restoreFromDetails() + if err != nil { + return err + } + + if !restored { + projectflockkandang, err := projectFlockKandangRepo.GetByID(c.Context(), population.ProjectFlockKandangId) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fiber.NewError(fiber.StatusNotFound, "Project flock kandang not found") + } + return err + } + + warehouse, err := warehouseRepoTx.GetByKandangID(c.Context(), projectflockkandang.KandangId) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fiber.NewError(fiber.StatusNotFound, "Warehouse not found for kandang") + } + return err + } + + productWarehouse, err := productWarehouseRepo.GetLatestByCategoryCodeAndWarehouseID(c.Context(), "DOC", warehouse.Id) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fiber.NewError(fiber.StatusNotFound, "Product Warehouse not found for the given Project Flock and Warehouse") + } + return err + } + + updatedQuantity := productWarehouse.Quantity + chickin.Quantity + + if err := productWarehouseRepo.PatchOne(c.Context(), productWarehouse.Id, map[string]any{"quantity": updatedQuantity}, nil); err != nil { + return err + } + } + + if err := chickinRepo.DeleteOne(c.Context(), id); err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fiber.NewError(fiber.StatusNotFound, "Chickin not found") + } + return err + } + + return nil + }) + + if err != nil { + if ferr, ok := err.(*fiber.Error); ok { + return ferr } return err } - chickinRepoTx := s.Repository.WithTx(tx) - pfkRepoTx := s.ProjectflockKandangRepo.WithTx(tx) - productWarehouseRepoTx := s.ProductWarehouseRepo.WithTx(tx) - - chickin, err := chickinRepoTx.GetByID(c.Context(), id, nil) - if errors.Is(err, gorm.ErrRecordNotFound) { - return rollback(fiber.NewError(fiber.StatusNotFound, "Chickin not found")) - } - if err != nil { - s.Log.Errorf("Failed get chickin by id: %+v", err) - return rollback(err) - } - - var population entity.ProjectFlockPopulation - if err := tx.WithContext(c.Context()).Where("project_flock_kandang_id = ?", chickin.ProjectFlockKandangId).First(&population).Error; err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return rollback(fiber.NewError(fiber.StatusNotFound, "Project flock population not found")) - } - s.Log.Errorf("Failed to get project flock population: %+v", err) - return rollback(err) - } - - newReserved := population.ReservedQuantity - chickin.Quantity - if newReserved < 0 { - newReserved = 0 - } - if err := tx.WithContext(c.Context()).Model(&entity.ProjectFlockPopulation{}).Where("id = ?", population.Id).Updates(map[string]any{"reserved_quantity": newReserved}).Error; err != nil { - s.Log.Errorf("Failed to update project flock population: %+v", err) - return rollback(err) - } - - restoreFromDetails := func() (bool, error) { - var details []entity.ProjectChickinDetail - if err := tx.WithContext(c.Context()).Where("project_chickin_id = ?", chickin.Id).Find(&details).Error; err != nil { - return false, err - } - if len(details) == 0 { - return false, nil - } - - for _, d := range details { - var pw entity.ProductWarehouse - if err := tx.WithContext(c.Context()).Where("id = ?", d.ProductWarehouseId).First(&pw).Error; err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - - continue - } - return false, err - } - - updatedQuantity := pw.Quantity + d.Quantity - if err := productWarehouseRepoTx.PatchOne(c.Context(), pw.Id, map[string]any{"quantity": updatedQuantity}, nil); err != nil { - return false, err - } - } - - if err := tx.WithContext(c.Context()).Where("project_chickin_id = ?", chickin.Id).Delete(&entity.ProjectChickinDetail{}).Error; err != nil { - return false, err - } - return true, nil - } - - restored, err := restoreFromDetails() - if err != nil { - s.Log.Errorf("Failed to restore from chickin details: %+v", err) - return rollback(err) - } - - if !restored { - - projectflockkandang, err := pfkRepoTx.GetByID(c.Context(), population.ProjectFlockKandangId) - if err != nil { - s.Log.Errorf("Failed to get projectflock kandang: %+v", err) - return rollback(err) - } - - var warehouse entity.Warehouse - if err := tx.WithContext(c.Context()).Where("kandang_id = ?", projectflockkandang.KandangId).First(&warehouse).Error; err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return rollback(fiber.NewError(fiber.StatusNotFound, "Warehouse not found for kandang")) - } - s.Log.Errorf("Failed to get warehouse: %+v", err) - return rollback(err) - } - - var productWarehouse entity.ProductWarehouse - err = tx.WithContext(c.Context()).Table("product_warehouses"). - Select("product_warehouses.*"). - Joins("JOIN products ON products.id = product_warehouses.product_id"). - Joins("JOIN product_categories ON product_categories.id = products.product_category_id"). - Where("product_categories.code = ? AND product_warehouses.warehouse_id = ?", "DOC", warehouse.Id). - Order("product_warehouses.created_at DESC"). - First(&productWarehouse).Error - - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return rollback(fiber.NewError(fiber.StatusNotFound, "Product Warehouse not found for the given Project Flock and Warehouse")) - } - s.Log.Errorf("Failed to get product warehouse: %+v", err) - return rollback(err) - } - - updatedQuantity := productWarehouse.Quantity + chickin.Quantity - if err := productWarehouseRepoTx.PatchOne(c.Context(), productWarehouse.Id, map[string]any{"quantity": updatedQuantity}, nil); err != nil { - s.Log.Errorf("Failed to update product warehouse quantity: %+v", err) - return rollback(err) - } - } - - // delete chickin (single place) - if err := chickinRepoTx.DeleteOne(c.Context(), id); err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return rollback(fiber.NewError(fiber.StatusNotFound, "Chickin not found")) - } - s.Log.Errorf("Failed to delete chickin: %+v", err) - return rollback(err) - } - - if err := tx.Commit().Error; err != nil { - s.Log.Errorf("Failed to commit transaction: %+v", err) - return rollback(err) - } - return nil } @@ -385,11 +372,8 @@ func (s *chickinService) Approve(c *fiber.Ctx, id uint) error { // todo: ini contoh akhir jika sudah approved - chickin, err := s.Repository.GetByID( - c.Context(), - id, - nil, - ) + chickin, err := s.Repository.GetByID(c.Context(), id, nil) + if errors.Is(err, gorm.ErrRecordNotFound) { return fiber.NewError(fiber.StatusNotFound, "Chickin not found") } diff --git a/internal/modules/production/project_flocks/repositories/project_flock_population_repository.go b/internal/modules/production/project_flocks/repositories/project_flock_population_repository.go index cb4b0d5f..3328d286 100644 --- a/internal/modules/production/project_flocks/repositories/project_flock_population_repository.go +++ b/internal/modules/production/project_flocks/repositories/project_flock_population_repository.go @@ -9,8 +9,16 @@ import ( ) type ProjectFlockPopulationRepository interface { - repository.BaseRepository[entity.ProjectFlockPopulation] + // domain-specific GetByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (*entity.ProjectFlockPopulation, error) + + // subset of base repository methods used by services + CreateOne(ctx context.Context, entity *entity.ProjectFlockPopulation, modifier func(*gorm.DB) *gorm.DB) error + PatchOne(ctx context.Context, id uint, updates map[string]any, modifier func(*gorm.DB) *gorm.DB) error + + // transaction helpers + WithTx(tx *gorm.DB) ProjectFlockPopulationRepository + DB() *gorm.DB } type projectFlockPopulationRepositoryImpl struct { @@ -23,6 +31,16 @@ func NewProjectFlockPopulationRepository(db *gorm.DB) ProjectFlockPopulationRepo } } +func (r *projectFlockPopulationRepositoryImpl) WithTx(tx *gorm.DB) ProjectFlockPopulationRepository { + return &projectFlockPopulationRepositoryImpl{ + BaseRepositoryImpl: repository.NewBaseRepository[entity.ProjectFlockPopulation](tx), + } +} + +func (r *projectFlockPopulationRepositoryImpl) DB() *gorm.DB { + return r.BaseRepositoryImpl.DB() +} + func (r *projectFlockPopulationRepositoryImpl) GetByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (*entity.ProjectFlockPopulation, error) { var record entity.ProjectFlockPopulation err := r.DB().WithContext(ctx). From 31bb28f7daf38c7116a1991be7688dafe8340448 Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Thu, 30 Oct 2025 09:06:21 +0700 Subject: [PATCH 02/12] Feat(BE-127): create migration for transfer to laying and inisiate module --- ...825_create_laying_transfers_table.down.sql | 1 + ...74825_create_laying_transfers_table.up.sql | 42 +++++ ...te_laying_kandang_transfers_table.down.sql | 1 + ...eate_laying_kandang_transfers_table.up.sql | 40 +++++ internal/entities/transfer_laying.go | 18 +++ internal/modules/production/route.go | 4 +- .../controllers/transfer_laying.controller.go | 144 ++++++++++++++++++ .../dto/transfer_laying.dto.go | 64 ++++++++ .../production/transfer_layings/module.go | 26 ++++ .../transfer_laying.repository.go | 21 +++ .../production/transfer_layings/route.go | 28 ++++ .../services/transfer_laying.service.go | 129 ++++++++++++++++ .../validations/transfer_laying.validation.go | 15 ++ 13 files changed, 532 insertions(+), 1 deletion(-) create mode 100644 internal/database/migrations/20251029074825_create_laying_transfers_table.down.sql create mode 100644 internal/database/migrations/20251029074825_create_laying_transfers_table.up.sql create mode 100644 internal/database/migrations/20251029081833_create_laying_kandang_transfers_table.down.sql create mode 100644 internal/database/migrations/20251029081833_create_laying_kandang_transfers_table.up.sql create mode 100644 internal/entities/transfer_laying.go create mode 100644 internal/modules/production/transfer_layings/controllers/transfer_laying.controller.go create mode 100644 internal/modules/production/transfer_layings/dto/transfer_laying.dto.go create mode 100644 internal/modules/production/transfer_layings/module.go create mode 100644 internal/modules/production/transfer_layings/repositories/transfer_laying.repository.go create mode 100644 internal/modules/production/transfer_layings/route.go create mode 100644 internal/modules/production/transfer_layings/services/transfer_laying.service.go create mode 100644 internal/modules/production/transfer_layings/validations/transfer_laying.validation.go diff --git a/internal/database/migrations/20251029074825_create_laying_transfers_table.down.sql b/internal/database/migrations/20251029074825_create_laying_transfers_table.down.sql new file mode 100644 index 00000000..6ad77820 --- /dev/null +++ b/internal/database/migrations/20251029074825_create_laying_transfers_table.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS laying_transfers; \ No newline at end of file diff --git a/internal/database/migrations/20251029074825_create_laying_transfers_table.up.sql b/internal/database/migrations/20251029074825_create_laying_transfers_table.up.sql new file mode 100644 index 00000000..8677ca71 --- /dev/null +++ b/internal/database/migrations/20251029074825_create_laying_transfers_table.up.sql @@ -0,0 +1,42 @@ +CREATE TABLE IF NOT EXISTS laying_transfers ( + id BIGSERIAL PRIMARY KEY, + from_project_flock_id BIGINT NOT NULL, + to_project_flock_id BIGINT NOT NULL, + total_quantity INTEGER, + notes TEXT, + created_at TIMESTAMPTZ DEFAULT now(), + updated_at TIMESTAMPTZ DEFAULT now(), + deleted_at TIMESTAMPTZ, + created_by BIGINT +); + +-- FOREIGN KEYS (dijalankan setelah semua tabel parent ada) +DO $$ +BEGIN + IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'project_flocks') THEN + ALTER TABLE laying_transfers + ADD CONSTRAINT fk_laying_from_project_flock + FOREIGN KEY (from_project_flock_id) + REFERENCES project_flocks(id) + ON DELETE RESTRICT ON UPDATE CASCADE; + ALTER TABLE laying_transfers + ADD CONSTRAINT fk_laying_to_project_flock + FOREIGN KEY (to_project_flock_id) + REFERENCES project_flocks(id) + ON DELETE RESTRICT ON UPDATE CASCADE; + END IF; + + IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'users') THEN + ALTER TABLE laying_transfers + ADD CONSTRAINT fk_laying_created_by + FOREIGN KEY (created_by) + REFERENCES users(id) + ON DELETE RESTRICT ON UPDATE CASCADE; + END IF; +END $$; + +-- INDEXES +CREATE INDEX IF NOT EXISTS idx_laying_transfers_from_project_flock_id ON laying_transfers(from_project_flock_id); +CREATE INDEX IF NOT EXISTS idx_laying_transfers_to_project_flock_id ON laying_transfers(to_project_flock_id); +CREATE INDEX IF NOT EXISTS idx_laying_transfers_created_by ON laying_transfers(created_by); +CREATE INDEX IF NOT EXISTS idx_laying_transfers_deleted_at ON laying_transfers(deleted_at); \ No newline at end of file diff --git a/internal/database/migrations/20251029081833_create_laying_kandang_transfers_table.down.sql b/internal/database/migrations/20251029081833_create_laying_kandang_transfers_table.down.sql new file mode 100644 index 00000000..caf4f52d --- /dev/null +++ b/internal/database/migrations/20251029081833_create_laying_kandang_transfers_table.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS laying_kandang_transfers; \ No newline at end of file diff --git a/internal/database/migrations/20251029081833_create_laying_kandang_transfers_table.up.sql b/internal/database/migrations/20251029081833_create_laying_kandang_transfers_table.up.sql new file mode 100644 index 00000000..786f8de3 --- /dev/null +++ b/internal/database/migrations/20251029081833_create_laying_kandang_transfers_table.up.sql @@ -0,0 +1,40 @@ +CREATE TABLE IF NOT EXISTS laying_kandang_transfers ( + id BIGSERIAL PRIMARY KEY, + kandang_id BIGINT, + product_warehouse_id BIGINT, + qty NUMERIC(15,3), + laying_transfer_id BIGINT NOT NULL +); + +-- FOREIGN KEYS (dijalankan setelah semua tabel parent ada) +DO $$ +BEGIN + IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'kandangs') THEN + ALTER TABLE laying_kandang_transfers + ADD CONSTRAINT fk_laying_kandang_transfers_kandang + FOREIGN KEY (kandang_id) + REFERENCES kandangs(id) + ON DELETE RESTRICT ON UPDATE CASCADE; + END IF; + + IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'product_warehouses') THEN + ALTER TABLE laying_kandang_transfers + ADD CONSTRAINT fk_laying_kandang_transfers_product_warehouse + FOREIGN KEY (product_warehouse_id) + REFERENCES product_warehouses(id) + ON DELETE RESTRICT ON UPDATE CASCADE; + END IF; + + IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'laying_transfers') THEN + ALTER TABLE laying_kandang_transfers + ADD CONSTRAINT fk_laying_kandang_transfers_laying_transfer + FOREIGN KEY (laying_transfer_id) + REFERENCES laying_transfers(id) + ON DELETE RESTRICT ON UPDATE CASCADE; + END IF; +END $$; + +-- INDEXES +CREATE INDEX IF NOT EXISTS idx_laying_kandang_transfers_kandang_id ON laying_kandang_transfers(kandang_id); +CREATE INDEX IF NOT EXISTS idx_laying_kandang_transfers_product_warehouse_id ON laying_kandang_transfers(product_warehouse_id); +CREATE INDEX IF NOT EXISTS idx_laying_kandang_transfers_laying_transfer_id ON laying_kandang_transfers(laying_transfer_id); \ No newline at end of file diff --git a/internal/entities/transfer_laying.go b/internal/entities/transfer_laying.go new file mode 100644 index 00000000..fd60a435 --- /dev/null +++ b/internal/entities/transfer_laying.go @@ -0,0 +1,18 @@ +package entities + +import ( + "time" + + "gorm.io/gorm" +) + +type TransferLaying struct { + Id uint `gorm:"primaryKey"` + Name string `gorm:"not null;uniqueIndex:idx_name,where:deleted_at IS NULL"` + CreatedBy uint `gorm:"not null"` + CreatedAt time.Time `gorm:"autoCreateTime"` + UpdatedAt time.Time `gorm:"autoUpdateTime"` + DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` + + CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"` +} diff --git a/internal/modules/production/route.go b/internal/modules/production/route.go index b41ef1e7..10effb58 100644 --- a/internal/modules/production/route.go +++ b/internal/modules/production/route.go @@ -10,6 +10,7 @@ import ( chickins "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins" projectflocks "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks" recordings "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings" + transferLayings "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings" // MODULE IMPORTS ) @@ -20,8 +21,9 @@ func RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Valida projectflocks.ProjectflockModule{}, recordings.RecordingModule{}, chickins.ChickinModule{}, + transferLayings.TransferLayingModule{}, // MODULE REGISTRY - } +} for _, m := range allModules { m.RegisterRoutes(group, db, validate) diff --git a/internal/modules/production/transfer_layings/controllers/transfer_laying.controller.go b/internal/modules/production/transfer_layings/controllers/transfer_laying.controller.go new file mode 100644 index 00000000..01f49867 --- /dev/null +++ b/internal/modules/production/transfer_layings/controllers/transfer_laying.controller.go @@ -0,0 +1,144 @@ +package controller + +import ( + "math" + "strconv" + + "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/dto" + service "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/services" + validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/validations" + "gitlab.com/mbugroup/lti-api.git/internal/response" + + "github.com/gofiber/fiber/v2" +) + +type TransferLayingController struct { + TransferLayingService service.TransferLayingService +} + +func NewTransferLayingController(transferLayingService service.TransferLayingService) *TransferLayingController { + return &TransferLayingController{ + TransferLayingService: transferLayingService, + } +} + +func (u *TransferLayingController) GetAll(c *fiber.Ctx) error { + query := &validation.Query{ + Page: c.QueryInt("page", 1), + Limit: c.QueryInt("limit", 10), + Search: c.Query("search", ""), + } + + if query.Page < 1 || query.Limit < 1 { + return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0") + } + + result, totalResults, err := u.TransferLayingService.GetAll(c, query) + if err != nil { + return err + } + + return c.Status(fiber.StatusOK). + JSON(response.SuccessWithPaginate[dto.TransferLayingListDTO]{ + Code: fiber.StatusOK, + Status: "success", + Message: "Get all transferLayings successfully", + Meta: response.Meta{ + Page: query.Page, + Limit: query.Limit, + TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))), + TotalResults: totalResults, + }, + Data: dto.ToTransferLayingListDTOs(result), + }) +} + +func (u *TransferLayingController) GetOne(c *fiber.Ctx) error { + param := c.Params("id") + + id, err := strconv.Atoi(param) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, "Invalid Id") + } + + result, err := u.TransferLayingService.GetOne(c, uint(id)) + if err != nil { + return err + } + + return c.Status(fiber.StatusOK). + JSON(response.Success{ + Code: fiber.StatusOK, + Status: "success", + Message: "Get transferLaying successfully", + Data: dto.ToTransferLayingListDTO(*result), + }) +} + +func (u *TransferLayingController) CreateOne(c *fiber.Ctx) error { + req := new(validation.Create) + + if err := c.BodyParser(req); err != nil { + return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") + } + + result, err := u.TransferLayingService.CreateOne(c, req) + if err != nil { + return err + } + + return c.Status(fiber.StatusCreated). + JSON(response.Success{ + Code: fiber.StatusCreated, + Status: "success", + Message: "Create transferLaying successfully", + Data: dto.ToTransferLayingListDTO(*result), + }) +} + +func (u *TransferLayingController) UpdateOne(c *fiber.Ctx) error { + req := new(validation.Update) + param := c.Params("id") + + id, err := strconv.Atoi(param) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, "Invalid Id") + } + + if err := c.BodyParser(req); err != nil { + return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") + } + + result, err := u.TransferLayingService.UpdateOne(c, req, uint(id)) + if err != nil { + return err + } + + return c.Status(fiber.StatusOK). + JSON(response.Success{ + Code: fiber.StatusOK, + Status: "success", + Message: "Update transferLaying successfully", + Data: dto.ToTransferLayingListDTO(*result), + }) +} + +func (u *TransferLayingController) DeleteOne(c *fiber.Ctx) error { + param := c.Params("id") + + id, err := strconv.Atoi(param) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, "Invalid Id") + } + + if err := u.TransferLayingService.DeleteOne(c, uint(id)); err != nil { + return err + } + + return c.Status(fiber.StatusOK). + JSON(response.Common{ + Code: fiber.StatusOK, + Status: "success", + Message: "Delete transferLaying successfully", + }) +} diff --git a/internal/modules/production/transfer_layings/dto/transfer_laying.dto.go b/internal/modules/production/transfer_layings/dto/transfer_laying.dto.go new file mode 100644 index 00000000..103c4c35 --- /dev/null +++ b/internal/modules/production/transfer_layings/dto/transfer_laying.dto.go @@ -0,0 +1,64 @@ +package dto + +import ( + "time" + + entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto" +) + +// === DTO Structs === + +type TransferLayingBaseDTO struct { + Id uint `json:"id"` + Name string `json:"name"` +} + +type TransferLayingListDTO struct { + TransferLayingBaseDTO + CreatedUser *userDTO.UserBaseDTO `json:"created_user"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +type TransferLayingDetailDTO struct { + TransferLayingListDTO +} + +// === Mapper Functions === + +func ToTransferLayingBaseDTO(e entity.TransferLaying) TransferLayingBaseDTO { + return TransferLayingBaseDTO{ + Id: e.Id, + Name: e.Name, + } +} + +func ToTransferLayingListDTO(e entity.TransferLaying) TransferLayingListDTO { + var createdUser *userDTO.UserBaseDTO + if e.CreatedUser.Id != 0 { + mapped := userDTO.ToUserBaseDTO(e.CreatedUser) + createdUser = &mapped + } + + return TransferLayingListDTO{ + TransferLayingBaseDTO: ToTransferLayingBaseDTO(e), + CreatedAt: e.CreatedAt, + UpdatedAt: e.UpdatedAt, + CreatedUser: createdUser, + } +} + +func ToTransferLayingListDTOs(e []entity.TransferLaying) []TransferLayingListDTO { + result := make([]TransferLayingListDTO, len(e)) + for i, r := range e { + result[i] = ToTransferLayingListDTO(r) + } + return result +} + +func ToTransferLayingDetailDTO(e entity.TransferLaying) TransferLayingDetailDTO { + return TransferLayingDetailDTO{ + TransferLayingListDTO: ToTransferLayingListDTO(e), + } +} diff --git a/internal/modules/production/transfer_layings/module.go b/internal/modules/production/transfer_layings/module.go new file mode 100644 index 00000000..d3c6279c --- /dev/null +++ b/internal/modules/production/transfer_layings/module.go @@ -0,0 +1,26 @@ +package transfer_layings + +import ( + "github.com/go-playground/validator/v10" + "github.com/gofiber/fiber/v2" + "gorm.io/gorm" + + rTransferLaying "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/repositories" + sTransferLaying "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/services" + + rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories" + sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" +) + +type TransferLayingModule struct{} + +func (TransferLayingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) { + transferLayingRepo := rTransferLaying.NewTransferLayingRepository(db) + userRepo := rUser.NewUserRepository(db) + + transferLayingService := sTransferLaying.NewTransferLayingService(transferLayingRepo, validate) + userService := sUser.NewUserService(userRepo, validate) + + TransferLayingRoutes(router, userService, transferLayingService) +} + diff --git a/internal/modules/production/transfer_layings/repositories/transfer_laying.repository.go b/internal/modules/production/transfer_layings/repositories/transfer_laying.repository.go new file mode 100644 index 00000000..32ea27ee --- /dev/null +++ b/internal/modules/production/transfer_layings/repositories/transfer_laying.repository.go @@ -0,0 +1,21 @@ +package repository + +import ( + entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + "gitlab.com/mbugroup/lti-api.git/internal/common/repository" + "gorm.io/gorm" +) + +type TransferLayingRepository interface { + repository.BaseRepository[entity.TransferLaying] +} + +type TransferLayingRepositoryImpl struct { + *repository.BaseRepositoryImpl[entity.TransferLaying] +} + +func NewTransferLayingRepository(db *gorm.DB) TransferLayingRepository { + return &TransferLayingRepositoryImpl{ + BaseRepositoryImpl: repository.NewBaseRepository[entity.TransferLaying](db), + } +} diff --git a/internal/modules/production/transfer_layings/route.go b/internal/modules/production/transfer_layings/route.go new file mode 100644 index 00000000..ee806506 --- /dev/null +++ b/internal/modules/production/transfer_layings/route.go @@ -0,0 +1,28 @@ +package transfer_layings + +import ( + // m "gitlab.com/mbugroup/lti-api.git/internal/middleware" + controller "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/controllers" + transferLaying "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/services" + user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" + + "github.com/gofiber/fiber/v2" +) + +func TransferLayingRoutes(v1 fiber.Router, u user.UserService, s transferLaying.TransferLayingService) { + ctrl := controller.NewTransferLayingController(s) + + route := v1.Group("/transfer_layings") + + // route.Get("/", m.Auth(u), ctrl.GetAll) + // route.Post("/", m.Auth(u), ctrl.CreateOne) + // route.Get("/:id", m.Auth(u), ctrl.GetOne) + // route.Patch("/:id", m.Auth(u), ctrl.UpdateOne) + // route.Delete("/:id", m.Auth(u), ctrl.DeleteOne) + + route.Get("/", ctrl.GetAll) + route.Post("/", ctrl.CreateOne) + route.Get("/:id", ctrl.GetOne) + route.Patch("/:id", ctrl.UpdateOne) + route.Delete("/:id", ctrl.DeleteOne) +} diff --git a/internal/modules/production/transfer_layings/services/transfer_laying.service.go b/internal/modules/production/transfer_layings/services/transfer_laying.service.go new file mode 100644 index 00000000..36541600 --- /dev/null +++ b/internal/modules/production/transfer_layings/services/transfer_laying.service.go @@ -0,0 +1,129 @@ +package service + +import ( + "errors" + + entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + repository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/repositories" + validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/validations" + "gitlab.com/mbugroup/lti-api.git/internal/utils" + + "github.com/go-playground/validator/v10" + "github.com/gofiber/fiber/v2" + "github.com/sirupsen/logrus" + "gorm.io/gorm" +) + +type TransferLayingService interface { + GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.TransferLaying, int64, error) + GetOne(ctx *fiber.Ctx, id uint) (*entity.TransferLaying, error) + CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.TransferLaying, error) + UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.TransferLaying, error) + DeleteOne(ctx *fiber.Ctx, id uint) error +} + +type transferLayingService struct { + Log *logrus.Logger + Validate *validator.Validate + Repository repository.TransferLayingRepository +} + +func NewTransferLayingService(repo repository.TransferLayingRepository, validate *validator.Validate) TransferLayingService { + return &transferLayingService{ + Log: utils.Log, + Validate: validate, + Repository: repo, + } +} + +func (s transferLayingService) withRelations(db *gorm.DB) *gorm.DB { + return db.Preload("CreatedUser") +} + +func (s transferLayingService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.TransferLaying, int64, error) { + if err := s.Validate.Struct(params); err != nil { + return nil, 0, err + } + + offset := (params.Page - 1) * params.Limit + + transferLayings, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB { + db = s.withRelations(db) + if params.Search != "" { + return db.Where("name LIKE ?", "%"+params.Search+"%") + } + return db.Order("created_at DESC").Order("updated_at DESC") + }) + + if err != nil { + s.Log.Errorf("Failed to get transferLayings: %+v", err) + return nil, 0, err + } + return transferLayings, total, nil +} + +func (s transferLayingService) GetOne(c *fiber.Ctx, id uint) (*entity.TransferLaying, error) { + transferLaying, err := s.Repository.GetByID(c.Context(), id, s.withRelations) + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fiber.NewError(fiber.StatusNotFound, "TransferLaying not found") + } + if err != nil { + s.Log.Errorf("Failed get transferLaying by id: %+v", err) + return nil, err + } + return transferLaying, nil +} + +func (s *transferLayingService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.TransferLaying, error) { + if err := s.Validate.Struct(req); err != nil { + return nil, err + } + + createBody := &entity.TransferLaying{ + Name: req.Name, + } + + if err := s.Repository.CreateOne(c.Context(), createBody, nil); err != nil { + s.Log.Errorf("Failed to create transferLaying: %+v", err) + return nil, err + } + + return s.GetOne(c, createBody.Id) +} + +func (s transferLayingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.TransferLaying, error) { + if err := s.Validate.Struct(req); err != nil { + return nil, err + } + + updateBody := make(map[string]any) + + if req.Name != nil { + updateBody["name"] = *req.Name + } + + if len(updateBody) == 0 { + return s.GetOne(c, id) + } + + if err := s.Repository.PatchOne(c.Context(), id, updateBody, nil); err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fiber.NewError(fiber.StatusNotFound, "TransferLaying not found") + } + s.Log.Errorf("Failed to update transferLaying: %+v", err) + return nil, err + } + + return s.GetOne(c, id) +} + +func (s transferLayingService) DeleteOne(c *fiber.Ctx, id uint) error { + if err := s.Repository.DeleteOne(c.Context(), id); err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fiber.NewError(fiber.StatusNotFound, "TransferLaying not found") + } + s.Log.Errorf("Failed to delete transferLaying: %+v", err) + return err + } + return nil +} diff --git a/internal/modules/production/transfer_layings/validations/transfer_laying.validation.go b/internal/modules/production/transfer_layings/validations/transfer_laying.validation.go new file mode 100644 index 00000000..7d16d3ee --- /dev/null +++ b/internal/modules/production/transfer_layings/validations/transfer_laying.validation.go @@ -0,0 +1,15 @@ +package validation + +type Create struct { + Name string `json:"name" validate:"required_strict,min=3"` +} + +type Update struct { + Name *string `json:"name,omitempty" validate:"omitempty"` +} + +type Query struct { + Page int `query:"page" validate:"omitempty,number,min=1,gt=0"` + Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"` + Search string `query:"search" validate:"omitempty,max=50"` +} From bf14ab78652d7e87fc6b34013a288a987ba36cd3 Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Fri, 31 Oct 2025 14:27:08 +0700 Subject: [PATCH 03/12] fix(BE): Change migration chickin and project flock population to refactored one --- ...74825_create_laying_transfers_table.up.sql | 14 +++-- ...1030134228_refactor_project_chikins.up.sql | 5 ++ ...27_recreate_project_chikins_table.down.sql | 0 ...4527_recreate_project_chikins_table.up.sql | 58 +++++++++++++++++++ ...ecreate_project_flock_populations.down.sql | 0 ..._recreate_project_flock_populations.up.sql | 57 ++++++++++++++++++ 6 files changed, 129 insertions(+), 5 deletions(-) create mode 100644 internal/database/migrations/20251030134228_refactor_project_chikins.up.sql create mode 100644 internal/database/migrations/20251030134527_recreate_project_chikins_table.down.sql create mode 100644 internal/database/migrations/20251030134527_recreate_project_chikins_table.up.sql create mode 100644 internal/database/migrations/20251030134542_recreate_project_flock_populations.down.sql create mode 100644 internal/database/migrations/20251030134542_recreate_project_flock_populations.up.sql diff --git a/internal/database/migrations/20251029074825_create_laying_transfers_table.up.sql b/internal/database/migrations/20251029074825_create_laying_transfers_table.up.sql index 8677ca71..10ced117 100644 --- a/internal/database/migrations/20251029074825_create_laying_transfers_table.up.sql +++ b/internal/database/migrations/20251029074825_create_laying_transfers_table.up.sql @@ -2,7 +2,8 @@ CREATE TABLE IF NOT EXISTS laying_transfers ( id BIGSERIAL PRIMARY KEY, from_project_flock_id BIGINT NOT NULL, to_project_flock_id BIGINT NOT NULL, - total_quantity INTEGER, + transfer_date DATE NOT NULL, + total_qty INTEGER, notes TEXT, created_at TIMESTAMPTZ DEFAULT now(), updated_at TIMESTAMPTZ DEFAULT now(), @@ -36,7 +37,10 @@ BEGIN END $$; -- INDEXES -CREATE INDEX IF NOT EXISTS idx_laying_transfers_from_project_flock_id ON laying_transfers(from_project_flock_id); -CREATE INDEX IF NOT EXISTS idx_laying_transfers_to_project_flock_id ON laying_transfers(to_project_flock_id); -CREATE INDEX IF NOT EXISTS idx_laying_transfers_created_by ON laying_transfers(created_by); -CREATE INDEX IF NOT EXISTS idx_laying_transfers_deleted_at ON laying_transfers(deleted_at); \ No newline at end of file +CREATE INDEX IF NOT EXISTS idx_laying_transfers_from_project_flock_id ON laying_transfers (from_project_flock_id); + +CREATE INDEX IF NOT EXISTS idx_laying_transfers_to_project_flock_id ON laying_transfers (to_project_flock_id); + +CREATE INDEX IF NOT EXISTS idx_laying_transfers_created_by ON laying_transfers (created_by); + +CREATE INDEX IF NOT EXISTS idx_laying_transfers_deleted_at ON laying_transfers (deleted_at); \ No newline at end of file diff --git a/internal/database/migrations/20251030134228_refactor_project_chikins.up.sql b/internal/database/migrations/20251030134228_refactor_project_chikins.up.sql new file mode 100644 index 00000000..98ac9b73 --- /dev/null +++ b/internal/database/migrations/20251030134228_refactor_project_chikins.up.sql @@ -0,0 +1,5 @@ +DROP TABLE IF EXISTS project_chickin_details; + +DROP TABLE IF EXISTS project_chickins; + +DROP TABLE IF EXISTS project_flock_populations; \ No newline at end of file diff --git a/internal/database/migrations/20251030134527_recreate_project_chikins_table.down.sql b/internal/database/migrations/20251030134527_recreate_project_chikins_table.down.sql new file mode 100644 index 00000000..e69de29b diff --git a/internal/database/migrations/20251030134527_recreate_project_chikins_table.up.sql b/internal/database/migrations/20251030134527_recreate_project_chikins_table.up.sql new file mode 100644 index 00000000..c00765b1 --- /dev/null +++ b/internal/database/migrations/20251030134527_recreate_project_chikins_table.up.sql @@ -0,0 +1,58 @@ +-- ============================================ +-- MIGRATION: project_chickins +-- ============================================ + +-- STEP 1: Hapus tabel jika sudah ada + +-- STEP 2: Buat tabel project_chickins +CREATE TABLE IF NOT EXISTS project_chickins ( + id BIGSERIAL PRIMARY KEY, + project_flock_kandang_id BIGINT NOT NULL, + product_warehouse_id BIGINT NOT NULL, + chick_in_date DATE NOT NULL, + usage_qty NUMERIC(15, 3) NOT NULL, + pending_usage_qty NUMERIC(15, 3) DEFAULT 0, + notes TEXT, + created_by BIGINT NOT NULL, + created_at TIMESTAMPTZ DEFAULT now(), + updated_at TIMESTAMPTZ DEFAULT now(), + deleted_at TIMESTAMPTZ +); + +-- STEP 3: FOREIGN KEYS +DO $$ +BEGIN + -- Relasi ke project_flock_kandangs + IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'project_flock_kandangs') THEN + ALTER TABLE project_chickins + ADD CONSTRAINT fk_project_chickins_kandang + FOREIGN KEY (project_flock_kandang_id) + REFERENCES project_flock_kandangs(id) + ON DELETE RESTRICT ON UPDATE CASCADE; + END IF; + + -- Relasi ke product_warehouses + IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'product_warehouses') THEN + ALTER TABLE project_chickins + ADD CONSTRAINT fk_project_chickins_warehouse + FOREIGN KEY (product_warehouse_id) + REFERENCES product_warehouses(id) + ON DELETE RESTRICT ON UPDATE CASCADE; + END IF; + + -- Relasi ke users + IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'users') THEN + ALTER TABLE project_chickins + ADD CONSTRAINT fk_project_chickins_created_by + FOREIGN KEY (created_by) + REFERENCES users(id) + ON DELETE RESTRICT ON UPDATE CASCADE; + END IF; +END $$; + +-- STEP 4: INDEXES +CREATE INDEX IF NOT EXISTS idx_chickins_kandang_id ON project_chickins (project_flock_kandang_id); + +CREATE INDEX IF NOT EXISTS idx_chickins_warehouse_id ON project_chickins (product_warehouse_id); + +CREATE INDEX IF NOT EXISTS idx_chickins_created_by ON project_chickins (created_by); \ No newline at end of file diff --git a/internal/database/migrations/20251030134542_recreate_project_flock_populations.down.sql b/internal/database/migrations/20251030134542_recreate_project_flock_populations.down.sql new file mode 100644 index 00000000..e69de29b diff --git a/internal/database/migrations/20251030134542_recreate_project_flock_populations.up.sql b/internal/database/migrations/20251030134542_recreate_project_flock_populations.up.sql new file mode 100644 index 00000000..2cb76e8f --- /dev/null +++ b/internal/database/migrations/20251030134542_recreate_project_flock_populations.up.sql @@ -0,0 +1,57 @@ +-- ============================================ +-- MIGRATION: project_flock_populations +-- ============================================ + +-- STEP 1: Hapus tabel jika sudah ada + +-- STEP 2: Buat tabel project_flock_populations +CREATE TABLE IF NOT EXISTS project_flock_populations ( + id BIGSERIAL PRIMARY KEY, + project_chickin_id BIGINT NOT NULL, + product_warehouse_id BIGINT NOT NULL, + total_qty NUMERIC(15, 3) NOT NULL, + total_used_qty NUMERIC(15, 3) DEFAULT 0, + notes TEXT, + created_by BIGINT NOT NULL, + created_at TIMESTAMPTZ DEFAULT now(), + updated_at TIMESTAMPTZ DEFAULT now(), + deleted_at TIMESTAMPTZ +); + +-- STEP 3: FOREIGN KEYS +DO $$ +BEGIN + -- Relasi ke project_chickins + IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'project_chickins') THEN + ALTER TABLE project_flock_populations + ADD CONSTRAINT fk_project_flock_populations_chickin + FOREIGN KEY (project_chickin_id) + REFERENCES project_chickins(id) + ON DELETE RESTRICT ON UPDATE CASCADE; + END IF; + + -- Relasi ke product_warehouses + IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'product_warehouses') THEN + ALTER TABLE project_flock_populations + ADD CONSTRAINT fk_project_flock_populations_warehouse + FOREIGN KEY (product_warehouse_id) + REFERENCES product_warehouses(id) + ON DELETE RESTRICT ON UPDATE CASCADE; + END IF; + + -- Relasi ke users + IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'users') THEN + ALTER TABLE project_flock_populations + ADD CONSTRAINT fk_project_flock_populations_created_by + FOREIGN KEY (created_by) + REFERENCES users(id) + ON DELETE RESTRICT ON UPDATE CASCADE; + END IF; +END $$; + +-- STEP 4: INDEXES +CREATE INDEX IF NOT EXISTS idx_populations_chickin_id ON project_flock_populations (project_chickin_id); + +CREATE INDEX IF NOT EXISTS idx_populations_warehouse_id ON project_flock_populations (product_warehouse_id); + +CREATE INDEX IF NOT EXISTS idx_populations_created_by ON project_flock_populations (created_by); \ No newline at end of file From c91d84b652468a7a19e9149518ff8dffe9e8e2b5 Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Fri, 31 Oct 2025 14:30:45 +0700 Subject: [PATCH 04/12] feat[BE-127]: inisiate transfer laying for base template API --- internal/entities/laying_kandang_transfer.go | 22 +++++ internal/entities/laying_transfer.go | 24 +++++ internal/entities/transfer_laying.go | 18 ---- .../dto/transfer_laying.dto.go | 12 +-- ...itory.go => laying_transfer.repository.go} | 9 +- .../services/transfer_laying.service.go | 88 ++++++++++++++----- .../validations/transfer_laying.validation.go | 13 ++- 7 files changed, 134 insertions(+), 52 deletions(-) create mode 100644 internal/entities/laying_kandang_transfer.go create mode 100644 internal/entities/laying_transfer.go delete mode 100644 internal/entities/transfer_laying.go rename internal/modules/production/transfer_layings/repositories/{transfer_laying.repository.go => laying_transfer.repository.go} (67%) diff --git a/internal/entities/laying_kandang_transfer.go b/internal/entities/laying_kandang_transfer.go new file mode 100644 index 00000000..8f514f71 --- /dev/null +++ b/internal/entities/laying_kandang_transfer.go @@ -0,0 +1,22 @@ +package entities + +import ( + "time" + + "gorm.io/gorm" +) + +type LayingKandangTransfer struct { + Id uint `gorm:"primaryKey"` + KandangId uint + ProductWarehouseId uint + Qty float64 `gorm:"type:numeric(15,3)"` + LayingTransferId uint `gorm:"not null"` + CreatedAt time.Time `gorm:"autoCreateTime"` + UpdatedAt time.Time `gorm:"autoUpdateTime"` + DeletedAt gorm.DeletedAt `gorm:"index"` + + Kandang *Kandang `gorm:"foreignKey:KandangId;references:Id"` + ProductWarehouse *ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"` + LayingTransfer *LayingTransfer `gorm:"foreignKey:LayingTransferId;references:Id"` +} diff --git a/internal/entities/laying_transfer.go b/internal/entities/laying_transfer.go new file mode 100644 index 00000000..75c5e23a --- /dev/null +++ b/internal/entities/laying_transfer.go @@ -0,0 +1,24 @@ +package entities + +import ( + "time" + + "gorm.io/gorm" +) + +type LayingTransfer struct { + Id uint `gorm:"primaryKey"` + FromProjectFlockId uint `gorm:"not null"` + ToProjectFlockId uint `gorm:"not null"` + TotalQty float64 `gorm:"type:numeric(15,3)"` + TransferDate time.Time `gorm:"type:date;not null"` + Notes string `gorm:"type:text"` + CreatedBy uint `gorm:"not null"` + CreatedAt time.Time `gorm:"autoCreateTime"` + UpdatedAt time.Time `gorm:"autoUpdateTime"` + DeletedAt gorm.DeletedAt `gorm:"index"` + + FromProjectFlock *ProjectFlock `gorm:"foreignKey:FromProjectFlockId;references:Id"` + ToProjectFlock *ProjectFlock `gorm:"foreignKey:ToProjectFlockId;references:Id"` + CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"` +} diff --git a/internal/entities/transfer_laying.go b/internal/entities/transfer_laying.go deleted file mode 100644 index fd60a435..00000000 --- a/internal/entities/transfer_laying.go +++ /dev/null @@ -1,18 +0,0 @@ -package entities - -import ( - "time" - - "gorm.io/gorm" -) - -type TransferLaying struct { - Id uint `gorm:"primaryKey"` - Name string `gorm:"not null;uniqueIndex:idx_name,where:deleted_at IS NULL"` - CreatedBy uint `gorm:"not null"` - CreatedAt time.Time `gorm:"autoCreateTime"` - UpdatedAt time.Time `gorm:"autoUpdateTime"` - DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` - - CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"` -} diff --git a/internal/modules/production/transfer_layings/dto/transfer_laying.dto.go b/internal/modules/production/transfer_layings/dto/transfer_laying.dto.go index 103c4c35..69aced33 100644 --- a/internal/modules/production/transfer_layings/dto/transfer_laying.dto.go +++ b/internal/modules/production/transfer_layings/dto/transfer_laying.dto.go @@ -27,17 +27,17 @@ type TransferLayingDetailDTO struct { // === Mapper Functions === -func ToTransferLayingBaseDTO(e entity.TransferLaying) TransferLayingBaseDTO { +func ToTransferLayingBaseDTO(e entity.LayingTransfer) TransferLayingBaseDTO { return TransferLayingBaseDTO{ Id: e.Id, - Name: e.Name, + } } -func ToTransferLayingListDTO(e entity.TransferLaying) TransferLayingListDTO { +func ToTransferLayingListDTO(e entity.LayingTransfer) TransferLayingListDTO { var createdUser *userDTO.UserBaseDTO if e.CreatedUser.Id != 0 { - mapped := userDTO.ToUserBaseDTO(e.CreatedUser) + mapped := userDTO.ToUserBaseDTO(*e.CreatedUser) createdUser = &mapped } @@ -49,7 +49,7 @@ func ToTransferLayingListDTO(e entity.TransferLaying) TransferLayingListDTO { } } -func ToTransferLayingListDTOs(e []entity.TransferLaying) []TransferLayingListDTO { +func ToTransferLayingListDTOs(e []entity.LayingTransfer) []TransferLayingListDTO { result := make([]TransferLayingListDTO, len(e)) for i, r := range e { result[i] = ToTransferLayingListDTO(r) @@ -57,7 +57,7 @@ func ToTransferLayingListDTOs(e []entity.TransferLaying) []TransferLayingListDTO return result } -func ToTransferLayingDetailDTO(e entity.TransferLaying) TransferLayingDetailDTO { +func ToTransferLayingDetailDTO(e entity.LayingTransfer) TransferLayingDetailDTO { return TransferLayingDetailDTO{ TransferLayingListDTO: ToTransferLayingListDTO(e), } diff --git a/internal/modules/production/transfer_layings/repositories/transfer_laying.repository.go b/internal/modules/production/transfer_layings/repositories/laying_transfer.repository.go similarity index 67% rename from internal/modules/production/transfer_layings/repositories/transfer_laying.repository.go rename to internal/modules/production/transfer_layings/repositories/laying_transfer.repository.go index 32ea27ee..e92e8f95 100644 --- a/internal/modules/production/transfer_layings/repositories/transfer_laying.repository.go +++ b/internal/modules/production/transfer_layings/repositories/laying_transfer.repository.go @@ -1,21 +1,22 @@ package repository import ( - entity "gitlab.com/mbugroup/lti-api.git/internal/entities" "gitlab.com/mbugroup/lti-api.git/internal/common/repository" + entity "gitlab.com/mbugroup/lti-api.git/internal/entities" "gorm.io/gorm" ) type TransferLayingRepository interface { - repository.BaseRepository[entity.TransferLaying] + repository.BaseRepository[entity.LayingTransfer] + } type TransferLayingRepositoryImpl struct { - *repository.BaseRepositoryImpl[entity.TransferLaying] + *repository.BaseRepositoryImpl[entity.LayingTransfer] } func NewTransferLayingRepository(db *gorm.DB) TransferLayingRepository { return &TransferLayingRepositoryImpl{ - BaseRepositoryImpl: repository.NewBaseRepository[entity.TransferLaying](db), + BaseRepositoryImpl: repository.NewBaseRepository[entity.LayingTransfer](db), } } diff --git a/internal/modules/production/transfer_layings/services/transfer_laying.service.go b/internal/modules/production/transfer_layings/services/transfer_laying.service.go index 36541600..bc66bd13 100644 --- a/internal/modules/production/transfer_layings/services/transfer_laying.service.go +++ b/internal/modules/production/transfer_layings/services/transfer_laying.service.go @@ -3,7 +3,9 @@ package service import ( "errors" + common "gitlab.com/mbugroup/lti-api.git/internal/common/service" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + ProjectFlockRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories" repository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/repositories" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/validations" "gitlab.com/mbugroup/lti-api.git/internal/utils" @@ -15,24 +17,28 @@ import ( ) type TransferLayingService interface { - GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.TransferLaying, int64, error) - GetOne(ctx *fiber.Ctx, id uint) (*entity.TransferLaying, error) - CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.TransferLaying, error) - UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.TransferLaying, error) + GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.LayingTransfer, int64, error) + GetOne(ctx *fiber.Ctx, id uint) (*entity.LayingTransfer, error) + CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.LayingTransfer, error) + UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.LayingTransfer, error) DeleteOne(ctx *fiber.Ctx, id uint) error } type transferLayingService struct { - Log *logrus.Logger - Validate *validator.Validate - Repository repository.TransferLayingRepository + Log *logrus.Logger + Validate *validator.Validate + Repository repository.TransferLayingRepository + ProjectFlockRepo ProjectFlockRepository.ProjectflockRepository + ProjectFlockKandangRepo ProjectFlockRepository.ProjectFlockKandangRepository } -func NewTransferLayingService(repo repository.TransferLayingRepository, validate *validator.Validate) TransferLayingService { +func NewTransferLayingService(repo repository.TransferLayingRepository, projectFlockRepo ProjectFlockRepository.ProjectflockRepository, projectFlockKandangRepo ProjectFlockRepository.ProjectFlockKandangRepository, validate *validator.Validate) TransferLayingService { return &transferLayingService{ - Log: utils.Log, - Validate: validate, - Repository: repo, + Log: utils.Log, + Validate: validate, + Repository: repo, + ProjectFlockRepo: projectFlockRepo, + ProjectFlockKandangRepo: projectFlockKandangRepo, } } @@ -40,7 +46,7 @@ func (s transferLayingService) withRelations(db *gorm.DB) *gorm.DB { return db.Preload("CreatedUser") } -func (s transferLayingService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.TransferLaying, int64, error) { +func (s transferLayingService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.LayingTransfer, int64, error) { if err := s.Validate.Struct(params); err != nil { return nil, 0, err } @@ -62,7 +68,7 @@ func (s transferLayingService) GetAll(c *fiber.Ctx, params *validation.Query) ([ return transferLayings, total, nil } -func (s transferLayingService) GetOne(c *fiber.Ctx, id uint) (*entity.TransferLaying, error) { +func (s transferLayingService) GetOne(c *fiber.Ctx, id uint) (*entity.LayingTransfer, error) { transferLaying, err := s.Repository.GetByID(c.Context(), id, s.withRelations) if errors.Is(err, gorm.ErrRecordNotFound) { return nil, fiber.NewError(fiber.StatusNotFound, "TransferLaying not found") @@ -74,24 +80,62 @@ func (s transferLayingService) GetOne(c *fiber.Ctx, id uint) (*entity.TransferLa return transferLaying, nil } -func (s *transferLayingService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.TransferLaying, error) { +func (s *transferLayingService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.LayingTransfer, error) { if err := s.Validate.Struct(req); err != nil { return nil, err } - createBody := &entity.TransferLaying{ - Name: req.Name, - } - - if err := s.Repository.CreateOne(c.Context(), createBody, nil); err != nil { - s.Log.Errorf("Failed to create transferLaying: %+v", err) + if err := common.EnsureRelations(c.Context(), + common.RelationCheck{Name: "Source Project Flock", ID: &req.SourceProjectFlockId, Exists: s.ProjectFlockRepo.IdExists}, + common.RelationCheck{Name: "Target Project Flock", ID: &req.TargetProjectFlockId, Exists: s.ProjectFlockRepo.IdExists}, + ); err != nil { return nil, err } - return s.GetOne(c, createBody.Id) + for _, detail := range req.Details { + if err := common.EnsureRelations(c.Context(), + common.RelationCheck{Name: "Project Flock Kandang", ID: &detail.SourceProjectFlockKandangId, Exists: s.ProjectFlockKandangRepo.IdExists}, + ); err != nil { + return nil, err + } + } + + transferDate, err := utils.ParseDateString(req.TransferDate) + if err != nil { + return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid transfer date format") + } + + var totalQty float64 + for _, item := range req.Details { + totalQty += item.Quantity + } + + err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { + + createBody := &entity.LayingTransfer{ + Notes: req.Reason, + FromProjectFlockId: req.SourceProjectFlockId, + ToProjectFlockId: req.TargetProjectFlockId, + TransferDate: transferDate, + TotalQty: totalQty, + CreatedBy: 1, //todo : harus diambil dari auth + } + + if err := s.Repository.WithTx(dbTransaction).CreateOne(c.Context(), createBody, nil); err != nil { + return err + } + + return nil + }) + + if err != nil { + return nil, err + } + + return nil, err } -func (s transferLayingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.TransferLaying, error) { +func (s transferLayingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.LayingTransfer, error) { if err := s.Validate.Struct(req); err != nil { return nil, err } diff --git a/internal/modules/production/transfer_layings/validations/transfer_laying.validation.go b/internal/modules/production/transfer_layings/validations/transfer_laying.validation.go index 7d16d3ee..3b83132f 100644 --- a/internal/modules/production/transfer_layings/validations/transfer_laying.validation.go +++ b/internal/modules/production/transfer_layings/validations/transfer_laying.validation.go @@ -1,11 +1,20 @@ package validation +type CreateDetail struct { + SourceProjectFlockKandangId uint `json:"source_project_flock_kandang_id" validate:"required"` + Quantity float64 `json:"quantity" validate:"required,gt=0"` +} + type Create struct { - Name string `json:"name" validate:"required_strict,min=3"` + TransferDate string `json:"transfer_date" validate:"required,datetime=2006-01-02"` + SourceProjectFlockId uint `json:"source_project_flock_id" validate:"required"` + TargetProjectFlockId uint `json:"target_project_flock_id" validate:"required"` + Details []CreateDetail `json:"details" validate:"required,min=1,dive,required"` + Reason string `json:"reason" validate:"omitempty,max=1000"` } type Update struct { - Name *string `json:"name,omitempty" validate:"omitempty"` + Name *string `json:"name,omitempty" validate:"omitempty"` } type Query struct { From 219a6a39edee3e6195cbc56a16c51f405bdd0f81 Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Fri, 31 Oct 2025 15:33:31 +0700 Subject: [PATCH 05/12] Feat[BE]: refactored Chickin createone and implement approvals and add more needed constant --- internal/database/seed/seeder.go | 272 ++++++------ internal/entities/project_chickin.go | 11 +- internal/entities/project_flock_population.go | 24 +- internal/entities/projectflock.go | 1 + internal/entities/projectflock_kandang.go | 14 +- .../repositories/constant.repository.go | 4 + .../controllers/chickin.controller.go | 28 +- .../production/chickins/dto/chickin.dto.go | 36 +- .../modules/production/chickins/module.go | 13 + internal/modules/production/chickins/route.go | 2 +- .../chickins/services/chickin.service.go | 414 +++++++++--------- .../validations/chickin.validation.go | 8 +- .../repositories/projectflock.repository.go | 5 + .../projectflock_kandang.repository.go | 5 + .../recordings/services/recording.service.go | 25 +- .../production/transfer_layings/module.go | 6 +- internal/utils/constant.go | 32 ++ 17 files changed, 485 insertions(+), 415 deletions(-) diff --git a/internal/database/seed/seeder.go b/internal/database/seed/seeder.go index 791cfddb..ca8e8cf3 100644 --- a/internal/database/seed/seeder.go +++ b/internal/database/seed/seeder.go @@ -93,9 +93,9 @@ func Run(db *gorm.DB) error { if err := seedTransferStock(tx, adminID); err != nil { return err } - if err := seedChickin(tx, adminID); err != nil { - return err - } + // if err := seedChickin(tx, adminID); err != nil { + // return err + // } fmt.Println("✅ Master data seeding completed") return nil @@ -1134,151 +1134,151 @@ func seedTransferStock(tx *gorm.DB, createdBy uint) error { return nil } -func seedChickin(tx *gorm.DB, createdBy uint) error { - seeds := []struct { - ProjectFlockKandangId uint - ChickInDate string - Quantity float64 - Note string - }{ - {ProjectFlockKandangId: 1, ChickInDate: "2025-10-20", Quantity: 100, Note: "Seeder chickin 1"}, - {ProjectFlockKandangId: 2, ChickInDate: "2025-10-21", Quantity: 200, Note: "Seeder chickin 2"}, - } +// func seedChickin(tx *gorm.DB, createdBy uint) error { +// seeds := []struct { +// ProjectFlockKandangId uint +// ChickInDate string +// Quantity float64 +// Note string +// }{ +// {ProjectFlockKandangId: 1, ChickInDate: "2025-10-20", Quantity: 100, Note: "Seeder chickin 1"}, +// {ProjectFlockKandangId: 2, ChickInDate: "2025-10-21", Quantity: 200, Note: "Seeder chickin 2"}, +// } - for _, seed := range seeds { - chickinDate, err := time.Parse("2006-01-02", seed.ChickInDate) - if err != nil { - return err - } +// for _, seed := range seeds { +// chickinDate, err := time.Parse("2006-01-02", seed.ChickInDate) +// if err != nil { +// return err +// } - // Insert ProjectChickin jika belum ada - var chickin entity.ProjectChickin - err = tx.Where("project_flock_kandang_id = ? AND chick_in_date = ?", seed.ProjectFlockKandangId, chickinDate). - First(&chickin).Error - if errors.Is(err, gorm.ErrRecordNotFound) { - chickin = entity.ProjectChickin{ - ProjectFlockKandangId: seed.ProjectFlockKandangId, - ChickInDate: chickinDate, - Quantity: seed.Quantity, - Note: seed.Note, - CreatedBy: createdBy, - } - if err := tx.Create(&chickin).Error; err != nil { - return err - } - } else if err != nil { - return err - } +// // Insert ProjectChickin jika belum ada +// var chickin entity.ProjectChickin +// err = tx.Where("project_flock_kandang_id = ? AND chick_in_date = ?", seed.ProjectFlockKandangId, chickinDate). +// First(&chickin).Error +// if errors.Is(err, gorm.ErrRecordNotFound) { +// chickin = entity.ProjectChickin{ +// ProjectFlockKandangId: seed.ProjectFlockKandangId, +// ChickInDate: chickinDate, +// Quantity: seed.Quantity, +// Note: seed.Note, +// CreatedBy: createdBy, +// } +// if err := tx.Create(&chickin).Error; err != nil { +// return err +// } +// } else if err != nil { +// return err +// } - var population entity.ProjectFlockPopulation - err = tx.Where("project_flock_kandang_id = ?", seed.ProjectFlockKandangId).First(&population).Error - if errors.Is(err, gorm.ErrRecordNotFound) { - population = entity.ProjectFlockPopulation{ - ProjectFlockKandangId: seed.ProjectFlockKandangId, - InitialQuantity: seed.Quantity, - CurrentQuantity: seed.Quantity, - ReservedQuantity: 0, - CreatedBy: createdBy, - } - if err := tx.Create(&population).Error; err != nil { - return err - } - } else if err != nil { - return err - } else { - // Update population quantities - if err := tx.Model(&entity.ProjectFlockPopulation{}). - Where("id = ?", population.Id). - Updates(map[string]any{ - "initial_quantity": population.InitialQuantity + seed.Quantity, - "current_quantity": population.CurrentQuantity + seed.Quantity, - "reserved_quantity": 0, - }).Error; err != nil { - return err - } - } +// var population entity.ProjectFlockPopulation +// err = tx.Where("project_flock_kandang_id = ?", seed.ProjectFlockKandangId).First(&population).Error +// if errors.Is(err, gorm.ErrRecordNotFound) { +// population = entity.ProjectFlockPopulation{ +// ProjectFlockKandangId: seed.ProjectFlockKandangId, +// InitialQuantity: seed.Quantity, +// CurrentQuantity: seed.Quantity, +// ReservedQuantity: 0, +// CreatedBy: createdBy, +// } +// if err := tx.Create(&population).Error; err != nil { +// return err +// } +// } else if err != nil { +// return err +// } else { +// // Update population quantities +// if err := tx.Model(&entity.ProjectFlockPopulation{}). +// Where("id = ?", population.Id). +// Updates(map[string]any{ +// "initial_quantity": population.InitialQuantity + seed.Quantity, +// "current_quantity": population.CurrentQuantity + seed.Quantity, +// "reserved_quantity": 0, +// }).Error; err != nil { +// return err +// } +// } - var pfk entity.ProjectFlockKandang - if err := tx.Where("id = ?", seed.ProjectFlockKandangId).First(&pfk).Error; err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - // no pivot found; skip creating details - continue - } - return err - } +// var pfk entity.ProjectFlockKandang +// if err := tx.Where("id = ?", seed.ProjectFlockKandangId).First(&pfk).Error; err != nil { +// if errors.Is(err, gorm.ErrRecordNotFound) { +// // no pivot found; skip creating details +// continue +// } +// return err +// } - var warehouse entity.Warehouse - if err := tx.Where("kandang_id = ?", pfk.KandangId).First(&warehouse).Error; err != nil { - // if warehouse not found, cannot create details - if errors.Is(err, gorm.ErrRecordNotFound) { - continue - } - return err - } +// var warehouse entity.Warehouse +// if err := tx.Where("kandang_id = ?", pfk.KandangId).First(&warehouse).Error; err != nil { +// // if warehouse not found, cannot create details +// if errors.Is(err, gorm.ErrRecordNotFound) { +// continue +// } +// return err +// } - var productWarehouses []entity.ProductWarehouse - err = tx.Table("product_warehouses"). - Select("product_warehouses.*"). - Joins("JOIN products ON products.id = product_warehouses.product_id"). - Joins("JOIN product_categories ON product_categories.id = products.product_category_id"). - Where("product_categories.code = ? AND product_warehouses.warehouse_id = ?", "DOC", warehouse.Id). - Order("product_warehouses.created_at DESC"). - Find(&productWarehouses).Error - if err != nil { - return err - } +// var productWarehouses []entity.ProductWarehouse +// err = tx.Table("product_warehouses"). +// Select("product_warehouses.*"). +// Joins("JOIN products ON products.id = product_warehouses.product_id"). +// Joins("JOIN product_categories ON product_categories.id = products.product_category_id"). +// Where("product_categories.code = ? AND product_warehouses.warehouse_id = ?", "DOC", warehouse.Id). +// Order("product_warehouses.created_at DESC"). +// Find(&productWarehouses).Error +// if err != nil { +// return err +// } - // If no product warehouses found, keep existing chickin.Quantity and skip details - if len(productWarehouses) == 0 { - continue - } +// // If no product warehouses found, keep existing chickin.Quantity and skip details +// if len(productWarehouses) == 0 { +// continue +// } - // sum all pw quantities and set chickin.Quantity to that total (mimic CreateOne) - totalQty := 0.0 - for _, pw := range productWarehouses { - totalQty += pw.Quantity - } +// // sum all pw quantities and set chickin.Quantity to that total (mimic CreateOne) +// totalQty := 0.0 +// for _, pw := range productWarehouses { +// totalQty += pw.Quantity +// } - if chickin.Quantity != totalQty { - if err := tx.Model(&entity.ProjectChickin{}).Where("id = ?", chickin.Id).Update("quantity", totalQty).Error; err != nil { - return err - } - chickin.Quantity = totalQty - } +// if chickin.Quantity != totalQty { +// if err := tx.Model(&entity.ProjectChickin{}).Where("id = ?", chickin.Id).Update("quantity", totalQty).Error; err != nil { +// return err +// } +// chickin.Quantity = totalQty +// } - for _, pw := range productWarehouses { - // ensure detail exists or create it with full pw.Quantity - var detail entity.ProjectChickinDetail - err = tx.Where("project_chickin_id = ? AND product_warehouse_id = ?", chickin.Id, pw.Id).First(&detail).Error - if errors.Is(err, gorm.ErrRecordNotFound) { - detail = entity.ProjectChickinDetail{ - ProjectChickinId: chickin.Id, - ProductWarehouseId: pw.Id, - Quantity: pw.Quantity, - CreatedBy: createdBy, - } - if err := tx.Create(&detail).Error; err != nil { - return err - } - } else if err != nil { - return err - } else { - if detail.Quantity != pw.Quantity { - if err := tx.Model(&entity.ProjectChickinDetail{}).Where("id = ?", detail.Id).Update("quantity", pw.Quantity).Error; err != nil { - return err - } - } - } +// for _, pw := range productWarehouses { +// // ensure detail exists or create it with full pw.Quantity +// var detail entity.ProjectChickinDetail +// err = tx.Where("project_chickin_id = ? AND product_warehouse_id = ?", chickin.Id, pw.Id).First(&detail).Error +// if errors.Is(err, gorm.ErrRecordNotFound) { +// detail = entity.ProjectChickinDetail{ +// ProjectChickinId: chickin.Id, +// ProductWarehouseId: pw.Id, +// Quantity: pw.Quantity, +// CreatedBy: createdBy, +// } +// if err := tx.Create(&detail).Error; err != nil { +// return err +// } +// } else if err != nil { +// return err +// } else { +// if detail.Quantity != pw.Quantity { +// if err := tx.Model(&entity.ProjectChickinDetail{}).Where("id = ?", detail.Id).Update("quantity", pw.Quantity).Error; err != nil { +// return err +// } +// } +// } - // zero out pw quantity - if err := tx.Model(&entity.ProductWarehouse{}).Where("id = ?", pw.Id).Update("quantity", 0).Error; err != nil { - return err - } - } - } +// // zero out pw quantity +// if err := tx.Model(&entity.ProductWarehouse{}).Where("id = ?", pw.Id).Update("quantity", 0).Error; err != nil { +// return err +// } +// } +// } - return nil -} +// return nil +// } func ptr[T any](v T) *T { return &v diff --git a/internal/entities/project_chickin.go b/internal/entities/project_chickin.go index 95a658c8..c3ca2671 100644 --- a/internal/entities/project_chickin.go +++ b/internal/entities/project_chickin.go @@ -12,13 +12,16 @@ type ProjectChickin struct { Id uint `gorm:"primaryKey"` ProjectFlockKandangId uint `gorm:"not null"` ChickInDate time.Time `gorm:"not null"` - Quantity float64 `gorm:"not null"` - Note string `gorm:"type:text"` + ProductWarehouseId uint `gorm:"not null"` + UsageQty float64 `gorm:"type:numeric(15,3);not null"` + PendingUsageQty float64 `gorm:"type:numeric(15,3);default:0"` + Notes string `gorm:"type:text"` CreatedBy uint `gorm:"not null"` CreatedAt time.Time `gorm:"autoCreateTime"` UpdatedAt time.Time `gorm:"autoUpdateTime"` DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` - ProjectFlockKandang ProjectFlockKandang `gorm:"foreignKey:ProjectFlockKandangId;references:Id"` - CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"` + ProjectFlockKandang *ProjectFlockKandang `gorm:"foreignKey:ProjectFlockKandangId;references:Id"` + ProductWarehouse *ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"` + CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"` } diff --git a/internal/entities/project_flock_population.go b/internal/entities/project_flock_population.go index 184ace65..e5b3216c 100644 --- a/internal/entities/project_flock_population.go +++ b/internal/entities/project_flock_population.go @@ -7,16 +7,18 @@ import ( ) type ProjectFlockPopulation struct { - Id uint `gorm:"primaryKey"` - ProjectFlockKandangId uint `gorm:"not null"` - InitialQuantity float64 `gorm:"type:numeric(15,3);not null"` - CurrentQuantity float64 `gorm:"type:numeric(15,3);not null"` - ReservedQuantity float64 `gorm:"type:numeric(15,3)"` - CreatedBy uint `gorm:"not null"` - CreatedAt time.Time `gorm:"autoCreateTime"` - UpdatedAt time.Time `gorm:"autoUpdateTime"` - DeletedAt gorm.DeletedAt `gorm:"index"` + Id uint `gorm:"primaryKey"` + ProjectChickinId uint `gorm:"not null"` + ProductWarehouseId uint `gorm:"not null"` + TotalQty float64 `gorm:"type:numeric(15,3);not null"` + TotalUsedQty float64 `gorm:"type:numeric(15,3);not null"` + Notes string `gorm:"type:text"` + CreatedBy uint `gorm:"not null"` + CreatedAt time.Time `gorm:"autoCreateTime"` + UpdatedAt time.Time `gorm:"autoUpdateTime"` + DeletedAt gorm.DeletedAt `gorm:"index"` - ProjectFlockKandang *ProjectFlockKandang `gorm:"foreignKey:ProjectFlockKandangId;references:Id"` - CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"` + ProjectChickin *ProjectChickin `gorm:"foreignKey:ProjectChickinId;references:Id"` + ProductWarehouse *ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"` + CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"` } diff --git a/internal/entities/projectflock.go b/internal/entities/projectflock.go index c840892f..4c05298f 100644 --- a/internal/entities/projectflock.go +++ b/internal/entities/projectflock.go @@ -28,3 +28,4 @@ type ProjectFlock struct { KandangHistory []ProjectFlockKandang `gorm:"foreignKey:ProjectFlockId;references:Id"` LatestApproval *Approval `gorm:"-" json:"-"` } + diff --git a/internal/entities/projectflock_kandang.go b/internal/entities/projectflock_kandang.go index 1c29c22e..f10dbd17 100644 --- a/internal/entities/projectflock_kandang.go +++ b/internal/entities/projectflock_kandang.go @@ -3,10 +3,12 @@ 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_unique"` - KandangId uint `gorm:"not null;index:idx_project_flock_kandangs_kandang;uniqueIndex:idx_project_flock_kandangs_unique"` - CreatedAt time.Time `gorm:"autoCreateTime"` - ProjectFlock ProjectFlock `gorm:"foreignKey:ProjectFlockId;references:Id"` - Kandang Kandang `gorm:"foreignKey:KandangId;references:Id"` + Id uint `gorm:"primaryKey"` + 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"` + + ProjectFlock ProjectFlock `gorm:"foreignKey:ProjectFlockId;references:Id"` + Kandang Kandang `gorm:"foreignKey:KandangId;references:Id"` + Chickins []ProjectChickin `gorm:"foreignKey:ProjectFlockKandangId;references:Id"` } diff --git a/internal/modules/constants/repositories/constant.repository.go b/internal/modules/constants/repositories/constant.repository.go index 4b44d553..493f4cb9 100644 --- a/internal/modules/constants/repositories/constant.repository.go +++ b/internal/modules/constants/repositories/constant.repository.go @@ -82,6 +82,10 @@ func (r *ConstantRepositoryImpl) GetConstants() map[string]interface{} { "LOKASI", "KANDANG", }, + "stock_log": map[string][]string{ + "log_types": []string{"TRANSFER", "ADJUSTMENT"}, + "transaction_types": []string{"INCREASE", "DECREASE"}, + }, "supplier_categories": []string{ "BOP", "SAPRONAK", diff --git a/internal/modules/production/chickins/controllers/chickin.controller.go b/internal/modules/production/chickins/controllers/chickin.controller.go index fadcbc3e..c132b279 100644 --- a/internal/modules/production/chickins/controllers/chickin.controller.go +++ b/internal/modules/production/chickins/controllers/chickin.controller.go @@ -139,23 +139,33 @@ func (u *ChickinController) DeleteOne(c *fiber.Ctx) error { }) } -func (u *ChickinController) Approve(c *fiber.Ctx) error { - param := c.Params("id") - - id, err := strconv.Atoi(param) - if err != nil { - return fiber.NewError(fiber.StatusBadRequest, "Invalid Id") +func (u *ChickinController) Approval(c *fiber.Ctx) error { + req := new(validation.Approve) + if err := c.BodyParser(req); err != nil { + return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") } - if err := u.ChickinService.Approve(c, uint(id)); err != nil { + results, err := u.ChickinService.Approval(c, req) + if err != nil { return err } + var ( + data interface{} + message = "Submit chickin approval successfully" + ) + if len(results) == 1 { + data = dto.ToChickinListDTO(results[0]) + } else { + message = "Submit chickin approvals successfully" + data = dto.ToChickinListDTOs(results) + } + return c.Status(fiber.StatusOK). JSON(response.Success{ Code: fiber.StatusOK, Status: "success", - Message: "Approve chickin successfully", - Data: nil, + Message: message, + Data: data, }) } diff --git a/internal/modules/production/chickins/dto/chickin.dto.go b/internal/modules/production/chickins/dto/chickin.dto.go index 193257b6..823cbfa5 100644 --- a/internal/modules/production/chickins/dto/chickin.dto.go +++ b/internal/modules/production/chickins/dto/chickin.dto.go @@ -18,8 +18,10 @@ type ChickinBaseDTO struct { Id uint `json:"id"` ProjectFlockKandang *ProjectFlockKandangDTO `json:"project_flock_kandang"` ChickInDate time.Time `json:"chick_in_date"` - Quantity float64 `json:"quantity"` - Note string `json:"note"` + ProductWarehouseId uint `json:"product_warehouse_id"` + UsageQty float64 `json:"usage_qty"` + PendingUsageQty float64 `json:"pending_usage_qty"` + Notes string `json:"notes"` } type ProjectFlockDTO struct { @@ -44,8 +46,10 @@ type ChickinSimpleDTO struct { Id uint `json:"id"` ProjectFlockKandangId uint `json:"project_flock_kandang_id"` ChickInDate time.Time `json:"chick_in_date"` - Quantity float64 `json:"quantity"` - Note string `json:"note"` + ProductWarehouseId uint `json:"product_warehouse_id"` + UsageQty float64 `json:"usage_qty"` + PendingUsageQty float64 `json:"pending_usage_qty"` + Notes string `json:"notes"` CreatedBy uint `json:"created_by"` } @@ -138,16 +142,18 @@ func ToProjectFlockKandangDTO(e entity.ProjectFlockKandang) ProjectFlockKandangD func ToChickinBaseDTO(e entity.ProjectChickin) ChickinBaseDTO { var pfk *ProjectFlockKandangDTO - if e.ProjectFlockKandang.Id != 0 { - mapped := ToProjectFlockKandangDTO(e.ProjectFlockKandang) + if e.ProjectFlockKandang != nil && e.ProjectFlockKandang.Id != 0 { + mapped := ToProjectFlockKandangDTO(*e.ProjectFlockKandang) pfk = &mapped } return ChickinBaseDTO{ Id: e.Id, ProjectFlockKandang: pfk, ChickInDate: e.ChickInDate, - Quantity: e.Quantity, - Note: e.Note, + ProductWarehouseId: e.ProductWarehouseId, + UsageQty: e.UsageQty, + PendingUsageQty: e.PendingUsageQty, + Notes: e.Notes, } } @@ -156,21 +162,23 @@ func ToChickinSimpleDTO(e entity.ProjectChickin) ChickinSimpleDTO { Id: e.Id, ProjectFlockKandangId: e.ProjectFlockKandangId, ChickInDate: e.ChickInDate, - Quantity: e.Quantity, - Note: e.Note, + ProductWarehouseId: e.ProductWarehouseId, + UsageQty: e.UsageQty, + PendingUsageQty: e.PendingUsageQty, + Notes: e.Notes, CreatedBy: e.CreatedBy, } } func ToChickinListDTO(e entity.ProjectChickin) ChickinListDTO { var createdUser *userBaseDTO.UserBaseDTO - if e.CreatedUser.Id != 0 { - mapped := userBaseDTO.ToUserBaseDTO(e.CreatedUser) + if e.CreatedUser != nil && e.CreatedUser.Id != 0 { + mapped := userBaseDTO.ToUserBaseDTO(*e.CreatedUser) createdUser = &mapped } var pfk *ProjectFlockKandangDTO - if e.ProjectFlockKandang.Id != 0 { - mapped := ToProjectFlockKandangDTO(e.ProjectFlockKandang) + if e.ProjectFlockKandang != nil && e.ProjectFlockKandang.Id != 0 { + mapped := ToProjectFlockKandangDTO(*e.ProjectFlockKandang) pfk = &mapped } return ChickinListDTO{ diff --git a/internal/modules/production/chickins/module.go b/internal/modules/production/chickins/module.go index f1e0baea..f6dd554b 100644 --- a/internal/modules/production/chickins/module.go +++ b/internal/modules/production/chickins/module.go @@ -1,10 +1,15 @@ package chickins import ( + "fmt" + "github.com/go-playground/validator/v10" "github.com/gofiber/fiber/v2" "gorm.io/gorm" + commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository" + commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service" + rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories" rKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories" rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories" @@ -15,6 +20,8 @@ import ( rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories" sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" + + utils "gitlab.com/mbugroup/lti-api.git/internal/utils" ) type ChickinModule struct{} @@ -32,6 +39,12 @@ func (ChickinModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate * userRepo := rUser.NewUserRepository(db) + approvalRepo := commonRepo.NewApprovalRepository(db) + approvalService := commonSvc.NewApprovalService(approvalRepo) + if err := approvalService.RegisterWorkflowSteps(utils.ApprovalWorkflowProjectFlockKandang, utils.ProjectFlockKandangApprovalSteps); err != nil { + panic(fmt.Sprintf("failed to register project flock kandang approval workflow: %v", err)) + } + chickinService := sChickin.NewChickinService(chickinRepo, kandangRepo, warehouseRepo, productWarehouseRepo, projectFlockRepo, projectflockkandangrepo, projectflockpopulationrepo, chickinDetailRepo, validate) userService := sUser.NewUserService(userRepo, validate) diff --git a/internal/modules/production/chickins/route.go b/internal/modules/production/chickins/route.go index 5fa5237a..0bb5e93d 100644 --- a/internal/modules/production/chickins/route.go +++ b/internal/modules/production/chickins/route.go @@ -25,5 +25,5 @@ func ChickinRoutes(v1 fiber.Router, u user.UserService, s chickin.ChickinService route.Get("/:id", ctrl.GetOne) route.Patch("/:id", ctrl.UpdateOne) route.Delete("/:id", ctrl.DeleteOne) - route.Post("/:id/approve", ctrl.Approve) + route.Post("/approvals", ctrl.Approval) } diff --git a/internal/modules/production/chickins/services/chickin.service.go b/internal/modules/production/chickins/services/chickin.service.go index 83d94ba5..2b44c242 100644 --- a/internal/modules/production/chickins/services/chickin.service.go +++ b/internal/modules/production/chickins/services/chickin.service.go @@ -2,7 +2,11 @@ package service import ( "errors" + "fmt" + "strings" + commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository" + commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories" KandangRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories" @@ -24,7 +28,7 @@ type ChickinService interface { CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.ProjectChickin, error) UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.ProjectChickin, error) DeleteOne(ctx *fiber.Ctx, id uint) error - Approve(ctx *fiber.Ctx, id uint) error + Approval(ctx *fiber.Ctx, req *validation.Approve) ([]entity.ProjectChickin, error) } type chickinService struct { @@ -110,107 +114,98 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entit return nil, err } - projectflockkandang, err := s.ProjectflockKandangRepo.GetByID(c.Context(), req.ProjectFlockKandangId) + projectFlockKandang, err := s.ProjectflockKandangRepo.GetByID(c.Context(), req.ProjectFlockKandangId) if err != nil { - s.Log.Errorf("Failed to get projectflock kandang: %+v", err) - return nil, err + return nil, fiber.NewError(fiber.StatusNotFound, "Project Flock Kandang not found") } - warehouse, err := s.WarehouseRepo.GetByKandangID(c.Context(), projectflockkandang.KandangId) + warehouse, err := s.WarehouseRepo.GetByKandangID(c.Context(), projectFlockKandang.KandangId) if err != nil { - s.Log.Errorf("Failed to get warehouse: %+v", err) - return nil, err + return nil, fiber.NewError(fiber.StatusNotFound, "Warehouse for Kandang not found") } - // move complex DB query into repository for cleaner service - productWarehouses, err := s.ProductWarehouseRepo.GetByCategoryCodeAndWarehouseID(c.Context(), "DOC", warehouse.Id) - if err != nil { - s.Log.Errorf("Failed to get product warehouses: %+v", err) - return nil, err - } - if len(productWarehouses) == 0 { - return nil, fiber.NewError(fiber.StatusNotFound, "Product Warehouse not found for the given Project Flock and Warehouse") - } - totalQuantity := 0.0 - for _, pw := range productWarehouses { - totalQuantity += pw.Quantity - } + var productWarehouses []entity.ProductWarehouse - if totalQuantity < 1 { - return nil, fiber.NewError(fiber.StatusBadRequest, "Insufficient quantity in Product Warehouses") + if strings.ToUpper(strings.TrimSpace(projectFlockKandang.ProjectFlock.Category)) == string(utils.ProjectFlockCategoryGrowing) { + + productWarehouses, err = s.ProductWarehouseRepo.GetByCategoryCodeAndWarehouseID(c.Context(), "DOC", warehouse.Id) + if err != nil || len(productWarehouses) == 0 { + return nil, fiber.NewError(fiber.StatusNotFound, "Product for growing category in the Kandang's warehouse not found") + } } chickinDate, err := utils.ParseDateString(req.ChickInDate) if err != nil { - s.Log.Errorf("Failed to parse chickin date: %+v", err) return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid ChickInDate format") } - newChickin := &entity.ProjectChickin{ - ProjectFlockKandangId: projectflockkandang.Id, - ChickInDate: chickinDate, - Quantity: totalQuantity, - Note: req.Note, - CreatedBy: 1, //todo: ganti dengan user login + + actorID := uint(1) // todo nanti ambil dari auth context + newChikins := make([]*entity.ProjectChickin, 0) + for _, productWarehouse := range productWarehouses { + + if productWarehouse.Quantity > 0 { + newChickin := &entity.ProjectChickin{ + ProjectFlockKandangId: req.ProjectFlockKandangId, + ChickInDate: chickinDate, + UsageQty: 0, + PendingUsageQty: productWarehouse.Quantity, + ProductWarehouseId: productWarehouse.Id, + Notes: req.Note, + CreatedBy: actorID, + } + + newChikins = append(newChikins, newChickin) + } } - err = s.Repository.CreateOne(c.Context(), newChickin, nil) + + if len(newChikins) == 0 { + return nil, fiber.NewError(fiber.StatusBadRequest, "No chickins to create") + } + + err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { + + approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction)) + productWarehouseTx := s.ProductWarehouseRepo.WithTx(dbTransaction) + + if err := s.Repository.WithTx(dbTransaction).CreateMany(c.Context(), newChikins, nil); err != nil { + return err + } + + latest, err := approvalSvcTx.LatestByTarget(c.Context(), utils.ApprovalWorkflowProjectFlockKandang, projectFlockKandang.Id, nil) + if err != nil { + return err + } + + for _, chickin := range newChikins { + + updates := map[string]any{"quantity": 0} + + if err := productWarehouseTx.PatchOne(c.Context(), chickin.ProductWarehouseId, updates, nil); err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fmt.Errorf("failed to update product warehouse quantity for id %d", chickin.ProductWarehouseId) + } + return err + } + } + + if latest == nil { + + action := entity.ApprovalActionCreated + if _, err := approvalSvcTx.CreateApproval(c.Context(), utils.ApprovalWorkflowProjectFlockKandang, projectFlockKandang.Id, utils.ProjectFlockKandangStepPengajuan, &action, actorID, nil); err != nil { + lower := strings.ToLower(err.Error()) + if !(strings.Contains(lower, "duplicate") || strings.Contains(lower, "unique constraint") || strings.Contains(lower, "23505")) { + return err + } + } + } + + return nil + }) if err != nil { - s.Log.Errorf("Failed to create chickin: %+v", err) - return nil, err + return nil, fiber.NewError(fiber.StatusBadRequest, err.Error()) } - // Update semua product warehouse: set quantity jadi 0 - for _, pw := range productWarehouses { - err = s.ProductWarehouseRepo.PatchOne(c.Context(), pw.Id, map[string]any{ - "quantity": 0, - }, nil) - if err != nil { - s.Log.Errorf("Failed to update product warehouse quantity: %+v", err) - return nil, err - } - - newChickinDetail := &entity.ProjectChickinDetail{ - ProjectChickinId: newChickin.Id, - ProductWarehouseId: pw.Id, - Quantity: pw.Quantity, - CreatedBy: 1, // todo: ganti dengan user login - } - err = s.ProjectChickinDetailRepo.CreateOne(c.Context(), newChickinDetail, nil) - if err != nil { - s.Log.Errorf("Failed to create chickin detail: %+v", err) - return nil, err - } - } - - existingPopulation, err := s.ProjectflockPopulationRepo.GetByProjectFlockKandangID(c.Context(), req.ProjectFlockKandangId) - if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { - s.Log.Errorf("Failed to get project flock population: %+v", err) - return nil, err - } - if existingPopulation != nil { - - err = s.ProjectflockPopulationRepo.PatchOne(c.Context(), existingPopulation.Id, map[string]any{ - "reserved_quantity": newChickin.Quantity + existingPopulation.ReservedQuantity, - }, nil) - if err != nil { - s.Log.Errorf("Failed to update project flock population: %+v", err) - return nil, err - } - } else { - newPopulation := &entity.ProjectFlockPopulation{ - ProjectFlockKandangId: req.ProjectFlockKandangId, - InitialQuantity: 0, - CurrentQuantity: 0, - ReservedQuantity: newChickin.Quantity, - CreatedBy: 1, // todo: ganti dengan user login - } - err = s.ProjectflockPopulationRepo.CreateOne(c.Context(), newPopulation, nil) - if err != nil { - s.Log.Errorf("Failed to create project flock population: %+v", err) - return nil, err - } - } - - return s.GetOne(c, newChickin.Id) + return newChikins[0], nil } func (s chickinService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.ProjectChickin, error) { @@ -224,7 +219,8 @@ func (s chickinService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) updateBody["chick_in_date"] = req.ChickInDate } if req.Note != "" { - updateBody["note"] = req.Note + // entity uses `Notes` => column `notes` + updateBody["notes"] = req.Note } if len(updateBody) == 0 { return s.GetOne(c, id) @@ -243,160 +239,140 @@ func (s chickinService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) func (s chickinService) DeleteOne(c *fiber.Ctx, id uint) error { + // Simplified delete: directly call repository delete. Complex restore logic removed for now. + if err := s.Repository.DeleteOne(c.Context(), id); err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fiber.NewError(fiber.StatusNotFound, "Chickin not found") + } + return err + } + + return nil +} + +func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entity.ProjectChickin, error) { + if err := s.Validate.Struct(req); err != nil { + return nil, err + } + + actorID := uint(1) // todo nanti ambil dari auth context + + var action entity.ApprovalAction + switch strings.ToUpper(strings.TrimSpace(req.Action)) { + case string(entity.ApprovalActionRejected): + action = entity.ApprovalActionRejected + case string(entity.ApprovalActionApproved): + action = entity.ApprovalActionApproved + default: + return nil, fiber.NewError(fiber.StatusBadRequest, "action must be APPROVED or REJECTED") + } + + approvableIDs := uniqueUintSlice(req.ApprovableIds) + if len(approvableIDs) == 0 { + return nil, fiber.NewError(fiber.StatusBadRequest, "approvable_ids must contain at least one id") + } + + step := utils.ProjectFlockKandangStepPengajuan + if action == entity.ApprovalActionApproved { + step = utils.ProjectFlockKandangStepDisetujui + } + err := s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { + approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction)) + ProjectFlockPopulationRepotx := s.ProjectflockPopulationRepo.WithTx(dbTransaction) + chickinRepoTx := s.Repository.WithTx(dbTransaction) - chickinRepo := repository.NewChickinRepository(dbTransaction) - projectFlockKandangRepo := s.ProjectflockKandangRepo.WithTx(dbTransaction) - productWarehouseRepo := s.ProductWarehouseRepo.WithTxRepo(dbTransaction) - projectFlockPopulationRepo := s.ProjectflockPopulationRepo.WithTx(dbTransaction) - projectChickinDetailRepo := s.ProjectChickinDetailRepo.WithTxRepo(dbTransaction) - warehouseRepoTx := rWarehouse.NewWarehouseRepository(dbTransaction) + for _, approvableID := range approvableIDs { - chickin, err := chickinRepo.GetByID(c.Context(), id, nil) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return fiber.NewError(fiber.StatusNotFound, "Chickin not found") - } - return err - } - - population, err := projectFlockPopulationRepo.GetByProjectFlockKandangID(c.Context(), chickin.ProjectFlockKandangId) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return fiber.NewError(fiber.StatusNotFound, "Project flock population not found") - } - return err - } - - newReserved := population.ReservedQuantity - chickin.Quantity - if newReserved < 0 { - newReserved = 0 - } - - err = projectFlockPopulationRepo.PatchOne(c.Context(), population.Id, map[string]any{ - "reserved_quantity": newReserved, - }, nil) - if err != nil { - return err - } - - restoreFromDetails := func() (bool, error) { - - details, err := projectChickinDetailRepo.GetByProjectChickinID(c.Context(), chickin.Id) + exists, err := s.ProjectflockKandangRepo.WithTx(dbTransaction).IdExists(c.Context(), approvableID) if err != nil { - return false, err + return err + } + if !exists { + return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("ProjectFlockKandang %d not found", approvableID)) } - for _, d := range details { - productWarehouse, err := productWarehouseRepo.GetByID(c.Context(), d.ProductWarehouseId, nil) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - continue + if _, err := approvalSvc.CreateApproval( + c.Context(), + utils.ApprovalWorkflowProjectFlockKandang, + approvableID, + step, + &action, + actorID, + req.Notes, + ); err != nil { + + lower := strings.ToLower(err.Error()) + if !(strings.Contains(lower, "duplicate") || strings.Contains(lower, "unique constraint") || strings.Contains(lower, "23505")) { + return err + } + s.Log.Infof("ignored duplicate approval for kandang %d: %v", approvableID, err) + } + + if action == entity.ApprovalActionApproved { + + var chickins []entity.ProjectChickin + if err := chickinRepoTx.DB().WithContext(c.Context()).Where("project_flock_kandang_id = ?", approvableID).Find(&chickins).Error; err != nil { + return err + } + + for _, chickin := range chickins { + population := &entity.ProjectFlockPopulation{ + ProjectChickinId: chickin.Id, + ProductWarehouseId: chickin.ProductWarehouseId, + TotalQty: chickin.PendingUsageQty, + TotalUsedQty: 0, + Notes: chickin.Notes, + CreatedBy: actorID, + } + if err := ProjectFlockPopulationRepotx.CreateOne(c.Context(), population, nil); err != nil { + lower := strings.ToLower(err.Error()) + if !(strings.Contains(lower, "duplicate") || strings.Contains(lower, "unique constraint") || strings.Contains(lower, "23505")) { + return err + } + s.Log.Infof("ignored duplicate population for chickin %d: %v", chickin.Id, err) } - return false, err } - updatedQuantity := productWarehouse.Quantity + d.Quantity - if err := productWarehouseRepo.PatchOne(c.Context(), productWarehouse.Id, map[string]any{"quantity": updatedQuantity}, nil); err != nil { - return false, err - } } - if err := projectChickinDetailRepo.DeleteMany(c.Context(), func(db *gorm.DB) *gorm.DB { - return db.Where("project_chickin_id = ?", chickin.Id) - }); err != nil { - return false, err - } - - return true, nil - } - - restored, err := restoreFromDetails() - if err != nil { - return err - } - - if !restored { - projectflockkandang, err := projectFlockKandangRepo.GetByID(c.Context(), population.ProjectFlockKandangId) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return fiber.NewError(fiber.StatusNotFound, "Project flock kandang not found") - } - return err - } - - warehouse, err := warehouseRepoTx.GetByKandangID(c.Context(), projectflockkandang.KandangId) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return fiber.NewError(fiber.StatusNotFound, "Warehouse not found for kandang") - } - return err - } - - productWarehouse, err := productWarehouseRepo.GetLatestByCategoryCodeAndWarehouseID(c.Context(), "DOC", warehouse.Id) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return fiber.NewError(fiber.StatusNotFound, "Product Warehouse not found for the given Project Flock and Warehouse") - } - return err - } - - updatedQuantity := productWarehouse.Quantity + chickin.Quantity - - if err := productWarehouseRepo.PatchOne(c.Context(), productWarehouse.Id, map[string]any{"quantity": updatedQuantity}, nil); err != nil { - return err - } - } - - if err := chickinRepo.DeleteOne(c.Context(), id); err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return fiber.NewError(fiber.StatusNotFound, "Chickin not found") - } - return err } return nil }) if err != nil { - if ferr, ok := err.(*fiber.Error); ok { - return ferr + if fiberErr, ok := err.(*fiber.Error); ok { + return nil, fiberErr } - return err + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fiber.NewError(fiber.StatusNotFound, "Chickin not found") + } + s.Log.Errorf("Failed to record approval for chickins %+v: %+v", approvableIDs, err) + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to record approval") } - return nil + updated := make([]entity.ProjectChickin, 0) + for _, kandangID := range approvableIDs { + var chickins []entity.ProjectChickin + if err := s.Repository.DB().WithContext(c.Context()).Where("project_flock_kandang_id = ?", kandangID).Find(&chickins).Error; err != nil { + return nil, err + } + updated = append(updated, chickins...) + } + + return updated, nil } -func (s *chickinService) Approve(c *fiber.Ctx, id uint) error { - - // todo: ini contoh akhir jika sudah approved - - chickin, err := s.Repository.GetByID(c.Context(), id, nil) - - if errors.Is(err, gorm.ErrRecordNotFound) { - return fiber.NewError(fiber.StatusNotFound, "Chickin not found") +func uniqueUintSlice(values []uint) []uint { + seen := make(map[uint]struct{}, len(values)) + result := make([]uint, 0, len(values)) + for _, v := range values { + if _, ok := seen[v]; ok { + continue + } + seen[v] = struct{}{} + result = append(result, v) } - if err != nil { - s.Log.Errorf("Failed get chickin by id: %+v", err) - return err - } - - population, err := s.ProjectflockPopulationRepo.GetByProjectFlockKandangID(c.Context(), chickin.ProjectFlockKandangId) - if err != nil { - s.Log.Errorf("Failed to get project flock population: %+v", err) - return err - } - - err = s.ProjectflockPopulationRepo.PatchOne(c.Context(), population.Id, map[string]any{ - "reserved_quantity": population.ReservedQuantity - chickin.Quantity, - "initial_quantity": population.InitialQuantity + chickin.Quantity, - "current_quantity": population.CurrentQuantity + chickin.Quantity, - }, nil) - if err != nil { - s.Log.Errorf("Failed to update project flock population: %+v", err) - return err - } - - return nil + return result } diff --git a/internal/modules/production/chickins/validations/chickin.validation.go b/internal/modules/production/chickins/validations/chickin.validation.go index 9747ee07..c2676130 100644 --- a/internal/modules/production/chickins/validations/chickin.validation.go +++ b/internal/modules/production/chickins/validations/chickin.validation.go @@ -3,7 +3,7 @@ package validation type Create struct { ProjectFlockKandangId uint `json:"project_flock_kandang_id" validate:"required,number,min=1"` ChickInDate string `json:"chick_in_date" validate:"required,datetime=2006-01-02"` - Note string `json:"note" validate:"omitempty` + Note string `json:"note" validate:"omitempty"` } type Update struct { @@ -16,3 +16,9 @@ type Query struct { Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"` ProjectFlockKandangId uint `query:"project_flock_kandang_id" validate:"omitempty,number,min=1"` } + +type Approve struct { + Action string `json:"action" validate:"required_strict"` + ApprovableIds []uint `json:"approvable_ids" validate:"required_strict,min=1,dive,gt=0"` + Notes *string `json:"notes,omitempty" validate:"omitempty,max=500"` +} diff --git a/internal/modules/production/project_flocks/repositories/projectflock.repository.go b/internal/modules/production/project_flocks/repositories/projectflock.repository.go index 476b061b..bc5a0c6b 100644 --- a/internal/modules/production/project_flocks/repositories/projectflock.repository.go +++ b/internal/modules/production/project_flocks/repositories/projectflock.repository.go @@ -16,6 +16,7 @@ type ProjectflockRepository interface { GetActiveByFlock(ctx context.Context, flockID uint) (*entity.ProjectFlock, error) GetMaxPeriodByFlock(ctx context.Context, flockID uint) (int, error) GetNextPeriodForFlock(ctx context.Context, flockID uint) (int, error) + IdExists(ctx context.Context, id uint) (bool, error) } type ProjectflockRepositoryImpl struct { @@ -28,6 +29,10 @@ func NewProjectflockRepository(db *gorm.DB) ProjectflockRepository { } } +func (r *ProjectflockRepositoryImpl) IdExists(ctx context.Context, id uint) (bool, error) { + return repository.Exists[entity.ProjectFlock](ctx, r.DB(), id) +} + func (r *ProjectflockRepositoryImpl) GetAllByFlock(ctx context.Context, flockID uint) ([]entity.ProjectFlock, error) { var records []entity.ProjectFlock if err := r.DB().WithContext(ctx). diff --git a/internal/modules/production/project_flocks/repositories/projectflock_kandang.repository.go b/internal/modules/production/project_flocks/repositories/projectflock_kandang.repository.go index 5c78f830..7205b72a 100644 --- a/internal/modules/production/project_flocks/repositories/projectflock_kandang.repository.go +++ b/internal/modules/production/project_flocks/repositories/projectflock_kandang.repository.go @@ -3,6 +3,7 @@ package repository import ( "context" + "gitlab.com/mbugroup/lti-api.git/internal/common/repository" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" "gorm.io/gorm" ) @@ -14,6 +15,7 @@ type ProjectFlockKandangRepository interface { DeleteMany(ctx context.Context, projectFlockID uint, kandangIDs []uint) error GetAll(ctx context.Context) ([]entity.ProjectFlockKandang, error) WithTx(tx *gorm.DB) ProjectFlockKandangRepository + IdExists(ctx context.Context, id uint) (bool, error) DB() *gorm.DB } @@ -67,6 +69,9 @@ func (r *projectFlockKandangRepositoryImpl) WithTx(tx *gorm.DB) ProjectFlockKand func (r *projectFlockKandangRepositoryImpl) DB() *gorm.DB { return r.db } +func (r *projectFlockKandangRepositoryImpl) IdExists(ctx context.Context, id uint) (bool, error) { + return repository.Exists[entity.ProjectFlockKandang](ctx, r.db, id) +} func (r *projectFlockKandangRepositoryImpl) GetByID(ctx context.Context, id uint) (*entity.ProjectFlockKandang, error) { record := new(entity.ProjectFlockKandang) diff --git a/internal/modules/production/recordings/services/recording.service.go b/internal/modules/production/recordings/services/recording.service.go index 46ba36cc..b3bdaeaf 100644 --- a/internal/modules/production/recordings/services/recording.service.go +++ b/internal/modules/production/recordings/services/recording.service.go @@ -670,18 +670,19 @@ func (s *recordingService) getPreviousRecording(tx *gorm.DB, projectFlockKandang } func (s *recordingService) getTotalChick(tx *gorm.DB, projectFlockKandangId uint) (int64, error) { - var population entity.ProjectFlockPopulation - err := tx. - Where("project_flock_kandang_id = ?", projectFlockKandangId). - Order("created_at DESC"). - First(&population).Error - if errors.Is(err, gorm.ErrRecordNotFound) { - return 0, nil - } - if err != nil { - return 0, err - } - return int64(math.Round(population.InitialQuantity)), nil + // var population entity.ProjectFlockPopulation + // err := tx. + // Where("project_flock_kandang_id = ?", projectFlockKandangId). + // Order("created_at DESC"). + // First(&population).Error + // if errors.Is(err, gorm.ErrRecordNotFound) { + // return 0, nil + // } + // if err != nil { + // return 0, err + // } + //todo : nanti ganti lagi mas saya hardcode dulu + return int64(math.Round(1000)), nil } func (s *recordingService) getAverageBodyWeight(tx *gorm.DB, recordingID uint) (float64, error) { diff --git a/internal/modules/production/transfer_layings/module.go b/internal/modules/production/transfer_layings/module.go index d3c6279c..b309f19e 100644 --- a/internal/modules/production/transfer_layings/module.go +++ b/internal/modules/production/transfer_layings/module.go @@ -5,6 +5,7 @@ import ( "github.com/gofiber/fiber/v2" "gorm.io/gorm" + rProjectFlock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories" rTransferLaying "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/repositories" sTransferLaying "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/services" @@ -17,10 +18,11 @@ type TransferLayingModule struct{} func (TransferLayingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) { transferLayingRepo := rTransferLaying.NewTransferLayingRepository(db) userRepo := rUser.NewUserRepository(db) + projectFlockRepo := rProjectFlock.NewProjectflockRepository(db) + projectFlockKandangRepo := rProjectFlock.NewProjectFlockKandangRepository(db) - transferLayingService := sTransferLaying.NewTransferLayingService(transferLayingRepo, validate) + transferLayingService := sTransferLaying.NewTransferLayingService(transferLayingRepo, projectFlockRepo, projectFlockKandangRepo, validate) userService := sUser.NewUserService(userRepo, validate) TransferLayingRoutes(router, userService, transferLayingService) } - diff --git a/internal/utils/constant.go b/internal/utils/constant.go index bdbc53b6..8328f4d7 100644 --- a/internal/utils/constant.go +++ b/internal/utils/constant.go @@ -79,6 +79,24 @@ const ( WarehouseTypeKandang WarehouseType = "KANDANG" ) +// ------------------------------------------------------------------- +// Stock log +// ------------------------------------------------------------------- + +type StockLogTransactionType string + +const ( + StockLogTransactionTypeIncrease StockLogTransactionType = "INCREASE" + StockLogTransactionTypeDecrease StockLogTransactionType = "DECREASE" +) + +type StockLogType string + +const ( + StockLogTypeAdjustment StockLogType = "ADJUSTMENT" + StockLogTypeTransfer StockLogType = "TRANSFER" +) + // ------------------------------------------------------------------- // WarehouseType // ------------------------------------------------------------------- @@ -140,6 +158,20 @@ var ProjectFlockApprovalSteps = map[approvalutils.ApprovalStep]string{ ProjectFlockStepAktif: "Aktif", } +// ------------------------------------------------------------------- +// Project Flock Kandang Approval +// ------------------------------------------------------------------- +const ( + ApprovalWorkflowProjectFlockKandang approvalutils.ApprovalWorkflowKey = approvalutils.ApprovalWorkflowKey("PROJECT_FLOCK_KANDANGS") + ProjectFlockKandangStepPengajuan approvalutils.ApprovalStep = 1 + ProjectFlockKandangStepDisetujui approvalutils.ApprovalStep = 2 +) + +var ProjectFlockKandangApprovalSteps = map[approvalutils.ApprovalStep]string{ + ProjectFlockKandangStepPengajuan: "Pengajuan", + ProjectFlockKandangStepDisetujui: "Disetujui", +} + // ------------------------------------------------------------------- // Validators // ------------------------------------------------------------------- From 20f1be2ef82a332b71343922431a503ec98d39df Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Sun, 2 Nov 2025 21:06:03 +0700 Subject: [PATCH 06/12] feat[BE]: Refactor Chickin create and approvals support chickin growing and chickin laying, and create get one project flock kandang API --- ...0649_create_project_chick_ins_table.up.sql | 36 -- ...ate_project_flock_populations_table.up.sql | 36 -- ...4829_create_project_chickin_details.up.sql | 2 +- ...4527_recreate_project_chikins_table.up.sql | 60 +-- ..._recreate_project_flock_populations.up.sql | 65 ++-- internal/database/seed/seeder.go | 21 ++ internal/entities/projectflock_kandang.go | 7 +- .../product_warehouse.repository.go | 17 + .../production/chickins/dto/chickin.dto.go | 36 +- .../project_chickin.repository.go | 17 +- .../chickins/services/chickin.service.go | 224 ++++++++--- .../project_flock_kandang.controller.go | 76 ++++ .../dto/project_flock_kandang.dto.go | 353 ++++++++++++++++++ .../project-flock-kandangs/module.go | 42 +++ .../project-flock-kandangs/route.go | 26 ++ .../services/project_flock_kandang.service.go | 147 ++++++++ .../project_flock_kandang.validation.go | 17 + .../project_flock_population_repository.go | 13 + .../projectflock_kandang.repository.go | 9 + internal/modules/production/route.go | 2 + internal/utils/constant.go | 2 + 21 files changed, 1002 insertions(+), 206 deletions(-) delete mode 100644 internal/database/migrations/20251018120649_create_project_chick_ins_table.up.sql delete mode 100644 internal/database/migrations/20251020022357_create_project_flock_populations_table.up.sql create mode 100644 internal/modules/production/project-flock-kandangs/controllers/project_flock_kandang.controller.go create mode 100644 internal/modules/production/project-flock-kandangs/dto/project_flock_kandang.dto.go create mode 100644 internal/modules/production/project-flock-kandangs/module.go create mode 100644 internal/modules/production/project-flock-kandangs/route.go create mode 100644 internal/modules/production/project-flock-kandangs/services/project_flock_kandang.service.go create mode 100644 internal/modules/production/project-flock-kandangs/validations/project_flock_kandang.validation.go diff --git a/internal/database/migrations/20251018120649_create_project_chick_ins_table.up.sql b/internal/database/migrations/20251018120649_create_project_chick_ins_table.up.sql deleted file mode 100644 index 25d3476d..00000000 --- a/internal/database/migrations/20251018120649_create_project_chick_ins_table.up.sql +++ /dev/null @@ -1,36 +0,0 @@ -CREATE TABLE IF NOT EXISTS project_chickins ( - id BIGSERIAL PRIMARY KEY, - project_flock_kandang_id BIGINT NOT NULL, - chick_in_date DATE NOT NULL, - quantity NUMERIC(15, 3) NOT NULL, - note TEXT, - created_by BIGINT NOT NULL, - created_at TIMESTAMPTZ DEFAULT now(), - updated_at TIMESTAMPTZ DEFAULT now(), - deleted_at TIMESTAMPTZ -); - --- FOREIGN KEYS (dijalankan setelah semua tabel parent ada) -DO $$ -BEGIN - IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'project_flock_kandangs') THEN - ALTER TABLE project_chickins - ADD CONSTRAINT fk_project_flock_kandang_id - FOREIGN KEY (project_flock_kandang_id) - REFERENCES project_flock_kandangs(id) - ON DELETE RESTRICT ON UPDATE CASCADE; - END IF; - - IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'users') THEN - ALTER TABLE project_chickins - ADD CONSTRAINT fk_created_by - FOREIGN KEY (created_by) - REFERENCES users(id) - ON DELETE RESTRICT ON UPDATE CASCADE; - END IF; -END $$; - --- INDEXES -CREATE INDEX IF NOT EXISTS idx_project_chickins_project_flock_kandang_id ON project_chickins (project_flock_kandang_id); - -CREATE INDEX IF NOT EXISTS idx_project_chickins_created_by ON project_chickins (created_by); \ No newline at end of file diff --git a/internal/database/migrations/20251020022357_create_project_flock_populations_table.up.sql b/internal/database/migrations/20251020022357_create_project_flock_populations_table.up.sql deleted file mode 100644 index 82b3e9a7..00000000 --- a/internal/database/migrations/20251020022357_create_project_flock_populations_table.up.sql +++ /dev/null @@ -1,36 +0,0 @@ -CREATE TABLE IF NOT EXISTS project_flock_populations ( - id BIGSERIAL PRIMARY KEY, - project_flock_kandang_id BIGINT NOT NULL, - initial_quantity NUMERIC(15, 3) NOT NULL, - current_quantity NUMERIC(15, 3) NOT NULL, - reserved_quantity NUMERIC(15, 3), - created_by BIGINT NOT NULL, - created_at TIMESTAMPTZ DEFAULT now(), - updated_at TIMESTAMPTZ DEFAULT now(), - deleted_at TIMESTAMPTZ -); - --- FOREIGN KEYS (dijalankan setelah semua tabel parent ada) -DO $$ -BEGIN - IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'project_flock_kandangs') THEN - ALTER TABLE project_flock_populations - ADD CONSTRAINT fk_project_flock_kandang_id - FOREIGN KEY (project_flock_kandang_id) - REFERENCES project_flock_kandangs(id) - ON DELETE RESTRICT ON UPDATE CASCADE; - END IF; - - IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'users') THEN - ALTER TABLE project_flock_populations - ADD CONSTRAINT fk_created_by - FOREIGN KEY (created_by) - REFERENCES users(id) - ON DELETE RESTRICT ON UPDATE CASCADE; - END IF; -END $$; - --- INDEXES -CREATE INDEX IF NOT EXISTS idx_project_flock_populations_project_flock_kandang_id ON project_flock_populations (project_flock_kandang_id); - -CREATE INDEX IF NOT EXISTS idx_project_flock_populations_created_by ON project_flock_populations (created_by); \ No newline at end of file diff --git a/internal/database/migrations/20251022024829_create_project_chickin_details.up.sql b/internal/database/migrations/20251022024829_create_project_chickin_details.up.sql index 349086ba..a2364abe 100644 --- a/internal/database/migrations/20251022024829_create_project_chickin_details.up.sql +++ b/internal/database/migrations/20251022024829_create_project_chickin_details.up.sql @@ -9,7 +9,7 @@ CREATE TABLE IF NOT EXISTS project_chickin_details ( deleted_at TIMESTAMPTZ ); --- FOREIGN KEYS (dijalankan setelah semua tabel parent ada) + DO $$ BEGIN IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'project_chickins') THEN diff --git a/internal/database/migrations/20251030134527_recreate_project_chikins_table.up.sql b/internal/database/migrations/20251030134527_recreate_project_chikins_table.up.sql index c00765b1..e029646b 100644 --- a/internal/database/migrations/20251030134527_recreate_project_chikins_table.up.sql +++ b/internal/database/migrations/20251030134527_recreate_project_chikins_table.up.sql @@ -3,6 +3,7 @@ -- ============================================ -- STEP 1: Hapus tabel jika sudah ada +DROP TABLE IF EXISTS project_chickins; -- STEP 2: Buat tabel project_chickins CREATE TABLE IF NOT EXISTS project_chickins ( @@ -20,39 +21,38 @@ CREATE TABLE IF NOT EXISTS project_chickins ( ); -- STEP 3: FOREIGN KEYS -DO $$ -BEGIN - -- Relasi ke project_flock_kandangs - IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'project_flock_kandangs') THEN - ALTER TABLE project_chickins - ADD CONSTRAINT fk_project_chickins_kandang - FOREIGN KEY (project_flock_kandang_id) - REFERENCES project_flock_kandangs(id) - ON DELETE RESTRICT ON UPDATE CASCADE; - END IF; +BEGIN; - -- Relasi ke product_warehouses - IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'product_warehouses') THEN - ALTER TABLE project_chickins - ADD CONSTRAINT fk_project_chickins_warehouse - FOREIGN KEY (product_warehouse_id) - REFERENCES product_warehouses(id) - ON DELETE RESTRICT ON UPDATE CASCADE; - END IF; +-- Relasi ke project_flock_kandangs +ALTER TABLE project_chickins +ADD CONSTRAINT fk_project_chickins_kandang FOREIGN KEY (project_flock_kandang_id) REFERENCES project_flock_kandangs (id) ON DELETE RESTRICT ON UPDATE CASCADE; - -- Relasi ke users - IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'users') THEN - ALTER TABLE project_chickins - ADD CONSTRAINT fk_project_chickins_created_by - FOREIGN KEY (created_by) - REFERENCES users(id) - ON DELETE RESTRICT ON UPDATE CASCADE; - END IF; -END $$; +-- Relasi ke product_warehouses +ALTER TABLE project_chickins +ADD CONSTRAINT fk_project_chickins_warehouse FOREIGN KEY (product_warehouse_id) REFERENCES product_warehouses (id) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- Relasi ke users +ALTER TABLE project_chickins +ADD CONSTRAINT fk_project_chickins_created_by FOREIGN KEY (created_by) REFERENCES users (id) ON DELETE RESTRICT ON UPDATE CASCADE; + +COMMIT; -- STEP 4: INDEXES -CREATE INDEX IF NOT EXISTS idx_chickins_kandang_id ON project_chickins (project_flock_kandang_id); +CREATE INDEX IF NOT EXISTS idx_chickins_kandang_id ON project_chickins (project_flock_kandang_id) +WHERE + deleted_at IS NULL; -CREATE INDEX IF NOT EXISTS idx_chickins_warehouse_id ON project_chickins (product_warehouse_id); +CREATE INDEX IF NOT EXISTS idx_chickins_warehouse_id ON project_chickins (product_warehouse_id) +WHERE + deleted_at IS NULL; -CREATE INDEX IF NOT EXISTS idx_chickins_created_by ON project_chickins (created_by); \ No newline at end of file +CREATE INDEX IF NOT EXISTS idx_chickins_created_by ON project_chickins (created_by); + +-- Composite index for common queries +CREATE INDEX IF NOT EXISTS idx_chickins_kandang_deleted ON project_chickins ( + project_flock_kandang_id, + deleted_at +); + +-- Index for soft delete queries +CREATE INDEX IF NOT EXISTS idx_chickins_deleted_at ON project_chickins (deleted_at); \ No newline at end of file diff --git a/internal/database/migrations/20251030134542_recreate_project_flock_populations.up.sql b/internal/database/migrations/20251030134542_recreate_project_flock_populations.up.sql index 2cb76e8f..aa529408 100644 --- a/internal/database/migrations/20251030134542_recreate_project_flock_populations.up.sql +++ b/internal/database/migrations/20251030134542_recreate_project_flock_populations.up.sql @@ -3,6 +3,7 @@ -- ============================================ -- STEP 1: Hapus tabel jika sudah ada +DROP TABLE IF EXISTS project_flock_populations; -- STEP 2: Buat tabel project_flock_populations CREATE TABLE IF NOT EXISTS project_flock_populations ( @@ -19,39 +20,43 @@ CREATE TABLE IF NOT EXISTS project_flock_populations ( ); -- STEP 3: FOREIGN KEYS -DO $$ -BEGIN - -- Relasi ke project_chickins - IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'project_chickins') THEN - ALTER TABLE project_flock_populations - ADD CONSTRAINT fk_project_flock_populations_chickin - FOREIGN KEY (project_chickin_id) - REFERENCES project_chickins(id) - ON DELETE RESTRICT ON UPDATE CASCADE; - END IF; +BEGIN; - -- Relasi ke product_warehouses - IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'product_warehouses') THEN - ALTER TABLE project_flock_populations - ADD CONSTRAINT fk_project_flock_populations_warehouse - FOREIGN KEY (product_warehouse_id) - REFERENCES product_warehouses(id) - ON DELETE RESTRICT ON UPDATE CASCADE; - END IF; +-- Relasi ke project_chickins +ALTER TABLE project_flock_populations +ADD CONSTRAINT fk_project_flock_populations_chickin FOREIGN KEY (project_chickin_id) REFERENCES project_chickins (id) ON DELETE RESTRICT ON UPDATE CASCADE; - -- Relasi ke users - IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'users') THEN - ALTER TABLE project_flock_populations - ADD CONSTRAINT fk_project_flock_populations_created_by - FOREIGN KEY (created_by) - REFERENCES users(id) - ON DELETE RESTRICT ON UPDATE CASCADE; - END IF; -END $$; +-- Relasi ke product_warehouses +ALTER TABLE project_flock_populations +ADD CONSTRAINT fk_project_flock_populations_warehouse FOREIGN KEY (product_warehouse_id) REFERENCES product_warehouses (id) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- Relasi ke users +ALTER TABLE project_flock_populations +ADD CONSTRAINT fk_project_flock_populations_created_by FOREIGN KEY (created_by) REFERENCES users (id) ON DELETE RESTRICT ON UPDATE CASCADE; + +COMMIT; -- STEP 4: INDEXES -CREATE INDEX IF NOT EXISTS idx_populations_chickin_id ON project_flock_populations (project_chickin_id); +CREATE INDEX IF NOT EXISTS idx_populations_chickin_id ON project_flock_populations (project_chickin_id) +WHERE + deleted_at IS NULL; -CREATE INDEX IF NOT EXISTS idx_populations_warehouse_id ON project_flock_populations (product_warehouse_id); +CREATE INDEX IF NOT EXISTS idx_populations_warehouse_id ON project_flock_populations (product_warehouse_id) +WHERE + deleted_at IS NULL; -CREATE INDEX IF NOT EXISTS idx_populations_created_by ON project_flock_populations (created_by); \ No newline at end of file +CREATE INDEX IF NOT EXISTS idx_populations_created_by ON project_flock_populations (created_by); + +-- Composite index for common queries +CREATE INDEX IF NOT EXISTS idx_populations_chickin_deleted ON project_flock_populations ( + project_chickin_id, + deleted_at +); + +-- Index for soft delete queries +CREATE INDEX IF NOT EXISTS idx_populations_deleted_at ON project_flock_populations (deleted_at); + +-- Unique constraint: one population per chickin +CREATE UNIQUE INDEX IF NOT EXISTS idx_populations_chickin_unique ON project_flock_populations (project_chickin_id) +WHERE + deleted_at IS NULL; \ No newline at end of file diff --git a/internal/database/seed/seeder.go b/internal/database/seed/seeder.go index ca8e8cf3..3b7378c4 100644 --- a/internal/database/seed/seeder.go +++ b/internal/database/seed/seeder.go @@ -573,6 +573,7 @@ func seedProductCategories(tx *gorm.DB, createdBy uint) (map[string]uint, error) }{ {"Bahan Baku", "RAW"}, {"Day Old Chick", "DOC"}, + {"Pullet", "PULLET"}, } result := make(map[string]uint, len(seeds)) @@ -787,6 +788,26 @@ func seedProducts(tx *gorm.DB, createdBy uint, uoms map[string]uint, categories Suppliers: []string{"PT CHAROEN POKPHAND INDONESIA Tbk"}, Flags: []utils.FlagType{utils.FlagPakan, utils.FlagStarter}, }, + { + Name: "DOC MAlindo", + Brand: "MAlindo", + Sku: "MAL0001", + Uom: "Ekor", + Category: "Day Old Chick", + Price: 8000, + Suppliers: []string{"PT CHAROEN POKPHAND INDONESIA Tbk"}, + Flags: []utils.FlagType{utils.FlagDOC}, + }, + { + Name: "Ayam Pullet", + Brand: "MBU Pullet", + Sku: "PUL0001", + Uom: "Ekor", + Category: "Pullet", + Price: 15000, + Suppliers: []string{"PT CHAROEN POKPHAND INDONESIA Tbk"}, + Flags: []utils.FlagType{utils.FlagPullet}, + }, } for _, seed := range seeds { diff --git a/internal/entities/projectflock_kandang.go b/internal/entities/projectflock_kandang.go index f10dbd17..c02eafe1 100644 --- a/internal/entities/projectflock_kandang.go +++ b/internal/entities/projectflock_kandang.go @@ -8,7 +8,8 @@ type ProjectFlockKandang struct { KandangId uint `gorm:"not null;index:idx_project_flock_kandangs_kandang;uniqueIndex:idx_project_flock_kandangs_unique"` CreatedAt time.Time `gorm:"autoCreateTime"` - ProjectFlock ProjectFlock `gorm:"foreignKey:ProjectFlockId;references:Id"` - Kandang Kandang `gorm:"foreignKey:KandangId;references:Id"` - Chickins []ProjectChickin `gorm:"foreignKey:ProjectFlockKandangId;references:Id"` + ProjectFlock ProjectFlock `gorm:"foreignKey:ProjectFlockId;references:Id"` + Kandang Kandang `gorm:"foreignKey:KandangId;references:Id"` + Chickins []ProjectChickin `gorm:"foreignKey:ProjectFlockKandangId;references:Id"` + LatestApproval *Approval `gorm:"-" json:"-"` } diff --git a/internal/modules/inventory/product-warehouses/repositories/product_warehouse.repository.go b/internal/modules/inventory/product-warehouses/repositories/product_warehouse.repository.go index 0017fe6c..3bf7f36f 100644 --- a/internal/modules/inventory/product-warehouses/repositories/product_warehouse.repository.go +++ b/internal/modules/inventory/product-warehouses/repositories/product_warehouse.repository.go @@ -18,6 +18,7 @@ type ProductWarehouseRepository interface { GetProductWarehouseByProductAndWarehouseID(ctx context.Context, productId, warehouseId uint) (*entity.ProductWarehouse, error) GetByCategoryCodeAndWarehouseID(ctx context.Context, categoryCode string, warehouseId uint) ([]entity.ProductWarehouse, error) GetLatestByCategoryCodeAndWarehouseID(ctx context.Context, categoryCode string, warehouseId uint) (*entity.ProductWarehouse, error) + GetFirstProductByCategoryCode(ctx context.Context, categoryCode string) (*entity.Product, error) WithTxRepo(tx *gorm.DB) ProductWarehouseRepository DB() *gorm.DB } @@ -89,6 +90,8 @@ func (r *ProductWarehouseRepositoryImpl) GetProductWarehouseByProductAndWarehous func (r *ProductWarehouseRepositoryImpl) GetByCategoryCodeAndWarehouseID(ctx context.Context, categoryCode string, warehouseId uint) ([]entity.ProductWarehouse, error) { var productWarehouses []entity.ProductWarehouse err := r.db.WithContext(ctx). + Preload("Product"). + Preload("Warehouse"). Table("product_warehouses"). Select("product_warehouses.*"). Joins("JOIN products ON products.id = product_warehouses.product_id"). @@ -105,6 +108,8 @@ func (r *ProductWarehouseRepositoryImpl) GetByCategoryCodeAndWarehouseID(ctx con func (r *ProductWarehouseRepositoryImpl) GetLatestByCategoryCodeAndWarehouseID(ctx context.Context, categoryCode string, warehouseId uint) (*entity.ProductWarehouse, error) { var productWarehouse entity.ProductWarehouse err := r.db.WithContext(ctx). + Preload("Product"). + Preload("Warehouse"). Table("product_warehouses"). Select("product_warehouses.*"). Joins("JOIN products ON products.id = product_warehouses.product_id"). @@ -118,3 +123,15 @@ func (r *ProductWarehouseRepositoryImpl) GetLatestByCategoryCodeAndWarehouseID(c } return &productWarehouse, nil } + +func (r *ProductWarehouseRepositoryImpl) GetFirstProductByCategoryCode(ctx context.Context, categoryCode string) (*entity.Product, error) { + var product entity.Product + err := r.db.WithContext(ctx). + Joins("JOIN product_categories ON products.product_category_id = product_categories.id"). + Where("product_categories.code = ?", categoryCode). + First(&product).Error + if err != nil { + return nil, err + } + return &product, nil +} diff --git a/internal/modules/production/chickins/dto/chickin.dto.go b/internal/modules/production/chickins/dto/chickin.dto.go index 823cbfa5..1f35c5a8 100644 --- a/internal/modules/production/chickins/dto/chickin.dto.go +++ b/internal/modules/production/chickins/dto/chickin.dto.go @@ -15,8 +15,8 @@ import ( // === DTO Structs (ordered) === type ChickinBaseDTO struct { - Id uint `json:"id"` - ProjectFlockKandang *ProjectFlockKandangDTO `json:"project_flock_kandang"` + Id uint `json:"id"` + ProjectFlockKandangId uint `json:"project_flock_kandang_id"` ChickInDate time.Time `json:"chick_in_date"` ProductWarehouseId uint `json:"product_warehouse_id"` UsageQty float64 `json:"usage_qty"` @@ -55,10 +55,9 @@ type ChickinSimpleDTO struct { type ChickinListDTO struct { ChickinBaseDTO - ProjectFlockKandang *ProjectFlockKandangDTO `json:"project_flock_kandang"` - CreatedUser *userBaseDTO.UserBaseDTO `json:"created_user"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + CreatedUser *userBaseDTO.UserBaseDTO `json:"created_user"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` } type ChickinDetailDTO struct { @@ -141,14 +140,17 @@ func ToProjectFlockKandangDTO(e entity.ProjectFlockKandang) ProjectFlockKandangD } func ToChickinBaseDTO(e entity.ProjectChickin) ChickinBaseDTO { - var pfk *ProjectFlockKandangDTO + var projectFlockKandangId uint + // Check if ProjectFlockKandang relation is loaded if e.ProjectFlockKandang != nil && e.ProjectFlockKandang.Id != 0 { - mapped := ToProjectFlockKandangDTO(*e.ProjectFlockKandang) - pfk = &mapped + projectFlockKandangId = e.ProjectFlockKandang.Id + } else if e.ProjectFlockKandangId != 0 { + // If relation is not loaded but ID is available, use the ID + projectFlockKandangId = e.ProjectFlockKandangId } return ChickinBaseDTO{ Id: e.Id, - ProjectFlockKandang: pfk, + ProjectFlockKandangId: projectFlockKandangId, ChickInDate: e.ChickInDate, ProductWarehouseId: e.ProductWarehouseId, UsageQty: e.UsageQty, @@ -176,17 +178,11 @@ func ToChickinListDTO(e entity.ProjectChickin) ChickinListDTO { mapped := userBaseDTO.ToUserBaseDTO(*e.CreatedUser) createdUser = &mapped } - var pfk *ProjectFlockKandangDTO - if e.ProjectFlockKandang != nil && e.ProjectFlockKandang.Id != 0 { - mapped := ToProjectFlockKandangDTO(*e.ProjectFlockKandang) - pfk = &mapped - } return ChickinListDTO{ - ChickinBaseDTO: ToChickinBaseDTO(e), - ProjectFlockKandang: pfk, - CreatedUser: createdUser, - CreatedAt: e.CreatedAt, - UpdatedAt: e.UpdatedAt, + ChickinBaseDTO: ToChickinBaseDTO(e), + CreatedUser: createdUser, + CreatedAt: e.CreatedAt, + UpdatedAt: e.UpdatedAt, } } diff --git a/internal/modules/production/chickins/repositories/project_chickin.repository.go b/internal/modules/production/chickins/repositories/project_chickin.repository.go index 64e2e4b4..fa6f13e6 100644 --- a/internal/modules/production/chickins/repositories/project_chickin.repository.go +++ b/internal/modules/production/chickins/repositories/project_chickin.repository.go @@ -11,21 +11,24 @@ import ( type ProjectChickinRepository interface { repository.BaseRepository[entity.ProjectChickin] GetFirstByProjectFlockID(ctx context.Context, projectFlockID uint) (*entity.ProjectChickin, error) + GetByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) ([]entity.ProjectChickin, error) } type ChickinRepositoryImpl struct { *repository.BaseRepositoryImpl[entity.ProjectChickin] + db *gorm.DB } func NewChickinRepository(db *gorm.DB) ProjectChickinRepository { return &ChickinRepositoryImpl{ BaseRepositoryImpl: repository.NewBaseRepository[entity.ProjectChickin](db), + db: db, } } func (r *ChickinRepositoryImpl) GetFirstByProjectFlockID(ctx context.Context, projectFlockID uint) (*entity.ProjectChickin, error) { var chickin entity.ProjectChickin - err := r.DB().WithContext(ctx). + err := r.db.WithContext(ctx). Where("project_floc_id = ?", projectFlockID). Where("deleted_at IS NULL"). First(&chickin).Error @@ -34,3 +37,15 @@ func (r *ChickinRepositoryImpl) GetFirstByProjectFlockID(ctx context.Context, pr } return &chickin, nil } + +func (r *ChickinRepositoryImpl) GetByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) ([]entity.ProjectChickin, error) { + var chickins []entity.ProjectChickin + err := r.db.WithContext(ctx). + Where("project_flock_kandang_id = ?", projectFlockKandangID). + Order("created_at DESC"). + Find(&chickins).Error + if err != nil { + return nil, err + } + return chickins, nil +} diff --git a/internal/modules/production/chickins/services/chickin.service.go b/internal/modules/production/chickins/services/chickin.service.go index 2b44c242..3074b36a 100644 --- a/internal/modules/production/chickins/services/chickin.service.go +++ b/internal/modules/production/chickins/services/chickin.service.go @@ -125,13 +125,21 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entit } var productWarehouses []entity.ProductWarehouse + category := strings.ToUpper(strings.TrimSpace(projectFlockKandang.ProjectFlock.Category)) - if strings.ToUpper(strings.TrimSpace(projectFlockKandang.ProjectFlock.Category)) == string(utils.ProjectFlockCategoryGrowing) { + var productCategoryCode string + switch category { + case string(utils.ProjectFlockCategoryGrowing): + productCategoryCode = "DOC" + case string(utils.ProjectFlockCategoryLaying): + productCategoryCode = "PULLET" + default: + return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Unknown category: %s", category)) + } - productWarehouses, err = s.ProductWarehouseRepo.GetByCategoryCodeAndWarehouseID(c.Context(), "DOC", warehouse.Id) - if err != nil || len(productWarehouses) == 0 { - return nil, fiber.NewError(fiber.StatusNotFound, "Product for growing category in the Kandang's warehouse not found") - } + productWarehouses, err = s.ProductWarehouseRepo.GetByCategoryCodeAndWarehouseID(c.Context(), productCategoryCode, warehouse.Id) + if err != nil || len(productWarehouses) == 0 { + return nil, fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Product for %s category in the Kandang's warehouse not found", strings.ToLower(category))) } chickinDate, err := utils.ParseDateString(req.ChickInDate) @@ -142,20 +150,17 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entit actorID := uint(1) // todo nanti ambil dari auth context newChikins := make([]*entity.ProjectChickin, 0) for _, productWarehouse := range productWarehouses { - - if productWarehouse.Quantity > 0 { - newChickin := &entity.ProjectChickin{ - ProjectFlockKandangId: req.ProjectFlockKandangId, - ChickInDate: chickinDate, - UsageQty: 0, - PendingUsageQty: productWarehouse.Quantity, - ProductWarehouseId: productWarehouse.Id, - Notes: req.Note, - CreatedBy: actorID, - } - - newChikins = append(newChikins, newChickin) + newChickin := &entity.ProjectChickin{ + ProjectFlockKandangId: req.ProjectFlockKandangId, + ChickInDate: chickinDate, + UsageQty: 0, + PendingUsageQty: productWarehouse.Quantity, + ProductWarehouseId: productWarehouse.Id, + Notes: req.Note, + CreatedBy: actorID, } + + newChikins = append(newChikins, newChickin) } if len(newChikins) == 0 { @@ -219,7 +224,6 @@ func (s chickinService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) updateBody["chick_in_date"] = req.ChickInDate } if req.Note != "" { - // entity uses `Notes` => column `notes` updateBody["notes"] = req.Note } if len(updateBody) == 0 { @@ -239,7 +243,6 @@ func (s chickinService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) func (s chickinService) DeleteOne(c *fiber.Ctx, id uint) error { - // Simplified delete: directly call repository delete. Complex restore logic removed for now. if err := s.Repository.DeleteOne(c.Context(), id); err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return fiber.NewError(fiber.StatusNotFound, "Chickin not found") @@ -254,7 +257,6 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit if err := s.Validate.Struct(req); err != nil { return nil, err } - actorID := uint(1) // todo nanti ambil dari auth context var action entity.ApprovalAction @@ -272,26 +274,40 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit return nil, fiber.NewError(fiber.StatusBadRequest, "approvable_ids must contain at least one id") } + // Validate all ProjectFlockKandang IDs exist and have valid approval status + approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(s.Repository.DB())) + for _, id := range approvableIDs { + idCopy := id + if err := commonSvc.EnsureRelations(c.Context(), commonSvc.RelationCheck{Name: "ProjectFlockKandang", ID: &idCopy, Exists: s.ProjectflockKandangRepo.IdExists}); err != nil { + return nil, err + } + + // Check latest approval status - must be PENGAJUAN to be approvable + latestApproval, err := approvalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowProjectFlockKandang, id, nil) + if err != nil { + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check approval status") + } + if latestApproval == nil { + return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("No approval found for ProjectFlockKandang %d - chickins must be created first", id)) + } + if latestApproval.StepNumber != uint16(utils.ProjectFlockKandangStepPengajuan) { + return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("ProjectFlockKandang %d cannot be approved - current status is not in PENGAJUAN stage", id)) + } + } + step := utils.ProjectFlockKandangStepPengajuan if action == entity.ApprovalActionApproved { step = utils.ProjectFlockKandangStepDisetujui } err := s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { + approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction)) - ProjectFlockPopulationRepotx := s.ProjectflockPopulationRepo.WithTx(dbTransaction) - chickinRepoTx := s.Repository.WithTx(dbTransaction) + chickinRepoTx := repository.NewChickinRepository(dbTransaction) + productWarehouseTx := s.ProductWarehouseRepo.WithTx(dbTransaction) for _, approvableID := range approvableIDs { - exists, err := s.ProjectflockKandangRepo.WithTx(dbTransaction).IdExists(c.Context(), approvableID) - if err != nil { - return err - } - if !exists { - return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("ProjectFlockKandang %d not found", approvableID)) - } - if _, err := approvalSvc.CreateApproval( c.Context(), utils.ApprovalWorkflowProjectFlockKandang, @@ -311,31 +327,54 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit if action == entity.ApprovalActionApproved { - var chickins []entity.ProjectChickin - if err := chickinRepoTx.DB().WithContext(c.Context()).Where("project_flock_kandang_id = ?", approvableID).Find(&chickins).Error; err != nil { + chickins, err := chickinRepoTx.GetByProjectFlockKandangID(c.Context(), approvableID) + if err != nil { return err } - for _, chickin := range chickins { - population := &entity.ProjectFlockPopulation{ - ProjectChickinId: chickin.Id, - ProductWarehouseId: chickin.ProductWarehouseId, - TotalQty: chickin.PendingUsageQty, - TotalUsedQty: 0, - Notes: chickin.Notes, - CreatedBy: actorID, - } - if err := ProjectFlockPopulationRepotx.CreateOne(c.Context(), population, nil); err != nil { - lower := strings.ToLower(err.Error()) - if !(strings.Contains(lower, "duplicate") || strings.Contains(lower, "unique constraint") || strings.Contains(lower, "23505")) { - return err - } - s.Log.Infof("ignored duplicate population for chickin %d: %v", chickin.Id, err) + kandangForApproval, err := s.ProjectflockKandangRepo.GetByID(c.Context(), approvableID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("ProjectFlockKandang %d not found", approvableID)) } + return err } - } + warehouse, err := s.WarehouseRepo.GetByKandangID(c.Context(), kandangForApproval.KandangId) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Warehouse for kandang %d not found", kandangForApproval.KandangId)) + } + return err + } + pulletPW, err := s.getOrCreatePulletProductWarehouse(c, warehouse.Id, dbTransaction, actorID) + if err != nil { + + continue + } + + if err := s.convertChickinsToPullet(c, chickins, pulletPW, dbTransaction, actorID); err != nil { + return err + } + + } else if action == entity.ApprovalActionRejected { + + chickins, err := chickinRepoTx.GetByProjectFlockKandangID(c.Context(), approvableID) + if err != nil { + s.Log.Warnf("failed to get chickins for rejection %d: %v", approvableID, err) + continue + } + + for _, chickin := range chickins { + + updates := map[string]any{"quantity": chickin.PendingUsageQty} + + if err := productWarehouseTx.PatchOne(c.Context(), chickin.ProductWarehouseId, updates, nil); err != nil { + s.Log.Warnf("failed to restore product warehouse quantity for rejection: %v", err) + } + } + } } return nil @@ -364,6 +403,93 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit return updated, nil } +func (s *chickinService) getOrCreatePulletProductWarehouse(ctx *fiber.Ctx, warehouseId uint, dbTransaction *gorm.DB, actorID uint) (*entity.ProductWarehouse, error) { + + pulletProducts, err := s.ProductWarehouseRepo.GetByCategoryCodeAndWarehouseID(ctx.Context(), "PULLET", warehouseId) + if err == nil && len(pulletProducts) > 0 { + return &pulletProducts[0], nil + } + + pulletProduct, err := s.ProductWarehouseRepo.GetFirstProductByCategoryCode(ctx.Context(), "PULLET") + if err != nil { + return nil, fmt.Errorf("failed to get PULLET product: %w", err) + } + if pulletProduct == nil { + return nil, fmt.Errorf("no PULLET product found in system") + } + + newPulletPW := &entity.ProductWarehouse{ + ProductId: pulletProduct.Id, + WarehouseId: warehouseId, + Quantity: 0, + CreatedBy: actorID, + } + + if err := s.ProductWarehouseRepo.WithTx(dbTransaction).CreateOne(ctx.Context(), newPulletPW, nil); err != nil { + return nil, fmt.Errorf("failed to create PULLET product warehouse: %w", err) + } + + return newPulletPW, nil +} + +func (s *chickinService) convertChickinsToPullet(ctx *fiber.Ctx, chickins []entity.ProjectChickin, pulletPW *entity.ProductWarehouse, dbTransaction *gorm.DB, actorID uint) error { + + if pulletPW == nil || pulletPW.Id == 0 { + return fmt.Errorf("invalid PULLET product warehouse") + } + + chickinRepoTx := repository.NewChickinRepository(dbTransaction) + productWarehouseTx := s.ProductWarehouseRepo.WithTx(dbTransaction) + ProjectFlockPopulationRepotx := s.ProjectflockPopulationRepo.WithTx(dbTransaction) + + for _, chickin := range chickins { + + quantityToConvert := chickin.PendingUsageQty + + if err := chickinRepoTx.PatchOne(ctx.Context(), chickin.Id, map[string]any{ + "usage_qty": quantityToConvert, + "pending_usage_qty": 0, + }, nil); err != nil { + return fmt.Errorf("failed to update chickin %d qty: %w", chickin.Id, err) + } + + // Update quantity di PULLET product warehouse dengan quantity dari chickin + if err := productWarehouseTx.PatchOne(ctx.Context(), pulletPW.Id, map[string]any{ + "quantity": gorm.Expr("quantity + ?", quantityToConvert), + }, nil); err != nil { + s.Log.Warnf("failed to update PULLET warehouse quantity: %v", err) + } + + populationExists, err := ProjectFlockPopulationRepotx.ExistsByProjectChickinID(ctx.Context(), chickin.Id) + if err != nil { + s.Log.Warnf("failed to check population existence for chickin %d: %v", chickin.Id, err) + continue + } + + if !populationExists { + population := &entity.ProjectFlockPopulation{ + ProjectChickinId: chickin.Id, + ProductWarehouseId: pulletPW.Id, + TotalQty: quantityToConvert, + TotalUsedQty: 0, + Notes: chickin.Notes, + CreatedBy: actorID, + } + if err := ProjectFlockPopulationRepotx.CreateOne(ctx.Context(), population, nil); err != nil { + lower := strings.ToLower(err.Error()) + if !(strings.Contains(lower, "duplicate") || strings.Contains(lower, "unique constraint") || strings.Contains(lower, "23505")) { + return err + } + s.Log.Infof("ignored duplicate population for chickin %d: %v", chickin.Id, err) + } + } else { + s.Log.Infof("population already exists for chickin %d", chickin.Id) + } + } + + return nil +} + func uniqueUintSlice(values []uint) []uint { seen := make(map[uint]struct{}, len(values)) result := make([]uint, 0, len(values)) diff --git a/internal/modules/production/project-flock-kandangs/controllers/project_flock_kandang.controller.go b/internal/modules/production/project-flock-kandangs/controllers/project_flock_kandang.controller.go new file mode 100644 index 00000000..92743035 --- /dev/null +++ b/internal/modules/production/project-flock-kandangs/controllers/project_flock_kandang.controller.go @@ -0,0 +1,76 @@ +package controller + +import ( + "math" + "strconv" + + "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project-flock-kandangs/dto" + service "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project-flock-kandangs/services" + validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project-flock-kandangs/validations" + "gitlab.com/mbugroup/lti-api.git/internal/response" + + "github.com/gofiber/fiber/v2" +) + +type ProjectFlockKandangController struct { + ProjectFlockKandangService service.ProjectFlockKandangService +} + +func NewProjectFlockKandangController(projectFlockKandangService service.ProjectFlockKandangService) *ProjectFlockKandangController { + return &ProjectFlockKandangController{ + ProjectFlockKandangService: projectFlockKandangService, + } +} + +func (u *ProjectFlockKandangController) GetAll(c *fiber.Ctx) error { + query := &validation.Query{ + Page: c.QueryInt("page", 1), + Limit: c.QueryInt("limit", 10), + Search: c.Query("search", ""), + } + + if query.Page < 1 || query.Limit < 1 { + return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0") + } + + result, totalResults, err := u.ProjectFlockKandangService.GetAll(c, query) + if err != nil { + return err + } + + return c.Status(fiber.StatusOK). + JSON(response.SuccessWithPaginate[dto.ProjectFlockKandangListDTO]{ + Code: fiber.StatusOK, + Status: "success", + Message: "Get all projectFlockKandangs successfully", + Meta: response.Meta{ + Page: query.Page, + Limit: query.Limit, + TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))), + TotalResults: totalResults, + }, + Data: dto.ToProjectFlockKandangListDTOs(result), + }) +} + +func (u *ProjectFlockKandangController) GetOne(c *fiber.Ctx) error { + param := c.Params("id") + + id, err := strconv.Atoi(param) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, "Invalid Id") + } + + result, availableQtys, err := u.ProjectFlockKandangService.GetOne(c, uint(id)) + if err != nil { + return err + } + + return c.Status(fiber.StatusOK). + JSON(response.Success{ + Code: fiber.StatusOK, + Status: "success", + Message: "Get projectFlockKandang successfully", + Data: dto.ToProjectFlockKandangListDTOWithAvailableQty(*result, availableQtys), + }) +} diff --git a/internal/modules/production/project-flock-kandangs/dto/project_flock_kandang.dto.go b/internal/modules/production/project-flock-kandangs/dto/project_flock_kandang.dto.go new file mode 100644 index 00000000..ac0678bd --- /dev/null +++ b/internal/modules/production/project-flock-kandangs/dto/project_flock_kandang.dto.go @@ -0,0 +1,353 @@ +package dto + +import ( + "time" + + entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + approvalDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals/dto" + 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" + chickinDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/dto" + projectFlockDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/dto" + userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto" +) + +type ProjectFlockKandangBaseDTO struct { + Id uint `json:"id"` +} + +type ProjectFlockCustomDTO struct { + Id uint `json:"id"` + Period int `json:"period"` + Flock *flockDTO.FlockBaseDTO `json:"flock,omitempty"` + Area *areaDTO.AreaBaseDTO `json:"area,omitempty"` + Category string `json:"category"` + Fcr *fcrDTO.FcrBaseDTO `json:"fcr,omitempty"` + Location *locationDTO.LocationBaseDTO `json:"location,omitempty"` + Kandangs []kandangDTO.KandangBaseDTO `json:"kandangs,omitempty"` + CreatedUser *userDTO.UserBaseDTO `json:"created_user,omitempty"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +type KandangCustomDTO struct { + Id uint `json:"id"` + Name string `json:"name"` + Status string `json:"status"` +} + +type ProductBaseDTO struct { + Id uint `json:"id"` + Name string `json:"name"` +} + +type WarehouseBaseDTO struct { + Id uint `json:"id"` + Name string `json:"name"` + Type string `json:"type"` +} + +type ProductWarehouseDTO struct { + Id uint `json:"id"` + Quantity float64 `json:"quantity"` + Product *ProductBaseDTO `json:"product,omitempty"` + Warehouse *WarehouseBaseDTO `json:"warehouse,omitempty"` +} + +type AvailableQtyDTO struct { + ProductWarehouse *ProductWarehouseDTO `json:"product_warehouse,omitempty"` +} + +type ProjectFlockKandangListDTO struct { + ProjectFlockKandangBaseDTO + ProjectFlock *ProjectFlockCustomDTO `json:"project_flock,omitempty"` + Kandang *KandangCustomDTO `json:"kandang,omitempty"` + Chickins []chickinDTO.ChickinBaseDTO `json:"chickins,omitempty"` + AvailableQtys []AvailableQtyDTO `json:"available_qtys,omitempty"` + CreatedUser *userDTO.UserBaseDTO `json:"created_user,omitempty"` + CreatedAt time.Time `json:"created_at"` + Approval *approvalDTO.ApprovalBaseDTO `json:"approval,omitempty"` +} + +type ProjectFlockKandangDetailDTO struct { + ProjectFlockKandangListDTO +} + +func ToProjectFlockKandangBaseDTO(e entity.ProjectFlockKandang) ProjectFlockKandangBaseDTO { + return ProjectFlockKandangBaseDTO{ + Id: e.Id, + } +} + +func toProjectFlockCustomDTO(pf *projectFlockDTO.ProjectFlockListDTO) *ProjectFlockCustomDTO { + if pf == nil { + return nil + } + + return &ProjectFlockCustomDTO{ + Id: pf.Id, + Period: pf.Period, + Flock: pf.Flock, + Area: pf.Area, + Category: pf.Category, + Fcr: pf.Fcr, + Location: pf.Location, + Kandangs: pf.Kandangs, + CreatedUser: pf.CreatedUser, + CreatedAt: pf.CreatedAt, + UpdatedAt: pf.UpdatedAt, + } +} + +func buildAvailableQtys(chickins []entity.ProjectChickin) []AvailableQtyDTO { + if len(chickins) == 0 { + return nil + } + + availableQtyMap := make(map[uint]AvailableQtyDTO) + for _, ch := range chickins { + if ch.ProductWarehouse == nil || ch.ProductWarehouse.Quantity <= 0 { + continue + } + + if _, exists := availableQtyMap[ch.ProductWarehouseId]; exists { + continue + } + + pwDTO := &ProductWarehouseDTO{ + Id: ch.ProductWarehouse.Id, + Quantity: ch.ProductWarehouse.Quantity, + } + + if ch.ProductWarehouse.Product.Id != 0 { + pwDTO.Product = &ProductBaseDTO{ + Id: ch.ProductWarehouse.Product.Id, + Name: ch.ProductWarehouse.Product.Name, + } + } + + if ch.ProductWarehouse.Warehouse.Id != 0 { + pwDTO.Warehouse = &WarehouseBaseDTO{ + Id: ch.ProductWarehouse.Warehouse.Id, + Name: ch.ProductWarehouse.Warehouse.Name, + Type: ch.ProductWarehouse.Warehouse.Type, + } + } + + availableQtyMap[ch.ProductWarehouseId] = AvailableQtyDTO{ + ProductWarehouse: pwDTO, + } + } + + if len(availableQtyMap) == 0 { + return nil + } + + result := make([]AvailableQtyDTO, 0, len(availableQtyMap)) + for _, v := range availableQtyMap { + result = append(result, v) + } + return result +} + +func buildChickins(chickins []entity.ProjectChickin) []chickinDTO.ChickinBaseDTO { + if len(chickins) == 0 { + return nil + } + + result := make([]chickinDTO.ChickinBaseDTO, len(chickins)) + for i, ch := range chickins { + result[i] = chickinDTO.ToChickinBaseDTO(ch) + } + return result +} + +func defaultProjectFlockKandangLatestApproval(e entity.ProjectFlockKandang) *approvalDTO.ApprovalBaseDTO { + if e.LatestApproval != nil { + result := approvalDTO.ToApprovalDTO(*e.LatestApproval) + return &result + } + return nil +} + +func ToProjectFlockKandangListDTO(e entity.ProjectFlockKandang) ProjectFlockKandangListDTO { + var projectFlockSummary *projectFlockDTO.ProjectFlockListDTO + if e.ProjectFlock.Id != 0 { + mapped := projectFlockDTO.ToProjectFlockListDTO(e.ProjectFlock) + projectFlockSummary = &mapped + } + + var kandangSummary *KandangCustomDTO + if e.Kandang.Id != 0 { + kandangSummary = &KandangCustomDTO{ + Id: e.Kandang.Id, + Name: e.Kandang.Name, + Status: e.Kandang.Status, + } + } + + var createdUser *userDTO.UserBaseDTO + if e.ProjectFlock.CreatedUser.Id != 0 { + mapped := userDTO.ToUserBaseDTO(e.ProjectFlock.CreatedUser) + createdUser = &mapped + } else if e.ProjectFlock.CreatedBy != 0 { + mapped := userDTO.UserBaseDTO{Id: e.ProjectFlock.CreatedBy, IdUser: int64(e.ProjectFlock.CreatedBy)} + createdUser = &mapped + } + + return ProjectFlockKandangListDTO{ + ProjectFlockKandangBaseDTO: ToProjectFlockKandangBaseDTO(e), + ProjectFlock: toProjectFlockCustomDTO(projectFlockSummary), + Kandang: kandangSummary, + Chickins: buildChickins(e.Chickins), + AvailableQtys: buildAvailableQtys(e.Chickins), + CreatedAt: e.CreatedAt, + CreatedUser: createdUser, + Approval: defaultProjectFlockKandangLatestApproval(e), + } +} + +func ToProjectFlockKandangListDTOs(e []entity.ProjectFlockKandang) []ProjectFlockKandangListDTO { + result := make([]ProjectFlockKandangListDTO, len(e)) + for i, r := range e { + result[i] = ToProjectFlockKandangListDTO(r) + } + return result +} + +func ToProjectFlockKandangDetailDTO(e entity.ProjectFlockKandang) ProjectFlockKandangDetailDTO { + return ProjectFlockKandangDetailDTO{ + ProjectFlockKandangListDTO: ToProjectFlockKandangListDTO(e), + } +} + +func ToProjectFlockKandangListDTOWithAvailableQty(e entity.ProjectFlockKandang, availableQtysRaw []map[string]interface{}) ProjectFlockKandangListDTO { + var projectFlockSummary *projectFlockDTO.ProjectFlockListDTO + if e.ProjectFlock.Id != 0 { + mapped := projectFlockDTO.ToProjectFlockListDTO(e.ProjectFlock) + projectFlockSummary = &mapped + } + + var kandangSummary *KandangCustomDTO + if e.Kandang.Id != 0 { + kandangSummary = &KandangCustomDTO{ + Id: e.Kandang.Id, + Name: e.Kandang.Name, + Status: e.Kandang.Status, + } + } + + var createdUser *userDTO.UserBaseDTO + if e.ProjectFlock.CreatedUser.Id != 0 { + mapped := userDTO.ToUserBaseDTO(e.ProjectFlock.CreatedUser) + createdUser = &mapped + } else if e.ProjectFlock.CreatedBy != 0 { + mapped := userDTO.UserBaseDTO{Id: e.ProjectFlock.CreatedBy, IdUser: int64(e.ProjectFlock.CreatedBy)} + createdUser = &mapped + } + + // convert available qtys from raw map data + var availableQtys []AvailableQtyDTO + if len(availableQtysRaw) > 0 { + availableQtys = buildAvailableQtysFromRaw(availableQtysRaw) + } + + return ProjectFlockKandangListDTO{ + ProjectFlockKandangBaseDTO: ToProjectFlockKandangBaseDTO(e), + ProjectFlock: toProjectFlockCustomDTO(projectFlockSummary), + Kandang: kandangSummary, + Chickins: buildChickins(e.Chickins), + AvailableQtys: availableQtys, + CreatedAt: e.CreatedAt, + CreatedUser: createdUser, + Approval: defaultProjectFlockKandangLatestApproval(e), + } +} + +func buildAvailableQtysFromRaw(availableQtysRaw []map[string]interface{}) []AvailableQtyDTO { + if len(availableQtysRaw) == 0 { + return nil + } + + result := make([]AvailableQtyDTO, len(availableQtysRaw)) + for i, v := range availableQtysRaw { + pwData, ok := v["product_warehouse"].(map[string]interface{}) + if !ok { + continue + } + + pwDTO := buildProductWarehouseFromMap(pwData) + result[i] = AvailableQtyDTO{ + ProductWarehouse: pwDTO, + } + } + return result +} + +func buildProductWarehouseFromMap(pwData map[string]interface{}) *ProductWarehouseDTO { + if pwData == nil { + return nil + } + + dto := &ProductWarehouseDTO{} + + if id, ok := pwData["id"].(float64); ok { + dto.Id = uint(id) + } else if id, ok := pwData["id"].(uint); ok { + dto.Id = id + } + + if qty, ok := pwData["quantity"].(float64); ok { + dto.Quantity = qty + } + + if pData, ok := pwData["product"].(map[string]interface{}); ok { + dto.Product = buildProductFromMap(pData) + } + + if wData, ok := pwData["warehouse"].(map[string]interface{}); ok { + dto.Warehouse = buildWarehouseFromMap(wData) + } + + return dto +} + +func buildProductFromMap(pData map[string]interface{}) *ProductBaseDTO { + if pData == nil { + return nil + } + + product := &ProductBaseDTO{} + if id, ok := pData["id"].(float64); ok { + product.Id = uint(id) + } else if id, ok := pData["id"].(uint); ok { + product.Id = id + } + if name, ok := pData["name"].(string); ok { + product.Name = name + } + return product +} + +func buildWarehouseFromMap(wData map[string]interface{}) *WarehouseBaseDTO { + if wData == nil { + return nil + } + + warehouse := &WarehouseBaseDTO{} + if id, ok := wData["id"].(float64); ok { + warehouse.Id = uint(id) + } else if id, ok := wData["id"].(uint); ok { + warehouse.Id = id + } + if name, ok := wData["name"].(string); ok { + warehouse.Name = name + } + if wType, ok := wData["type"].(string); ok { + warehouse.Type = wType + } + return warehouse +} diff --git a/internal/modules/production/project-flock-kandangs/module.go b/internal/modules/production/project-flock-kandangs/module.go new file mode 100644 index 00000000..2457f691 --- /dev/null +++ b/internal/modules/production/project-flock-kandangs/module.go @@ -0,0 +1,42 @@ +package project_flock_kandangs + +import ( + "fmt" + + "github.com/go-playground/validator/v10" + "github.com/gofiber/fiber/v2" + "gorm.io/gorm" + + sProjectFlockKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project-flock-kandangs/services" + rProjectFlockKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories" + + commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository" + commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service" + rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories" + rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories" + "gitlab.com/mbugroup/lti-api.git/internal/utils" + + rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories" + sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" +) + +type ProjectFlockKandangModule struct{} + +func (ProjectFlockKandangModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) { + projectFlockKandangRepo := rProjectFlockKandang.NewProjectFlockKandangRepository(db) + userRepo := rUser.NewUserRepository(db) + warehouseRepo := rWarehouse.NewWarehouseRepository(db) + productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db) + + approvalRepo := commonRepo.NewApprovalRepository(db) + approvalService := commonSvc.NewApprovalService(approvalRepo) + // register workflow steps for project flock kandang approvals + if err := approvalService.RegisterWorkflowSteps(utils.ApprovalWorkflowProjectFlockKandang, utils.ProjectFlockKandangApprovalSteps); err != nil { + panic(fmt.Sprintf("failed to register project flock kandang approval workflow: %v", err)) + } + + projectFlockKandangService := sProjectFlockKandang.NewProjectFlockKandangService(projectFlockKandangRepo, approvalService, warehouseRepo, productWarehouseRepo, validate) + userService := sUser.NewUserService(userRepo, validate) + + ProjectFlockKandangRoutes(router, userService, projectFlockKandangService) +} diff --git a/internal/modules/production/project-flock-kandangs/route.go b/internal/modules/production/project-flock-kandangs/route.go new file mode 100644 index 00000000..8057e847 --- /dev/null +++ b/internal/modules/production/project-flock-kandangs/route.go @@ -0,0 +1,26 @@ +package project_flock_kandangs + +import ( + // m "gitlab.com/mbugroup/lti-api.git/internal/middleware" + controller "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project-flock-kandangs/controllers" + projectFlockKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project-flock-kandangs/services" + user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" + + "github.com/gofiber/fiber/v2" +) + +func ProjectFlockKandangRoutes(v1 fiber.Router, u user.UserService, s projectFlockKandang.ProjectFlockKandangService) { + ctrl := controller.NewProjectFlockKandangController(s) + + route := v1.Group("/project-flock-kandangs") + + // route.Get("/", m.Auth(u), ctrl.GetAll) + // route.Post("/", m.Auth(u), ctrl.CreateOne) + // route.Get("/:id", m.Auth(u), ctrl.GetOne) + // route.Patch("/:id", m.Auth(u), ctrl.UpdateOne) + // route.Delete("/:id", m.Auth(u), ctrl.DeleteOne) + + route.Get("/", ctrl.GetAll) + route.Get("/:id", ctrl.GetOne) + +} diff --git a/internal/modules/production/project-flock-kandangs/services/project_flock_kandang.service.go b/internal/modules/production/project-flock-kandangs/services/project_flock_kandang.service.go new file mode 100644 index 00000000..d3f1f0b7 --- /dev/null +++ b/internal/modules/production/project-flock-kandangs/services/project_flock_kandang.service.go @@ -0,0 +1,147 @@ +package service + +import ( + "errors" + + commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service" + entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories" + rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories" + validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project-flock-kandangs/validations" + repository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories" + "gitlab.com/mbugroup/lti-api.git/internal/utils" + + "github.com/go-playground/validator/v10" + "github.com/gofiber/fiber/v2" + "github.com/sirupsen/logrus" + "gorm.io/gorm" +) + +type ProjectFlockKandangService interface { + GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.ProjectFlockKandang, int64, error) + GetOne(ctx *fiber.Ctx, id uint) (*entity.ProjectFlockKandang, []map[string]interface{}, error) +} + +type projectFlockKandangService struct { + Log *logrus.Logger + Validate *validator.Validate + Repository repository.ProjectFlockKandangRepository + ApprovalSvc commonSvc.ApprovalService + WarehouseRepo rWarehouse.WarehouseRepository + ProductWarehouseRepo rProductWarehouse.ProductWarehouseRepository +} + +func NewProjectFlockKandangService(repo repository.ProjectFlockKandangRepository, approvalSvc commonSvc.ApprovalService, warehouseRepo rWarehouse.WarehouseRepository, productWarehouseRepo rProductWarehouse.ProductWarehouseRepository, validate *validator.Validate) ProjectFlockKandangService { + return &projectFlockKandangService{ + Log: utils.Log, + Validate: validate, + Repository: repo, + ApprovalSvc: approvalSvc, + WarehouseRepo: warehouseRepo, + ProductWarehouseRepo: productWarehouseRepo, + } +} + +func (s projectFlockKandangService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.ProjectFlockKandang, int64, error) { + if err := s.Validate.Struct(params); err != nil { + return nil, 0, err + } + + projectFlockKandangs, err := s.Repository.GetAll(c.Context()) + if err != nil { + s.Log.Errorf("Failed to get projectFlockKandangs: %+v", err) + return nil, 0, err + } + total := int64(len(projectFlockKandangs)) + + return projectFlockKandangs, total, nil +} + +func (s projectFlockKandangService) GetOne(c *fiber.Ctx, id uint) (*entity.ProjectFlockKandang, []map[string]interface{}, error) { + projectFlockKandang, err := s.Repository.GetByID(c.Context(), id) + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, nil, fiber.NewError(fiber.StatusNotFound, "ProjectFlockKandang not found") + } + if err != nil { + s.Log.Errorf("Failed get projectFlockKandang by id: %+v", err) + return nil, nil, err + } + + if len(projectFlockKandang.Chickins) > 0 && s.ApprovalSvc != nil { + latest, err := s.ApprovalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowProjectFlockKandang, projectFlockKandang.Id, nil) + if err != nil { + s.Log.Errorf("Failed to fetch latest kandang approval for projectFlockKandang %d: %+v", projectFlockKandang.Id, err) + } + + if latest != nil { + projectFlockKandang.LatestApproval = latest + } + } + + availableQtys, err := s.getAvailableQuantities(c, projectFlockKandang) + if err != nil { + s.Log.Errorf("Failed to fetch available quantities for kandang %d: %+v", projectFlockKandang.Kandang.Id, err) + availableQtys = nil + } + + return projectFlockKandang, availableQtys, nil +} + +func (s projectFlockKandangService) getAvailableQuantities(c *fiber.Ctx, projectFlockKandang *entity.ProjectFlockKandang) ([]map[string]interface{}, error) { + if projectFlockKandang.Kandang.Id == 0 || s.WarehouseRepo == nil || s.ProductWarehouseRepo == nil { + return nil, nil + } + + warehouse, err := s.WarehouseRepo.GetByKandangID(c.Context(), projectFlockKandang.Kandang.Id) + if err != nil || warehouse == nil { + return nil, nil + } + + var productCategoryCode string + if projectFlockKandang.ProjectFlock.Category == string(utils.ProjectFlockCategoryGrowing) { + productCategoryCode = "DOC" + } else if projectFlockKandang.ProjectFlock.Category == string(utils.ProjectFlockCategoryLaying) { + productCategoryCode = "PULLET" + } else { + + return nil, nil + } + + products, err := s.ProductWarehouseRepo.GetByCategoryCodeAndWarehouseID(c.Context(), productCategoryCode, warehouse.Id) + if err != nil || len(products) == 0 { + return nil, nil + } + + var result []map[string]interface{} + for _, pw := range products { + if pw.Quantity > 0 { + // Build product data + productData := map[string]interface{}{ + "id": pw.Product.Id, + "name": pw.Product.Name, + } + + // Build warehouse data + warehouseData := map[string]interface{}{ + "id": pw.Warehouse.Id, + "name": pw.Warehouse.Name, + "type": pw.Warehouse.Type, + } + + // Build product warehouse data with nested product and warehouse + productWarehouseData := map[string]interface{}{ + "id": pw.Id, + "quantity": pw.Quantity, + "product": productData, + "warehouse": warehouseData, + } + + result = append(result, map[string]interface{}{ + "available_qty": pw.Quantity, + "product_warehouse": productWarehouseData, + }) + } + } + + return result, nil +} diff --git a/internal/modules/production/project-flock-kandangs/validations/project_flock_kandang.validation.go b/internal/modules/production/project-flock-kandangs/validations/project_flock_kandang.validation.go new file mode 100644 index 00000000..1e95fd2c --- /dev/null +++ b/internal/modules/production/project-flock-kandangs/validations/project_flock_kandang.validation.go @@ -0,0 +1,17 @@ +package validation + +type Create struct { + ProjectFlockId uint `json:"project_flock_id" validate:"required"` + KandangId uint `json:"kandang_id" validate:"required"` +} + +type Update struct { + ProjectFlockId *uint `json:"project_flock_id,omitempty" validate:"omitempty"` + KandangId *uint `json:"kandang_id,omitempty" validate:"omitempty"` +} + +type Query struct { + Page int `query:"page" validate:"omitempty,number,min=1,gt=0"` + Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"` + Search string `query:"search" validate:"omitempty,max=50"` +} diff --git a/internal/modules/production/project_flocks/repositories/project_flock_population_repository.go b/internal/modules/production/project_flocks/repositories/project_flock_population_repository.go index 3328d286..fdb1249f 100644 --- a/internal/modules/production/project_flocks/repositories/project_flock_population_repository.go +++ b/internal/modules/production/project_flocks/repositories/project_flock_population_repository.go @@ -11,6 +11,7 @@ import ( type ProjectFlockPopulationRepository interface { // domain-specific GetByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (*entity.ProjectFlockPopulation, error) + ExistsByProjectChickinID(ctx context.Context, projectChickinID uint) (bool, error) // subset of base repository methods used by services CreateOne(ctx context.Context, entity *entity.ProjectFlockPopulation, modifier func(*gorm.DB) *gorm.DB) error @@ -51,3 +52,15 @@ func (r *projectFlockPopulationRepositoryImpl) GetByProjectFlockKandangID(ctx co } return &record, nil } + +func (r *projectFlockPopulationRepositoryImpl) ExistsByProjectChickinID(ctx context.Context, projectChickinID uint) (bool, error) { + var count int64 + err := r.DB().WithContext(ctx). + Where("project_chickin_id = ?", projectChickinID). + Model(&entity.ProjectFlockPopulation{}). + Count(&count).Error + if err != nil { + return false, err + } + return count > 0, nil +} diff --git a/internal/modules/production/project_flocks/repositories/projectflock_kandang.repository.go b/internal/modules/production/project_flocks/repositories/projectflock_kandang.repository.go index 7205b72a..dba1489b 100644 --- a/internal/modules/production/project_flocks/repositories/projectflock_kandang.repository.go +++ b/internal/modules/production/project_flocks/repositories/projectflock_kandang.repository.go @@ -55,6 +55,9 @@ func (r *projectFlockKandangRepositoryImpl) GetAll(ctx context.Context) ([]entit Preload("ProjectFlock.Kandangs"). Preload("ProjectFlock.KandangHistory"). Preload("Kandang"). + Preload("Chickins"). + Preload("Chickins.CreatedUser"). + Preload("Chickins.ProductWarehouse"). Order("project_flock_id ASC, created_at ASC"). Find(&records).Error; err != nil { return nil, err @@ -85,6 +88,9 @@ func (r *projectFlockKandangRepositoryImpl) GetByID(ctx context.Context, id uint Preload("ProjectFlock.Kandangs"). Preload("ProjectFlock.KandangHistory"). Preload("Kandang"). + Preload("Chickins"). + Preload("Chickins.CreatedUser"). + Preload("Chickins.ProductWarehouse"). First(record, id).Error; err != nil { return nil, err } @@ -104,6 +110,9 @@ func (r *projectFlockKandangRepositoryImpl) GetByProjectFlockAndKandang(ctx cont Preload("ProjectFlock.Kandangs"). Preload("ProjectFlock.KandangHistory"). Preload("Kandang"). + Preload("Chickins"). + Preload("Chickins.CreatedUser"). + Preload("Chickins.ProductWarehouse"). First(record).Error; err != nil { return nil, err } diff --git a/internal/modules/production/route.go b/internal/modules/production/route.go index 10effb58..d1425b7c 100644 --- a/internal/modules/production/route.go +++ b/internal/modules/production/route.go @@ -11,6 +11,7 @@ import ( projectflocks "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks" recordings "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings" transferLayings "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings" + projectFlockKandangs "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project-flock-kandangs" // MODULE IMPORTS ) @@ -22,6 +23,7 @@ func RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Valida recordings.RecordingModule{}, chickins.ChickinModule{}, transferLayings.TransferLayingModule{}, + projectFlockKandangs.ProjectFlockKandangModule{}, // MODULE REGISTRY } diff --git a/internal/utils/constant.go b/internal/utils/constant.go index 8328f4d7..c7bcc99e 100644 --- a/internal/utils/constant.go +++ b/internal/utils/constant.go @@ -18,6 +18,7 @@ const ( FlagIsActive FlagType = "IS_ACTIVE" FlagDOC FlagType = "DOC" + FlagPullet FlagType = "PULLET" FlagPakan FlagType = "PAKAN" FlagPreStarter FlagType = "PRE-STARTER" FlagStarter FlagType = "STARTER" @@ -37,6 +38,7 @@ const ( var flagGroupOptions = map[FlagGroup][]FlagType{ FlagGroupProduct: { FlagDOC, + FlagPullet, FlagPakan, FlagPreStarter, FlagStarter, From 86f37a89c1f11f92b9a2328be203b02635295388 Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Mon, 3 Nov 2025 09:16:29 +0700 Subject: [PATCH 07/12] Feat[BE]: add multilpple type of chickin growing and laying, make convertion product when chickin approved, add projectflockkandangid on projectflock api --- internal/database/seed/seeder.go | 11 + .../controllers/chickin.controller.go | 189 ++++++----- .../production/chickins/dto/chickin.dto.go | 64 +++- internal/modules/production/chickins/route.go | 6 +- .../chickins/services/chickin.service.go | 196 ++++++----- .../dto/project_flock_kandang.dto.go | 313 +++++++++--------- .../project_flocks/dto/projectflock.dto.go | 46 ++- .../services/projectflock.service.go | 4 +- internal/utils/constant.go | 2 + 9 files changed, 485 insertions(+), 346 deletions(-) diff --git a/internal/database/seed/seeder.go b/internal/database/seed/seeder.go index 3b7378c4..93c75b29 100644 --- a/internal/database/seed/seeder.go +++ b/internal/database/seed/seeder.go @@ -574,6 +574,7 @@ func seedProductCategories(tx *gorm.DB, createdBy uint) (map[string]uint, error) {"Bahan Baku", "RAW"}, {"Day Old Chick", "DOC"}, {"Pullet", "PULLET"}, + {"Layer", "LAYER"}, } result := make(map[string]uint, len(seeds)) @@ -808,6 +809,16 @@ func seedProducts(tx *gorm.DB, createdBy uint, uoms map[string]uint, categories Suppliers: []string{"PT CHAROEN POKPHAND INDONESIA Tbk"}, Flags: []utils.FlagType{utils.FlagPullet}, }, + { + Name: "Ayam Layer", + Brand: "MBU Layer", + Sku: "LAY0001", + Uom: "Ekor", + Category: "Layer", + Price: 20000, + Suppliers: []string{"PT CHAROEN POKPHAND INDONESIA Tbk"}, + Flags: []utils.FlagType{utils.FlagLayer}, + }, } for _, seed := range seeds { diff --git a/internal/modules/production/chickins/controllers/chickin.controller.go b/internal/modules/production/chickins/controllers/chickin.controller.go index c132b279..7f8e0d5b 100644 --- a/internal/modules/production/chickins/controllers/chickin.controller.go +++ b/internal/modules/production/chickins/controllers/chickin.controller.go @@ -1,7 +1,6 @@ package controller import ( - "math" "strconv" "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/dto" @@ -22,30 +21,84 @@ func NewChickinController(chickinService service.ChickinService) *ChickinControl } } -func (u *ChickinController) GetAll(c *fiber.Ctx) error { - query := &validation.Query{ - Page: c.QueryInt("page", 1), - Limit: c.QueryInt("limit", 10), - ProjectFlockKandangId: uint(c.QueryInt("project_flock_kandang_id", 0)), +// func (u *ChickinController) GetAll(c *fiber.Ctx) error { +// query := &validation.Query{ +// Page: c.QueryInt("page", 1), +// Limit: c.QueryInt("limit", 10), +// ProjectFlockKandangId: uint(c.QueryInt("project_flock_kandang_id", 0)), +// } + +// result, totalResults, err := u.ChickinService.GetAll(c, query) +// if err != nil { +// return err +// } + +// return c.Status(fiber.StatusOK). +// JSON(response.SuccessWithPaginate[dto.ChickinListDTO]{ +// Code: fiber.StatusOK, +// Status: "success", +// Message: "Get all chickins successfully", +// Meta: response.Meta{ +// Page: query.Page, +// Limit: query.Limit, +// TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))), +// TotalResults: totalResults, +// }, +// Data: dto.ToChickinListDTOs(result), +// }) +// } + +// func (u *ChickinController) GetOne(c *fiber.Ctx) error { +// param := c.Params("id") + +// id, err := strconv.Atoi(param) +// if err != nil { +// return fiber.NewError(fiber.StatusBadRequest, "Invalid Id") +// } + +// result, err := u.ChickinService.GetOne(c, uint(id)) +// if err != nil { +// return err +// } + +// return c.Status(fiber.StatusOK). +// JSON(response.Success{ +// Code: fiber.StatusOK, +// Status: "success", +// Message: "Get chickin successfully", +// Data: dto.ToChickinListDTO(*result), +// }) +// } + +func (u *ChickinController) CreateOne(c *fiber.Ctx) error { + req := new(validation.Create) + + if err := c.BodyParser(req); err != nil { + return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") } - result, totalResults, err := u.ChickinService.GetAll(c, query) + results, err := u.ChickinService.CreateOne(c, req) if err != nil { return err } - return c.Status(fiber.StatusOK). - JSON(response.SuccessWithPaginate[dto.ChickinListDTO]{ - Code: fiber.StatusOK, + var ( + data interface{} + message = "Create chickin successfully" + ) + if len(results) == 1 { + data = dto.ToChickinListDTO(results[0]) + } else { + message = "Create chickins successfully" + data = dto.ToChickinListDTOs(results) + } + + return c.Status(fiber.StatusCreated). + JSON(response.Success{ + Code: fiber.StatusCreated, Status: "success", - Message: "Get all chickins successfully", - Meta: response.Meta{ - Page: query.Page, - Limit: query.Limit, - TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))), - TotalResults: totalResults, - }, - Data: dto.ToChickinListDTOs(result), + Message: message, + Data: data, }) } @@ -67,80 +120,60 @@ func (u *ChickinController) GetOne(c *fiber.Ctx) error { Code: fiber.StatusOK, Status: "success", Message: "Get chickin successfully", - Data: dto.ToChickinListDTO(*result), + Data: dto.ToChickinDetailDTO(*result), }) } -func (u *ChickinController) CreateOne(c *fiber.Ctx) error { - req := new(validation.Create) +// func (u *ChickinController) UpdateOne(c *fiber.Ctx) error { +// req := new(validation.Update) +// param := c.Params("id") - if err := c.BodyParser(req); err != nil { - return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") - } +// id, err := strconv.Atoi(param) +// if err != nil { +// return fiber.NewError(fiber.StatusBadRequest, "Invalid Id") +// } - result, err := u.ChickinService.CreateOne(c, req) - if err != nil { - return err - } +// if err := c.BodyParser(req); err != nil { +// return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") +// } - return c.Status(fiber.StatusCreated). - JSON(response.Success{ - Code: fiber.StatusCreated, - Status: "success", - Message: "Create chickin successfully", - Data: dto.ToChickinListDTO(*result), - }) -} +// result, err := u.ChickinService.UpdateOne(c, req, uint(id)) +// if err != nil { +// return err +// } -func (u *ChickinController) UpdateOne(c *fiber.Ctx) error { - req := new(validation.Update) - param := c.Params("id") +// return c.Status(fiber.StatusOK). +// JSON(response.Success{ +// Code: fiber.StatusOK, +// Status: "success", +// Message: "Update chickin successfully", +// Data: dto.ToChickinListDTO(*result), +// }) +// } - id, err := strconv.Atoi(param) - if err != nil { - return fiber.NewError(fiber.StatusBadRequest, "Invalid Id") - } +// func (u *ChickinController) DeleteOne(c *fiber.Ctx) error { +// param := c.Params("id") - if err := c.BodyParser(req); err != nil { - return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") - } +// id, err := strconv.Atoi(param) +// if err != nil { +// return fiber.NewError(fiber.StatusBadRequest, "Invalid Id") +// } - result, err := u.ChickinService.UpdateOne(c, req, uint(id)) - if err != nil { - return err - } +// if err := u.ChickinService.DeleteOne(c, uint(id)); err != nil { +// return err +// } - return c.Status(fiber.StatusOK). - JSON(response.Success{ - Code: fiber.StatusOK, - Status: "success", - Message: "Update chickin successfully", - Data: dto.ToChickinListDTO(*result), - }) -} - -func (u *ChickinController) DeleteOne(c *fiber.Ctx) error { - param := c.Params("id") - - id, err := strconv.Atoi(param) - if err != nil { - return fiber.NewError(fiber.StatusBadRequest, "Invalid Id") - } - - if err := u.ChickinService.DeleteOne(c, uint(id)); err != nil { - return err - } - - return c.Status(fiber.StatusOK). - JSON(response.Common{ - Code: fiber.StatusOK, - Status: "success", - Message: "Delete chickin successfully", - }) -} +// return c.Status(fiber.StatusOK). +// JSON(response.Common{ +// Code: fiber.StatusOK, +// Status: "success", +// Message: "Delete chickin successfully", +// }) +// } func (u *ChickinController) Approval(c *fiber.Ctx) error { req := new(validation.Approve) + if err := c.BodyParser(req); err != nil { return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") } diff --git a/internal/modules/production/chickins/dto/chickin.dto.go b/internal/modules/production/chickins/dto/chickin.dto.go index 1f35c5a8..44373b11 100644 --- a/internal/modules/production/chickins/dto/chickin.dto.go +++ b/internal/modules/production/chickins/dto/chickin.dto.go @@ -15,13 +15,13 @@ import ( // === DTO Structs (ordered) === type ChickinBaseDTO struct { - Id uint `json:"id"` - ProjectFlockKandangId uint `json:"project_flock_kandang_id"` - ChickInDate time.Time `json:"chick_in_date"` - ProductWarehouseId uint `json:"product_warehouse_id"` - UsageQty float64 `json:"usage_qty"` - PendingUsageQty float64 `json:"pending_usage_qty"` - Notes string `json:"notes"` + Id uint `json:"id"` + ProjectFlockKandangId uint `json:"project_flock_kandang_id"` + ChickInDate time.Time `json:"chick_in_date"` + ProductWarehouseId uint `json:"product_warehouse_id"` + UsageQty float64 `json:"usage_qty"` + PendingUsageQty float64 `json:"pending_usage_qty"` + Notes string `json:"notes"` } type ProjectFlockDTO struct { @@ -61,7 +61,17 @@ type ChickinListDTO struct { } type ChickinDetailDTO struct { - ChickinListDTO + Id uint `json:"id"` + ProjectFlockKandangId uint `json:"project_flock_kandang_id"` + ChickInDate time.Time `json:"chick_in_date"` + ProductWarehouseId uint `json:"product_warehouse_id"` + UsageQty float64 `json:"usage_qty"` + PendingUsageQty float64 `json:"pending_usage_qty"` + Notes string `json:"notes"` + CreatedBy uint `json:"created_by"` + CreatedUser *userBaseDTO.UserBaseDTO `json:"created_user"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` } // === Mapper Functions (ordered) === @@ -149,13 +159,13 @@ func ToChickinBaseDTO(e entity.ProjectChickin) ChickinBaseDTO { projectFlockKandangId = e.ProjectFlockKandangId } return ChickinBaseDTO{ - Id: e.Id, + Id: e.Id, ProjectFlockKandangId: projectFlockKandangId, - ChickInDate: e.ChickInDate, - ProductWarehouseId: e.ProductWarehouseId, - UsageQty: e.UsageQty, - PendingUsageQty: e.PendingUsageQty, - Notes: e.Notes, + ChickInDate: e.ChickInDate, + ProductWarehouseId: e.ProductWarehouseId, + UsageQty: e.UsageQty, + PendingUsageQty: e.PendingUsageQty, + Notes: e.Notes, } } @@ -203,7 +213,31 @@ func ToChickinSimpleDTOs(e []entity.ProjectChickin) []ChickinSimpleDTO { } func ToChickinDetailDTO(e entity.ProjectChickin) ChickinDetailDTO { + var createdUser *userBaseDTO.UserBaseDTO + if e.CreatedUser != nil && e.CreatedUser.Id != 0 { + mapped := userBaseDTO.ToUserBaseDTO(*e.CreatedUser) + createdUser = &mapped + } + return ChickinDetailDTO{ - ChickinListDTO: ToChickinListDTO(e), + Id: e.Id, + ProjectFlockKandangId: e.ProjectFlockKandangId, + ChickInDate: e.ChickInDate, + ProductWarehouseId: e.ProductWarehouseId, + UsageQty: e.UsageQty, + PendingUsageQty: e.PendingUsageQty, + Notes: e.Notes, + CreatedBy: e.CreatedBy, + CreatedUser: createdUser, + CreatedAt: e.CreatedAt, + UpdatedAt: e.UpdatedAt, } } + +func ToChickinDetailDTOs(e []entity.ProjectChickin) []ChickinDetailDTO { + result := make([]ChickinDetailDTO, len(e)) + for i, r := range e { + result[i] = ToChickinDetailDTO(r) + } + return result +} diff --git a/internal/modules/production/chickins/route.go b/internal/modules/production/chickins/route.go index 0bb5e93d..e2b64846 100644 --- a/internal/modules/production/chickins/route.go +++ b/internal/modules/production/chickins/route.go @@ -20,10 +20,10 @@ func ChickinRoutes(v1 fiber.Router, u user.UserService, s chickin.ChickinService // route.Patch("/:id", m.Auth(u), ctrl.UpdateOne) // route.Delete("/:id", m.Auth(u), ctrl.DeleteOne) - route.Get("/", ctrl.GetAll) + // route.Get("/", ctrl.GetAll) route.Post("/", ctrl.CreateOne) route.Get("/:id", ctrl.GetOne) - route.Patch("/:id", ctrl.UpdateOne) - route.Delete("/:id", ctrl.DeleteOne) + // route.Patch("/:id", ctrl.UpdateOne) + // route.Delete("/:id", ctrl.DeleteOne) route.Post("/approvals", ctrl.Approval) } diff --git a/internal/modules/production/chickins/services/chickin.service.go b/internal/modules/production/chickins/services/chickin.service.go index 3074b36a..243bdf52 100644 --- a/internal/modules/production/chickins/services/chickin.service.go +++ b/internal/modules/production/chickins/services/chickin.service.go @@ -25,7 +25,7 @@ import ( type ChickinService interface { GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.ProjectChickin, int64, error) GetOne(ctx *fiber.Ctx, id uint) (*entity.ProjectChickin, error) - CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.ProjectChickin, error) + CreateOne(ctx *fiber.Ctx, req *validation.Create) ([]entity.ProjectChickin, error) UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.ProjectChickin, error) DeleteOne(ctx *fiber.Ctx, id uint) error Approval(ctx *fiber.Ctx, req *validation.Approve) ([]entity.ProjectChickin, error) @@ -109,7 +109,7 @@ func (s chickinService) GetOne(c *fiber.Ctx, id uint) (*entity.ProjectChickin, e return chickin, nil } -func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.ProjectChickin, error) { +func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) ([]entity.ProjectChickin, error) { if err := s.Validate.Struct(req); err != nil { return nil, err } @@ -150,6 +150,11 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entit actorID := uint(1) // todo nanti ambil dari auth context newChikins := make([]*entity.ProjectChickin, 0) for _, productWarehouse := range productWarehouses { + + if productWarehouse.Quantity <= 0 { + continue + } + newChickin := &entity.ProjectChickin{ ProjectFlockKandangId: req.ProjectFlockKandangId, ChickInDate: chickinDate, @@ -167,6 +172,12 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entit return nil, fiber.NewError(fiber.StatusBadRequest, "No chickins to create") } + existingChikins, err := s.Repository.GetByProjectFlockKandangID(c.Context(), req.ProjectFlockKandangId) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check existing chickins") + } + isFirstTime := len(existingChikins) == 0 + err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction)) @@ -187,16 +198,27 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entit if err := productWarehouseTx.PatchOne(c.Context(), chickin.ProductWarehouseId, updates, nil); err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { - return fmt.Errorf("failed to update product warehouse quantity for id %d", chickin.ProductWarehouseId) + return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Product warehouse %d not found", chickin.ProductWarehouseId)) } - return err + return fiber.NewError(fiber.StatusInternalServerError, "Failed to update product warehouse quantity") } } + var approvalAction entity.ApprovalAction + if isFirstTime { + approvalAction = entity.ApprovalActionCreated + } else { + approvalAction = entity.ApprovalActionUpdated + } if latest == nil { - - action := entity.ApprovalActionCreated - if _, err := approvalSvcTx.CreateApproval(c.Context(), utils.ApprovalWorkflowProjectFlockKandang, projectFlockKandang.Id, utils.ProjectFlockKandangStepPengajuan, &action, actorID, nil); err != nil { + if _, err := approvalSvcTx.CreateApproval(c.Context(), utils.ApprovalWorkflowProjectFlockKandang, projectFlockKandang.Id, utils.ProjectFlockKandangStepPengajuan, &approvalAction, actorID, nil); err != nil { + lower := strings.ToLower(err.Error()) + if !(strings.Contains(lower, "duplicate") || strings.Contains(lower, "unique constraint") || strings.Contains(lower, "23505")) { + return err + } + } + } else if latest.StepNumber != uint16(utils.ProjectFlockKandangStepPengajuan) { + if _, err := approvalSvcTx.CreateApproval(c.Context(), utils.ApprovalWorkflowProjectFlockKandang, projectFlockKandang.Id, utils.ProjectFlockKandangStepPengajuan, &approvalAction, actorID, nil); err != nil { lower := strings.ToLower(err.Error()) if !(strings.Contains(lower, "duplicate") || strings.Contains(lower, "unique constraint") || strings.Contains(lower, "23505")) { return err @@ -210,7 +232,19 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entit return nil, fiber.NewError(fiber.StatusBadRequest, err.Error()) } - return newChikins[0], nil + result := make([]entity.ProjectChickin, 0, len(newChikins)) + for _, chickin := range newChikins { + loaded, err := s.GetOne(c, chickin.Id) + if err != nil { + return nil, fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to reload chickin %d with relations: %v", chickin.Id, err)) + } + result = append(result, *loaded) + } + if len(result) == 0 { + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to load created chickins") + } + + return result, nil } func (s chickinService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.ProjectChickin, error) { @@ -257,7 +291,8 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit if err := s.Validate.Struct(req); err != nil { return nil, err } - actorID := uint(1) // todo nanti ambil dari auth context + + approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(s.Repository.DB())) var action entity.ApprovalAction switch strings.ToUpper(strings.TrimSpace(req.Action)) { @@ -269,21 +304,19 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit return nil, fiber.NewError(fiber.StatusBadRequest, "action must be APPROVED or REJECTED") } - approvableIDs := uniqueUintSlice(req.ApprovableIds) + approvableIDs := utils.UniqueUintSlice(req.ApprovableIds) if len(approvableIDs) == 0 { return nil, fiber.NewError(fiber.StatusBadRequest, "approvable_ids must contain at least one id") } - // Validate all ProjectFlockKandang IDs exist and have valid approval status - approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(s.Repository.DB())) for _, id := range approvableIDs { idCopy := id if err := commonSvc.EnsureRelations(c.Context(), commonSvc.RelationCheck{Name: "ProjectFlockKandang", ID: &idCopy, Exists: s.ProjectflockKandangRepo.IdExists}); err != nil { return nil, err } - // Check latest approval status - must be PENGAJUAN to be approvable latestApproval, err := approvalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowProjectFlockKandang, id, nil) + if err != nil { return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check approval status") } @@ -308,6 +341,7 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit for _, approvableID := range approvableIDs { + actorID := uint(1) // todo nanti ambil dari auth context if _, err := approvalSvc.CreateApproval( c.Context(), utils.ApprovalWorkflowProjectFlockKandang, @@ -348,21 +382,34 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit return err } - pulletPW, err := s.getOrCreatePulletProductWarehouse(c, warehouse.Id, dbTransaction, actorID) - if err != nil { + category := strings.ToUpper(strings.TrimSpace(kandangForApproval.ProjectFlock.Category)) + var conversionCategoryCode string - continue + switch category { + case string(utils.ProjectFlockCategoryGrowing): + conversionCategoryCode = "PULLET" + case string(utils.ProjectFlockCategoryLaying): + conversionCategoryCode = "LAYER" + default: + return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Unknown category for conversion: %s", category)) } - if err := s.convertChickinsToPullet(c, chickins, pulletPW, dbTransaction, actorID); err != nil { + targetPW, err := s.getOrCreateProductWarehouse(c, warehouse.Id, conversionCategoryCode, dbTransaction, actorID) + if err != nil { + return fmt.Errorf("failed to get/create %s product warehouse: %w", conversionCategoryCode, err) + } + if err := s.convertChickinsToTarget(c, chickins, targetPW, dbTransaction, actorID); err != nil { return err } } else if action == entity.ApprovalActionRejected { chickins, err := chickinRepoTx.GetByProjectFlockKandangID(c.Context(), approvableID) - if err != nil { - s.Log.Warnf("failed to get chickins for rejection %d: %v", approvableID, err) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return fmt.Errorf("failed to get chickins for rejection %d: %w", approvableID, err) + } + + if len(chickins) == 0 { continue } @@ -371,12 +418,21 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit updates := map[string]any{"quantity": chickin.PendingUsageQty} if err := productWarehouseTx.PatchOne(c.Context(), chickin.ProductWarehouseId, updates, nil); err != nil { - s.Log.Warnf("failed to restore product warehouse quantity for rejection: %v", err) + if errors.Is(err, gorm.ErrRecordNotFound) { + return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Product warehouse %d not found during rejection", chickin.ProductWarehouseId)) + } + return fmt.Errorf("failed to restore product warehouse quantity for chickin %d: %w", chickin.Id, err) + } + + if err := chickinRepoTx.DeleteOne(c.Context(), chickin.Id); err != nil { + if !errors.Is(err, gorm.ErrRecordNotFound) { + return fmt.Errorf("failed to delete rejected chickin %d: %w", chickin.Id, err) + } + s.Log.Infof("chickin %d already deleted during rejection", chickin.Id) } } } } - return nil }) @@ -387,7 +443,6 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit if errors.Is(err, gorm.ErrRecordNotFound) { return nil, fiber.NewError(fiber.StatusNotFound, "Chickin not found") } - s.Log.Errorf("Failed to record approval for chickins %+v: %+v", approvableIDs, err) return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to record approval") } @@ -403,39 +458,39 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit return updated, nil } -func (s *chickinService) getOrCreatePulletProductWarehouse(ctx *fiber.Ctx, warehouseId uint, dbTransaction *gorm.DB, actorID uint) (*entity.ProductWarehouse, error) { +func (s *chickinService) getOrCreateProductWarehouse(ctx *fiber.Ctx, warehouseId uint, categoryCode string, dbTransaction *gorm.DB, actorID uint) (*entity.ProductWarehouse, error) { - pulletProducts, err := s.ProductWarehouseRepo.GetByCategoryCodeAndWarehouseID(ctx.Context(), "PULLET", warehouseId) - if err == nil && len(pulletProducts) > 0 { - return &pulletProducts[0], nil + products, err := s.ProductWarehouseRepo.GetByCategoryCodeAndWarehouseID(ctx.Context(), categoryCode, warehouseId) + if err == nil && len(products) > 0 { + return &products[0], nil } - pulletProduct, err := s.ProductWarehouseRepo.GetFirstProductByCategoryCode(ctx.Context(), "PULLET") + product, err := s.ProductWarehouseRepo.GetFirstProductByCategoryCode(ctx.Context(), categoryCode) if err != nil { - return nil, fmt.Errorf("failed to get PULLET product: %w", err) + return nil, fmt.Errorf("failed to get %s product: %w", categoryCode, err) } - if pulletProduct == nil { - return nil, fmt.Errorf("no PULLET product found in system") + if product == nil { + return nil, fmt.Errorf("no %s product found in system", categoryCode) } - newPulletPW := &entity.ProductWarehouse{ - ProductId: pulletProduct.Id, + newPW := &entity.ProductWarehouse{ + ProductId: product.Id, WarehouseId: warehouseId, Quantity: 0, CreatedBy: actorID, } - if err := s.ProductWarehouseRepo.WithTx(dbTransaction).CreateOne(ctx.Context(), newPulletPW, nil); err != nil { - return nil, fmt.Errorf("failed to create PULLET product warehouse: %w", err) + if err := s.ProductWarehouseRepo.WithTx(dbTransaction).CreateOne(ctx.Context(), newPW, nil); err != nil { + return nil, fmt.Errorf("failed to create %s product warehouse: %w", categoryCode, err) } - return newPulletPW, nil + return newPW, nil } -func (s *chickinService) convertChickinsToPullet(ctx *fiber.Ctx, chickins []entity.ProjectChickin, pulletPW *entity.ProductWarehouse, dbTransaction *gorm.DB, actorID uint) error { +func (s *chickinService) convertChickinsToTarget(ctx *fiber.Ctx, chickins []entity.ProjectChickin, targetPW *entity.ProductWarehouse, dbTransaction *gorm.DB, actorID uint) error { - if pulletPW == nil || pulletPW.Id == 0 { - return fmt.Errorf("invalid PULLET product warehouse") + if targetPW == nil || targetPW.Id == 0 { + return fmt.Errorf("invalid target product warehouse") } chickinRepoTx := repository.NewChickinRepository(dbTransaction) @@ -444,6 +499,16 @@ func (s *chickinService) convertChickinsToPullet(ctx *fiber.Ctx, chickins []enti for _, chickin := range chickins { + populationExists, err := ProjectFlockPopulationRepotx.ExistsByProjectChickinID(ctx.Context(), chickin.Id) + if err != nil { + return fmt.Errorf("failed to check population existence for chickin %d: %w", chickin.Id, err) + } + + if populationExists { + s.Log.Infof("population already exists for chickin %d, skipping", chickin.Id) + continue + } + quantityToConvert := chickin.PendingUsageQty if err := chickinRepoTx.PatchOne(ctx.Context(), chickin.Id, map[string]any{ @@ -453,52 +518,31 @@ func (s *chickinService) convertChickinsToPullet(ctx *fiber.Ctx, chickins []enti return fmt.Errorf("failed to update chickin %d qty: %w", chickin.Id, err) } - // Update quantity di PULLET product warehouse dengan quantity dari chickin - if err := productWarehouseTx.PatchOne(ctx.Context(), pulletPW.Id, map[string]any{ + if err := productWarehouseTx.PatchOne(ctx.Context(), targetPW.Id, map[string]any{ "quantity": gorm.Expr("quantity + ?", quantityToConvert), }, nil); err != nil { - s.Log.Warnf("failed to update PULLET warehouse quantity: %v", err) + if errors.Is(err, gorm.ErrRecordNotFound) { + return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Target product warehouse %d not found", targetPW.Id)) + } + return fmt.Errorf("failed to update target warehouse quantity: %w", err) } - populationExists, err := ProjectFlockPopulationRepotx.ExistsByProjectChickinID(ctx.Context(), chickin.Id) - if err != nil { - s.Log.Warnf("failed to check population existence for chickin %d: %v", chickin.Id, err) - continue + population := &entity.ProjectFlockPopulation{ + ProjectChickinId: chickin.Id, + ProductWarehouseId: targetPW.Id, + TotalQty: quantityToConvert, + TotalUsedQty: 0, + Notes: chickin.Notes, + CreatedBy: actorID, } - - if !populationExists { - population := &entity.ProjectFlockPopulation{ - ProjectChickinId: chickin.Id, - ProductWarehouseId: pulletPW.Id, - TotalQty: quantityToConvert, - TotalUsedQty: 0, - Notes: chickin.Notes, - CreatedBy: actorID, + if err := ProjectFlockPopulationRepotx.CreateOne(ctx.Context(), population, nil); err != nil { + lower := strings.ToLower(err.Error()) + if !(strings.Contains(lower, "duplicate") || strings.Contains(lower, "unique constraint") || strings.Contains(lower, "23505")) { + return err } - if err := ProjectFlockPopulationRepotx.CreateOne(ctx.Context(), population, nil); err != nil { - lower := strings.ToLower(err.Error()) - if !(strings.Contains(lower, "duplicate") || strings.Contains(lower, "unique constraint") || strings.Contains(lower, "23505")) { - return err - } - s.Log.Infof("ignored duplicate population for chickin %d: %v", chickin.Id, err) - } - } else { - s.Log.Infof("population already exists for chickin %d", chickin.Id) + s.Log.Infof("ignored duplicate population for chickin %d: %v", chickin.Id, err) } } return nil } - -func uniqueUintSlice(values []uint) []uint { - seen := make(map[uint]struct{}, len(values)) - result := make([]uint, 0, len(values)) - for _, v := range values { - if _, ok := seen[v]; ok { - continue - } - seen[v] = struct{}{} - result = append(result, v) - } - return result -} diff --git a/internal/modules/production/project-flock-kandangs/dto/project_flock_kandang.dto.go b/internal/modules/production/project-flock-kandangs/dto/project_flock_kandang.dto.go index ac0678bd..1656eaba 100644 --- a/internal/modules/production/project-flock-kandangs/dto/project_flock_kandang.dto.go +++ b/internal/modules/production/project-flock-kandangs/dto/project_flock_kandang.dto.go @@ -8,8 +8,9 @@ import ( 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" + productDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/dto" + warehouseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/dto" chickinDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/dto" projectFlockDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/dto" userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto" @@ -27,7 +28,7 @@ type ProjectFlockCustomDTO struct { Category string `json:"category"` Fcr *fcrDTO.FcrBaseDTO `json:"fcr,omitempty"` Location *locationDTO.LocationBaseDTO `json:"location,omitempty"` - Kandangs []kandangDTO.KandangBaseDTO `json:"kandangs,omitempty"` + Kandangs []KandangCustomDTO `json:"kandangs,omitempty"` CreatedUser *userDTO.UserBaseDTO `json:"created_user,omitempty"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` @@ -39,22 +40,11 @@ type KandangCustomDTO struct { Status string `json:"status"` } -type ProductBaseDTO struct { - Id uint `json:"id"` - Name string `json:"name"` -} - -type WarehouseBaseDTO struct { - Id uint `json:"id"` - Name string `json:"name"` - Type string `json:"type"` -} - type ProductWarehouseDTO struct { - Id uint `json:"id"` - Quantity float64 `json:"quantity"` - Product *ProductBaseDTO `json:"product,omitempty"` - Warehouse *WarehouseBaseDTO `json:"warehouse,omitempty"` + Id uint `json:"id"` + Quantity float64 `json:"quantity"` + Product *productDTO.ProductBaseDTO `json:"product,omitempty"` + Warehouse *warehouseDTO.WarehouseBaseDTO `json:"warehouse,omitempty"` } type AvailableQtyDTO struct { @@ -95,74 +85,38 @@ func toProjectFlockCustomDTO(pf *projectFlockDTO.ProjectFlockListDTO) *ProjectFl Category: pf.Category, Fcr: pf.Fcr, Location: pf.Location, - Kandangs: pf.Kandangs, + Kandangs: buildKandangCustomDTOs(pf.Kandangs), CreatedUser: pf.CreatedUser, CreatedAt: pf.CreatedAt, UpdatedAt: pf.UpdatedAt, } } -func buildAvailableQtys(chickins []entity.ProjectChickin) []AvailableQtyDTO { - if len(chickins) == 0 { - return nil +func ToProjectFlockKandangListDTOWithAvailableQty(e entity.ProjectFlockKandang, availableQtysRaw []map[string]interface{}) ProjectFlockKandangListDTO { + var projectFlockSummary *projectFlockDTO.ProjectFlockListDTO + if e.ProjectFlock.Id != 0 { + mapped := projectFlockDTO.ToProjectFlockListDTO(e.ProjectFlock) + projectFlockSummary = &mapped } - availableQtyMap := make(map[uint]AvailableQtyDTO) - for _, ch := range chickins { - if ch.ProductWarehouse == nil || ch.ProductWarehouse.Quantity <= 0 { - continue - } - - if _, exists := availableQtyMap[ch.ProductWarehouseId]; exists { - continue - } - - pwDTO := &ProductWarehouseDTO{ - Id: ch.ProductWarehouse.Id, - Quantity: ch.ProductWarehouse.Quantity, - } - - if ch.ProductWarehouse.Product.Id != 0 { - pwDTO.Product = &ProductBaseDTO{ - Id: ch.ProductWarehouse.Product.Id, - Name: ch.ProductWarehouse.Product.Name, - } - } - - if ch.ProductWarehouse.Warehouse.Id != 0 { - pwDTO.Warehouse = &WarehouseBaseDTO{ - Id: ch.ProductWarehouse.Warehouse.Id, - Name: ch.ProductWarehouse.Warehouse.Name, - Type: ch.ProductWarehouse.Warehouse.Type, - } - } - - availableQtyMap[ch.ProductWarehouseId] = AvailableQtyDTO{ - ProductWarehouse: pwDTO, - } + return ProjectFlockKandangListDTO{ + ProjectFlockKandangBaseDTO: ToProjectFlockKandangBaseDTO(e), + ProjectFlock: toProjectFlockCustomDTO(projectFlockSummary), + Kandang: buildKandangFromEntity(e.Kandang), + Chickins: buildChickins(e.Chickins), + AvailableQtys: buildAvailableQtysFromRaw(availableQtysRaw), + CreatedAt: e.CreatedAt, + CreatedUser: buildCreatedUser(e.ProjectFlock), + Approval: defaultProjectFlockKandangLatestApproval(e), } - - if len(availableQtyMap) == 0 { - return nil - } - - result := make([]AvailableQtyDTO, 0, len(availableQtyMap)) - for _, v := range availableQtyMap { - result = append(result, v) - } - return result } -func buildChickins(chickins []entity.ProjectChickin) []chickinDTO.ChickinBaseDTO { - if len(chickins) == 0 { - return nil +func toKandangCustomDTO(k projectFlockDTO.KandangWithProjectFlockIdDTO) KandangCustomDTO { + return KandangCustomDTO{ + Id: k.Id, + Name: k.Name, + Status: k.Status, } - - result := make([]chickinDTO.ChickinBaseDTO, len(chickins)) - for i, ch := range chickins { - result[i] = chickinDTO.ToChickinBaseDTO(ch) - } - return result } func defaultProjectFlockKandangLatestApproval(e entity.ProjectFlockKandang) *approvalDTO.ApprovalBaseDTO { @@ -180,32 +134,14 @@ func ToProjectFlockKandangListDTO(e entity.ProjectFlockKandang) ProjectFlockKand projectFlockSummary = &mapped } - var kandangSummary *KandangCustomDTO - if e.Kandang.Id != 0 { - kandangSummary = &KandangCustomDTO{ - Id: e.Kandang.Id, - Name: e.Kandang.Name, - Status: e.Kandang.Status, - } - } - - var createdUser *userDTO.UserBaseDTO - if e.ProjectFlock.CreatedUser.Id != 0 { - mapped := userDTO.ToUserBaseDTO(e.ProjectFlock.CreatedUser) - createdUser = &mapped - } else if e.ProjectFlock.CreatedBy != 0 { - mapped := userDTO.UserBaseDTO{Id: e.ProjectFlock.CreatedBy, IdUser: int64(e.ProjectFlock.CreatedBy)} - createdUser = &mapped - } - return ProjectFlockKandangListDTO{ ProjectFlockKandangBaseDTO: ToProjectFlockKandangBaseDTO(e), ProjectFlock: toProjectFlockCustomDTO(projectFlockSummary), - Kandang: kandangSummary, + Kandang: buildKandangFromEntity(e.Kandang), Chickins: buildChickins(e.Chickins), AvailableQtys: buildAvailableQtys(e.Chickins), CreatedAt: e.CreatedAt, - CreatedUser: createdUser, + CreatedUser: buildCreatedUser(e.ProjectFlock), Approval: defaultProjectFlockKandangLatestApproval(e), } } @@ -224,69 +160,6 @@ func ToProjectFlockKandangDetailDTO(e entity.ProjectFlockKandang) ProjectFlockKa } } -func ToProjectFlockKandangListDTOWithAvailableQty(e entity.ProjectFlockKandang, availableQtysRaw []map[string]interface{}) ProjectFlockKandangListDTO { - var projectFlockSummary *projectFlockDTO.ProjectFlockListDTO - if e.ProjectFlock.Id != 0 { - mapped := projectFlockDTO.ToProjectFlockListDTO(e.ProjectFlock) - projectFlockSummary = &mapped - } - - var kandangSummary *KandangCustomDTO - if e.Kandang.Id != 0 { - kandangSummary = &KandangCustomDTO{ - Id: e.Kandang.Id, - Name: e.Kandang.Name, - Status: e.Kandang.Status, - } - } - - var createdUser *userDTO.UserBaseDTO - if e.ProjectFlock.CreatedUser.Id != 0 { - mapped := userDTO.ToUserBaseDTO(e.ProjectFlock.CreatedUser) - createdUser = &mapped - } else if e.ProjectFlock.CreatedBy != 0 { - mapped := userDTO.UserBaseDTO{Id: e.ProjectFlock.CreatedBy, IdUser: int64(e.ProjectFlock.CreatedBy)} - createdUser = &mapped - } - - // convert available qtys from raw map data - var availableQtys []AvailableQtyDTO - if len(availableQtysRaw) > 0 { - availableQtys = buildAvailableQtysFromRaw(availableQtysRaw) - } - - return ProjectFlockKandangListDTO{ - ProjectFlockKandangBaseDTO: ToProjectFlockKandangBaseDTO(e), - ProjectFlock: toProjectFlockCustomDTO(projectFlockSummary), - Kandang: kandangSummary, - Chickins: buildChickins(e.Chickins), - AvailableQtys: availableQtys, - CreatedAt: e.CreatedAt, - CreatedUser: createdUser, - Approval: defaultProjectFlockKandangLatestApproval(e), - } -} - -func buildAvailableQtysFromRaw(availableQtysRaw []map[string]interface{}) []AvailableQtyDTO { - if len(availableQtysRaw) == 0 { - return nil - } - - result := make([]AvailableQtyDTO, len(availableQtysRaw)) - for i, v := range availableQtysRaw { - pwData, ok := v["product_warehouse"].(map[string]interface{}) - if !ok { - continue - } - - pwDTO := buildProductWarehouseFromMap(pwData) - result[i] = AvailableQtyDTO{ - ProductWarehouse: pwDTO, - } - } - return result -} - func buildProductWarehouseFromMap(pwData map[string]interface{}) *ProductWarehouseDTO { if pwData == nil { return nil @@ -315,12 +188,12 @@ func buildProductWarehouseFromMap(pwData map[string]interface{}) *ProductWarehou return dto } -func buildProductFromMap(pData map[string]interface{}) *ProductBaseDTO { +func buildProductFromMap(pData map[string]interface{}) *productDTO.ProductBaseDTO { if pData == nil { return nil } - product := &ProductBaseDTO{} + product := &productDTO.ProductBaseDTO{} if id, ok := pData["id"].(float64); ok { product.Id = uint(id) } else if id, ok := pData["id"].(uint); ok { @@ -332,12 +205,12 @@ func buildProductFromMap(pData map[string]interface{}) *ProductBaseDTO { return product } -func buildWarehouseFromMap(wData map[string]interface{}) *WarehouseBaseDTO { +func buildWarehouseFromMap(wData map[string]interface{}) *warehouseDTO.WarehouseBaseDTO { if wData == nil { return nil } - warehouse := &WarehouseBaseDTO{} + warehouse := &warehouseDTO.WarehouseBaseDTO{} if id, ok := wData["id"].(float64); ok { warehouse.Id = uint(id) } else if id, ok := wData["id"].(uint); ok { @@ -351,3 +224,123 @@ func buildWarehouseFromMap(wData map[string]interface{}) *WarehouseBaseDTO { } return warehouse } + +func buildCreatedUser(pf entity.ProjectFlock) *userDTO.UserBaseDTO { + if pf.CreatedUser.Id != 0 { + mapped := userDTO.ToUserBaseDTO(pf.CreatedUser) + return &mapped + } else if pf.CreatedBy != 0 { + return &userDTO.UserBaseDTO{ + Id: pf.CreatedBy, + IdUser: int64(pf.CreatedBy), + } + } + return nil +} + +func buildKandangCustomDTOs(kandangs []projectFlockDTO.KandangWithProjectFlockIdDTO) []KandangCustomDTO { + if len(kandangs) == 0 { + return nil + } + + result := make([]KandangCustomDTO, len(kandangs)) + for i, k := range kandangs { + result[i] = toKandangCustomDTO(k) + } + return result +} + +func buildKandangFromEntity(kandang entity.Kandang) *KandangCustomDTO { + if kandang.Id == 0 { + return nil + } + + return &KandangCustomDTO{ + Id: kandang.Id, + Name: kandang.Name, + Status: kandang.Status, + } +} + +func buildChickins(chickins []entity.ProjectChickin) []chickinDTO.ChickinBaseDTO { + if len(chickins) == 0 { + return nil + } + + result := make([]chickinDTO.ChickinBaseDTO, len(chickins)) + for i, ch := range chickins { + result[i] = chickinDTO.ToChickinBaseDTO(ch) + } + return result +} + +func buildAvailableQtys(chickins []entity.ProjectChickin) []AvailableQtyDTO { + if len(chickins) == 0 { + return nil + } + + availableQtyMap := make(map[uint]AvailableQtyDTO) + for _, ch := range chickins { + if ch.ProductWarehouse == nil || ch.ProductWarehouse.Quantity <= 0 { + continue + } + + if _, exists := availableQtyMap[ch.ProductWarehouseId]; exists { + continue + } + + pwDTO := &ProductWarehouseDTO{ + Id: ch.ProductWarehouse.Id, + Quantity: ch.ProductWarehouse.Quantity, + } + + if ch.ProductWarehouse.Product.Id != 0 { + pwDTO.Product = &productDTO.ProductBaseDTO{ + Id: ch.ProductWarehouse.Product.Id, + Name: ch.ProductWarehouse.Product.Name, + } + } + + if ch.ProductWarehouse.Warehouse.Id != 0 { + pwDTO.Warehouse = &warehouseDTO.WarehouseBaseDTO{ + Id: ch.ProductWarehouse.Warehouse.Id, + Name: ch.ProductWarehouse.Warehouse.Name, + Type: ch.ProductWarehouse.Warehouse.Type, + } + } + + availableQtyMap[ch.ProductWarehouseId] = AvailableQtyDTO{ + ProductWarehouse: pwDTO, + } + } + + if len(availableQtyMap) == 0 { + return nil + } + + result := make([]AvailableQtyDTO, 0, len(availableQtyMap)) + for _, v := range availableQtyMap { + result = append(result, v) + } + return result +} + +func buildAvailableQtysFromRaw(availableQtysRaw []map[string]interface{}) []AvailableQtyDTO { + if len(availableQtysRaw) == 0 { + return nil + } + + result := make([]AvailableQtyDTO, len(availableQtysRaw)) + for i, v := range availableQtysRaw { + pwData, ok := v["product_warehouse"].(map[string]interface{}) + if !ok { + continue + } + + pwDTO := buildProductWarehouseFromMap(pwData) + result[i] = AvailableQtyDTO{ + ProductWarehouse: pwDTO, + } + } + return result +} diff --git a/internal/modules/production/project_flocks/dto/projectflock.dto.go b/internal/modules/production/project_flocks/dto/projectflock.dto.go index dff3bc61..2efd979b 100644 --- a/internal/modules/production/project_flocks/dto/projectflock.dto.go +++ b/internal/modules/production/project_flocks/dto/projectflock.dto.go @@ -20,18 +20,25 @@ type ProjectFlockBaseDTO struct { Period int `json:"period"` } +type KandangWithProjectFlockIdDTO struct { + Id uint `json:"id"` + Name string `json:"name"` + Status string `json:"status"` + ProjectFlockKandangId uint `json:"project_flock_kandang_id"` +} + type ProjectFlockListDTO struct { ProjectFlockBaseDTO - Flock *flockDTO.FlockBaseDTO `json:"flock,omitempty"` - Area *areaDTO.AreaBaseDTO `json:"area,omitempty"` - Category string `json:"category"` - Fcr *fcrDTO.FcrBaseDTO `json:"fcr,omitempty"` - Location *locationDTO.LocationBaseDTO `json:"location,omitempty"` - Kandangs []kandangDTO.KandangBaseDTO `json:"kandangs,omitempty"` - CreatedUser *userDTO.UserBaseDTO `json:"created_user"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - Approval approvalDTO.ApprovalBaseDTO `json:"approval"` + Flock *flockDTO.FlockBaseDTO `json:"flock,omitempty"` + Area *areaDTO.AreaBaseDTO `json:"area,omitempty"` + Category string `json:"category"` + Fcr *fcrDTO.FcrBaseDTO `json:"fcr,omitempty"` + Location *locationDTO.LocationBaseDTO `json:"location,omitempty"` + Kandangs []KandangWithProjectFlockIdDTO `json:"kandangs,omitempty"` + CreatedUser *userDTO.UserBaseDTO `json:"created_user"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + Approval approvalDTO.ApprovalBaseDTO `json:"approval"` } type ProjectFlockDetailDTO struct { @@ -50,11 +57,24 @@ func ToProjectFlockListDTO(e entity.ProjectFlock) ProjectFlockListDTO { createdUser = &mapped } - var kandangSummaries []kandangDTO.KandangBaseDTO + var kandangSummaries []KandangWithProjectFlockIdDTO if len(e.Kandangs) > 0 { - kandangSummaries = make([]kandangDTO.KandangBaseDTO, len(e.Kandangs)) + kandangSummaries = make([]KandangWithProjectFlockIdDTO, len(e.Kandangs)) + + // Create a map of KandangId -> ProjectFlockKandangId from KandangHistory + kandangIdToProjectFlockKandangId := make(map[uint]uint) + for _, kh := range e.KandangHistory { + kandangIdToProjectFlockKandangId[kh.KandangId] = kh.Id + } + for i, kandang := range e.Kandangs { - kandangSummaries[i] = kandangDTO.ToKandangBaseDTO(kandang) + baseDTO := kandangDTO.ToKandangBaseDTO(kandang) + kandangSummaries[i] = KandangWithProjectFlockIdDTO{ + Id: baseDTO.Id, + Name: baseDTO.Name, + Status: baseDTO.Status, + ProjectFlockKandangId: kandangIdToProjectFlockKandangId[kandang.Id], + } } } diff --git a/internal/modules/production/project_flocks/services/projectflock.service.go b/internal/modules/production/project_flocks/services/projectflock.service.go index 23097585..6f99ccb0 100644 --- a/internal/modules/production/project_flocks/services/projectflock.service.go +++ b/internal/modules/production/project_flocks/services/projectflock.service.go @@ -85,7 +85,9 @@ func (s projectflockService) withRelations(db *gorm.DB) *gorm.DB { Preload("Area"). Preload("Fcr"). Preload("Location"). - Preload("Kandangs") + Preload("Kandangs"). + Preload("KandangHistory"). + Preload("KandangHistory.Kandang") } func (s projectflockService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.ProjectFlock, int64, error) { diff --git a/internal/utils/constant.go b/internal/utils/constant.go index c7bcc99e..ef08eaed 100644 --- a/internal/utils/constant.go +++ b/internal/utils/constant.go @@ -19,6 +19,7 @@ const ( FlagDOC FlagType = "DOC" FlagPullet FlagType = "PULLET" + FlagLayer FlagType = "LAYER" FlagPakan FlagType = "PAKAN" FlagPreStarter FlagType = "PRE-STARTER" FlagStarter FlagType = "STARTER" @@ -39,6 +40,7 @@ var flagGroupOptions = map[FlagGroup][]FlagType{ FlagGroupProduct: { FlagDOC, FlagPullet, + FlagLayer, FlagPakan, FlagPreStarter, FlagStarter, From c72db5bd18a3856c42a8733a9c5ec6bc0ff8d40e Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Mon, 3 Nov 2025 09:29:00 +0700 Subject: [PATCH 08/12] FIX[BE]: delete redudant kandang response on projectflockkandang getone API --- .../dto/project_flock_kandang.dto.go | 14 -------------- .../services/project_flock_kandang.service.go | 4 +--- 2 files changed, 1 insertion(+), 17 deletions(-) diff --git a/internal/modules/production/project-flock-kandangs/dto/project_flock_kandang.dto.go b/internal/modules/production/project-flock-kandangs/dto/project_flock_kandang.dto.go index 1656eaba..888964a7 100644 --- a/internal/modules/production/project-flock-kandangs/dto/project_flock_kandang.dto.go +++ b/internal/modules/production/project-flock-kandangs/dto/project_flock_kandang.dto.go @@ -28,7 +28,6 @@ type ProjectFlockCustomDTO struct { Category string `json:"category"` Fcr *fcrDTO.FcrBaseDTO `json:"fcr,omitempty"` Location *locationDTO.LocationBaseDTO `json:"location,omitempty"` - Kandangs []KandangCustomDTO `json:"kandangs,omitempty"` CreatedUser *userDTO.UserBaseDTO `json:"created_user,omitempty"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` @@ -85,7 +84,6 @@ func toProjectFlockCustomDTO(pf *projectFlockDTO.ProjectFlockListDTO) *ProjectFl Category: pf.Category, Fcr: pf.Fcr, Location: pf.Location, - Kandangs: buildKandangCustomDTOs(pf.Kandangs), CreatedUser: pf.CreatedUser, CreatedAt: pf.CreatedAt, UpdatedAt: pf.UpdatedAt, @@ -238,18 +236,6 @@ func buildCreatedUser(pf entity.ProjectFlock) *userDTO.UserBaseDTO { return nil } -func buildKandangCustomDTOs(kandangs []projectFlockDTO.KandangWithProjectFlockIdDTO) []KandangCustomDTO { - if len(kandangs) == 0 { - return nil - } - - result := make([]KandangCustomDTO, len(kandangs)) - for i, k := range kandangs { - result[i] = toKandangCustomDTO(k) - } - return result -} - func buildKandangFromEntity(kandang entity.Kandang) *KandangCustomDTO { if kandang.Id == 0 { return nil diff --git a/internal/modules/production/project-flock-kandangs/services/project_flock_kandang.service.go b/internal/modules/production/project-flock-kandangs/services/project_flock_kandang.service.go index d3f1f0b7..6258e34a 100644 --- a/internal/modules/production/project-flock-kandangs/services/project_flock_kandang.service.go +++ b/internal/modules/production/project-flock-kandangs/services/project_flock_kandang.service.go @@ -115,20 +115,18 @@ func (s projectFlockKandangService) getAvailableQuantities(c *fiber.Ctx, project var result []map[string]interface{} for _, pw := range products { if pw.Quantity > 0 { - // Build product data + productData := map[string]interface{}{ "id": pw.Product.Id, "name": pw.Product.Name, } - // Build warehouse data warehouseData := map[string]interface{}{ "id": pw.Warehouse.Id, "name": pw.Warehouse.Name, "type": pw.Warehouse.Type, } - // Build product warehouse data with nested product and warehouse productWarehouseData := map[string]interface{}{ "id": pw.Id, "quantity": pw.Quantity, From 8220e343029bf557a340589f32284111632199d5 Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Tue, 4 Nov 2025 08:24:38 +0700 Subject: [PATCH 09/12] FIX[BE]: fix logic on Chickin Laying not convert to layer but still Pullet, and inisiate laying transfer migration and base basic API --- ...825_create_laying_transfers_table.down.sql | 2 +- ...74825_create_laying_transfers_table.up.sql | 9 +- ...te_laying_kandang_transfers_table.down.sql | 1 - ...eate_laying_kandang_transfers_table.up.sql | 40 ---- ...ansfer_sources_and_targets_tables.down.sql | 5 + ...transfer_sources_and_targets_tables.up.sql | 93 +++++++++ internal/database/seed/seeder.go | 32 ++-- internal/entities/laying_transfer.go | 12 +- internal/entities/laying_transfer_source.go | 23 +++ internal/entities/laying_transfer_target.go | 24 +++ .../project_chickin.repository.go | 15 ++ .../chickins/services/chickin.service.go | 177 +++++++++++++----- .../dto/project_flock_kandang.dto.go | 15 +- .../project-flock-kandangs/module.go | 3 +- .../services/project_flock_kandang.service.go | 100 +++++++--- .../project_flock_population_repository.go | 25 +++ .../controllers/transfer_laying.controller.go | 9 +- .../laying_transfer.repository.go | 18 +- .../laying_transfer_source.repository.go | 38 ++++ .../laying_transfer_target.repository.go | 38 ++++ .../services/transfer_laying.service.go | 37 +++- .../validations/transfer_laying.validation.go | 34 ++-- 22 files changed, 587 insertions(+), 163 deletions(-) delete mode 100644 internal/database/migrations/20251029081833_create_laying_kandang_transfers_table.down.sql delete mode 100644 internal/database/migrations/20251029081833_create_laying_kandang_transfers_table.up.sql create mode 100644 internal/database/migrations/20251103054536_add_laying_transfer_sources_and_targets_tables.down.sql create mode 100644 internal/database/migrations/20251103054536_add_laying_transfer_sources_and_targets_tables.up.sql create mode 100644 internal/entities/laying_transfer_source.go create mode 100644 internal/entities/laying_transfer_target.go create mode 100644 internal/modules/production/transfer_layings/repositories/laying_transfer_source.repository.go create mode 100644 internal/modules/production/transfer_layings/repositories/laying_transfer_target.repository.go diff --git a/internal/database/migrations/20251029074825_create_laying_transfers_table.down.sql b/internal/database/migrations/20251029074825_create_laying_transfers_table.down.sql index 6ad77820..29313fe4 100644 --- a/internal/database/migrations/20251029074825_create_laying_transfers_table.down.sql +++ b/internal/database/migrations/20251029074825_create_laying_transfers_table.down.sql @@ -1 +1 @@ -DROP TABLE IF EXISTS laying_transfers; \ No newline at end of file +DROP TABLE IF EXISTS laying_transfers CASCADE; \ No newline at end of file diff --git a/internal/database/migrations/20251029074825_create_laying_transfers_table.up.sql b/internal/database/migrations/20251029074825_create_laying_transfers_table.up.sql index 10ced117..a01f6e02 100644 --- a/internal/database/migrations/20251029074825_create_laying_transfers_table.up.sql +++ b/internal/database/migrations/20251029074825_create_laying_transfers_table.up.sql @@ -1,14 +1,15 @@ CREATE TABLE IF NOT EXISTS laying_transfers ( id BIGSERIAL PRIMARY KEY, + transfer_number VARCHAR(50) UNIQUE NOT NULL, from_project_flock_id BIGINT NOT NULL, to_project_flock_id BIGINT NOT NULL, transfer_date DATE NOT NULL, - total_qty INTEGER, + total_qty NUMERIC(15, 3) NOT NULL, notes TEXT, created_at TIMESTAMPTZ DEFAULT now(), updated_at TIMESTAMPTZ DEFAULT now(), deleted_at TIMESTAMPTZ, - created_by BIGINT + created_by BIGINT NOT NULL ); -- FOREIGN KEYS (dijalankan setelah semua tabel parent ada) @@ -37,6 +38,10 @@ BEGIN END $$; -- INDEXES +CREATE UNIQUE INDEX IF NOT EXISTS idx_laying_transfers_transfer_number ON laying_transfers (transfer_number) +WHERE + deleted_at IS NULL; + CREATE INDEX IF NOT EXISTS idx_laying_transfers_from_project_flock_id ON laying_transfers (from_project_flock_id); CREATE INDEX IF NOT EXISTS idx_laying_transfers_to_project_flock_id ON laying_transfers (to_project_flock_id); diff --git a/internal/database/migrations/20251029081833_create_laying_kandang_transfers_table.down.sql b/internal/database/migrations/20251029081833_create_laying_kandang_transfers_table.down.sql deleted file mode 100644 index caf4f52d..00000000 --- a/internal/database/migrations/20251029081833_create_laying_kandang_transfers_table.down.sql +++ /dev/null @@ -1 +0,0 @@ -DROP TABLE IF EXISTS laying_kandang_transfers; \ No newline at end of file diff --git a/internal/database/migrations/20251029081833_create_laying_kandang_transfers_table.up.sql b/internal/database/migrations/20251029081833_create_laying_kandang_transfers_table.up.sql deleted file mode 100644 index 786f8de3..00000000 --- a/internal/database/migrations/20251029081833_create_laying_kandang_transfers_table.up.sql +++ /dev/null @@ -1,40 +0,0 @@ -CREATE TABLE IF NOT EXISTS laying_kandang_transfers ( - id BIGSERIAL PRIMARY KEY, - kandang_id BIGINT, - product_warehouse_id BIGINT, - qty NUMERIC(15,3), - laying_transfer_id BIGINT NOT NULL -); - --- FOREIGN KEYS (dijalankan setelah semua tabel parent ada) -DO $$ -BEGIN - IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'kandangs') THEN - ALTER TABLE laying_kandang_transfers - ADD CONSTRAINT fk_laying_kandang_transfers_kandang - FOREIGN KEY (kandang_id) - REFERENCES kandangs(id) - ON DELETE RESTRICT ON UPDATE CASCADE; - END IF; - - IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'product_warehouses') THEN - ALTER TABLE laying_kandang_transfers - ADD CONSTRAINT fk_laying_kandang_transfers_product_warehouse - FOREIGN KEY (product_warehouse_id) - REFERENCES product_warehouses(id) - ON DELETE RESTRICT ON UPDATE CASCADE; - END IF; - - IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'laying_transfers') THEN - ALTER TABLE laying_kandang_transfers - ADD CONSTRAINT fk_laying_kandang_transfers_laying_transfer - FOREIGN KEY (laying_transfer_id) - REFERENCES laying_transfers(id) - ON DELETE RESTRICT ON UPDATE CASCADE; - END IF; -END $$; - --- INDEXES -CREATE INDEX IF NOT EXISTS idx_laying_kandang_transfers_kandang_id ON laying_kandang_transfers(kandang_id); -CREATE INDEX IF NOT EXISTS idx_laying_kandang_transfers_product_warehouse_id ON laying_kandang_transfers(product_warehouse_id); -CREATE INDEX IF NOT EXISTS idx_laying_kandang_transfers_laying_transfer_id ON laying_kandang_transfers(laying_transfer_id); \ No newline at end of file diff --git a/internal/database/migrations/20251103054536_add_laying_transfer_sources_and_targets_tables.down.sql b/internal/database/migrations/20251103054536_add_laying_transfer_sources_and_targets_tables.down.sql new file mode 100644 index 00000000..2b890114 --- /dev/null +++ b/internal/database/migrations/20251103054536_add_laying_transfer_sources_and_targets_tables.down.sql @@ -0,0 +1,5 @@ +-- Rollback laying_transfer_sources dan laying_transfer_targets tables + +DROP TABLE IF EXISTS laying_transfer_targets CASCADE; + +DROP TABLE IF EXISTS laying_transfer_sources CASCADE; \ No newline at end of file diff --git a/internal/database/migrations/20251103054536_add_laying_transfer_sources_and_targets_tables.up.sql b/internal/database/migrations/20251103054536_add_laying_transfer_sources_and_targets_tables.up.sql new file mode 100644 index 00000000..023fc7b4 --- /dev/null +++ b/internal/database/migrations/20251103054536_add_laying_transfer_sources_and_targets_tables.up.sql @@ -0,0 +1,93 @@ +-- Create laying_transfer_sources dan laying_transfer_targets tables + +-- 1. Create laying_transfer_sources table (detail sumber - kandang asal growing) +CREATE TABLE laying_transfer_sources ( + id BIGSERIAL PRIMARY KEY, + laying_transfer_id BIGINT NOT NULL, + source_project_flock_kandang_id BIGINT NOT NULL, + product_warehouse_id BIGINT, + qty NUMERIC(15, 3) NOT NULL, + note TEXT, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW(), + deleted_at TIMESTAMPTZ +); + +-- Add foreign keys untuk laying_transfer_sources +DO $$ +BEGIN + IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'laying_transfers') THEN + ALTER TABLE laying_transfer_sources + ADD CONSTRAINT fk_laying_transfer_sources_laying_transfer_id + FOREIGN KEY (laying_transfer_id) REFERENCES laying_transfers(id) ON DELETE CASCADE; + END IF; + + IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'project_flock_kandangs') THEN + ALTER TABLE laying_transfer_sources + ADD CONSTRAINT fk_laying_transfer_sources_project_flock_kandang_id + FOREIGN KEY (source_project_flock_kandang_id) REFERENCES project_flock_kandangs(id) ON DELETE RESTRICT; + END IF; + + IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'product_warehouses') THEN + ALTER TABLE laying_transfer_sources + ADD CONSTRAINT fk_laying_transfer_sources_product_warehouse_id + FOREIGN KEY (product_warehouse_id) REFERENCES product_warehouses(id) ON DELETE SET NULL; + END IF; +END $$; + +-- 2. Create laying_transfer_targets table (detail tujuan - kandang laying) +CREATE TABLE laying_transfer_targets ( + id BIGSERIAL PRIMARY KEY, + laying_transfer_id BIGINT NOT NULL, + target_project_flock_kandang_id BIGINT NOT NULL, + qty NUMERIC(15, 3) NOT NULL, + product_warehouse_id BIGINT, + note TEXT, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW(), + deleted_at TIMESTAMPTZ +); + +-- Add foreign keys untuk laying_transfer_targets +DO $$ +BEGIN + IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'laying_transfers') THEN + ALTER TABLE laying_transfer_targets + ADD CONSTRAINT fk_laying_transfer_targets_laying_transfer_id + FOREIGN KEY (laying_transfer_id) REFERENCES laying_transfers(id) ON DELETE CASCADE; + END IF; + + IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'project_flock_kandangs') THEN + ALTER TABLE laying_transfer_targets + ADD CONSTRAINT fk_laying_transfer_targets_project_flock_kandang_id + FOREIGN KEY (target_project_flock_kandang_id) REFERENCES project_flock_kandangs(id) ON DELETE RESTRICT; + END IF; + + IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'product_warehouses') THEN + ALTER TABLE laying_transfer_targets + ADD CONSTRAINT fk_laying_transfer_targets_product_warehouse_id + FOREIGN KEY (product_warehouse_id) REFERENCES product_warehouses(id) ON DELETE SET NULL; + END IF; +END $$; + +-- 3. Create indexes untuk laying_transfer_sources +CREATE INDEX idx_laying_transfer_sources_laying_transfer_id ON laying_transfer_sources (laying_transfer_id); + +CREATE INDEX idx_laying_transfer_sources_source_kandang_id ON laying_transfer_sources ( + source_project_flock_kandang_id +); + +CREATE INDEX idx_laying_transfer_sources_product_warehouse_id ON laying_transfer_sources (product_warehouse_id); + +CREATE INDEX idx_laying_transfer_sources_deleted_at ON laying_transfer_sources (deleted_at); + +-- 4. Create indexes untuk laying_transfer_targets +CREATE INDEX idx_laying_transfer_targets_laying_transfer_id ON laying_transfer_targets (laying_transfer_id); + +CREATE INDEX idx_laying_transfer_targets_target_kandang_id ON laying_transfer_targets ( + target_project_flock_kandang_id +); + +CREATE INDEX idx_laying_transfer_targets_product_warehouse_id ON laying_transfer_targets (product_warehouse_id); + +CREATE INDEX idx_laying_transfer_targets_deleted_at ON laying_transfer_targets (deleted_at); \ No newline at end of file diff --git a/internal/database/seed/seeder.go b/internal/database/seed/seeder.go index 93c75b29..3e4f1eb9 100644 --- a/internal/database/seed/seeder.go +++ b/internal/database/seed/seeder.go @@ -258,7 +258,7 @@ func seedProjectFlocks(tx *gorm.DB, createdBy uint, flocks, areas, fcrs, locatio Flock: "Flock Priangan", Area: "Priangan", Category: utils.ProjectFlockCategoryGrowing, - Fcr: "FCR Layer", + Fcr: "FCR DOC", Location: "Singaparna", Period: 1, }, @@ -267,7 +267,7 @@ func seedProjectFlocks(tx *gorm.DB, createdBy uint, flocks, areas, fcrs, locatio Flock: "Flock Banten", Area: "Banten", Category: utils.ProjectFlockCategoryGrowing, - Fcr: "FCR Layer", + Fcr: "FCR DOC", Location: "Cikaum", Period: 1, }, @@ -574,7 +574,6 @@ func seedProductCategories(tx *gorm.DB, createdBy uint) (map[string]uint, error) {"Bahan Baku", "RAW"}, {"Day Old Chick", "DOC"}, {"Pullet", "PULLET"}, - {"Layer", "LAYER"}, } result := make(map[string]uint, len(seeds)) @@ -698,14 +697,25 @@ func seedFcr(tx *gorm.DB, createdBy uint) (map[string]uint, error) { } }{ { - Name: "FCR Layer", + Name: "FCR DOC", Standards: []struct { Weight float64 FcrNumber float64 Mortality float64 }{ - {Weight: 0.8, FcrNumber: 1.60, Mortality: 2.0}, - {Weight: 1.5, FcrNumber: 1.75, Mortality: 3.5}, + {Weight: 0.1, FcrNumber: 1.20, Mortality: 1.0}, + {Weight: 0.3, FcrNumber: 1.35, Mortality: 1.5}, + }, + }, + { + Name: "FCR Pullet", + Standards: []struct { + Weight float64 + FcrNumber float64 + Mortality float64 + }{ + {Weight: 0.5, FcrNumber: 1.45, Mortality: 2.0}, + {Weight: 0.8, FcrNumber: 1.50, Mortality: 2.5}, }, }, } @@ -809,16 +819,6 @@ func seedProducts(tx *gorm.DB, createdBy uint, uoms map[string]uint, categories Suppliers: []string{"PT CHAROEN POKPHAND INDONESIA Tbk"}, Flags: []utils.FlagType{utils.FlagPullet}, }, - { - Name: "Ayam Layer", - Brand: "MBU Layer", - Sku: "LAY0001", - Uom: "Ekor", - Category: "Layer", - Price: 20000, - Suppliers: []string{"PT CHAROEN POKPHAND INDONESIA Tbk"}, - Flags: []utils.FlagType{utils.FlagLayer}, - }, } for _, seed := range seeds { diff --git a/internal/entities/laying_transfer.go b/internal/entities/laying_transfer.go index 75c5e23a..8c2058df 100644 --- a/internal/entities/laying_transfer.go +++ b/internal/entities/laying_transfer.go @@ -8,17 +8,21 @@ import ( type LayingTransfer struct { Id uint `gorm:"primaryKey"` + TransferNumber string `gorm:"uniqueIndex;not null"` FromProjectFlockId uint `gorm:"not null"` ToProjectFlockId uint `gorm:"not null"` - TotalQty float64 `gorm:"type:numeric(15,3)"` TransferDate time.Time `gorm:"type:date;not null"` + TotalQty float64 `gorm:"type:numeric(15,3);not null"` Notes string `gorm:"type:text"` CreatedBy uint `gorm:"not null"` CreatedAt time.Time `gorm:"autoCreateTime"` UpdatedAt time.Time `gorm:"autoUpdateTime"` DeletedAt gorm.DeletedAt `gorm:"index"` - FromProjectFlock *ProjectFlock `gorm:"foreignKey:FromProjectFlockId;references:Id"` - ToProjectFlock *ProjectFlock `gorm:"foreignKey:ToProjectFlockId;references:Id"` - CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"` + FromProjectFlock *ProjectFlock `gorm:"foreignKey:FromProjectFlockId;references:Id"` + ToProjectFlock *ProjectFlock `gorm:"foreignKey:ToProjectFlockId;references:Id"` + CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"` + Sources []LayingTransferSource `gorm:"foreignKey:LayingTransferId;constraint:OnDelete:CASCADE"` + Targets []LayingTransferTarget `gorm:"foreignKey:LayingTransferId;constraint:OnDelete:CASCADE"` } + diff --git a/internal/entities/laying_transfer_source.go b/internal/entities/laying_transfer_source.go new file mode 100644 index 00000000..6b54bd84 --- /dev/null +++ b/internal/entities/laying_transfer_source.go @@ -0,0 +1,23 @@ +package entities + +import ( + "time" + + "gorm.io/gorm" +) + +type LayingTransferSource struct { + Id uint `gorm:"primaryKey"` + LayingTransferId uint `gorm:"index;not null"` + SourceProjectFlockKandangId uint `gorm:"not null"` + ProductWarehouseId *uint `gorm:""` + Qty float64 `gorm:"type:numeric(15,3);not null"` + Note string `gorm:"type:text"` + CreatedAt time.Time `gorm:"autoCreateTime"` + UpdatedAt time.Time `gorm:"autoUpdateTime"` + DeletedAt gorm.DeletedAt `gorm:"index"` + + LayingTransfer *LayingTransfer `gorm:"foreignKey:LayingTransferId;references:Id"` + SourceProjectFlockKandang *ProjectFlockKandang `gorm:"foreignKey:SourceProjectFlockKandangId;references:Id"` + ProductWarehouse *ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"` +} diff --git a/internal/entities/laying_transfer_target.go b/internal/entities/laying_transfer_target.go new file mode 100644 index 00000000..dec98f1f --- /dev/null +++ b/internal/entities/laying_transfer_target.go @@ -0,0 +1,24 @@ +package entities + +import ( + "time" + + "gorm.io/gorm" +) + +type LayingTransferTarget struct { + Id uint `gorm:"primaryKey"` + LayingTransferId uint `gorm:"index;not null"` + TargetProjectFlockKandangId uint `gorm:"not null"` + Qty float64 `gorm:"type:numeric(15,3);not null"` + ProductWarehouseId *uint `gorm:""` + Note string `gorm:"type:text"` + CreatedAt time.Time `gorm:"autoCreateTime"` + UpdatedAt time.Time `gorm:"autoUpdateTime"` + DeletedAt gorm.DeletedAt `gorm:"index"` + + LayingTransfer *LayingTransfer `gorm:"foreignKey:LayingTransferId;references:Id"` + TargetProjectFlockKandang *ProjectFlockKandang `gorm:"foreignKey:TargetProjectFlockKandangId;references:Id"` + ProductWarehouse *ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"` +} + diff --git a/internal/modules/production/chickins/repositories/project_chickin.repository.go b/internal/modules/production/chickins/repositories/project_chickin.repository.go index fa6f13e6..c0f522ad 100644 --- a/internal/modules/production/chickins/repositories/project_chickin.repository.go +++ b/internal/modules/production/chickins/repositories/project_chickin.repository.go @@ -12,6 +12,7 @@ type ProjectChickinRepository interface { repository.BaseRepository[entity.ProjectChickin] GetFirstByProjectFlockID(ctx context.Context, projectFlockID uint) (*entity.ProjectChickin, error) GetByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) ([]entity.ProjectChickin, error) + GetPendingByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) ([]entity.ProjectChickin, error) } type ChickinRepositoryImpl struct { @@ -49,3 +50,17 @@ func (r *ChickinRepositoryImpl) GetByProjectFlockKandangID(ctx context.Context, } return chickins, nil } + +func (r *ChickinRepositoryImpl) GetPendingByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) ([]entity.ProjectChickin, error) { + var chickins []entity.ProjectChickin + err := r.db.WithContext(ctx). + Where("project_flock_kandang_id = ?", projectFlockKandangID). + Where("usage_qty = 0"). + Where("pending_usage_qty > 0"). + Order("created_at DESC"). + Find(&chickins).Error + if err != nil { + return nil, err + } + return chickins, nil +} diff --git a/internal/modules/production/chickins/services/chickin.service.go b/internal/modules/production/chickins/services/chickin.service.go index 243bdf52..1970354a 100644 --- a/internal/modules/production/chickins/services/chickin.service.go +++ b/internal/modules/production/chickins/services/chickin.service.go @@ -150,8 +150,12 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) ([]enti actorID := uint(1) // todo nanti ambil dari auth context newChikins := make([]*entity.ProjectChickin, 0) for _, productWarehouse := range productWarehouses { + availableQty, err := s.calculateAvailableQuantity(c, req.ProjectFlockKandangId, &productWarehouse, category) + if err != nil { + s.Log.Warnf("Failed to calculate available quantity for product warehouse %d: %v", productWarehouse.Id, err) + } - if productWarehouse.Quantity <= 0 { + if availableQty <= 0 { continue } @@ -159,7 +163,7 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) ([]enti ProjectFlockKandangId: req.ProjectFlockKandangId, ChickInDate: chickinDate, UsageQty: 0, - PendingUsageQty: productWarehouse.Quantity, + PendingUsageQty: availableQty, ProductWarehouseId: productWarehouse.Id, Notes: req.Note, CreatedBy: actorID, @@ -176,6 +180,7 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) ([]enti if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check existing chickins") } + isFirstTime := len(existingChikins) == 0 err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { @@ -192,17 +197,19 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) ([]enti return err } - for _, chickin := range newChikins { + if category == string(utils.ProjectFlockCategoryLaying) { + for _, chickin := range newChikins { + updates := map[string]any{"quantity": gorm.Expr("quantity - ?", chickin.PendingUsageQty)} - updates := map[string]any{"quantity": 0} - - if err := productWarehouseTx.PatchOne(c.Context(), chickin.ProductWarehouseId, updates, nil); err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Product warehouse %d not found", chickin.ProductWarehouseId)) + if err := productWarehouseTx.PatchOne(c.Context(), chickin.ProductWarehouseId, updates, nil); err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Product warehouse %d not found", chickin.ProductWarehouseId)) + } + return fiber.NewError(fiber.StatusInternalServerError, "Failed to update product warehouse quantity") } - return fiber.NewError(fiber.StatusInternalServerError, "Failed to update product warehouse quantity") } } + var approvalAction entity.ApprovalAction if isFirstTime { approvalAction = entity.ApprovalActionCreated @@ -287,6 +294,56 @@ func (s chickinService) DeleteOne(c *fiber.Ctx, id uint) error { return nil } +func (s chickinService) calculateAvailableQuantity(ctx *fiber.Ctx, projectFlockKandangID uint, productWarehouse *entity.ProductWarehouse, category string) (float64, error) { + availableQty := productWarehouse.Quantity + + if category == string(utils.ProjectFlockCategoryGrowing) { + var totalPendingQty float64 + + chickins, err := s.Repository.GetByProjectFlockKandangID(ctx.Context(), projectFlockKandangID) + if err == nil { + for _, chickin := range chickins { + + if chickin.ProductWarehouseId == productWarehouse.Id && chickin.DeletedAt.Time.IsZero() && chickin.PendingUsageQty > 0 { + totalPendingQty += chickin.PendingUsageQty + } + } + } + + availableQty = productWarehouse.Quantity - totalPendingQty + if availableQty < 0 { + availableQty = 0 + } + } else if category == string(utils.ProjectFlockCategoryLaying) { + var totalPopulation float64 + var totalPendingQty float64 + + populations, err := s.ProjectflockPopulationRepo.GetByProjectFlockKandangIDAndProductWarehouseID(ctx.Context(), projectFlockKandangID, productWarehouse.Id) + if err == nil { + for _, pop := range populations { + totalPopulation += pop.TotalQty + } + } + + chickins, err := s.Repository.GetByProjectFlockKandangID(ctx.Context(), projectFlockKandangID) + if err == nil { + for _, chickin := range chickins { + + if chickin.ProductWarehouseId == productWarehouse.Id && chickin.DeletedAt.Time.IsZero() && chickin.PendingUsageQty > 0 { + totalPendingQty += chickin.PendingUsageQty + } + } + } + + availableQty = productWarehouse.Quantity - totalPopulation - totalPendingQty + if availableQty < 0 { + availableQty = 0 + } + } + + return availableQty, nil +} + func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entity.ProjectChickin, error) { if err := s.Validate.Struct(req); err != nil { return nil, err @@ -356,7 +413,7 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit if !(strings.Contains(lower, "duplicate") || strings.Contains(lower, "unique constraint") || strings.Contains(lower, "23505")) { return err } - s.Log.Infof("ignored duplicate approval for kandang %d: %v", approvableID, err) + } if action == entity.ApprovalActionApproved { @@ -374,54 +431,76 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit return err } - warehouse, err := s.WarehouseRepo.GetByKandangID(c.Context(), kandangForApproval.KandangId) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Warehouse for kandang %d not found", kandangForApproval.KandangId)) - } - return err - } - category := strings.ToUpper(strings.TrimSpace(kandangForApproval.ProjectFlock.Category)) - var conversionCategoryCode string - switch category { - case string(utils.ProjectFlockCategoryGrowing): - conversionCategoryCode = "PULLET" - case string(utils.ProjectFlockCategoryLaying): - conversionCategoryCode = "LAYER" - default: - return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Unknown category for conversion: %s", category)) + if category == string(utils.ProjectFlockCategoryGrowing) { + warehouse, err := s.WarehouseRepo.GetByKandangID(c.Context(), kandangForApproval.KandangId) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Warehouse for kandang %d not found", kandangForApproval.KandangId)) + } + return err + } + + targetPW, err := s.getOrCreateProductWarehouse(c, warehouse.Id, "PULLET", dbTransaction, actorID) + if err != nil { + return fmt.Errorf("failed to get/create PULLET product warehouse: %w", err) + } + if err := s.convertChickinsToTarget(c, chickins, targetPW, dbTransaction, actorID); err != nil { + return err + } + } else if category == string(utils.ProjectFlockCategoryLaying) { + + warehouse, err := s.WarehouseRepo.GetByKandangID(c.Context(), kandangForApproval.KandangId) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Warehouse for kandang %d not found", kandangForApproval.KandangId)) + } + return err + } + + targetPW, err := s.getOrCreateProductWarehouse(c, warehouse.Id, "PULLET", dbTransaction, actorID) + if err != nil { + return fmt.Errorf("failed to get/create PULLET product warehouse: %w", err) + } + if err := s.convertChickinsToTarget(c, chickins, targetPW, dbTransaction, actorID); err != nil { + return err + } } + } - targetPW, err := s.getOrCreateProductWarehouse(c, warehouse.Id, conversionCategoryCode, dbTransaction, actorID) + if action == entity.ApprovalActionRejected { + + chickins, err := chickinRepoTx.GetPendingByProjectFlockKandangID(c.Context(), approvableID) if err != nil { - return fmt.Errorf("failed to get/create %s product warehouse: %w", conversionCategoryCode, err) - } - if err := s.convertChickinsToTarget(c, chickins, targetPW, dbTransaction, actorID); err != nil { - return err - } - - } else if action == entity.ApprovalActionRejected { - - chickins, err := chickinRepoTx.GetByProjectFlockKandangID(c.Context(), approvableID) - if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { - return fmt.Errorf("failed to get chickins for rejection %d: %w", approvableID, err) + return fmt.Errorf("failed to get pending chickins for rejection %d: %w", approvableID, err) } if len(chickins) == 0 { continue } + kandangForRejection, err := s.ProjectflockKandangRepo.GetByID(c.Context(), approvableID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("ProjectFlockKandang %d not found", approvableID)) + } + return err + } + + categoryForRejection := strings.ToUpper(strings.TrimSpace(kandangForRejection.ProjectFlock.Category)) + for _, chickin := range chickins { - updates := map[string]any{"quantity": chickin.PendingUsageQty} + if categoryForRejection == string(utils.ProjectFlockCategoryGrowing) { + updates := map[string]any{"quantity": gorm.Expr("quantity + ?", chickin.PendingUsageQty)} - if err := productWarehouseTx.PatchOne(c.Context(), chickin.ProductWarehouseId, updates, nil); err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Product warehouse %d not found during rejection", chickin.ProductWarehouseId)) + if err := productWarehouseTx.PatchOne(c.Context(), chickin.ProductWarehouseId, updates, nil); err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Product warehouse %d not found during rejection", chickin.ProductWarehouseId)) + } + return fmt.Errorf("failed to restore product warehouse quantity for chickin %d: %w", chickin.Id, err) } - return fmt.Errorf("failed to restore product warehouse quantity for chickin %d: %w", chickin.Id, err) } if err := chickinRepoTx.DeleteOne(c.Context(), chickin.Id); err != nil { @@ -518,6 +597,18 @@ func (s *chickinService) convertChickinsToTarget(ctx *fiber.Ctx, chickins []enti return fmt.Errorf("failed to update chickin %d qty: %w", chickin.Id, err) } + if chickin.ProductWarehouseId != targetPW.Id { + if err := productWarehouseTx.PatchOne(ctx.Context(), chickin.ProductWarehouseId, map[string]any{ + "quantity": gorm.Expr("quantity - ?", quantityToConvert), + }, nil); err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Source product warehouse %d not found", chickin.ProductWarehouseId)) + } + return fmt.Errorf("failed to deduct source warehouse quantity for chickin %d: %w", chickin.Id, err) + } + } + + // Add to target product warehouse if err := productWarehouseTx.PatchOne(ctx.Context(), targetPW.Id, map[string]any{ "quantity": gorm.Expr("quantity + ?", quantityToConvert), }, nil); err != nil { diff --git a/internal/modules/production/project-flock-kandangs/dto/project_flock_kandang.dto.go b/internal/modules/production/project-flock-kandangs/dto/project_flock_kandang.dto.go index 888964a7..b7dcdbef 100644 --- a/internal/modules/production/project-flock-kandangs/dto/project_flock_kandang.dto.go +++ b/internal/modules/production/project-flock-kandangs/dto/project_flock_kandang.dto.go @@ -41,12 +41,12 @@ type KandangCustomDTO struct { type ProductWarehouseDTO struct { Id uint `json:"id"` - Quantity float64 `json:"quantity"` Product *productDTO.ProductBaseDTO `json:"product,omitempty"` Warehouse *warehouseDTO.WarehouseBaseDTO `json:"warehouse,omitempty"` } type AvailableQtyDTO struct { + AvailableQty float64 `json:"available_qty"` ProductWarehouse *ProductWarehouseDTO `json:"product_warehouse,omitempty"` } @@ -171,10 +171,6 @@ func buildProductWarehouseFromMap(pwData map[string]interface{}) *ProductWarehou dto.Id = id } - if qty, ok := pwData["quantity"].(float64); ok { - dto.Quantity = qty - } - if pData, ok := pwData["product"].(map[string]interface{}); ok { dto.Product = buildProductFromMap(pData) } @@ -276,8 +272,7 @@ func buildAvailableQtys(chickins []entity.ProjectChickin) []AvailableQtyDTO { } pwDTO := &ProductWarehouseDTO{ - Id: ch.ProductWarehouse.Id, - Quantity: ch.ProductWarehouse.Quantity, + Id: ch.ProductWarehouse.Id, } if ch.ProductWarehouse.Product.Id != 0 { @@ -324,7 +319,13 @@ func buildAvailableQtysFromRaw(availableQtysRaw []map[string]interface{}) []Avai } pwDTO := buildProductWarehouseFromMap(pwData) + availableQty := 0.0 + if qty, ok := v["available_qty"].(float64); ok { + availableQty = qty + } + result[i] = AvailableQtyDTO{ + AvailableQty: availableQty, ProductWarehouse: pwDTO, } } diff --git a/internal/modules/production/project-flock-kandangs/module.go b/internal/modules/production/project-flock-kandangs/module.go index 2457f691..160cec5e 100644 --- a/internal/modules/production/project-flock-kandangs/module.go +++ b/internal/modules/production/project-flock-kandangs/module.go @@ -24,6 +24,7 @@ type ProjectFlockKandangModule struct{} func (ProjectFlockKandangModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) { projectFlockKandangRepo := rProjectFlockKandang.NewProjectFlockKandangRepository(db) + projectFlockPopulationRepo := rProjectFlockKandang.NewProjectFlockPopulationRepository(db) userRepo := rUser.NewUserRepository(db) warehouseRepo := rWarehouse.NewWarehouseRepository(db) productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db) @@ -35,7 +36,7 @@ func (ProjectFlockKandangModule) RegisterRoutes(router fiber.Router, db *gorm.DB panic(fmt.Sprintf("failed to register project flock kandang approval workflow: %v", err)) } - projectFlockKandangService := sProjectFlockKandang.NewProjectFlockKandangService(projectFlockKandangRepo, approvalService, warehouseRepo, productWarehouseRepo, validate) + projectFlockKandangService := sProjectFlockKandang.NewProjectFlockKandangService(projectFlockKandangRepo, approvalService, warehouseRepo, productWarehouseRepo, projectFlockPopulationRepo, validate) userService := sUser.NewUserService(userRepo, validate) ProjectFlockKandangRoutes(router, userService, projectFlockKandangService) diff --git a/internal/modules/production/project-flock-kandangs/services/project_flock_kandang.service.go b/internal/modules/production/project-flock-kandangs/services/project_flock_kandang.service.go index 6258e34a..ec292d90 100644 --- a/internal/modules/production/project-flock-kandangs/services/project_flock_kandang.service.go +++ b/internal/modules/production/project-flock-kandangs/services/project_flock_kandang.service.go @@ -29,9 +29,10 @@ type projectFlockKandangService struct { ApprovalSvc commonSvc.ApprovalService WarehouseRepo rWarehouse.WarehouseRepository ProductWarehouseRepo rProductWarehouse.ProductWarehouseRepository + PopulationRepo repository.ProjectFlockPopulationRepository } -func NewProjectFlockKandangService(repo repository.ProjectFlockKandangRepository, approvalSvc commonSvc.ApprovalService, warehouseRepo rWarehouse.WarehouseRepository, productWarehouseRepo rProductWarehouse.ProductWarehouseRepository, validate *validator.Validate) ProjectFlockKandangService { +func NewProjectFlockKandangService(repo repository.ProjectFlockKandangRepository, approvalSvc commonSvc.ApprovalService, warehouseRepo rWarehouse.WarehouseRepository, productWarehouseRepo rProductWarehouse.ProductWarehouseRepository, populationRepo repository.ProjectFlockPopulationRepository, validate *validator.Validate) ProjectFlockKandangService { return &projectFlockKandangService{ Log: utils.Log, Validate: validate, @@ -39,6 +40,7 @@ func NewProjectFlockKandangService(repo repository.ProjectFlockKandangRepository ApprovalSvc: approvalSvc, WarehouseRepo: warehouseRepo, ProductWarehouseRepo: productWarehouseRepo, + PopulationRepo: populationRepo, } } @@ -114,32 +116,80 @@ func (s projectFlockKandangService) getAvailableQuantities(c *fiber.Ctx, project var result []map[string]interface{} for _, pw := range products { - if pw.Quantity > 0 { - - productData := map[string]interface{}{ - "id": pw.Product.Id, - "name": pw.Product.Name, - } - - warehouseData := map[string]interface{}{ - "id": pw.Warehouse.Id, - "name": pw.Warehouse.Name, - "type": pw.Warehouse.Type, - } - - productWarehouseData := map[string]interface{}{ - "id": pw.Id, - "quantity": pw.Quantity, - "product": productData, - "warehouse": warehouseData, - } - - result = append(result, map[string]interface{}{ - "available_qty": pw.Quantity, - "product_warehouse": productWarehouseData, - }) + availableQty, err := s.calculateAvailableQuantityForProductWarehouse(c, projectFlockKandang, &pw) + if err != nil { + s.Log.Warnf("Failed to calculate available quantity for product warehouse %d: %v", pw.Id, err) } + + // Only include product warehouse if available_qty > 0 + if availableQty <= 0 { + continue + } + + productData := map[string]interface{}{ + "id": pw.Product.Id, + "name": pw.Product.Name, + } + + warehouseData := map[string]interface{}{ + "id": pw.Warehouse.Id, + "name": pw.Warehouse.Name, + "type": pw.Warehouse.Type, + } + + productWarehouseData := map[string]interface{}{ + "id": pw.Id, + "product": productData, + "warehouse": warehouseData, + } + + result = append(result, map[string]interface{}{ + "available_qty": availableQty, + "product_warehouse": productWarehouseData, + }) } return result, nil } + +func (s projectFlockKandangService) calculateAvailableQuantityForProductWarehouse(c *fiber.Ctx, projectFlockKandang *entity.ProjectFlockKandang, productWarehouse *entity.ProductWarehouse) (float64, error) { + availableQty := productWarehouse.Quantity + + if projectFlockKandang.ProjectFlock.Category == string(utils.ProjectFlockCategoryGrowing) { + var totalPendingQty float64 + + for _, chickin := range projectFlockKandang.Chickins { + if chickin.ProductWarehouseId == productWarehouse.Id && chickin.DeletedAt.Time.IsZero() && chickin.PendingUsageQty > 0 { + totalPendingQty += chickin.PendingUsageQty + } + } + + availableQty = productWarehouse.Quantity - totalPendingQty + if availableQty < 0 { + availableQty = 0 + } + } else if projectFlockKandang.ProjectFlock.Category == string(utils.ProjectFlockCategoryLaying) { + var totalPopulation float64 + var totalPendingQty float64 + + populations, err := s.PopulationRepo.GetByProjectFlockKandangIDAndProductWarehouseID(c.Context(), projectFlockKandang.Id, productWarehouse.Id) + if err == nil { + for _, pop := range populations { + totalPopulation += pop.TotalQty + } + } + + for _, chickin := range projectFlockKandang.Chickins { + if chickin.ProductWarehouseId == productWarehouse.Id && chickin.DeletedAt.Time.IsZero() && chickin.PendingUsageQty > 0 { + totalPendingQty += chickin.PendingUsageQty + } + } + + availableQty = productWarehouse.Quantity - totalPopulation - totalPendingQty + if availableQty < 0 { + availableQty = 0 + } + } + + return availableQty, nil +} diff --git a/internal/modules/production/project_flocks/repositories/project_flock_population_repository.go b/internal/modules/production/project_flocks/repositories/project_flock_population_repository.go index fdb1249f..2f622656 100644 --- a/internal/modules/production/project_flocks/repositories/project_flock_population_repository.go +++ b/internal/modules/production/project_flocks/repositories/project_flock_population_repository.go @@ -12,6 +12,8 @@ type ProjectFlockPopulationRepository interface { // domain-specific GetByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (*entity.ProjectFlockPopulation, error) ExistsByProjectChickinID(ctx context.Context, projectChickinID uint) (bool, error) + GetByProjectChickinIDAndProductWarehouseID(ctx context.Context, projectChickinID uint, productWarehouseID uint) ([]entity.ProjectFlockPopulation, error) + GetByProjectFlockKandangIDAndProductWarehouseID(ctx context.Context, projectFlockKandangID uint, productWarehouseID uint) ([]entity.ProjectFlockPopulation, error) // subset of base repository methods used by services CreateOne(ctx context.Context, entity *entity.ProjectFlockPopulation, modifier func(*gorm.DB) *gorm.DB) error @@ -64,3 +66,26 @@ func (r *projectFlockPopulationRepositoryImpl) ExistsByProjectChickinID(ctx cont } return count > 0, nil } + +func (r *projectFlockPopulationRepositoryImpl) GetByProjectChickinIDAndProductWarehouseID(ctx context.Context, projectChickinID uint, productWarehouseID uint) ([]entity.ProjectFlockPopulation, error) { + var records []entity.ProjectFlockPopulation + err := r.DB().WithContext(ctx). + Where("project_chickin_id = ? AND product_warehouse_id = ?", projectChickinID, productWarehouseID). + Find(&records).Error + if err != nil { + return nil, err + } + return records, nil +} + +func (r *projectFlockPopulationRepositoryImpl) GetByProjectFlockKandangIDAndProductWarehouseID(ctx context.Context, projectFlockKandangID uint, productWarehouseID uint) ([]entity.ProjectFlockPopulation, error) { + var records []entity.ProjectFlockPopulation + err := r.DB().WithContext(ctx). + Joins("JOIN project_chickins ON project_chickins.id = project_flock_populations.project_chickin_id"). + Where("project_chickins.project_flock_kandang_id = ? AND project_flock_populations.product_warehouse_id = ?", projectFlockKandangID, productWarehouseID). + Find(&records).Error + if err != nil { + return nil, err + } + return records, nil +} diff --git a/internal/modules/production/transfer_layings/controllers/transfer_laying.controller.go b/internal/modules/production/transfer_layings/controllers/transfer_laying.controller.go index 01f49867..f962c263 100644 --- a/internal/modules/production/transfer_layings/controllers/transfer_laying.controller.go +++ b/internal/modules/production/transfer_layings/controllers/transfer_laying.controller.go @@ -24,9 +24,12 @@ func NewTransferLayingController(transferLayingService service.TransferLayingSer func (u *TransferLayingController) GetAll(c *fiber.Ctx) error { 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), + SourceProjectFlockId: uint(c.QueryInt("source_project_flock_id", 0)), + TargetProjectFlockId: uint(c.QueryInt("target_project_flock_id", 0)), + TransferDateFrom: c.Query("transfer_date_from", ""), + TransferDateTo: c.Query("transfer_date_to", ""), } if query.Page < 1 || query.Limit < 1 { diff --git a/internal/modules/production/transfer_layings/repositories/laying_transfer.repository.go b/internal/modules/production/transfer_layings/repositories/laying_transfer.repository.go index e92e8f95..2e5437da 100644 --- a/internal/modules/production/transfer_layings/repositories/laying_transfer.repository.go +++ b/internal/modules/production/transfer_layings/repositories/laying_transfer.repository.go @@ -1,6 +1,8 @@ package repository import ( + "context" + "gitlab.com/mbugroup/lti-api.git/internal/common/repository" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" "gorm.io/gorm" @@ -8,15 +10,29 @@ import ( type TransferLayingRepository interface { repository.BaseRepository[entity.LayingTransfer] - + GetByTransferNumber(ctx context.Context, transferNumber string) (*entity.LayingTransfer, error) } type TransferLayingRepositoryImpl struct { *repository.BaseRepositoryImpl[entity.LayingTransfer] + db *gorm.DB } func NewTransferLayingRepository(db *gorm.DB) TransferLayingRepository { return &TransferLayingRepositoryImpl{ BaseRepositoryImpl: repository.NewBaseRepository[entity.LayingTransfer](db), + db: db, } } + +func (r *TransferLayingRepositoryImpl) GetByTransferNumber(ctx context.Context, transferNumber string) (*entity.LayingTransfer, error) { + var transfer entity.LayingTransfer + err := r.db.WithContext(ctx). + Where("transfer_number = ?", transferNumber). + Where("deleted_at IS NULL"). + First(&transfer).Error + if err != nil { + return nil, err + } + return &transfer, nil +} diff --git a/internal/modules/production/transfer_layings/repositories/laying_transfer_source.repository.go b/internal/modules/production/transfer_layings/repositories/laying_transfer_source.repository.go new file mode 100644 index 00000000..6c52683e --- /dev/null +++ b/internal/modules/production/transfer_layings/repositories/laying_transfer_source.repository.go @@ -0,0 +1,38 @@ +package repository + +import ( + "context" + + "gitlab.com/mbugroup/lti-api.git/internal/common/repository" + entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + "gorm.io/gorm" +) + +type LayingTransferSourceRepository interface { + repository.BaseRepository[entity.LayingTransferSource] + GetByLayingTransferId(ctx context.Context, layingTransferId uint) ([]entity.LayingTransferSource, error) +} + +type LayingTransferSourceRepositoryImpl struct { + *repository.BaseRepositoryImpl[entity.LayingTransferSource] + db *gorm.DB +} + +func NewLayingTransferSourceRepository(db *gorm.DB) LayingTransferSourceRepository { + return &LayingTransferSourceRepositoryImpl{ + BaseRepositoryImpl: repository.NewBaseRepository[entity.LayingTransferSource](db), + db: db, + } +} + +func (r *LayingTransferSourceRepositoryImpl) GetByLayingTransferId(ctx context.Context, layingTransferId uint) ([]entity.LayingTransferSource, error) { + var sources []entity.LayingTransferSource + err := r.db.WithContext(ctx). + Where("laying_transfer_id = ?", layingTransferId). + Order("created_at DESC"). + Find(&sources).Error + if err != nil { + return nil, err + } + return sources, nil +} diff --git a/internal/modules/production/transfer_layings/repositories/laying_transfer_target.repository.go b/internal/modules/production/transfer_layings/repositories/laying_transfer_target.repository.go new file mode 100644 index 00000000..486008cc --- /dev/null +++ b/internal/modules/production/transfer_layings/repositories/laying_transfer_target.repository.go @@ -0,0 +1,38 @@ +package repository + +import ( + "context" + + "gitlab.com/mbugroup/lti-api.git/internal/common/repository" + entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + "gorm.io/gorm" +) + +type LayingTransferTargetRepository interface { + repository.BaseRepository[entity.LayingTransferTarget] + GetByLayingTransferId(ctx context.Context, layingTransferId uint) ([]entity.LayingTransferTarget, error) +} + +type LayingTransferTargetRepositoryImpl struct { + *repository.BaseRepositoryImpl[entity.LayingTransferTarget] + db *gorm.DB +} + +func NewLayingTransferTargetRepository(db *gorm.DB) LayingTransferTargetRepository { + return &LayingTransferTargetRepositoryImpl{ + BaseRepositoryImpl: repository.NewBaseRepository[entity.LayingTransferTarget](db), + db: db, + } +} + +func (r *LayingTransferTargetRepositoryImpl) GetByLayingTransferId(ctx context.Context, layingTransferId uint) ([]entity.LayingTransferTarget, error) { + var targets []entity.LayingTransferTarget + err := r.db.WithContext(ctx). + Where("laying_transfer_id = ?", layingTransferId). + Order("created_at DESC"). + Find(&targets).Error + if err != nil { + return nil, err + } + return targets, nil +} diff --git a/internal/modules/production/transfer_layings/services/transfer_laying.service.go b/internal/modules/production/transfer_layings/services/transfer_laying.service.go index bc66bd13..0a5b5dd2 100644 --- a/internal/modules/production/transfer_layings/services/transfer_laying.service.go +++ b/internal/modules/production/transfer_layings/services/transfer_laying.service.go @@ -55,8 +55,17 @@ func (s transferLayingService) GetAll(c *fiber.Ctx, params *validation.Query) ([ transferLayings, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB { db = s.withRelations(db) - if params.Search != "" { - return db.Where("name LIKE ?", "%"+params.Search+"%") + if params.SourceProjectFlockId != 0 { + db = db.Where("from_project_flock_id = ?", params.SourceProjectFlockId) + } + if params.TargetProjectFlockId != 0 { + db = db.Where("to_project_flock_id = ?", params.TargetProjectFlockId) + } + if params.TransferDateFrom != "" { + db = db.Where("transfer_date >= ?", params.TransferDateFrom) + } + if params.TransferDateTo != "" { + db = db.Where("transfer_date <= ?", params.TransferDateTo) } return db.Order("created_at DESC").Order("updated_at DESC") }) @@ -92,9 +101,19 @@ func (s *transferLayingService) CreateOne(c *fiber.Ctx, req *validation.Create) return nil, err } - for _, detail := range req.Details { + // Validate source kandangs + for _, detail := range req.SourceKandangs { if err := common.EnsureRelations(c.Context(), - common.RelationCheck{Name: "Project Flock Kandang", ID: &detail.SourceProjectFlockKandangId, Exists: s.ProjectFlockKandangRepo.IdExists}, + common.RelationCheck{Name: "Source Project Flock Kandang", ID: &detail.ProjectFlockKandangId, Exists: s.ProjectFlockKandangRepo.IdExists}, + ); err != nil { + return nil, err + } + } + + // Validate target kandangs + for _, detail := range req.TargetKandangs { + if err := common.EnsureRelations(c.Context(), + common.RelationCheck{Name: "Target Project Flock Kandang", ID: &detail.ProjectFlockKandangId, Exists: s.ProjectFlockKandangRepo.IdExists}, ); err != nil { return nil, err } @@ -106,7 +125,7 @@ func (s *transferLayingService) CreateOne(c *fiber.Ctx, req *validation.Create) } var totalQty float64 - for _, item := range req.Details { + for _, item := range req.SourceKandangs { totalQty += item.Quantity } @@ -142,8 +161,12 @@ func (s transferLayingService) UpdateOne(c *fiber.Ctx, req *validation.Update, i updateBody := make(map[string]any) - if req.Name != nil { - updateBody["name"] = *req.Name + if req.TransferDate != nil { + updateBody["transfer_date"] = *req.TransferDate + } + + if req.Reason != nil { + updateBody["notes"] = *req.Reason } if len(updateBody) == 0 { diff --git a/internal/modules/production/transfer_layings/validations/transfer_laying.validation.go b/internal/modules/production/transfer_layings/validations/transfer_laying.validation.go index 3b83132f..1d291ac5 100644 --- a/internal/modules/production/transfer_layings/validations/transfer_laying.validation.go +++ b/internal/modules/production/transfer_layings/validations/transfer_laying.validation.go @@ -1,24 +1,34 @@ package validation -type CreateDetail struct { - SourceProjectFlockKandangId uint `json:"source_project_flock_kandang_id" validate:"required"` - Quantity float64 `json:"quantity" validate:"required,gt=0"` +type SourceKandangDetail struct { + ProjectFlockKandangId uint `json:"project_flock_kandang_id" validate:"required"` + Quantity float64 `json:"quantity" validate:"required,gt=0"` +} + +type TargetKandangDetail struct { + ProjectFlockKandangId uint `json:"project_flock_kandang_id" validate:"required"` + Quantity float64 `json:"quantity" validate:"required,gt=0"` } type Create struct { - TransferDate string `json:"transfer_date" validate:"required,datetime=2006-01-02"` - SourceProjectFlockId uint `json:"source_project_flock_id" validate:"required"` - TargetProjectFlockId uint `json:"target_project_flock_id" validate:"required"` - Details []CreateDetail `json:"details" validate:"required,min=1,dive,required"` - Reason string `json:"reason" validate:"omitempty,max=1000"` + TransferDate string `json:"transfer_date" validate:"required,datetime=2006-01-02"` + SourceProjectFlockId uint `json:"source_project_flock_id" validate:"required"` + TargetProjectFlockId uint `json:"target_project_flock_id" validate:"required"` + SourceKandangs []SourceKandangDetail `json:"source_kandangs" validate:"required,min=1,dive,required"` + TargetKandangs []TargetKandangDetail `json:"target_kandangs" validate:"required,min=1,dive,required"` + Reason string `json:"reason" validate:"omitempty,max=1000"` } type Update struct { - Name *string `json:"name,omitempty" validate:"omitempty"` + TransferDate *string `json:"transfer_date,omitempty" validate:"omitempty,datetime=2006-01-02"` + Reason *string `json:"reason,omitempty" validate:"omitempty,max=1000"` } type Query struct { - Page int `query:"page" validate:"omitempty,number,min=1,gt=0"` - Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"` - Search string `query:"search" validate:"omitempty,max=50"` + Page int `query:"page" validate:"omitempty,number,min=1,gt=0"` + Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"` + SourceProjectFlockId uint `query:"source_project_flock_id" validate:"omitempty"` + TargetProjectFlockId uint `query:"target_project_flock_id" validate:"omitempty"` + TransferDateFrom string `query:"transfer_date_from" validate:"omitempty,datetime=2006-01-02"` + TransferDateTo string `query:"transfer_date_to" validate:"omitempty,datetime=2006-01-02"` } From 48730e1b74231caccb6ceba1adfb89075598e033 Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Tue, 4 Nov 2025 16:34:36 +0700 Subject: [PATCH 10/12] FIX[BE]: fix error handling on chickin service to better handler --- .../chickins/services/chickin.service.go | 90 +++++++++---------- 1 file changed, 42 insertions(+), 48 deletions(-) diff --git a/internal/modules/production/chickins/services/chickin.service.go b/internal/modules/production/chickins/services/chickin.service.go index 1970354a..c43a4817 100644 --- a/internal/modules/production/chickins/services/chickin.service.go +++ b/internal/modules/production/chickins/services/chickin.service.go @@ -152,7 +152,7 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) ([]enti for _, productWarehouse := range productWarehouses { availableQty, err := s.calculateAvailableQuantity(c, req.ProjectFlockKandangId, &productWarehouse, category) if err != nil { - s.Log.Warnf("Failed to calculate available quantity for product warehouse %d: %v", productWarehouse.Id, err) + return nil, fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to calculate available quantity for product warehouse %d", productWarehouse.Id)) } if availableQty <= 0 { @@ -189,12 +189,12 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) ([]enti productWarehouseTx := s.ProductWarehouseRepo.WithTx(dbTransaction) if err := s.Repository.WithTx(dbTransaction).CreateMany(c.Context(), newChikins, nil); err != nil { - return err + return fiber.NewError(fiber.StatusInternalServerError, "Failed to create chickins") } latest, err := approvalSvcTx.LatestByTarget(c.Context(), utils.ApprovalWorkflowProjectFlockKandang, projectFlockKandang.Id, nil) if err != nil { - return err + return fiber.NewError(fiber.StatusInternalServerError, "Failed to get latest approval") } if category == string(utils.ProjectFlockCategoryLaying) { @@ -218,17 +218,29 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) ([]enti } if latest == nil { - if _, err := approvalSvcTx.CreateApproval(c.Context(), utils.ApprovalWorkflowProjectFlockKandang, projectFlockKandang.Id, utils.ProjectFlockKandangStepPengajuan, &approvalAction, actorID, nil); err != nil { - lower := strings.ToLower(err.Error()) - if !(strings.Contains(lower, "duplicate") || strings.Contains(lower, "unique constraint") || strings.Contains(lower, "23505")) { - return err + if _, err := approvalSvcTx.CreateApproval( + c.Context(), + utils.ApprovalWorkflowProjectFlockKandang, + projectFlockKandang.Id, + utils.ProjectFlockKandangStepPengajuan, + &approvalAction, + actorID, + nil); err != nil { + if !errors.Is(err, gorm.ErrDuplicatedKey) { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to create approval") } } } else if latest.StepNumber != uint16(utils.ProjectFlockKandangStepPengajuan) { - if _, err := approvalSvcTx.CreateApproval(c.Context(), utils.ApprovalWorkflowProjectFlockKandang, projectFlockKandang.Id, utils.ProjectFlockKandangStepPengajuan, &approvalAction, actorID, nil); err != nil { - lower := strings.ToLower(err.Error()) - if !(strings.Contains(lower, "duplicate") || strings.Contains(lower, "unique constraint") || strings.Contains(lower, "23505")) { - return err + if _, err := approvalSvcTx.CreateApproval( + c.Context(), + utils.ApprovalWorkflowProjectFlockKandang, + projectFlockKandang.Id, + utils.ProjectFlockKandangStepPengajuan, + &approvalAction, + actorID, + nil); err != nil { + if !errors.Is(err, gorm.ErrDuplicatedKey) { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to create approval") } } } @@ -236,7 +248,10 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) ([]enti return nil }) if err != nil { - return nil, fiber.NewError(fiber.StatusBadRequest, err.Error()) + if fiberErr, ok := err.(*fiber.Error); ok { + return nil, fiberErr + } + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to create chickins") } result := make([]entity.ProjectChickin, 0, len(newChikins)) @@ -324,7 +339,6 @@ func (s chickinService) calculateAvailableQuantity(ctx *fiber.Ctx, projectFlockK totalPopulation += pop.TotalQty } } - chickins, err := s.Repository.GetByProjectFlockKandangID(ctx.Context(), projectFlockKandangID) if err == nil { for _, chickin := range chickins { @@ -334,7 +348,6 @@ func (s chickinService) calculateAvailableQuantity(ctx *fiber.Ctx, projectFlockK } } } - availableQty = productWarehouse.Quantity - totalPopulation - totalPendingQty if availableQty < 0 { availableQty = 0 @@ -397,7 +410,6 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit productWarehouseTx := s.ProductWarehouseRepo.WithTx(dbTransaction) for _, approvableID := range approvableIDs { - actorID := uint(1) // todo nanti ambil dari auth context if _, err := approvalSvc.CreateApproval( c.Context(), @@ -408,19 +420,13 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit actorID, req.Notes, ); err != nil { - - lower := strings.ToLower(err.Error()) - if !(strings.Contains(lower, "duplicate") || strings.Contains(lower, "unique constraint") || strings.Contains(lower, "23505")) { - return err - } - + return fiber.NewError(fiber.StatusInternalServerError, "Failed to create approval") } if action == entity.ApprovalActionApproved { - chickins, err := chickinRepoTx.GetByProjectFlockKandangID(c.Context(), approvableID) if err != nil { - return err + return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to get chickins for approval %d", approvableID)) } kandangForApproval, err := s.ProjectflockKandangRepo.GetByID(c.Context(), approvableID) @@ -428,7 +434,7 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit if errors.Is(err, gorm.ErrRecordNotFound) { return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("ProjectFlockKandang %d not found", approvableID)) } - return err + return fiber.NewError(fiber.StatusInternalServerError, "Failed to get ProjectFlockKandang") } category := strings.ToUpper(strings.TrimSpace(kandangForApproval.ProjectFlock.Category)) @@ -439,41 +445,38 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit if errors.Is(err, gorm.ErrRecordNotFound) { return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Warehouse for kandang %d not found", kandangForApproval.KandangId)) } - return err + return fiber.NewError(fiber.StatusInternalServerError, "Failed to get warehouse") } targetPW, err := s.getOrCreateProductWarehouse(c, warehouse.Id, "PULLET", dbTransaction, actorID) if err != nil { - return fmt.Errorf("failed to get/create PULLET product warehouse: %w", err) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to get/create PULLET product warehouse") } if err := s.convertChickinsToTarget(c, chickins, targetPW, dbTransaction, actorID); err != nil { - return err + return fiber.NewError(fiber.StatusInternalServerError, "Failed to convert chickins to target") } } else if category == string(utils.ProjectFlockCategoryLaying) { - warehouse, err := s.WarehouseRepo.GetByKandangID(c.Context(), kandangForApproval.KandangId) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Warehouse for kandang %d not found", kandangForApproval.KandangId)) } - return err + return fiber.NewError(fiber.StatusInternalServerError, "Failed to get warehouse") } targetPW, err := s.getOrCreateProductWarehouse(c, warehouse.Id, "PULLET", dbTransaction, actorID) if err != nil { - return fmt.Errorf("failed to get/create PULLET product warehouse: %w", err) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to get/create PULLET product warehouse") } if err := s.convertChickinsToTarget(c, chickins, targetPW, dbTransaction, actorID); err != nil { - return err + return fiber.NewError(fiber.StatusInternalServerError, "Failed to convert chickins to target") } } } - if action == entity.ApprovalActionRejected { - chickins, err := chickinRepoTx.GetPendingByProjectFlockKandangID(c.Context(), approvableID) if err != nil { - return fmt.Errorf("failed to get pending chickins for rejection %d: %w", approvableID, err) + return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to get pending chickins for rejection %d", approvableID)) } if len(chickins) == 0 { @@ -485,7 +488,7 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit if errors.Is(err, gorm.ErrRecordNotFound) { return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("ProjectFlockKandang %d not found", approvableID)) } - return err + return fiber.NewError(fiber.StatusInternalServerError, "Failed to get ProjectFlockKandang") } categoryForRejection := strings.ToUpper(strings.TrimSpace(kandangForRejection.ProjectFlock.Category)) @@ -499,15 +502,14 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit if errors.Is(err, gorm.ErrRecordNotFound) { return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Product warehouse %d not found during rejection", chickin.ProductWarehouseId)) } - return fmt.Errorf("failed to restore product warehouse quantity for chickin %d: %w", chickin.Id, err) + return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to restore product warehouse quantity for chickin %d", chickin.Id)) } } if err := chickinRepoTx.DeleteOne(c.Context(), chickin.Id); err != nil { if !errors.Is(err, gorm.ErrRecordNotFound) { - return fmt.Errorf("failed to delete rejected chickin %d: %w", chickin.Id, err) + return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to delete rejected chickin %d", chickin.Id)) } - s.Log.Infof("chickin %d already deleted during rejection", chickin.Id) } } } @@ -519,9 +521,6 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit if fiberErr, ok := err.(*fiber.Error); ok { return nil, fiberErr } - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, fiber.NewError(fiber.StatusNotFound, "Chickin not found") - } return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to record approval") } @@ -529,7 +528,7 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit for _, kandangID := range approvableIDs { var chickins []entity.ProjectChickin if err := s.Repository.DB().WithContext(c.Context()).Where("project_flock_kandang_id = ?", kandangID).Find(&chickins).Error; err != nil { - return nil, err + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to load approved chickins") } updated = append(updated, chickins...) } @@ -608,7 +607,6 @@ func (s *chickinService) convertChickinsToTarget(ctx *fiber.Ctx, chickins []enti } } - // Add to target product warehouse if err := productWarehouseTx.PatchOne(ctx.Context(), targetPW.Id, map[string]any{ "quantity": gorm.Expr("quantity + ?", quantityToConvert), }, nil); err != nil { @@ -627,11 +625,7 @@ func (s *chickinService) convertChickinsToTarget(ctx *fiber.Ctx, chickins []enti CreatedBy: actorID, } if err := ProjectFlockPopulationRepotx.CreateOne(ctx.Context(), population, nil); err != nil { - lower := strings.ToLower(err.Error()) - if !(strings.Contains(lower, "duplicate") || strings.Contains(lower, "unique constraint") || strings.Contains(lower, "23505")) { - return err - } - s.Log.Infof("ignored duplicate population for chickin %d: %v", chickin.Id, err) + return err } } From 3a5c49c511251de1c550c3502c2c3b0b17b994cc Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Wed, 5 Nov 2025 08:40:27 +0700 Subject: [PATCH 11/12] fix[BE]: fix naming on project_flock_kandang dto to standarized project --- .../dto/project_flock_kandang.dto.go | 94 +++++++++---------- 1 file changed, 46 insertions(+), 48 deletions(-) diff --git a/internal/modules/production/project-flock-kandangs/dto/project_flock_kandang.dto.go b/internal/modules/production/project-flock-kandangs/dto/project_flock_kandang.dto.go index b7dcdbef..10572c78 100644 --- a/internal/modules/production/project-flock-kandangs/dto/project_flock_kandang.dto.go +++ b/internal/modules/production/project-flock-kandangs/dto/project_flock_kandang.dto.go @@ -16,11 +16,13 @@ import ( userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto" ) +// === DTO Structs (ordered) === + type ProjectFlockKandangBaseDTO struct { Id uint `json:"id"` } -type ProjectFlockCustomDTO struct { +type ProjectFlockDTO struct { Id uint `json:"id"` Period int `json:"period"` Flock *flockDTO.FlockBaseDTO `json:"flock,omitempty"` @@ -33,7 +35,7 @@ type ProjectFlockCustomDTO struct { UpdatedAt time.Time `json:"updated_at"` } -type KandangCustomDTO struct { +type KandangDTO struct { Id uint `json:"id"` Name string `json:"name"` Status string `json:"status"` @@ -52,8 +54,8 @@ type AvailableQtyDTO struct { type ProjectFlockKandangListDTO struct { ProjectFlockKandangBaseDTO - ProjectFlock *ProjectFlockCustomDTO `json:"project_flock,omitempty"` - Kandang *KandangCustomDTO `json:"kandang,omitempty"` + ProjectFlock *ProjectFlockDTO `json:"project_flock,omitempty"` + Kandang *KandangDTO `json:"kandang,omitempty"` Chickins []chickinDTO.ChickinBaseDTO `json:"chickins,omitempty"` AvailableQtys []AvailableQtyDTO `json:"available_qtys,omitempty"` CreatedUser *userDTO.UserBaseDTO `json:"created_user,omitempty"` @@ -65,18 +67,20 @@ type ProjectFlockKandangDetailDTO struct { ProjectFlockKandangListDTO } +// === Mapper Functions (ordered) === + func ToProjectFlockKandangBaseDTO(e entity.ProjectFlockKandang) ProjectFlockKandangBaseDTO { return ProjectFlockKandangBaseDTO{ Id: e.Id, } } -func toProjectFlockCustomDTO(pf *projectFlockDTO.ProjectFlockListDTO) *ProjectFlockCustomDTO { +func toProjectFlockDTO(pf *projectFlockDTO.ProjectFlockListDTO) *ProjectFlockDTO { if pf == nil { return nil } - return &ProjectFlockCustomDTO{ + return &ProjectFlockDTO{ Id: pf.Id, Period: pf.Period, Flock: pf.Flock, @@ -99,28 +103,32 @@ func ToProjectFlockKandangListDTOWithAvailableQty(e entity.ProjectFlockKandang, return ProjectFlockKandangListDTO{ ProjectFlockKandangBaseDTO: ToProjectFlockKandangBaseDTO(e), - ProjectFlock: toProjectFlockCustomDTO(projectFlockSummary), - Kandang: buildKandangFromEntity(e.Kandang), - Chickins: buildChickins(e.Chickins), - AvailableQtys: buildAvailableQtysFromRaw(availableQtysRaw), + ProjectFlock: toProjectFlockDTO(projectFlockSummary), + Kandang: toKandangDTO(e.Kandang), + Chickins: toChickinDTOs(e.Chickins), + AvailableQtys: toAvailableQtyDTOsFromRaw(availableQtysRaw), CreatedAt: e.CreatedAt, - CreatedUser: buildCreatedUser(e.ProjectFlock), - Approval: defaultProjectFlockKandangLatestApproval(e), + CreatedUser: toCreatedUserDTO(e.ProjectFlock), + Approval: toApprovalDTO(e), } } -func toKandangCustomDTO(k projectFlockDTO.KandangWithProjectFlockIdDTO) KandangCustomDTO { - return KandangCustomDTO{ - Id: k.Id, - Name: k.Name, - Status: k.Status, +func toKandangDTO(kandang entity.Kandang) *KandangDTO { + if kandang.Id == 0 { + return nil + } + + return &KandangDTO{ + Id: kandang.Id, + Name: kandang.Name, + Status: kandang.Status, } } -func defaultProjectFlockKandangLatestApproval(e entity.ProjectFlockKandang) *approvalDTO.ApprovalBaseDTO { +func toApprovalDTO(e entity.ProjectFlockKandang) *approvalDTO.ApprovalBaseDTO { if e.LatestApproval != nil { - result := approvalDTO.ToApprovalDTO(*e.LatestApproval) - return &result + mapped := approvalDTO.ToApprovalDTO(*e.LatestApproval) + return &mapped } return nil } @@ -134,13 +142,13 @@ func ToProjectFlockKandangListDTO(e entity.ProjectFlockKandang) ProjectFlockKand return ProjectFlockKandangListDTO{ ProjectFlockKandangBaseDTO: ToProjectFlockKandangBaseDTO(e), - ProjectFlock: toProjectFlockCustomDTO(projectFlockSummary), - Kandang: buildKandangFromEntity(e.Kandang), - Chickins: buildChickins(e.Chickins), - AvailableQtys: buildAvailableQtys(e.Chickins), + ProjectFlock: toProjectFlockDTO(projectFlockSummary), + Kandang: toKandangDTO(e.Kandang), + Chickins: toChickinDTOs(e.Chickins), + AvailableQtys: toAvailableQtyDTOs(e.Chickins), CreatedAt: e.CreatedAt, - CreatedUser: buildCreatedUser(e.ProjectFlock), - Approval: defaultProjectFlockKandangLatestApproval(e), + CreatedUser: toCreatedUserDTO(e.ProjectFlock), + Approval: toApprovalDTO(e), } } @@ -158,7 +166,9 @@ func ToProjectFlockKandangDetailDTO(e entity.ProjectFlockKandang) ProjectFlockKa } } -func buildProductWarehouseFromMap(pwData map[string]interface{}) *ProductWarehouseDTO { +// === Helper Functions (ordered) === + +func toProductWarehouseDTO(pwData map[string]interface{}) *ProductWarehouseDTO { if pwData == nil { return nil } @@ -172,17 +182,17 @@ func buildProductWarehouseFromMap(pwData map[string]interface{}) *ProductWarehou } if pData, ok := pwData["product"].(map[string]interface{}); ok { - dto.Product = buildProductFromMap(pData) + dto.Product = toProductDTO(pData) } if wData, ok := pwData["warehouse"].(map[string]interface{}); ok { - dto.Warehouse = buildWarehouseFromMap(wData) + dto.Warehouse = toWarehouseDTO(wData) } return dto } -func buildProductFromMap(pData map[string]interface{}) *productDTO.ProductBaseDTO { +func toProductDTO(pData map[string]interface{}) *productDTO.ProductBaseDTO { if pData == nil { return nil } @@ -199,7 +209,7 @@ func buildProductFromMap(pData map[string]interface{}) *productDTO.ProductBaseDT return product } -func buildWarehouseFromMap(wData map[string]interface{}) *warehouseDTO.WarehouseBaseDTO { +func toWarehouseDTO(wData map[string]interface{}) *warehouseDTO.WarehouseBaseDTO { if wData == nil { return nil } @@ -219,7 +229,7 @@ func buildWarehouseFromMap(wData map[string]interface{}) *warehouseDTO.Warehouse return warehouse } -func buildCreatedUser(pf entity.ProjectFlock) *userDTO.UserBaseDTO { +func toCreatedUserDTO(pf entity.ProjectFlock) *userDTO.UserBaseDTO { if pf.CreatedUser.Id != 0 { mapped := userDTO.ToUserBaseDTO(pf.CreatedUser) return &mapped @@ -232,19 +242,7 @@ func buildCreatedUser(pf entity.ProjectFlock) *userDTO.UserBaseDTO { return nil } -func buildKandangFromEntity(kandang entity.Kandang) *KandangCustomDTO { - if kandang.Id == 0 { - return nil - } - - return &KandangCustomDTO{ - Id: kandang.Id, - Name: kandang.Name, - Status: kandang.Status, - } -} - -func buildChickins(chickins []entity.ProjectChickin) []chickinDTO.ChickinBaseDTO { +func toChickinDTOs(chickins []entity.ProjectChickin) []chickinDTO.ChickinBaseDTO { if len(chickins) == 0 { return nil } @@ -256,7 +254,7 @@ func buildChickins(chickins []entity.ProjectChickin) []chickinDTO.ChickinBaseDTO return result } -func buildAvailableQtys(chickins []entity.ProjectChickin) []AvailableQtyDTO { +func toAvailableQtyDTOs(chickins []entity.ProjectChickin) []AvailableQtyDTO { if len(chickins) == 0 { return nil } @@ -306,7 +304,7 @@ func buildAvailableQtys(chickins []entity.ProjectChickin) []AvailableQtyDTO { return result } -func buildAvailableQtysFromRaw(availableQtysRaw []map[string]interface{}) []AvailableQtyDTO { +func toAvailableQtyDTOsFromRaw(availableQtysRaw []map[string]interface{}) []AvailableQtyDTO { if len(availableQtysRaw) == 0 { return nil } @@ -318,7 +316,7 @@ func buildAvailableQtysFromRaw(availableQtysRaw []map[string]interface{}) []Avai continue } - pwDTO := buildProductWarehouseFromMap(pwData) + pwDTO := toProductWarehouseDTO(pwData) availableQty := 0.0 if qty, ok := v["available_qty"].(float64); ok { availableQty = qty From 1ee97b91a5c86dd7b4f1c546d0ff97d069d539f9 Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Wed, 5 Nov 2025 08:56:18 +0700 Subject: [PATCH 12/12] feat[BE-127]: Createing transfer laying create one, approvals, get one, get all, update, delete, but Still unfinished --- ...74825_create_laying_transfers_table.up.sql | 3 +- internal/entities/laying_transfer.go | 5 +- .../repositories/warehouse.repository.go | 14 + .../project_chickin.repository.go | 15 + .../project_flock_population_repository.go | 14 +- .../controllers/transfer_laying.controller.go | 39 +- .../dto/transfer_laying.dto.go | 233 ++++++- .../production/transfer_layings/module.go | 27 +- .../laying_transfer.repository.go | 4 + .../production/transfer_layings/route.go | 2 + .../services/transfer_laying.service.go | 620 ++++++++++++++++-- .../validations/transfer_laying.validation.go | 10 + internal/utils/constant.go | 16 + 13 files changed, 928 insertions(+), 74 deletions(-) diff --git a/internal/database/migrations/20251029074825_create_laying_transfers_table.up.sql b/internal/database/migrations/20251029074825_create_laying_transfers_table.up.sql index a01f6e02..69b0fb5d 100644 --- a/internal/database/migrations/20251029074825_create_laying_transfers_table.up.sql +++ b/internal/database/migrations/20251029074825_create_laying_transfers_table.up.sql @@ -4,7 +4,8 @@ CREATE TABLE IF NOT EXISTS laying_transfers ( from_project_flock_id BIGINT NOT NULL, to_project_flock_id BIGINT NOT NULL, transfer_date DATE NOT NULL, - total_qty NUMERIC(15, 3) NOT NULL, + pending_usage_qty NUMERIC(15, 3), + usage_qty NUMERIC(15, 3), notes TEXT, created_at TIMESTAMPTZ DEFAULT now(), updated_at TIMESTAMPTZ DEFAULT now(), diff --git a/internal/entities/laying_transfer.go b/internal/entities/laying_transfer.go index 8c2058df..dd173042 100644 --- a/internal/entities/laying_transfer.go +++ b/internal/entities/laying_transfer.go @@ -12,7 +12,8 @@ type LayingTransfer struct { FromProjectFlockId uint `gorm:"not null"` ToProjectFlockId uint `gorm:"not null"` TransferDate time.Time `gorm:"type:date;not null"` - TotalQty float64 `gorm:"type:numeric(15,3);not null"` + PendingUsageQty *float64 `gorm:"type:numeric(15,3)"` + UsageQty *float64 `gorm:"type:numeric(15,3)"` Notes string `gorm:"type:text"` CreatedBy uint `gorm:"not null"` CreatedAt time.Time `gorm:"autoCreateTime"` @@ -24,5 +25,5 @@ type LayingTransfer struct { CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"` Sources []LayingTransferSource `gorm:"foreignKey:LayingTransferId;constraint:OnDelete:CASCADE"` Targets []LayingTransferTarget `gorm:"foreignKey:LayingTransferId;constraint:OnDelete:CASCADE"` + LatestApproval *Approval `gorm:"-" json:"-"` } - diff --git a/internal/modules/master/warehouses/repositories/warehouse.repository.go b/internal/modules/master/warehouses/repositories/warehouse.repository.go index 956c30ef..e879e01a 100644 --- a/internal/modules/master/warehouses/repositories/warehouse.repository.go +++ b/internal/modules/master/warehouses/repositories/warehouse.repository.go @@ -16,6 +16,7 @@ type WarehouseRepository interface { NameExists(ctx context.Context, name string, excludeID *uint) (bool, error) IdExists(ctx context.Context, id uint) (bool, error) GetByKandangID(ctx context.Context, kandangId uint) (*entity.Warehouse, error) + GetLatestByKandangID(ctx context.Context, kandangId uint) (*entity.Warehouse, error) } type WarehouseRepositoryImpl struct { @@ -60,3 +61,16 @@ func (r *WarehouseRepositoryImpl) GetByKandangID(ctx context.Context, kandangId } return &warehouse, nil } + +func (r *WarehouseRepositoryImpl) GetLatestByKandangID(ctx context.Context, kandangId uint) (*entity.Warehouse, error) { + var warehouse entity.Warehouse + err := r.db.WithContext(ctx). + Where("kandang_id = ?", kandangId). + Where("deleted_at IS NULL"). + Order("id DESC"). + First(&warehouse).Error + if err != nil { + return nil, err + } + return &warehouse, nil +} diff --git a/internal/modules/production/chickins/repositories/project_chickin.repository.go b/internal/modules/production/chickins/repositories/project_chickin.repository.go index c0f522ad..a98dab67 100644 --- a/internal/modules/production/chickins/repositories/project_chickin.repository.go +++ b/internal/modules/production/chickins/repositories/project_chickin.repository.go @@ -13,6 +13,7 @@ type ProjectChickinRepository interface { GetFirstByProjectFlockID(ctx context.Context, projectFlockID uint) (*entity.ProjectChickin, error) GetByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) ([]entity.ProjectChickin, error) GetPendingByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) ([]entity.ProjectChickin, error) + GetTotalPendingUsageQtyByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (float64, error) } type ChickinRepositoryImpl struct { @@ -64,3 +65,17 @@ func (r *ChickinRepositoryImpl) GetPendingByProjectFlockKandangID(ctx context.Co } return chickins, nil } + +func (r *ChickinRepositoryImpl) GetTotalPendingUsageQtyByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (float64, error) { + var total float64 + err := r.db.WithContext(ctx). + Model(&entity.ProjectChickin{}). + Where("project_flock_kandang_id = ?", projectFlockKandangID). + Where("pending_usage_qty > 0"). + Select("COALESCE(SUM(pending_usage_qty), 0)"). + Row().Scan(&total) + if err != nil { + return 0, err + } + return total, nil +} diff --git a/internal/modules/production/project_flocks/repositories/project_flock_population_repository.go b/internal/modules/production/project_flocks/repositories/project_flock_population_repository.go index 2f622656..d914b128 100644 --- a/internal/modules/production/project_flocks/repositories/project_flock_population_repository.go +++ b/internal/modules/production/project_flocks/repositories/project_flock_population_repository.go @@ -10,7 +10,7 @@ import ( type ProjectFlockPopulationRepository interface { // domain-specific - GetByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (*entity.ProjectFlockPopulation, error) + GetByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) ([]entity.ProjectFlockPopulation, error) ExistsByProjectChickinID(ctx context.Context, projectChickinID uint) (bool, error) GetByProjectChickinIDAndProductWarehouseID(ctx context.Context, projectChickinID uint, productWarehouseID uint) ([]entity.ProjectFlockPopulation, error) GetByProjectFlockKandangIDAndProductWarehouseID(ctx context.Context, projectFlockKandangID uint, productWarehouseID uint) ([]entity.ProjectFlockPopulation, error) @@ -44,15 +44,17 @@ func (r *projectFlockPopulationRepositoryImpl) DB() *gorm.DB { return r.BaseRepositoryImpl.DB() } -func (r *projectFlockPopulationRepositoryImpl) GetByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (*entity.ProjectFlockPopulation, error) { - var record entity.ProjectFlockPopulation +func (r *projectFlockPopulationRepositoryImpl) GetByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) ([]entity.ProjectFlockPopulation, error) { + var records []entity.ProjectFlockPopulation err := r.DB().WithContext(ctx). - Where("project_flock_kandang_id = ?", projectFlockKandangID). - First(&record).Error + Joins("JOIN project_chickins ON project_chickins.id = project_flock_populations.project_chickin_id"). + Where("project_chickins.project_flock_kandang_id = ?", projectFlockKandangID). + Preload("ProjectChickin"). + Find(&records).Error if err != nil { return nil, err } - return &record, nil + return records, nil } func (r *projectFlockPopulationRepositoryImpl) ExistsByProjectChickinID(ctx context.Context, projectChickinID uint) (bool, error) { diff --git a/internal/modules/production/transfer_layings/controllers/transfer_laying.controller.go b/internal/modules/production/transfer_layings/controllers/transfer_laying.controller.go index f962c263..62d64127 100644 --- a/internal/modules/production/transfer_layings/controllers/transfer_laying.controller.go +++ b/internal/modules/production/transfer_layings/controllers/transfer_laying.controller.go @@ -30,6 +30,10 @@ func (u *TransferLayingController) GetAll(c *fiber.Ctx) error { TargetProjectFlockId: uint(c.QueryInt("target_project_flock_id", 0)), TransferDateFrom: c.Query("transfer_date_from", ""), TransferDateTo: c.Query("transfer_date_to", ""), + ApprovalStatus: c.Query("approval_status", ""), + TransferNumber: c.Query("transfer_number", ""), + Sort: c.Query("sort", "created_at"), + Order: c.Query("order", "desc"), } if query.Page < 1 || query.Limit < 1 { @@ -64,7 +68,7 @@ func (u *TransferLayingController) GetOne(c *fiber.Ctx) error { return fiber.NewError(fiber.StatusBadRequest, "Invalid Id") } - result, err := u.TransferLayingService.GetOne(c, uint(id)) + result, approval, err := u.TransferLayingService.GetOneWithApproval(c, uint(id)) if err != nil { return err } @@ -74,7 +78,7 @@ func (u *TransferLayingController) GetOne(c *fiber.Ctx) error { Code: fiber.StatusOK, Status: "success", Message: "Get transferLaying successfully", - Data: dto.ToTransferLayingListDTO(*result), + Data: dto.ToTransferLayingDetailDTOWithSingleApproval(*result, approval), }) } @@ -145,3 +149,34 @@ func (u *TransferLayingController) DeleteOne(c *fiber.Ctx) error { Message: "Delete transferLaying successfully", }) } + +func (u *TransferLayingController) Approval(c *fiber.Ctx) error { + req := new(validation.Approve) + if err := c.BodyParser(req); err != nil { + return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") + } + + results, err := u.TransferLayingService.Approval(c, req) + if err != nil { + return err + } + + var ( + data interface{} + message = "Submit transfer laying approval successfully" + ) + if len(results) == 1 { + data = dto.ToTransferLayingListDTO(results[0]) + } else { + message = "Submit transfer laying approvals successfully" + data = dto.ToTransferLayingListDTOs(results) + } + + return c.Status(fiber.StatusOK). + JSON(response.Success{ + Code: fiber.StatusOK, + Status: "success", + Message: message, + Data: data, + }) +} diff --git a/internal/modules/production/transfer_layings/dto/transfer_laying.dto.go b/internal/modules/production/transfer_layings/dto/transfer_laying.dto.go index 69aced33..719e458a 100644 --- a/internal/modules/production/transfer_layings/dto/transfer_laying.dto.go +++ b/internal/modules/production/transfer_layings/dto/transfer_laying.dto.go @@ -4,61 +4,252 @@ import ( "time" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + approvalDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals/dto" userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto" ) // === DTO Structs === type TransferLayingBaseDTO struct { - Id uint `json:"id"` - Name string `json:"name"` + Id uint `json:"id"` + TransferNumber string `json:"transfer_number"` + TransferDate time.Time `json:"transfer_date"` + Notes string `json:"notes"` +} + +type ProjectFlockSummaryDTO struct { + Id uint `json:"id"` + Period int `json:"period"` + Category string `json:"category"` +} + +type ProductSummaryDTO struct { + Id uint `json:"id"` + Name string `json:"name"` +} + +type WarehouseSummaryDTO struct { + Id uint `json:"id"` + Name string `json:"name"` + Type string `json:"type"` +} + +type ProductWarehouseSummaryDTO struct { + Product *ProductSummaryDTO `json:"product,omitempty"` + Warehouse *WarehouseSummaryDTO `json:"warehouse,omitempty"` +} + +type ProjectFlockKandangSummaryDTO struct { + Id uint `json:"id"` + KandangId uint `json:"kandang_id"` +} + +type LayingTransferSourceDTO struct { + SourceProjectFlockKandang *ProjectFlockKandangSummaryDTO `json:"source_project_flock_kandang,omitempty"` + Qty float64 `json:"qty"` + ProductWarehouse *ProductWarehouseSummaryDTO `json:"product_warehouse,omitempty"` + Note string `json:"note,omitempty"` +} + +type LayingTransferTargetDTO struct { + TargetProjectFlockKandang *ProjectFlockKandangSummaryDTO `json:"target_project_flock_kandang,omitempty"` + Qty float64 `json:"qty"` + ProductWarehouse *ProductWarehouseSummaryDTO `json:"product_warehouse,omitempty"` + Note string `json:"note,omitempty"` } type TransferLayingListDTO struct { TransferLayingBaseDTO - CreatedUser *userDTO.UserBaseDTO `json:"created_user"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + FromProjectFlock *ProjectFlockSummaryDTO `json:"from_project_flock,omitempty"` + ToProjectFlock *ProjectFlockSummaryDTO `json:"to_project_flock,omitempty"` + PendingUsageQty *float64 `json:"pending_usage_qty"` + UsageQty *float64 `json:"usage_qty"` + CreatedBy uint `json:"created_by"` + CreatedUser *userDTO.UserBaseDTO `json:"created_user,omitempty"` + CreatedAt time.Time `json:"created_at"` + Approval *approvalDTO.ApprovalBaseDTO `json:"approval,omitempty"` } type TransferLayingDetailDTO struct { TransferLayingListDTO + Sources []LayingTransferSourceDTO `json:"sources,omitempty"` + Targets []LayingTransferTargetDTO `json:"targets,omitempty"` + Approval *approvalDTO.ApprovalBaseDTO `json:"approval,omitempty"` } // === Mapper Functions === +func ToProjectFlockSummaryDTO(pf *entity.ProjectFlock) *ProjectFlockSummaryDTO { + if pf == nil || pf.Id == 0 { + return nil + } + + return &ProjectFlockSummaryDTO{ + Id: pf.Id, + Period: pf.Period, + Category: pf.Category, + } +} + +func ToProjectFlockKandangSummaryDTO(pfk *entity.ProjectFlockKandang) *ProjectFlockKandangSummaryDTO { + if pfk == nil || pfk.Id == 0 { + return nil + } + + return &ProjectFlockKandangSummaryDTO{ + Id: pfk.Id, + KandangId: pfk.KandangId, + } +} + +func ToProductSummaryDTO(product *entity.Product) *ProductSummaryDTO { + if product == nil || product.Id == 0 { + return nil + } + + return &ProductSummaryDTO{ + Id: product.Id, + Name: product.Name, + } +} + +func ToWarehouseSummaryDTO(warehouse *entity.Warehouse) *WarehouseSummaryDTO { + if warehouse == nil || warehouse.Id == 0 { + return nil + } + + return &WarehouseSummaryDTO{ + Id: warehouse.Id, + Name: warehouse.Name, + Type: warehouse.Type, + } +} + +func ToProductWarehouseSummaryDTO(pw *entity.ProductWarehouse) *ProductWarehouseSummaryDTO { + if pw == nil || pw.Id == 0 { + return nil + } + + return &ProductWarehouseSummaryDTO{ + Product: ToProductSummaryDTO(&pw.Product), + Warehouse: ToWarehouseSummaryDTO(&pw.Warehouse), + } +} + +func ToLayingTransferSourceDTO(source entity.LayingTransferSource) LayingTransferSourceDTO { + return LayingTransferSourceDTO{ + SourceProjectFlockKandang: ToProjectFlockKandangSummaryDTO(source.SourceProjectFlockKandang), + Qty: source.Qty, + ProductWarehouse: ToProductWarehouseSummaryDTO(source.ProductWarehouse), + Note: source.Note, + } +} + +func ToLayingTransferSourceDTOs(sources []entity.LayingTransferSource) []LayingTransferSourceDTO { + if len(sources) == 0 { + return []LayingTransferSourceDTO{} + } + result := make([]LayingTransferSourceDTO, len(sources)) + for i, s := range sources { + result[i] = ToLayingTransferSourceDTO(s) + } + return result +} + +func ToLayingTransferTargetDTO(target entity.LayingTransferTarget) LayingTransferTargetDTO { + return LayingTransferTargetDTO{ + TargetProjectFlockKandang: ToProjectFlockKandangSummaryDTO(target.TargetProjectFlockKandang), + Qty: target.Qty, + ProductWarehouse: ToProductWarehouseSummaryDTO(target.ProductWarehouse), + Note: target.Note, + } +} + +func ToLayingTransferTargetDTOs(targets []entity.LayingTransferTarget) []LayingTransferTargetDTO { + if len(targets) == 0 { + return []LayingTransferTargetDTO{} + } + result := make([]LayingTransferTargetDTO, len(targets)) + for i, t := range targets { + result[i] = ToLayingTransferTargetDTO(t) + } + return result +} + func ToTransferLayingBaseDTO(e entity.LayingTransfer) TransferLayingBaseDTO { return TransferLayingBaseDTO{ - Id: e.Id, - + Id: e.Id, + TransferNumber: e.TransferNumber, + TransferDate: e.TransferDate, + Notes: e.Notes, } } func ToTransferLayingListDTO(e entity.LayingTransfer) TransferLayingListDTO { var createdUser *userDTO.UserBaseDTO - if e.CreatedUser.Id != 0 { + if e.CreatedUser != nil && e.CreatedUser.Id != 0 { mapped := userDTO.ToUserBaseDTO(*e.CreatedUser) createdUser = &mapped } return TransferLayingListDTO{ TransferLayingBaseDTO: ToTransferLayingBaseDTO(e), - CreatedAt: e.CreatedAt, - UpdatedAt: e.UpdatedAt, - CreatedUser: createdUser, + FromProjectFlock: ToProjectFlockSummaryDTO(e.FromProjectFlock), + ToProjectFlock: ToProjectFlockSummaryDTO(e.ToProjectFlock), + PendingUsageQty: e.PendingUsageQty, + UsageQty: e.UsageQty, + CreatedBy: e.CreatedBy, + CreatedUser: createdUser, + CreatedAt: e.CreatedAt, } } -func ToTransferLayingListDTOs(e []entity.LayingTransfer) []TransferLayingListDTO { - result := make([]TransferLayingListDTO, len(e)) - for i, r := range e { - result[i] = ToTransferLayingListDTO(r) +func ToTransferLayingDetailDTO(e entity.LayingTransfer, approvals []entity.Approval) TransferLayingDetailDTO { + var latestApproval *approvalDTO.ApprovalBaseDTO + + // Use LatestApproval from entity if available + if e.LatestApproval != nil { + mapped := approvalDTO.ToApprovalDTO(*e.LatestApproval) + latestApproval = &mapped + } else if len(approvals) > 0 { + // Fallback to approvals slice + latest := approvalDTO.ToApprovalDTO(approvals[len(approvals)-1]) + latestApproval = &latest + } + + return TransferLayingDetailDTO{ + TransferLayingListDTO: ToTransferLayingListDTO(e), + Sources: ToLayingTransferSourceDTOs(e.Sources), + Targets: ToLayingTransferTargetDTOs(e.Targets), + Approval: latestApproval, + } +} + +func ToTransferLayingDetailDTOWithSingleApproval(e entity.LayingTransfer, approval *entity.Approval) TransferLayingDetailDTO { + var mappedApproval *approvalDTO.ApprovalBaseDTO + + // Prefer LatestApproval from entity + if e.LatestApproval != nil && e.LatestApproval.Id != 0 { + mapped := approvalDTO.ToApprovalDTO(*e.LatestApproval) + mappedApproval = &mapped + } else if approval != nil && approval.Id != 0 { + // Fallback to passed approval parameter + mapped := approvalDTO.ToApprovalDTO(*approval) + mappedApproval = &mapped + } + + return TransferLayingDetailDTO{ + TransferLayingListDTO: ToTransferLayingListDTO(e), + Sources: ToLayingTransferSourceDTOs(e.Sources), + Targets: ToLayingTransferTargetDTOs(e.Targets), + Approval: mappedApproval, + } +} + +func ToTransferLayingListDTOs(items []entity.LayingTransfer) []TransferLayingListDTO { + result := make([]TransferLayingListDTO, len(items)) + for i, item := range items { + result[i] = ToTransferLayingListDTO(item) } return result } - -func ToTransferLayingDetailDTO(e entity.LayingTransfer) TransferLayingDetailDTO { - return TransferLayingDetailDTO{ - TransferLayingListDTO: ToTransferLayingListDTO(e), - } -} diff --git a/internal/modules/production/transfer_layings/module.go b/internal/modules/production/transfer_layings/module.go index b309f19e..27851b71 100644 --- a/internal/modules/production/transfer_layings/module.go +++ b/internal/modules/production/transfer_layings/module.go @@ -1,14 +1,21 @@ package transfer_layings import ( + "fmt" + "github.com/go-playground/validator/v10" "github.com/gofiber/fiber/v2" "gorm.io/gorm" + commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository" + commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service" rProjectFlock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories" rTransferLaying "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/repositories" sTransferLaying "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/services" + "gitlab.com/mbugroup/lti-api.git/internal/utils" + rInventory "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories" + rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories" rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories" sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" ) @@ -20,8 +27,26 @@ func (TransferLayingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, val userRepo := rUser.NewUserRepository(db) projectFlockRepo := rProjectFlock.NewProjectflockRepository(db) projectFlockKandangRepo := rProjectFlock.NewProjectFlockKandangRepository(db) + projectFlockPopulationRepo := rProjectFlock.NewProjectFlockPopulationRepository(db) + productWarehouseRepo := rInventory.NewProductWarehouseRepository(db) + warehouseRepo := rWarehouse.NewWarehouseRepository(db) - transferLayingService := sTransferLaying.NewTransferLayingService(transferLayingRepo, projectFlockRepo, projectFlockKandangRepo, validate) + approvalRepo := commonRepo.NewApprovalRepository(db) + approvalService := commonSvc.NewApprovalService(approvalRepo) + if err := approvalService.RegisterWorkflowSteps(utils.ApprovalWorkflowTransferToLaying, utils.TransferToLayingApprovalSteps); err != nil { + panic(fmt.Sprintf("failed to register transfer to laying approval workflow: %v", err)) + } + + transferLayingService := sTransferLaying.NewTransferLayingService( + transferLayingRepo, + projectFlockRepo, + projectFlockKandangRepo, + projectFlockPopulationRepo, + productWarehouseRepo, + warehouseRepo, + approvalService, + validate, + ) userService := sUser.NewUserService(userRepo, validate) TransferLayingRoutes(router, userService, transferLayingService) diff --git a/internal/modules/production/transfer_layings/repositories/laying_transfer.repository.go b/internal/modules/production/transfer_layings/repositories/laying_transfer.repository.go index 2e5437da..3dab5120 100644 --- a/internal/modules/production/transfer_layings/repositories/laying_transfer.repository.go +++ b/internal/modules/production/transfer_layings/repositories/laying_transfer.repository.go @@ -11,6 +11,7 @@ import ( type TransferLayingRepository interface { repository.BaseRepository[entity.LayingTransfer] GetByTransferNumber(ctx context.Context, transferNumber string) (*entity.LayingTransfer, error) + IdExists(ctx context.Context, id uint) (bool, error) } type TransferLayingRepositoryImpl struct { @@ -24,6 +25,9 @@ func NewTransferLayingRepository(db *gorm.DB) TransferLayingRepository { db: db, } } +func (r *TransferLayingRepositoryImpl) IdExists(ctx context.Context, id uint) (bool, error) { + return repository.Exists[entity.LayingTransfer](ctx, r.db, id) +} func (r *TransferLayingRepositoryImpl) GetByTransferNumber(ctx context.Context, transferNumber string) (*entity.LayingTransfer, error) { var transfer entity.LayingTransfer diff --git a/internal/modules/production/transfer_layings/route.go b/internal/modules/production/transfer_layings/route.go index ee806506..13a0bc8f 100644 --- a/internal/modules/production/transfer_layings/route.go +++ b/internal/modules/production/transfer_layings/route.go @@ -19,10 +19,12 @@ func TransferLayingRoutes(v1 fiber.Router, u user.UserService, s transferLaying. // route.Get("/:id", m.Auth(u), ctrl.GetOne) // route.Patch("/:id", m.Auth(u), ctrl.UpdateOne) // route.Delete("/:id", m.Auth(u), ctrl.DeleteOne) + // route.Post("/approval", m.Auth(u), ctrl.Approval) route.Get("/", ctrl.GetAll) route.Post("/", ctrl.CreateOne) route.Get("/:id", ctrl.GetOne) route.Patch("/:id", ctrl.UpdateOne) route.Delete("/:id", ctrl.DeleteOne) + route.Post("/approvals", ctrl.Approval) } diff --git a/internal/modules/production/transfer_layings/services/transfer_laying.service.go b/internal/modules/production/transfer_layings/services/transfer_laying.service.go index 0a5b5dd2..f9fc5987 100644 --- a/internal/modules/production/transfer_layings/services/transfer_laying.service.go +++ b/internal/modules/production/transfer_layings/services/transfer_laying.service.go @@ -1,10 +1,17 @@ package service import ( + "context" "errors" + "fmt" + "strings" + "time" - common "gitlab.com/mbugroup/lti-api.git/internal/common/service" + commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository" + commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + rInventory "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories" + rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories" ProjectFlockRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories" repository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/repositories" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/validations" @@ -19,31 +26,61 @@ import ( type TransferLayingService interface { GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.LayingTransfer, int64, error) GetOne(ctx *fiber.Ctx, id uint) (*entity.LayingTransfer, error) + GetOneWithApproval(ctx *fiber.Ctx, id uint) (*entity.LayingTransfer, *entity.Approval, error) CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.LayingTransfer, error) UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.LayingTransfer, error) DeleteOne(ctx *fiber.Ctx, id uint) error + Approval(ctx *fiber.Ctx, req *validation.Approve) ([]entity.LayingTransfer, error) } type transferLayingService struct { - Log *logrus.Logger - Validate *validator.Validate - Repository repository.TransferLayingRepository - ProjectFlockRepo ProjectFlockRepository.ProjectflockRepository - ProjectFlockKandangRepo ProjectFlockRepository.ProjectFlockKandangRepository + Log *logrus.Logger + Validate *validator.Validate + Repository repository.TransferLayingRepository + ProjectFlockRepo ProjectFlockRepository.ProjectflockRepository + ProjectFlockKandangRepo ProjectFlockRepository.ProjectFlockKandangRepository + ProjectFlockPopulationRepo ProjectFlockRepository.ProjectFlockPopulationRepository + ProductWarehouseRepo rInventory.ProductWarehouseRepository + WarehouseRepo rWarehouse.WarehouseRepository + ApprovalService commonSvc.ApprovalService } -func NewTransferLayingService(repo repository.TransferLayingRepository, projectFlockRepo ProjectFlockRepository.ProjectflockRepository, projectFlockKandangRepo ProjectFlockRepository.ProjectFlockKandangRepository, validate *validator.Validate) TransferLayingService { +func NewTransferLayingService( + repo repository.TransferLayingRepository, + projectFlockRepo ProjectFlockRepository.ProjectflockRepository, + projectFlockKandangRepo ProjectFlockRepository.ProjectFlockKandangRepository, + projectFlockPopulationRepo ProjectFlockRepository.ProjectFlockPopulationRepository, + productWarehouseRepo rInventory.ProductWarehouseRepository, + warehouseRepo rWarehouse.WarehouseRepository, + approvalService commonSvc.ApprovalService, + validate *validator.Validate, +) TransferLayingService { return &transferLayingService{ - Log: utils.Log, - Validate: validate, - Repository: repo, - ProjectFlockRepo: projectFlockRepo, - ProjectFlockKandangRepo: projectFlockKandangRepo, + Log: utils.Log, + Validate: validate, + Repository: repo, + ProjectFlockRepo: projectFlockRepo, + ProjectFlockKandangRepo: projectFlockKandangRepo, + ProjectFlockPopulationRepo: projectFlockPopulationRepo, + ProductWarehouseRepo: productWarehouseRepo, + WarehouseRepo: warehouseRepo, + ApprovalService: approvalService, } } func (s transferLayingService) withRelations(db *gorm.DB) *gorm.DB { - return db.Preload("CreatedUser") + return db. + Preload("CreatedUser"). + Preload("Sources"). + Preload("Sources.SourceProjectFlockKandang"). + Preload("Sources.ProductWarehouse"). + Preload("Sources.ProductWarehouse.Product"). + Preload("Sources.ProductWarehouse.Warehouse"). + Preload("Targets"). + Preload("Targets.TargetProjectFlockKandang"). + Preload("Targets.ProductWarehouse"). + Preload("Targets.ProductWarehouse.Product"). + Preload("Targets.ProductWarehouse.Warehouse") } func (s transferLayingService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.LayingTransfer, int64, error) { @@ -67,13 +104,45 @@ func (s transferLayingService) GetAll(c *fiber.Ctx, params *validation.Query) ([ if params.TransferDateTo != "" { db = db.Where("transfer_date <= ?", params.TransferDateTo) } - return db.Order("created_at DESC").Order("updated_at DESC") + if params.TransferNumber != "" { + db = db.Where("transfer_number ILIKE ?", "%"+params.TransferNumber+"%") + } + + // Handle sort + sortField := "created_at" + if params.Sort != "" { + sortField = params.Sort + } + sortOrder := "DESC" + if params.Order == "asc" { + sortOrder = "ASC" + } + db = db.Order(fmt.Sprintf("%s %s", sortField, sortOrder)) + + return db }) if err != nil { s.Log.Errorf("Failed to get transferLayings: %+v", err) return nil, 0, err } + + // Filter by approval status if requested + if params.ApprovalStatus != "" { + var filtered []entity.LayingTransfer + approvalRepo := commonRepo.NewApprovalRepository(s.Repository.DB()) + + for _, transfer := range transferLayings { + latestApproval, err := approvalRepo.LatestByTarget(c.Context(), string(utils.ApprovalWorkflowTransferToLaying), transfer.Id, nil) + if err == nil && latestApproval != nil && latestApproval.Action != nil { + if string(*latestApproval.Action) == params.ApprovalStatus { + filtered = append(filtered, transfer) + } + } + } + transferLayings = filtered + } + return transferLayings, total, nil } @@ -86,37 +155,69 @@ func (s transferLayingService) GetOne(c *fiber.Ctx, id uint) (*entity.LayingTran s.Log.Errorf("Failed get transferLaying by id: %+v", err) return nil, err } + + // Fetch and populate latest approval + approvalRepo := commonRepo.NewApprovalRepository(s.Repository.DB()) + latestApproval, err := approvalRepo.LatestByTarget(c.Context(), string(utils.ApprovalWorkflowTransferToLaying), transferLaying.Id, nil) + if err == nil && latestApproval != nil { + transferLaying.LatestApproval = latestApproval + } + return transferLaying, nil } +func (s transferLayingService) GetOneWithApproval(c *fiber.Ctx, id uint) (*entity.LayingTransfer, *entity.Approval, error) { + transferLaying, err := s.GetOne(c, id) + if err != nil { + return nil, nil, err + } + + // Return the LatestApproval that was populated in GetOne + return transferLaying, transferLaying.LatestApproval, nil +} + func (s *transferLayingService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.LayingTransfer, error) { if err := s.Validate.Struct(req); err != nil { return nil, err } - if err := common.EnsureRelations(c.Context(), - common.RelationCheck{Name: "Source Project Flock", ID: &req.SourceProjectFlockId, Exists: s.ProjectFlockRepo.IdExists}, - common.RelationCheck{Name: "Target Project Flock", ID: &req.TargetProjectFlockId, Exists: s.ProjectFlockRepo.IdExists}, + if err := commonSvc.EnsureRelations(c.Context(), + commonSvc.RelationCheck{Name: "Source Project Flock", ID: &req.SourceProjectFlockId, Exists: s.ProjectFlockRepo.IdExists}, + commonSvc.RelationCheck{Name: "Target Project Flock", ID: &req.TargetProjectFlockId, Exists: s.ProjectFlockRepo.IdExists}, ); err != nil { return nil, err } - // Validate source kandangs for _, detail := range req.SourceKandangs { - if err := common.EnsureRelations(c.Context(), - common.RelationCheck{Name: "Source Project Flock Kandang", ID: &detail.ProjectFlockKandangId, Exists: s.ProjectFlockKandangRepo.IdExists}, + if err := commonSvc.EnsureRelations(c.Context(), + commonSvc.RelationCheck{Name: "Source Project Flock Kandang", ID: &detail.ProjectFlockKandangId, Exists: s.ProjectFlockKandangRepo.IdExists}, ); err != nil { return nil, err } + + pfk, err := s.ProjectFlockKandangRepo.GetByID(c.Context(), detail.ProjectFlockKandangId) + if err != nil { + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get source project flock kandang") + } + if pfk.ProjectFlockId != req.SourceProjectFlockId { + return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Source kandang %d does not belong to source project flock %d", detail.ProjectFlockKandangId, req.SourceProjectFlockId)) + } } - // Validate target kandangs for _, detail := range req.TargetKandangs { - if err := common.EnsureRelations(c.Context(), - common.RelationCheck{Name: "Target Project Flock Kandang", ID: &detail.ProjectFlockKandangId, Exists: s.ProjectFlockKandangRepo.IdExists}, + if err := commonSvc.EnsureRelations(c.Context(), + commonSvc.RelationCheck{Name: "Target Project Flock Kandang", ID: &detail.ProjectFlockKandangId, Exists: s.ProjectFlockKandangRepo.IdExists}, ); err != nil { return nil, err } + + pfk, err := s.ProjectFlockKandangRepo.GetByID(c.Context(), detail.ProjectFlockKandangId) + if err != nil { + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get target project flock kandang") + } + if pfk.ProjectFlockId != req.TargetProjectFlockId { + return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Target kandang %d does not belong to target project flock %d", detail.ProjectFlockKandangId, req.TargetProjectFlockId)) + } } transferDate, err := utils.ParseDateString(req.TransferDate) @@ -124,34 +225,133 @@ func (s *transferLayingService) CreateOne(c *fiber.Ctx, req *validation.Create) return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid transfer date format") } - var totalQty float64 - for _, item := range req.SourceKandangs { - totalQty += item.Quantity + var totalSourceQty, totalTargetQty float64 + sourceWarehouseMap := make(map[uint]uint) + + for _, sourceDetail := range req.SourceKandangs { + if sourceDetail.Quantity <= 0 { + return nil, fiber.NewError(fiber.StatusBadRequest, "Source kandang quantity must be greater than 0") + } + totalSourceQty += sourceDetail.Quantity + + populations, err := s.ProjectFlockPopulationRepo.GetByProjectFlockKandangID(c.Context(), sourceDetail.ProjectFlockKandangId) + if err != nil { + return nil, err + } + + var totalPopulation float64 + var productWarehouseId uint + for _, pop := range populations { + totalPopulation += pop.TotalQty + if productWarehouseId == 0 { + productWarehouseId = pop.ProductWarehouseId + } + } + + if totalPopulation == 0 { + return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Source kandang %d has no population available for transfer", sourceDetail.ProjectFlockKandangId)) + } + + if totalPopulation < sourceDetail.Quantity { + return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Source kandang %d has insufficient quantity. Available: %.0f, Requested: %.0f", sourceDetail.ProjectFlockKandangId, totalPopulation, sourceDetail.Quantity)) + } + + sourceWarehouseMap[sourceDetail.ProjectFlockKandangId] = productWarehouseId + } + + for _, targetDetail := range req.TargetKandangs { + if targetDetail.Quantity <= 0 { + return nil, fiber.NewError(fiber.StatusBadRequest, "Target kandang quantity must be greater than 0") + } + totalTargetQty += targetDetail.Quantity + } + + if totalSourceQty != totalTargetQty { + return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Total source quantity (%f) must equal total target quantity (%f)", totalSourceQty, totalTargetQty)) + } + + transferNumber := fmt.Sprintf("TL-%d", time.Now().UnixNano()) + + createBody := &entity.LayingTransfer{ + TransferNumber: transferNumber, + Notes: req.Reason, + FromProjectFlockId: req.SourceProjectFlockId, + ToProjectFlockId: req.TargetProjectFlockId, + TransferDate: transferDate, + PendingUsageQty: &totalSourceQty, + CreatedBy: 1, //todo : harus diambil dari auth } err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { - createBody := &entity.LayingTransfer{ - Notes: req.Reason, - FromProjectFlockId: req.SourceProjectFlockId, - ToProjectFlockId: req.TargetProjectFlockId, - TransferDate: transferDate, - TotalQty: totalQty, - CreatedBy: 1, //todo : harus diambil dari auth + if err := s.Repository.WithTx(dbTransaction).CreateOne(c.Context(), createBody, nil); err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to create transfer laying record") } - if err := s.Repository.WithTx(dbTransaction).CreateOne(c.Context(), createBody, nil); err != nil { - return err + productWarehouseRepoTx := s.ProductWarehouseRepo.WithTxRepo(dbTransaction) + projectFlockPopulationRepoTx := s.ProjectFlockPopulationRepo.WithTx(dbTransaction) + + for _, sourceDetail := range req.SourceKandangs { + productWarehouseId := sourceWarehouseMap[sourceDetail.ProjectFlockKandangId] + + source := entity.LayingTransferSource{ + LayingTransferId: createBody.Id, + SourceProjectFlockKandangId: sourceDetail.ProjectFlockKandangId, + Qty: sourceDetail.Quantity, + ProductWarehouseId: &productWarehouseId, + } + if err := dbTransaction.Create(&source).Error; err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to create transfer source") + } + + if err := s.reduceProjectFlockPopulation(c.Context(), projectFlockPopulationRepoTx, sourceDetail.ProjectFlockKandangId, sourceDetail.Quantity); err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to reduce project flock population") + } + + if err := productWarehouseRepoTx.PatchOne(c.Context(), productWarehouseId, map[string]any{"quantity": gorm.Expr("quantity - ?", sourceDetail.Quantity)}, nil); err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to update source warehouse quantity") + } + } + + for _, targetDetail := range req.TargetKandangs { + + targetPFK, err := s.ProjectFlockKandangRepo.GetByID(c.Context(), targetDetail.ProjectFlockKandangId) + if err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to get target project flock kandang") + } + + // Get warehouse for this kandang + targetWarehouse, err := s.WarehouseRepo.GetLatestByKandangID(c.Context(), targetPFK.KandangId) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("No warehouse found for target kandang %d", targetDetail.ProjectFlockKandangId)) + } + return fiber.NewError(fiber.StatusInternalServerError, "Failed to get target warehouse") + } + + target := entity.LayingTransferTarget{ + LayingTransferId: createBody.Id, + TargetProjectFlockKandangId: targetDetail.ProjectFlockKandangId, + Qty: targetDetail.Quantity, + ProductWarehouseId: &targetWarehouse.Id, + } + if err := dbTransaction.Create(&target).Error; err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to create transfer target") + } + } + + if err := createApprovalTransferLaying(c.Context(), dbTransaction, createBody.Id, createBody.CreatedBy); err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to create transfer approval") } return nil }) if err != nil { - return nil, err + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to create transfer laying") } - return nil, err + return s.GetOne(c, createBody.Id) } func (s transferLayingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.LayingTransfer, error) { @@ -159,6 +359,30 @@ func (s transferLayingService) UpdateOne(c *fiber.Ctx, req *validation.Update, i return nil, err } + // Check if transfer laying exists + _, err := s.Repository.GetByID(c.Context(), id, nil) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fiber.NewError(fiber.StatusNotFound, "TransferLaying not found") + } + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get transfer laying") + } + + // Check if latest approval is PENDING (not approved) + approvalRepo := commonRepo.NewApprovalRepository(s.Repository.DB()) + latestApproval, err := approvalRepo.LatestByTarget(c.Context(), string(utils.ApprovalWorkflowTransferToLaying), id, nil) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check approval status") + } + + // If latest approval exists and is APPROVED or REJECTED, cannot update + if latestApproval != nil && latestApproval.Action != nil { + action := string(*latestApproval.Action) + if action == string(entity.ApprovalActionApproved) || action == string(entity.ApprovalActionRejected) { + return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Cannot update transfer laying with status %s", action)) + } + } + updateBody := make(map[string]any) if req.TransferDate != nil { @@ -178,19 +402,333 @@ func (s transferLayingService) UpdateOne(c *fiber.Ctx, req *validation.Update, i return nil, fiber.NewError(fiber.StatusNotFound, "TransferLaying not found") } s.Log.Errorf("Failed to update transferLaying: %+v", err) - return nil, err + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to update transfer laying") } return s.GetOne(c, id) } func (s transferLayingService) DeleteOne(c *fiber.Ctx, id uint) error { - if err := s.Repository.DeleteOne(c.Context(), id); err != nil { + // Verify transfer laying exists + _, err := s.Repository.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB { + return db.Preload("Sources.ProductWarehouse").Preload("Targets") + }) + if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return fiber.NewError(fiber.StatusNotFound, "TransferLaying not found") } - s.Log.Errorf("Failed to delete transferLaying: %+v", err) - return err + return fiber.NewError(fiber.StatusInternalServerError, "Failed to get transfer laying") } + + // Check if latest approval is PENDING (not approved/rejected) + approvalRepo := commonRepo.NewApprovalRepository(s.Repository.DB()) + latestApproval, err := approvalRepo.LatestByTarget(c.Context(), string(utils.ApprovalWorkflowTransferToLaying), id, nil) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to check approval status") + } + + // If latest approval exists and is APPROVED or REJECTED, cannot delete + if latestApproval != nil && latestApproval.Action != nil { + action := string(*latestApproval.Action) + if action == string(entity.ApprovalActionApproved) || action == string(entity.ApprovalActionRejected) { + return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Cannot delete transfer laying with status %s", action)) + } + } + + // Delete in transaction to handle cascades and qty restoration + err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { + // Restore source warehouse quantities + productWarehouseRepoTx := s.ProductWarehouseRepo.WithTxRepo(dbTransaction) + + // Get source repository for detail info + sourceRepoTx := repository.NewLayingTransferSourceRepository(dbTransaction) + sources, err := sourceRepoTx.GetByLayingTransferId(c.Context(), id) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to get transfer sources") + } + + // Restore quantity for each source that was reduced + for _, source := range sources { + if source.ProductWarehouseId != nil && source.Qty > 0 { + // Add back the quantity that was transferred + if err := productWarehouseRepoTx.PatchOne(c.Context(), *source.ProductWarehouseId, map[string]any{ + "quantity": gorm.Expr("quantity + ?", source.Qty), + }, nil); err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to restore source warehouse quantity") + } + } + } + + // Restore project flock population that was reduced + projectFlockPopulationRepoTx := s.ProjectFlockPopulationRepo.WithTx(dbTransaction) + for _, source := range sources { + populations, err := projectFlockPopulationRepoTx.GetByProjectFlockKandangID(c.Context(), source.SourceProjectFlockKandangId) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to get populations for restoration") + } + + // Restore to latest populations first + remainingToRestore := source.Qty + for i := len(populations) - 1; i >= 0 && remainingToRestore > 0; i-- { + pop := populations[i] + restoreAmount := remainingToRestore + if remainingToRestore < pop.TotalQty { + // Cap restore to what can fit in this population + restoreAmount = remainingToRestore + } + + newQty := pop.TotalQty + restoreAmount + if err := projectFlockPopulationRepoTx.PatchOne(c.Context(), pop.Id, map[string]any{"total_qty": newQty}, nil); err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to restore population quantity") + } + + remainingToRestore -= restoreAmount + } + } + + // Delete the transfer laying (cascade will delete sources and targets) + if err := s.Repository.WithTx(dbTransaction).DeleteOne(c.Context(), id); err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to delete transfer laying") + } + + return nil + }) + + if err != nil { + if fiberErr, ok := err.(*fiber.Error); ok { + return fiberErr + } + s.Log.Errorf("Failed to delete transferLaying: %+v", err) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to delete transfer laying") + } + + return nil +} + +func (s transferLayingService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entity.LayingTransfer, error) { + if err := s.Validate.Struct(req); err != nil { + return nil, err + } + + actorID := uint(1) // TODO: change from auth context + var action entity.ApprovalAction + switch strings.ToUpper(strings.TrimSpace(req.Action)) { + case string(entity.ApprovalActionRejected): + action = entity.ApprovalActionRejected + case string(entity.ApprovalActionApproved): + action = entity.ApprovalActionApproved + default: + return nil, fiber.NewError(fiber.StatusBadRequest, "action must be APPROVED or REJECTED") + } + + approvableIDs := utils.UniqueUintSlice(req.ApprovableIds) + if len(approvableIDs) == 0 { + return nil, fiber.NewError(fiber.StatusBadRequest, "approvable_ids must contain at least one id") + } + + step := utils.TransferToLayingStepPengajuan + if action == entity.ApprovalActionApproved { + step = utils.TransferToLayingStepDisetujui + } + + err := s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { + + approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction)) + sourceRepoTx := repository.NewLayingTransferSourceRepository(dbTransaction) + targetRepoTx := repository.NewLayingTransferTargetRepository(dbTransaction) + productWarehouseRepoTx := s.ProductWarehouseRepo.WithTxRepo(dbTransaction) + + for _, approvableID := range approvableIDs { + transfer, err := s.Repository.GetByID(c.Context(), approvableID, nil) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("TransferLaying %d not found", approvableID)) + } + return err + } + + if _, err := approvalSvcTx.CreateApproval( + c.Context(), + utils.ApprovalWorkflowTransferToLaying, + approvableID, + step, + &action, + actorID, + req.Notes, + ); err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to record approval") + } + + if action == entity.ApprovalActionApproved && transfer.PendingUsageQty != nil && *transfer.PendingUsageQty > 0 { + + sources, err := sourceRepoTx.GetByLayingTransferId(c.Context(), approvableID) + if err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to get transfer sources") + } + + targets, err := targetRepoTx.GetByLayingTransferId(c.Context(), approvableID) + if err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to get transfer targets") + } + + if len(sources) > 0 && len(targets) > 0 { + firstSource := sources[0] + if firstSource.ProductWarehouseId == nil { + return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Source product warehouse not found for transfer %d", approvableID)) + } + + sourceWarehouse, err := productWarehouseRepoTx.GetByID(c.Context(), *firstSource.ProductWarehouseId, nil) + if err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to get source warehouse") + } + + for _, target := range targets { + + targetPFK, err := s.ProjectFlockKandangRepo.GetByID(c.Context(), target.TargetProjectFlockKandangId) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + continue + } + return fiber.NewError(fiber.StatusInternalServerError, "Failed to get target project flock kandang") + } + + targetWarehouse, err := s.WarehouseRepo.GetLatestByKandangID(c.Context(), targetPFK.KandangId) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + continue + } + return fiber.NewError(fiber.StatusInternalServerError, "Failed to get target warehouse") + } + + if _, err := s.getOrCreateProductWarehouse( + c.Context(), + dbTransaction, + sourceWarehouse.ProductId, + targetWarehouse.Id, + target.Qty, + ); err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to create or update product warehouse") + } + } + } + + usageQty := *transfer.PendingUsageQty + updateData := map[string]any{ + "usage_qty": usageQty, + "pending_usage_qty": nil, + } + if err := s.Repository.WithTx(dbTransaction).PatchOne(c.Context(), approvableID, updateData, nil); err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to update transfer laying status") + } + } + } + + return nil + }) + + if err != nil { + if fiberErr, ok := err.(*fiber.Error); ok { + return nil, fiberErr + } + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to record approval") + } + + updated := make([]entity.LayingTransfer, 0, len(approvableIDs)) + for _, approvableID := range approvableIDs { + transfer, err := s.GetOne(c, approvableID) + if err != nil { + return nil, err + } + updated = append(updated, *transfer) + } + + return updated, nil +} + +func createApprovalTransferLaying(ctx context.Context, tx *gorm.DB, transferLayingID uint, actorID uint) error { + if transferLayingID == 0 || actorID == 0 { + return nil + } + + approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(tx)) + action := entity.ApprovalActionCreated + + _, err := approvalSvc.CreateApproval( + ctx, + utils.ApprovalWorkflowTransferToLaying, + transferLayingID, + utils.TransferToLayingStepPengajuan, + &action, + actorID, + nil, + ) + return err +} + +func (s *transferLayingService) getOrCreateProductWarehouse(ctx context.Context, tx *gorm.DB, productID uint, warehouseID uint, quantity float64) (*entity.ProductWarehouse, error) { + + productWarehouseRepoTx := s.ProductWarehouseRepo.WithTxRepo(tx) + + existing, err := productWarehouseRepoTx.GetProductWarehouseByProductAndWarehouseID(ctx, productID, warehouseID) + if err == nil && existing != nil { + + if err := productWarehouseRepoTx.PatchOne(ctx, existing.Id, map[string]any{"quantity": gorm.Expr("quantity + ?", quantity)}, nil); err != nil { + return nil, err + } + return existing, nil + } + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return nil, err + } + + newWarehouse := &entity.ProductWarehouse{ + ProductId: productID, + WarehouseId: warehouseID, + Quantity: quantity, + } + + if err := productWarehouseRepoTx.CreateOne(ctx, newWarehouse, nil); err != nil { + return nil, err + } + + return newWarehouse, nil +} + +func (s *transferLayingService) reduceProjectFlockPopulation(ctx context.Context, populationRepo ProjectFlockRepository.ProjectFlockPopulationRepository, projectFlockKandangID uint, quantityToReduce float64) error { + + populations, err := populationRepo.GetByProjectFlockKandangID(ctx, projectFlockKandangID) + if err != nil { + return err + } + + if len(populations) == 0 { + return fiber.NewError(fiber.StatusBadRequest, "No populations found for reduction") + } + + remainingToReduce := quantityToReduce + + for i := len(populations) - 1; i >= 0; i-- { + if remainingToReduce <= 0 { + break + } + + pop := populations[i] + reductionAmount := remainingToReduce + if pop.TotalQty < remainingToReduce { + reductionAmount = pop.TotalQty + } + + newQty := pop.TotalQty - reductionAmount + if err := populationRepo.PatchOne(ctx, pop.Id, map[string]any{"total_qty": newQty}, nil); err != nil { + return err + } + + remainingToReduce -= reductionAmount + } + + if remainingToReduce > 0 { + return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Insufficient population to reduce. Still need to reduce: %.0f", remainingToReduce)) + } + return nil } diff --git a/internal/modules/production/transfer_layings/validations/transfer_laying.validation.go b/internal/modules/production/transfer_layings/validations/transfer_laying.validation.go index 1d291ac5..e35ba4f3 100644 --- a/internal/modules/production/transfer_layings/validations/transfer_laying.validation.go +++ b/internal/modules/production/transfer_layings/validations/transfer_laying.validation.go @@ -31,4 +31,14 @@ type Query struct { TargetProjectFlockId uint `query:"target_project_flock_id" validate:"omitempty"` TransferDateFrom string `query:"transfer_date_from" validate:"omitempty,datetime=2006-01-02"` TransferDateTo string `query:"transfer_date_to" validate:"omitempty,datetime=2006-01-02"` + ApprovalStatus string `query:"approval_status" validate:"omitempty,oneof=PENDING APPROVED REJECTED"` // Filter by latest approval status + TransferNumber string `query:"transfer_number" validate:"omitempty"` // Search by transfer number + Sort string `query:"sort" validate:"omitempty,oneof=created_at transfer_date"` // Sort by field + Order string `query:"order" validate:"omitempty,oneof=asc desc"` // Sort order +} + +type Approve struct { + Action string `json:"action" validate:"required_strict"` + ApprovableIds []uint `json:"approvable_ids" validate:"required_strict,min=1,dive,gt=0"` + Notes *string `json:"notes,omitempty" validate:"omitempty,max=500"` } diff --git a/internal/utils/constant.go b/internal/utils/constant.go index ef08eaed..4098c91c 100644 --- a/internal/utils/constant.go +++ b/internal/utils/constant.go @@ -176,6 +176,22 @@ var ProjectFlockKandangApprovalSteps = map[approvalutils.ApprovalStep]string{ ProjectFlockKandangStepDisetujui: "Disetujui", } +// ------------------------------------------------------------------- +// Transfer To laying Approval +// ------------------------------------------------------------------- +const ( + ApprovalWorkflowTransferToLaying approvalutils.ApprovalWorkflowKey = approvalutils.ApprovalWorkflowKey("TRANSFER_TO_LAYINGS") + TransferToLayingStepPengajuan approvalutils.ApprovalStep = 1 + TransferToLayingStepDisetujui approvalutils.ApprovalStep = 2 +) + +var TransferToLayingApprovalSteps = map[approvalutils.ApprovalStep]string{ + TransferToLayingStepPengajuan: "Pengajuan", + TransferToLayingStepDisetujui: "Disetujui", +} + +// ------------------------------------------------------------------- + // ------------------------------------------------------------------- // Validators // -------------------------------------------------------------------