From 34a3fc44a89265fe52166f7184857f9ae29abd18 Mon Sep 17 00:00:00 2001 From: Adnan Zahir Date: Sat, 4 Apr 2026 09:52:59 +0700 Subject: [PATCH] codex/fix: inconsistent stock options and availability --- .../product_warehouse.controller.go | 12 +++++++++ .../product_warehouse.controller_test.go | 8 +++++- .../services/product_warehouse.service.go | 26 ++++++++++++++++++ .../product_warehouse.service_test.go | 27 ++++++++++++++----- .../product_warehouse.validation.go | 2 ++ 5 files changed, 68 insertions(+), 7 deletions(-) diff --git a/internal/modules/inventory/product-warehouses/controllers/product_warehouse.controller.go b/internal/modules/inventory/product-warehouses/controllers/product_warehouse.controller.go index 5fd060dd..95ac6b82 100644 --- a/internal/modules/inventory/product-warehouses/controllers/product_warehouse.controller.go +++ b/internal/modules/inventory/product-warehouses/controllers/product_warehouse.controller.go @@ -3,6 +3,7 @@ package controller import ( "math" "strconv" + "strings" "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/dto" service "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/services" @@ -27,11 +28,13 @@ func (u *ProductWarehouseController) GetAll(c *fiber.Ctx) error { query := &validation.Query{ Page: c.QueryInt("page", 1), Limit: c.QueryInt("limit", 10), + Search: c.Query("search", ""), ProductId: uint(c.QueryInt("product_id", 0)), WarehouseId: uint(c.QueryInt("warehouse_id", 0)), LocationId: uint(c.QueryInt("location_id", 0)), Flags: c.Query("flags", ""), KandangId: uint(c.QueryInt("kandang_id", 0)), + AvailableOnly: parseBoolQuery(c.Query("available_only", "")), TransferContext: c.Query(utils.TransferContextKey, ""), StockMode: c.Query("stock_mode", ""), Type: c.Query("type", ""), @@ -61,6 +64,15 @@ func (u *ProductWarehouseController) GetAll(c *fiber.Ctx) error { }) } +func parseBoolQuery(raw string) bool { + switch strings.TrimSpace(strings.ToLower(raw)) { + case "1", "true", "yes", "y": + return true + default: + return false + } +} + func (u *ProductWarehouseController) GetOne(c *fiber.Ctx) error { param := c.Params("id") diff --git a/internal/modules/inventory/product-warehouses/controllers/product_warehouse.controller_test.go b/internal/modules/inventory/product-warehouses/controllers/product_warehouse.controller_test.go index 93a015ca..8905c47c 100644 --- a/internal/modules/inventory/product-warehouses/controllers/product_warehouse.controller_test.go +++ b/internal/modules/inventory/product-warehouses/controllers/product_warehouse.controller_test.go @@ -34,7 +34,7 @@ func TestGetAllParsesLocationID(t *testing.T) { ctrl := NewProductWarehouseController(stub) app.Get("/product-warehouses", ctrl.GetAll) - req := httptest.NewRequest("GET", "/product-warehouses?location_id=16&kandang_id=59&limit=25", nil) + req := httptest.NewRequest("GET", "/product-warehouses?location_id=16&kandang_id=59&limit=25&search=tektrol&available_only=true", nil) resp, err := app.Test(req) if err != nil { t.Fatalf("unexpected error: %v", err) @@ -54,6 +54,12 @@ func TestGetAllParsesLocationID(t *testing.T) { if stub.lastQuery.Limit != 25 { t.Fatalf("expected limit 25, got %d", stub.lastQuery.Limit) } + if stub.lastQuery.Search != "tektrol" { + t.Fatalf("expected search tektrol, got %s", stub.lastQuery.Search) + } + if !stub.lastQuery.AvailableOnly { + t.Fatalf("expected available_only true") + } } func TestStubImplementsServiceContract(t *testing.T) { diff --git a/internal/modules/inventory/product-warehouses/services/product_warehouse.service.go b/internal/modules/inventory/product-warehouses/services/product_warehouse.service.go index 15beaf58..61c4536a 100644 --- a/internal/modules/inventory/product-warehouses/services/product_warehouse.service.go +++ b/internal/modules/inventory/product-warehouses/services/product_warehouse.service.go @@ -71,6 +71,13 @@ func applyWarehouseSelectionFilter(db *gorm.DB, kandangID, locationID uint) *gor } } +func applyAvailableOnlyFilter(db *gorm.DB, availableOnly bool) *gorm.DB { + if !availableOnly { + return db + } + return db.Where("COALESCE(product_warehouses.qty, 0) > 0") +} + func (s productWarehouseService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.ProductWarehouse, int64, error) { if err := s.Validate.Struct(params); err != nil { return nil, 0, err @@ -151,12 +158,31 @@ func (s productWarehouseService) GetAll(c *fiber.Ctx, params *validation.Query) db = db.Where("product_id = ?", params.ProductId) } + db = applyAvailableOnlyFilter(db, params.AvailableOnly) + db = applyWarehouseSelectionFilter(db, params.KandangId, params.LocationId) if params.WarehouseId != 0 { db = db.Where("warehouse_id = ?", params.WarehouseId) } + if strings.TrimSpace(params.Search) != "" { + searchPattern := "%" + strings.TrimSpace(params.Search) + "%" + db = db.Where( + `( + EXISTS ( + SELECT 1 + FROM products p_search + WHERE p_search.id = product_warehouses.product_id + AND p_search.name ILIKE ? + ) + OR w_scope.name ILIKE ? + )`, + searchPattern, + searchPattern, + ) + } + if len(marketingTypes) > 0 { flagSet := make(map[string]struct{}) for _, t := range marketingTypes { diff --git a/internal/modules/inventory/product-warehouses/services/product_warehouse.service_test.go b/internal/modules/inventory/product-warehouses/services/product_warehouse.service_test.go index 0dc954df..b71b5f2f 100644 --- a/internal/modules/inventory/product-warehouses/services/product_warehouse.service_test.go +++ b/internal/modules/inventory/product-warehouses/services/product_warehouse.service_test.go @@ -49,6 +49,20 @@ func TestApplyWarehouseSelectionFilterSupportsLocationOnlyQuery(t *testing.T) { assertUintIDs(t, ids, []uint{1, 2, 3}) } +func TestApplyAvailableOnlyFilterRemovesZeroQtyRows(t *testing.T) { + db := setupProductWarehouseServiceTestDB(t) + + var ids []uint + err := applyAvailableOnlyFilter(baseProductWarehouseSelectionQuery(db), true). + Order("product_warehouses.id"). + Pluck("product_warehouses.id", &ids).Error + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + assertUintIDs(t, ids, []uint{1, 2, 4}) +} + func setupProductWarehouseServiceTestDB(t *testing.T) *gorm.DB { t.Helper() @@ -67,18 +81,19 @@ func setupProductWarehouseServiceTestDB(t *testing.T) *gorm.DB { )`, `CREATE TABLE product_warehouses ( id INTEGER PRIMARY KEY, - warehouse_id INTEGER NOT NULL + warehouse_id INTEGER NOT NULL, + qty NUMERIC NULL )`, `INSERT INTO warehouses (id, type, location_id, kandang_id, deleted_at) VALUES (1, 'KANDANG', 101, 11, NULL), (2, 'LOKASI', 101, NULL, NULL), (3, 'KANDANG', 101, 12, NULL), (4, 'LOKASI', 102, NULL, NULL)`, - `INSERT INTO product_warehouses (id, warehouse_id) VALUES - (1, 1), - (2, 2), - (3, 3), - (4, 4)`, + `INSERT INTO product_warehouses (id, warehouse_id, qty) VALUES + (1, 1, 10), + (2, 2, 20), + (3, 3, 0), + (4, 4, 15)`, } for _, stmt := range statements { diff --git a/internal/modules/inventory/product-warehouses/validations/product_warehouse.validation.go b/internal/modules/inventory/product-warehouses/validations/product_warehouse.validation.go index 0e3ad15d..164c96bc 100644 --- a/internal/modules/inventory/product-warehouses/validations/product_warehouse.validation.go +++ b/internal/modules/inventory/product-warehouses/validations/product_warehouse.validation.go @@ -15,11 +15,13 @@ type Update struct { type Query struct { Page int `query:"page" validate:"omitempty,number,min=1"` Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"` + Search string `query:"search" validate:"omitempty"` ProductId uint `query:"product_id" validate:"omitempty,number,min=1"` WarehouseId uint `query:"warehouse_id" validate:"omitempty,number,min=1"` LocationId uint `query:"location_id" validate:"omitempty,number,min=1"` Flags string `query:"flags" validate:"omitempty"` KandangId uint `query:"kandang_id" validate:"omitempty,number,min=1"` + AvailableOnly bool `query:"available_only"` TransferContext string `query:"transfer_context" validate:"omitempty,oneof=inventory_transfer"` StockMode string `query:"stock_mode" validate:"omitempty,oneof=exclude_chickin"` Type string `query:"type" validate:"omitempty"`