Merge branch 'codex/sales-at-farm-level' into 'development'

codex/fix: inconsistent stock options and availability

See merge request mbugroup/lti-api!391
This commit is contained in:
Adnan Zahir
2026-04-04 10:08:24 +07:00
5 changed files with 68 additions and 7 deletions
@@ -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")
@@ -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) {
@@ -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 {
@@ -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 {
@@ -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"`