diff --git a/internal/database/migrations/20260305105044_add_ayam_flag_member_fifo_v2.down.sql b/internal/database/migrations/20260305105044_add_ayam_flag_member_fifo_v2.down.sql new file mode 100644 index 00000000..52747c92 --- /dev/null +++ b/internal/database/migrations/20260305105044_add_ayam_flag_member_fifo_v2.down.sql @@ -0,0 +1,7 @@ +BEGIN; + +DELETE FROM fifo_stock_v2_flag_members +WHERE flag_name = 'AYAM' + AND flag_group_code = 'AYAM'; + +COMMIT; diff --git a/internal/database/migrations/20260305105044_add_ayam_flag_member_fifo_v2.up.sql b/internal/database/migrations/20260305105044_add_ayam_flag_member_fifo_v2.up.sql new file mode 100644 index 00000000..28c495ed --- /dev/null +++ b/internal/database/migrations/20260305105044_add_ayam_flag_member_fifo_v2.up.sql @@ -0,0 +1,13 @@ +BEGIN; + +INSERT INTO fifo_stock_v2_flag_members(flag_name, flag_group_code, priority, is_active, created_at, updated_at) +VALUES + ('AYAM', 'AYAM', 5, TRUE, NOW(), NOW()) +ON CONFLICT (flag_name) DO UPDATE +SET + flag_group_code = EXCLUDED.flag_group_code, + priority = EXCLUDED.priority, + is_active = TRUE, + updated_at = NOW(); + +COMMIT; diff --git a/internal/modules/master/products/controllers/product.controller.go b/internal/modules/master/products/controllers/product.controller.go index 197a6b5f..050dc2d9 100644 --- a/internal/modules/master/products/controllers/product.controller.go +++ b/internal/modules/master/products/controllers/product.controller.go @@ -30,6 +30,14 @@ func (u *ProductController) GetAll(c *fiber.Ctx) error { ProductCategoryID: c.QueryInt("product_category_id", 0), } + if isDepletionParam := c.Query("is_depletion", ""); isDepletionParam != "" { + value, err := strconv.ParseBool(isDepletionParam) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, "invalid is_depletion value") + } + query.IsDepletion = &value + } + if query.Page < 1 || query.Limit < 1 { return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0") } diff --git a/internal/modules/master/products/services/product.service.go b/internal/modules/master/products/services/product.service.go index f2281d1e..454b0a93 100644 --- a/internal/modules/master/products/services/product.service.go +++ b/internal/modules/master/products/services/product.service.go @@ -31,6 +31,12 @@ type productService struct { Repository repository.ProductRepository } +var depletionProductFlags = []string{ + string(utils.FlagAyamAfkir), + string(utils.FlagAyamCulling), + string(utils.FlagAyamMati), +} + func normalizeProductFlags(raw []string) ([]string, error) { normalized, invalid := utils.NormalizeFlagsForGroup(raw, utils.FlagGroupProduct) if len(invalid) > 0 { @@ -223,12 +229,32 @@ func (s productService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity products, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB { db = s.withRelations(db) - db = db.Where("is_visible = ?", true) + // Depletion master products are system products and often stored with is_visible = false. + // When requested explicitly via is_depletion=true, include hidden records. + if params.IsDepletion == nil || !*params.IsDepletion { + db = db.Where("is_visible = ?", true) + } if params.Search != "" { - return db.Where("name ILIKE ?", "%"+params.Search+"%") + db = db.Where("name ILIKE ?", "%"+params.Search+"%") } if params.ProductCategoryID != 0 { - return db.Where("product_category_id = ?", params.ProductCategoryID) + db = db.Where("product_category_id = ?", params.ProductCategoryID) + } + if params.IsDepletion != nil { + existsQuery := ` + EXISTS ( + SELECT 1 + FROM flags f + WHERE f.flagable_type = ? + AND f.flagable_id = products.id + AND UPPER(f.name) IN ? + ) + ` + if *params.IsDepletion { + db = db.Where(existsQuery, entity.FlagableTypeProduct, depletionProductFlags) + } else { + db = db.Where("NOT "+existsQuery, entity.FlagableTypeProduct, depletionProductFlags) + } } return db.Order("created_at DESC").Order("updated_at DESC") }) diff --git a/internal/modules/master/products/validations/product.validation.go b/internal/modules/master/products/validations/product.validation.go index 0a383192..4b01e066 100644 --- a/internal/modules/master/products/validations/product.validation.go +++ b/internal/modules/master/products/validations/product.validation.go @@ -44,4 +44,5 @@ type Query struct { Limit int `query:"limit" validate:"omitempty,number,min=1"` Search string `query:"search" validate:"omitempty,max=50"` ProductCategoryID int `query:"product_category_id" validate:"omitempty,number,min=1"` + IsDepletion *bool `query:"is_depletion" validate:"omitempty"` } diff --git a/internal/modules/production/chickins/services/chickin.service.go b/internal/modules/production/chickins/services/chickin.service.go index 85127792..11daaf41 100644 --- a/internal/modules/production/chickins/services/chickin.service.go +++ b/internal/modules/production/chickins/services/chickin.service.go @@ -189,30 +189,31 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) ([]enti return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Product warehouse %d belongs to different flock. Only product warehouses with project_flock_kandang_id = NULL or = %d can be used", chickinReq.ProductWarehouseId, req.ProjectFlockKandangId)) } - if productWarehouse.Product.Id != 0 { + if productWarehouse.Product.Id != 0 { + category := strings.ToUpper(strings.TrimSpace(projectFlockKandang.ProjectFlock.Category)) + if category != string(utils.ProjectFlockCategoryGrowing) && category != string(utils.ProjectFlockCategoryLaying) { + return nil, fmt.Errorf("invalid flock category for chickin") + } - var requiredFlag utils.FlagType - if projectFlockKandang.ProjectFlock.Category == string(utils.ProjectFlockCategoryGrowing) { - requiredFlag = utils.FlagDOC - } else if projectFlockKandang.ProjectFlock.Category == string(utils.ProjectFlockCategoryLaying) { - requiredFlag = utils.FlagPullet - } else { - return nil, fmt.Errorf("invalid flock category for chickin") - } + hasAyamFlag := false + for _, flag := range productWarehouse.Product.Flags { + if utils.CanonicalFlagType(flag.Name) == utils.FlagAyam { + hasAyamFlag = true + break + } + } - hasRequiredFlag := false - for _, flag := range productWarehouse.Product.Flags { - if utils.FlagType(flag.Name) == requiredFlag { - hasRequiredFlag = true - break + if !hasAyamFlag { + return nil, fmt.Errorf( + "product warehouse %d cannot be used for %s chickin. Product must have AYAM flag (or legacy alias DOC/PULLET/LAYER) (product ID: %d, warehouse ID: %d)", + chickinReq.ProductWarehouseId, + projectFlockKandang.ProjectFlock.Category, + productWarehouse.Product.Id, + productWarehouse.Id, + ) } } - if !hasRequiredFlag { - return nil, fmt.Errorf("product warehouse %d cannot be used for %s chickin. Product must have %s flag (product ID: %d, warehouse ID: %d)", chickinReq.ProductWarehouseId, projectFlockKandang.ProjectFlock.Category, requiredFlag, productWarehouse.Product.Id, productWarehouse.Id) - } - } - chickinDate, err := utils.ParseDateString(chickinReq.ChickInDate) if err != nil { return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid ChickInDate format for product warehouse %d", chickinReq.ProductWarehouseId))