Merge branch 'feat/BE/sso-adjustment' into 'development'

[FIX/BE-US]add feature restrict by location and areas in roles

See merge request mbugroup/lti-api!189
This commit is contained in:
Adnan Zahir
2026-01-28 11:22:47 +07:00
50 changed files with 2584 additions and 171 deletions
@@ -74,6 +74,10 @@ func (s *adjustmentService) withRelations(db *gorm.DB) *gorm.DB {
}
func (s *adjustmentService) GetOne(c *fiber.Ctx, id uint) (*entity.AdjustmentStock, error) {
if err := m.EnsureStockLogAccess(c, s.StockLogsRepository.DB(), id); err != nil {
return nil, err
}
adjustmentStock, err := s.AdjustmentStockRepository.GetByID(c.Context(), id, s.withRelations)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
@@ -95,6 +99,9 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e
if err != nil {
return nil, err
}
if err := m.EnsureWarehouseAccess(c, s.WarehouseRepo.DB(), uint(req.WarehouseID)); err != nil {
return nil, err
}
if err := common.EnsureRelations(c.Context(),
common.RelationCheck{Name: "Product", ID: &req.ProductID, Exists: s.ProductRepo.IdExists},
common.RelationCheck{Name: "Warehouse", ID: &req.WarehouseID, Exists: s.WarehouseRepo.IdExists},
@@ -304,6 +311,19 @@ func (s *adjustmentService) AdjustmentHistory(c *fiber.Ctx, query *validation.Qu
Preload("ProductWarehouse.Warehouse").
Preload("StockLog.CreatedUser")
scope, scopeErr := m.ResolveLocationScope(c, s.AdjustmentStockRepository.DB())
if scopeErr != nil {
return nil, 0, scopeErr
}
if scope.Restrict {
if len(scope.IDs) == 0 {
return []*entity.AdjustmentStock{}, 0, nil
}
q = q.Joins("JOIN product_warehouses pw_scope ON pw_scope.id = adjustment_stocks.product_warehouse_id").
Joins("JOIN warehouses w_scope ON w_scope.id = pw_scope.warehouse_id")
q = m.ApplyScopeFilter(q, scope, "w_scope.location_id")
}
if query.ProductID > 0 {
q = q.Joins("JOIN product_warehouses ON product_warehouses.id = adjustment_stocks.product_warehouse_id").
Where("product_warehouses.product_id = ?", query.ProductID)
@@ -4,6 +4,7 @@ import (
"errors"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-stocks/validations"
productRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/repositories"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
@@ -36,19 +37,49 @@ func NewProductStockService(
}
}
func (s productStockService) withRelations(db *gorm.DB) *gorm.DB {
func (s productStockService) withRelations(db *gorm.DB, locationScope, areaScope m.ScopeFilter) *gorm.DB {
warehouseScope := func(db *gorm.DB) *gorm.DB {
if locationScope.Restrict {
db = db.Where("warehouses.location_id IN ?", locationScope.IDs)
}
if areaScope.Restrict {
db = db.Where("warehouses.area_id IN ?", areaScope.IDs)
}
return db
}
productWarehouseScope := func(db *gorm.DB) *gorm.DB {
db = db.Joins("JOIN warehouses w ON w.id = product_warehouses.warehouse_id")
if locationScope.Restrict {
db = db.Where("w.location_id IN ?", locationScope.IDs)
}
if areaScope.Restrict {
db = db.Where("w.area_id IN ?", areaScope.IDs)
}
return db
}
stockLogScope := func(db *gorm.DB) *gorm.DB {
db = db.
Joins("JOIN product_warehouses pw ON pw.id = stock_logs.product_warehouse_id").
Joins("JOIN warehouses w ON w.id = pw.warehouse_id")
if locationScope.Restrict {
db = db.Where("w.location_id IN ?", locationScope.IDs)
}
if areaScope.Restrict {
db = db.Where("w.area_id IN ?", areaScope.IDs)
}
return db.Order("stock_logs.created_at ASC")
}
return db.
Preload("CreatedUser").
Preload("Uom").
Preload("ProductCategory").
Preload("Flags").
Preload("ProductWarehouses").
Preload("ProductWarehouses.Warehouse").
Preload("ProductWarehouses", productWarehouseScope).
Preload("ProductWarehouses.Warehouse", warehouseScope).
Preload("ProductWarehouses.Warehouse.Location").
Preload("ProductWarehouses.Warehouse.Location.Area").
Preload("ProductWarehouses.StockLogs", func(db *gorm.DB) *gorm.DB {
return db.Order("created_at ASC")
}).
Preload("ProductWarehouses.StockLogs", stockLogScope).
Preload("ProductWarehouses.StockLogs.CreatedUser").
Preload("ProductSuppliers").
Preload("ProductSuppliers.Supplier", func(db *gorm.DB) *gorm.DB {
@@ -61,17 +92,40 @@ func (s productStockService) GetAll(c *fiber.Ctx, params *validation.Query) ([]e
return nil, 0, err
}
locationScope, areaScope, err := m.ResolveLocationAreaScopes(c, s.ProductRepository.DB())
if err != nil {
return nil, 0, err
}
offset := (params.Page - 1) * params.Limit
productStocks, total, err := s.ProductRepository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
db = db.Where(`EXISTS (
SELECT 1
FROM product_warehouses pw
WHERE pw.product_id = products.id
AND pw.qty > 0
)`)
if locationScope.Restrict || areaScope.Restrict {
if (locationScope.Restrict && len(locationScope.IDs) == 0) || (areaScope.Restrict && len(areaScope.IDs) == 0) {
return db.Where("1 = 0")
}
db = db.Where(`EXISTS (
SELECT 1
FROM product_warehouses pw
JOIN warehouses w ON w.id = pw.warehouse_id
WHERE pw.product_id = products.id
AND pw.qty > 0
AND (? OR w.location_id IN ?)
AND (? OR w.area_id IN ?)
)`,
!locationScope.Restrict, locationScope.IDs,
!areaScope.Restrict, areaScope.IDs,
)
} else {
db = db.Where(`EXISTS (
SELECT 1
FROM product_warehouses pw
WHERE pw.product_id = products.id
AND pw.qty > 0
)`)
}
db = s.withRelations(db)
db = s.withRelations(db, locationScope, areaScope)
if params.Search != "" {
db = db.Where("products.name ILIKE ?", "%"+params.Search+"%")
}
@@ -86,7 +140,34 @@ func (s productStockService) GetAll(c *fiber.Ctx, params *validation.Query) ([]e
}
func (s productStockService) GetOne(c *fiber.Ctx, id uint) (*entity.Product, error) {
product, err := s.ProductRepository.GetByID(c.Context(), id, s.withRelations)
locationScope, areaScope, err := m.ResolveLocationAreaScopes(c, s.ProductRepository.DB())
if err != nil {
return nil, err
}
if locationScope.Restrict || areaScope.Restrict {
if (locationScope.Restrict && len(locationScope.IDs) == 0) || (areaScope.Restrict && len(areaScope.IDs) == 0) {
return nil, fiber.NewError(fiber.StatusNotFound, "Product not found")
}
var count int64
if err := s.ProductRepository.DB().WithContext(c.Context()).
Table("product_warehouses pw").
Joins("JOIN warehouses w ON w.id = pw.warehouse_id").
Where("pw.product_id = ?", id).
Where("pw.qty > 0").
Where("(? OR w.location_id IN ?)", !locationScope.Restrict, locationScope.IDs).
Where("(? OR w.area_id IN ?)", !areaScope.Restrict, areaScope.IDs).
Count(&count).Error; err != nil {
return nil, err
}
if count == 0 {
return nil, fiber.NewError(fiber.StatusNotFound, "Product not found")
}
}
product, err := s.ProductRepository.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB {
return s.withRelations(db, locationScope, areaScope)
})
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Product not found")
}
@@ -8,6 +8,7 @@ import (
service "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/services"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/validations"
"gitlab.com/mbugroup/lti-api.git/internal/response"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
"github.com/gofiber/fiber/v2"
)
@@ -24,12 +25,13 @@ func NewProductWarehouseController(productWarehouseService service.ProductWareho
func (u *ProductWarehouseController) GetAll(c *fiber.Ctx) error {
query := &validation.Query{
Page: c.QueryInt("page", 1),
Limit: c.QueryInt("limit", 10),
ProductId: uint(c.QueryInt("product_id", 0)),
WarehouseId: uint(c.QueryInt("warehouse_id", 0)),
Flags: c.Query("flags", ""),
KandangId: uint(c.QueryInt("kandang_id", 0)),
Page: c.QueryInt("page", 1),
Limit: c.QueryInt("limit", 10),
ProductId: uint(c.QueryInt("product_id", 0)),
WarehouseId: uint(c.QueryInt("warehouse_id", 0)),
Flags: c.Query("flags", ""),
KandangId: uint(c.QueryInt("kandang_id", 0)),
TransferContext: c.Query(utils.TransferContextKey, ""),
}
if query.Page < 1 || query.Limit < 1 {
@@ -7,6 +7,7 @@ import (
"github.com/gofiber/fiber/v2"
"github.com/sirupsen/logrus"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/validations"
kandangrepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories"
@@ -53,6 +54,19 @@ func (s productWarehouseService) GetAll(c *fiber.Ctx, params *validation.Query)
return nil, 0, err
}
applyScope := true
if params.TransferContext == utils.TransferContextInventoryTransfer {
applyScope = !m.HasPermission(c, m.P_TransferCreateOne)
}
var scope m.ScopeFilter
var err error
if applyScope {
scope, err = m.ResolveLocationScope(c, s.Repository.DB())
if err != nil {
return nil, 0, err
}
}
if params.ProductId > 0 {
isProductExist, err := s.Repository.IsProductExist(c.Context(), params.ProductId)
if err != nil {
@@ -90,6 +104,17 @@ func (s productWarehouseService) GetAll(c *fiber.Ctx, params *validation.Query)
productWarehouses, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
db = s.withRelations(db)
db = db.Joins("JOIN warehouses w_scope ON product_warehouses.warehouse_id = w_scope.id").
Where("w_scope.deleted_at IS NULL")
if applyScope {
if scope.Restrict {
if len(scope.IDs) == 0 {
return db.Where("1 = 0")
}
db = db.Where("w_scope.location_id IN ?", scope.IDs)
}
}
if params.ProductId != 0 {
db = db.Where("product_id = ?", params.ProductId)
}
@@ -116,7 +141,33 @@ func (s productWarehouseService) GetAll(c *fiber.Ctx, params *validation.Query)
}
func (s productWarehouseService) GetOne(c *fiber.Ctx, id uint) (*entity.ProductWarehouse, error) {
productWarehouse, err := s.Repository.GetByID(c.Context(), id, s.withRelations)
applyScope := true
if c.Query(utils.TransferContextKey, "") == utils.TransferContextInventoryTransfer {
applyScope = !m.HasPermission(c, m.P_TransferCreateOne)
}
var scope m.ScopeFilter
var err error
if applyScope {
scope, err = m.ResolveLocationScope(c, s.Repository.DB())
if err != nil {
return nil, err
}
}
productWarehouse, err := s.Repository.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB {
db = s.withRelations(db)
db = db.Joins("JOIN warehouses w_scope ON product_warehouses.warehouse_id = w_scope.id").
Where("w_scope.deleted_at IS NULL")
if applyScope {
if scope.Restrict {
if len(scope.IDs) == 0 {
return db.Where("1 = 0")
}
db = db.Where("w_scope.location_id IN ?", scope.IDs)
}
}
return db
})
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "ProductWarehouse not found")
}
@@ -13,10 +13,11 @@ 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"`
ProductId uint `query:"product_id" validate:"omitempty,number,min=1"`
WarehouseId uint `query:"warehouse_id" validate:"omitempty,number,min=1"`
Flags string `query:"flags" validate:"omitempty"`
KandangId uint `query:"kandang_id" validate:"omitempty,number,min=1"`
Page int `query:"page" validate:"omitempty,number,min=1"`
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"`
ProductId uint `query:"product_id" validate:"omitempty,number,min=1"`
WarehouseId uint `query:"warehouse_id" validate:"omitempty,number,min=1"`
Flags string `query:"flags" validate:"omitempty"`
KandangId uint `query:"kandang_id" validate:"omitempty,number,min=1"`
TransferContext string `query:"transfer_context" validate:"omitempty,oneof=inventory_transfer"`
}
@@ -94,10 +94,24 @@ func (s transferService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entit
return nil, 0, err
}
scope, err := m.ResolveLocationScope(c, s.StockTransferRepo.DB())
if err != nil {
return nil, 0, err
}
offset := (params.Page - 1) * params.Limit
transfers, total, err := s.StockTransferRepo.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
db = s.withRelations(db)
if scope.Restrict {
if len(scope.IDs) == 0 {
return db.Where("1 = 0")
}
db = db.
Joins("JOIN warehouses w_from ON w_from.id = stock_transfers.from_warehouse_id").
Joins("JOIN warehouses w_to ON w_to.id = stock_transfers.to_warehouse_id").
Where("w_from.location_id IN ? OR w_to.location_id IN ?", scope.IDs, scope.IDs)
}
if params.Search != "" {
searchTerm := "%" + strings.TrimSpace(params.Search) + "%"
db = db.Joins("LEFT JOIN warehouses AS from_warehouses ON from_warehouses.id = stock_transfers.from_warehouse_id").
@@ -116,6 +130,28 @@ func (s transferService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entit
}
func (s transferService) GetOne(c *fiber.Ctx, id uint) (*entity.StockTransfer, error) {
scope, err := m.ResolveLocationScope(c, s.StockTransferRepo.DB())
if err != nil {
return nil, err
}
if scope.Restrict {
if len(scope.IDs) == 0 {
return nil, fiber.NewError(fiber.StatusNotFound, "Transfer not found")
}
var count int64
if err := s.StockTransferRepo.DB().WithContext(c.Context()).
Table("stock_transfers").
Joins("JOIN warehouses w_from ON w_from.id = stock_transfers.from_warehouse_id").
Joins("JOIN warehouses w_to ON w_to.id = stock_transfers.to_warehouse_id").
Where("stock_transfers.id = ?", id).
Where("w_from.location_id IN ? OR w_to.location_id IN ?", scope.IDs, scope.IDs).
Count(&count).Error; err != nil {
return nil, err
}
if count == 0 {
return nil, fiber.NewError(fiber.StatusNotFound, "Transfer not found")
}
}
transferPtr, err := s.StockTransferRepo.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB {
return s.withRelations(db)