From dff9e73ab104e9adf4ca3ab3fefec80769e55c63 Mon Sep 17 00:00:00 2001 From: ragilap Date: Wed, 14 Jan 2026 13:27:52 +0700 Subject: [PATCH 1/7] [FIX/BE-US]add feature restrict by location and areas in roles --- cmd/api/main.go | 2 +- internal/middleware/auth.go | 2 +- internal/middleware/role_scope.go | 280 +++++++++++++++ .../services/daily-checklist.service.go | 11 +- .../controllers/dashboard.controller.go | 32 ++ .../dashboards/services/dashboard.service.go | 6 + .../expense_realization.repository.go | 21 +- .../expenses/services/expense.service.go | 17 +- .../services/product-stock.service.go | 56 ++- .../services/product_warehouse.service.go | 32 +- .../transfers/services/transfer.service.go | 36 ++ .../master/areas/services/area.service.go | 27 +- .../kandangs/services/kandang.service.go | 17 +- .../locations/services/location.service.go | 27 +- .../warehouses/services/warehouse.service.go | 17 +- .../services/project_flock_kandang.service.go | 29 +- .../repositories/projectflock.repository.go | 14 + .../projectflock_kandang.repository.go | 99 ++++++ .../services/projectflock.service.go | 18 +- .../purchases/services/purchase.service.go | 57 ++- .../controllers/repport.controller.go | 35 ++ .../purchase_supplier.repository.go | 32 +- .../repports/services/repport.service.go | 64 ++++ .../validations/repport.validation.go | 3 + .../sso/controllers/master_data.controller.go | 331 ++++++++++++++++++ .../modules/sso/controllers/sso.controller.go | 2 +- .../sso/controllers/user_sync.controller.go | 13 +- internal/modules/sso/route.go | 3 + .../{sso => modules/sso/verifier}/profile.go | 12 + .../{sso => modules/sso/verifier}/verifier.go | 0 30 files changed, 1258 insertions(+), 37 deletions(-) create mode 100644 internal/middleware/role_scope.go create mode 100644 internal/modules/sso/controllers/master_data.controller.go rename internal/{sso => modules/sso/verifier}/profile.go (95%) rename internal/{sso => modules/sso/verifier}/verifier.go (100%) diff --git a/cmd/api/main.go b/cmd/api/main.go index a7c278d7..76de7729 100644 --- a/cmd/api/main.go +++ b/cmd/api/main.go @@ -14,8 +14,8 @@ import ( "gitlab.com/mbugroup/lti-api.git/internal/database" "gitlab.com/mbugroup/lti-api.git/internal/middleware" "gitlab.com/mbugroup/lti-api.git/internal/modules/sso/session" + sso "gitlab.com/mbugroup/lti-api.git/internal/modules/sso/verifier" "gitlab.com/mbugroup/lti-api.git/internal/route" - "gitlab.com/mbugroup/lti-api.git/internal/sso" "gitlab.com/mbugroup/lti-api.git/internal/utils" "github.com/gofiber/fiber/v2" diff --git a/internal/middleware/auth.go b/internal/middleware/auth.go index a831c25b..1b670c14 100644 --- a/internal/middleware/auth.go +++ b/internal/middleware/auth.go @@ -7,8 +7,8 @@ import ( "gitlab.com/mbugroup/lti-api.git/internal/config" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" "gitlab.com/mbugroup/lti-api.git/internal/modules/sso/session" + sso "gitlab.com/mbugroup/lti-api.git/internal/modules/sso/verifier" service "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" - "gitlab.com/mbugroup/lti-api.git/internal/sso" "gitlab.com/mbugroup/lti-api.git/internal/utils" ) diff --git a/internal/middleware/role_scope.go b/internal/middleware/role_scope.go new file mode 100644 index 00000000..f00e12d9 --- /dev/null +++ b/internal/middleware/role_scope.go @@ -0,0 +1,280 @@ +package middleware + +import ( + "errors" + "strings" + + "github.com/gofiber/fiber/v2" + "gorm.io/gorm" + + "gitlab.com/mbugroup/lti-api.git/internal/config" + entity "gitlab.com/mbugroup/lti-api.git/internal/entities" +) + +type ScopeFilter struct { + IDs []uint + Restrict bool +} + +type roleScope struct { + allArea bool + allLocation bool + areaIDs []uint + locationIDs []uint + hasAnyScopes bool +} + +func ResolveAreaScope(c *fiber.Ctx, db *gorm.DB) (ScopeFilter, error) { + scope, err := collectRoleScope(c) + if err != nil || !scope.hasAnyScopes { + return ScopeFilter{}, err + } + + if scope.allArea || scope.allLocation { + return ScopeFilter{}, nil + } + + allowed := uniqueUint(scope.areaIDs) + if len(scope.locationIDs) > 0 { + derived, err := areaIDsByLocationIDs(db, scope.locationIDs) + if err != nil { + return ScopeFilter{}, err + } + allowed = uniqueUint(append(allowed, derived...)) + } + + if len(allowed) == 0 { + return ScopeFilter{Restrict: true}, nil + } + return ScopeFilter{IDs: allowed, Restrict: true}, nil +} + +func ResolveLocationScope(c *fiber.Ctx, db *gorm.DB) (ScopeFilter, error) { + scope, err := collectRoleScope(c) + if err != nil || !scope.hasAnyScopes { + return ScopeFilter{}, err + } + + if scope.allLocation || scope.allArea { + return ScopeFilter{}, nil + } + + areaIDs := uniqueUint(scope.areaIDs) + locationIDs := uniqueUint(scope.locationIDs) + + switch { + case len(locationIDs) > 0 && len(areaIDs) > 0: + filtered, err := filterLocationIDsByAreaIDs(db, locationIDs, areaIDs) + if err != nil { + return ScopeFilter{}, err + } + locationIDs = filtered + case len(locationIDs) == 0 && len(areaIDs) > 0: + derived, err := locationIDsByAreaIDs(db, areaIDs) + if err != nil { + return ScopeFilter{}, err + } + locationIDs = derived + } + + locationIDs = uniqueUint(locationIDs) + if len(locationIDs) == 0 { + return ScopeFilter{Restrict: true}, nil + } + return ScopeFilter{IDs: locationIDs, Restrict: true}, nil +} + +func collectRoleScope(c *fiber.Ctx) (roleScope, error) { + ctx, ok := AuthDetails(c) + if !ok || ctx == nil || len(ctx.Roles) == 0 { + return roleScope{}, nil + } + + clientAlias := resolveClientAlias(ctx) + + scope := roleScope{} + areaSet := make(map[uint]struct{}) + locationSet := make(map[uint]struct{}) + + for _, role := range ctx.Roles { + if clientAlias != "" && !strings.EqualFold(strings.TrimSpace(role.ClientAlias), clientAlias) { + continue + } + scope.hasAnyScopes = true + if role.AllArea { + scope.allArea = true + } + if role.AllLocation { + scope.allLocation = true + } + for _, id := range role.AreaIDs { + if id == 0 { + continue + } + areaSet[id] = struct{}{} + } + for _, id := range role.LocationIDs { + if id == 0 { + continue + } + locationSet[id] = struct{}{} + } + } + + scope.areaIDs = keysUint(areaSet) + scope.locationIDs = keysUint(locationSet) + + scope.hasAnyScopes = scope.hasAnyScopes && + (scope.allArea || scope.allLocation || len(scope.areaIDs) > 0 || len(scope.locationIDs) > 0) + + return scope, nil +} + +func areaIDsByLocationIDs(db *gorm.DB, locationIDs []uint) ([]uint, error) { + if db == nil { + return nil, errors.New("database not configured") + } + if len(locationIDs) == 0 { + return nil, nil + } + var areaIDs []uint + if err := db.Model(&entity.Location{}). + Where("deleted_at IS NULL"). + Where("id IN ?", locationIDs). + Distinct("area_id"). + Pluck("area_id", &areaIDs).Error; err != nil { + return nil, err + } + return areaIDs, nil +} + +func locationIDsByAreaIDs(db *gorm.DB, areaIDs []uint) ([]uint, error) { + if db == nil { + return nil, errors.New("database not configured") + } + if len(areaIDs) == 0 { + return nil, nil + } + var locationIDs []uint + if err := db.Model(&entity.Location{}). + Where("deleted_at IS NULL"). + Where("area_id IN ?", areaIDs). + Distinct("id"). + Pluck("id", &locationIDs).Error; err != nil { + return nil, err + } + return locationIDs, nil +} + +func filterLocationIDsByAreaIDs(db *gorm.DB, locationIDs, areaIDs []uint) ([]uint, error) { + if db == nil { + return nil, errors.New("database not configured") + } + if len(locationIDs) == 0 || len(areaIDs) == 0 { + return nil, nil + } + var filtered []uint + if err := db.Model(&entity.Location{}). + Where("deleted_at IS NULL"). + Where("id IN ?", locationIDs). + Where("area_id IN ?", areaIDs). + Distinct("id"). + Pluck("id", &filtered).Error; err != nil { + return nil, err + } + return filtered, nil +} + +func uniqueUint(ids []uint) []uint { + if len(ids) == 0 { + return nil + } + seen := make(map[uint]struct{}, len(ids)) + result := make([]uint, 0, len(ids)) + for _, id := range ids { + if id == 0 { + continue + } + if _, ok := seen[id]; ok { + continue + } + seen[id] = struct{}{} + result = append(result, id) + } + return result +} + +func keysUint(set map[uint]struct{}) []uint { + if len(set) == 0 { + return nil + } + out := make([]uint, 0, len(set)) + for id := range set { + out = append(out, id) + } + return out +} + +func resolveClientAlias(ctx *AuthContext) string { + if ctx == nil || ctx.Verification == nil || ctx.Verification.Claims == nil { + return "" + } + + scopes := ctx.Verification.Claims.Scopes() + if len(scopes) == 0 { + return "" + } + + seen := make(map[string]struct{}) + for _, scope := range scopes { + scope = strings.ToLower(strings.TrimSpace(scope)) + if scope == "" { + continue + } + prefix := scope + if idx := strings.IndexAny(prefix, ".:"); idx > 0 { + prefix = prefix[:idx] + } + prefix = strings.TrimSpace(prefix) + if prefix == "" { + continue + } + if alias := matchAlias(prefix); alias != "" { + seen[alias] = struct{}{} + } + } + + if len(seen) != 1 { + return "" + } + for alias := range seen { + return alias + } + return "" +} + +func matchAlias(alias string) string { + alias = strings.ToLower(strings.TrimSpace(alias)) + if alias == "" { + return "" + } + if _, ok := config.SSOClients[alias]; ok { + return alias + } + for key := range config.SSOClients { + if strings.EqualFold(key, alias) { + return strings.ToLower(strings.TrimSpace(key)) + } + } + return "" +} + +func ApplyScopeFilter(db *gorm.DB, scope ScopeFilter, column string) *gorm.DB { + if db == nil || !scope.Restrict { + return db + } + if len(scope.IDs) == 0 { + return db.Where("1 = 0") + } + return db.Where(column+" IN ?", scope.IDs) +} diff --git a/internal/modules/daily-checklists/services/daily-checklist.service.go b/internal/modules/daily-checklists/services/daily-checklist.service.go index f306c74d..64802560 100644 --- a/internal/modules/daily-checklists/services/daily-checklist.service.go +++ b/internal/modules/daily-checklists/services/daily-checklist.service.go @@ -9,7 +9,7 @@ import ( "time" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" - middleware "gitlab.com/mbugroup/lti-api.git/internal/middleware" + m "gitlab.com/mbugroup/lti-api.git/internal/middleware" repository "gitlab.com/mbugroup/lti-api.git/internal/modules/daily-checklists/repositories" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/daily-checklists/validations" phaseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/phasess/repositories" @@ -456,7 +456,7 @@ func (s dailyChecklistService) UpdateOne(c *fiber.Ctx, req *validation.Update, i updateBody["reject_reason"] = *req.RejectReason } - actorID, err := middleware.ActorIDFromContext(c) + actorID, err := m.ActorIDFromContext(c) if err != nil { return &entity.DailyChecklist{}, fiber.NewError(fiber.StatusUnauthorized, "Failed to get actor ID from context") } @@ -946,6 +946,11 @@ func (s dailyChecklistService) GetReport(c *fiber.Ctx, params *validation.Report return nil, 0, err } + scope, err := m.ResolveLocationScope(c, s.Repository.DB()) + if err != nil { + return nil, 0, err + } + offset := (params.Page - 1) * params.Limit buildBase := func() *gorm.DB { @@ -962,6 +967,8 @@ func (s dailyChecklistService) GetReport(c *fiber.Ctx, params *validation.Report Where("EXTRACT(YEAR FROM dc.date) = ?", params.Year). Where("dc.status = ?", "APPROVED") + db = m.ApplyScopeFilter(db, scope, "loc.id") + if params.AreaID != nil { db = db.Where("a.id = ?", *params.AreaID) } diff --git a/internal/modules/dashboards/controllers/dashboard.controller.go b/internal/modules/dashboards/controllers/dashboard.controller.go index bebad10f..f74f31a7 100644 --- a/internal/modules/dashboards/controllers/dashboard.controller.go +++ b/internal/modules/dashboards/controllers/dashboard.controller.go @@ -6,6 +6,7 @@ import ( "strings" "time" + m "gitlab.com/mbugroup/lti-api.git/internal/middleware" "gitlab.com/mbugroup/lti-api.git/internal/modules/dashboards/dto" service "gitlab.com/mbugroup/lti-api.git/internal/modules/dashboards/services" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/dashboards/validations" @@ -81,6 +82,20 @@ func (u *DashboardController) GetAll(c *fiber.Ctx) error { return fiber.NewError(fiber.StatusBadRequest, "Invalid include") } + scope, err := m.ResolveLocationScope(c, u.DashboardService.DB()) + if err != nil { + return err + } + if scope.Restrict { + if len(scope.IDs) == 0 { + lokasiIds = []uint{} + } else if len(lokasiIds) > 0 { + lokasiIds = intersectUint(lokasiIds, scope.IDs) + } else { + lokasiIds = scope.IDs + } + } + analysisMode := strings.ToUpper(strings.TrimSpace(c.Query("analysis_mode", validation.AnalysisModeOverview))) metric := strings.ToLower(strings.TrimSpace(c.Query("metric", ""))) @@ -176,6 +191,23 @@ func defaultUintSlice(values []uint) []uint { return values } +func intersectUint(a, b []uint) []uint { + if len(a) == 0 || len(b) == 0 { + return nil + } + set := make(map[uint]struct{}, len(b)) + for _, id := range b { + set[id] = struct{}{} + } + out := make([]uint, 0, len(a)) + for _, id := range a { + if _, ok := set[id]; ok { + out = append(out, id) + } + } + return out +} + func parsePeriodDates(startDateRaw, endDateRaw string, location *time.Location) (time.Time, time.Time, time.Time, error) { now := time.Now().In(location) startDate := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, location) diff --git a/internal/modules/dashboards/services/dashboard.service.go b/internal/modules/dashboards/services/dashboard.service.go index b4635b2e..afdbd1c1 100644 --- a/internal/modules/dashboards/services/dashboard.service.go +++ b/internal/modules/dashboards/services/dashboard.service.go @@ -17,10 +17,12 @@ import ( "github.com/go-playground/validator/v10" "github.com/sirupsen/logrus" + "gorm.io/gorm" ) type DashboardService interface { GetAll(ctx context.Context, params *validation.Query) (dto.DashboardPerformanceOverviewDTO, int64, error) + DB() *gorm.DB } type dashboardService struct { @@ -37,6 +39,10 @@ func NewDashboardService(repo repository.DashboardRepository, validate *validato } } +func (s dashboardService) DB() *gorm.DB { + return s.Repository.DB() +} + func (s dashboardService) GetAll(ctx context.Context, params *validation.Query) (dto.DashboardPerformanceOverviewDTO, int64, error) { if err := s.Validate.Struct(params); err != nil { return dto.DashboardPerformanceOverviewDTO{}, 0, err diff --git a/internal/modules/expenses/repositories/expense_realization.repository.go b/internal/modules/expenses/repositories/expense_realization.repository.go index 60ec97a7..504e65ad 100644 --- a/internal/modules/expenses/repositories/expense_realization.repository.go +++ b/internal/modules/expenses/repositories/expense_realization.repository.go @@ -138,9 +138,28 @@ func (r *ExpenseRealizationRepositoryImpl) GetAllWithFilters(ctx context.Context locationID := filters.LocationId areaID := filters.AreaId - if locationID > 0 || areaID > 0 { + if filters.AllowedLocationIDs != nil || filters.AllowedAreaIDs != nil || locationID > 0 || areaID > 0 { db = db.Joins("JOIN kandangs ON kandangs.id = expense_nonstocks.kandang_id") + } + if filters.AllowedLocationIDs != nil { + if len(filters.AllowedLocationIDs) == 0 { + db = db.Where("1 = 0") + } else { + db = db.Where("kandangs.location_id IN ?", filters.AllowedLocationIDs) + } + } + + if filters.AllowedAreaIDs != nil { + if len(filters.AllowedAreaIDs) == 0 { + db = db.Where("1 = 0") + } else { + db = db.Joins("JOIN locations ON locations.id = kandangs.location_id"). + Where("locations.area_id IN ?", filters.AllowedAreaIDs) + } + } + + if locationID > 0 || areaID > 0 { if locationID > 0 { db = db.Where("kandangs.location_id = ?", uint(locationID)) } diff --git a/internal/modules/expenses/services/expense.service.go b/internal/modules/expenses/services/expense.service.go index 3bf2db55..27b4a07f 100644 --- a/internal/modules/expenses/services/expense.service.go +++ b/internal/modules/expenses/services/expense.service.go @@ -87,10 +87,16 @@ func (s expenseService) GetAll(c *fiber.Ctx, params *validation.Query) ([]expens return nil, 0, err } + scope, err := middleware.ResolveLocationScope(c, s.Repository.DB()) + if err != nil { + return nil, 0, err + } + offset := (params.Page - 1) * params.Limit expenses, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB { db = s.withRelations(db) + db = middleware.ApplyScopeFilter(db, scope, "location_id") if params.Search != "" { return db.Where("category ILIKE ?", "%"+params.Search+"%") } @@ -117,7 +123,16 @@ func (s expenseService) GetAll(c *fiber.Ctx, params *validation.Query) ([]expens } func (s expenseService) GetOne(c *fiber.Ctx, id uint) (*expenseDto.ExpenseDetailDTO, error) { - expense, err := s.Repository.GetByID(c.Context(), id, s.withRelations) + scope, err := middleware.ResolveLocationScope(c, s.Repository.DB()) + if err != nil { + return nil, err + } + + expense, err := s.Repository.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB { + db = s.withRelations(db) + db = middleware.ApplyScopeFilter(db, scope, "location_id") + return db + }) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { diff --git a/internal/modules/inventory/product-stocks/services/product-stock.service.go b/internal/modules/inventory/product-stocks/services/product-stock.service.go index 11475109..a4e404d6 100644 --- a/internal/modules/inventory/product-stocks/services/product-stock.service.go +++ b/internal/modules/inventory/product-stocks/services/product-stock.service.go @@ -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" @@ -61,15 +62,34 @@ func (s productStockService) GetAll(c *fiber.Ctx, params *validation.Query) ([]e return nil, 0, err } + scope, err := m.ResolveLocationScope(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 scope.Restrict { + if len(scope.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 w.location_id IN ? + )`, scope.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) if params.Search != "" { @@ -86,6 +106,30 @@ func (s productStockService) GetAll(c *fiber.Ctx, params *validation.Query) ([]e } func (s productStockService) GetOne(c *fiber.Ctx, id uint) (*entity.Product, error) { + scope, err := m.ResolveLocationScope(c, s.ProductRepository.DB()) + if err != nil { + return nil, err + } + + if scope.Restrict { + if len(scope.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("w.location_id IN ?", scope.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, s.withRelations) if errors.Is(err, gorm.ErrRecordNotFound) { return nil, fiber.NewError(fiber.StatusNotFound, "Product not found") 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 152bfa24..5c162084 100644 --- a/internal/modules/inventory/product-warehouses/services/product_warehouse.service.go +++ b/internal/modules/inventory/product-warehouses/services/product_warehouse.service.go @@ -8,7 +8,7 @@ import ( 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" "gitlab.com/mbugroup/lti-api.git/internal/utils" - + m "gitlab.com/mbugroup/lti-api.git/internal/middleware" "github.com/go-playground/validator/v10" "github.com/gofiber/fiber/v2" "github.com/sirupsen/logrus" @@ -53,6 +53,11 @@ func (s productWarehouseService) GetAll(c *fiber.Ctx, params *validation.Query) return nil, 0, err } + 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 +95,14 @@ 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) + if scope.Restrict { + if len(scope.IDs) == 0 { + return db.Where("1 = 0") + } + db = db.Joins("JOIN warehouses w_scope ON product_warehouses.warehouse_id = w_scope.id"). + Where("w_scope.location_id IN ?", scope.IDs) + } + if params.ProductId != 0 { db = db.Where("product_id = ?", params.ProductId) } @@ -116,7 +129,22 @@ 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) + 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) + if scope.Restrict { + if len(scope.IDs) == 0 { + return db.Where("1 = 0") + } + db = db.Joins("JOIN warehouses w_scope ON product_warehouses.warehouse_id = w_scope.id"). + 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") } diff --git a/internal/modules/inventory/transfers/services/transfer.service.go b/internal/modules/inventory/transfers/services/transfer.service.go index 3f12b444..b4652a5d 100644 --- a/internal/modules/inventory/transfers/services/transfer.service.go +++ b/internal/modules/inventory/transfers/services/transfer.service.go @@ -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 != "" { db = db.Where("movement_number ILIKE ?", "%"+strings.TrimSpace(params.Search)+"%") } @@ -112,6 +126,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) diff --git a/internal/modules/master/areas/services/area.service.go b/internal/modules/master/areas/services/area.service.go index e6f9205c..1ec30d8d 100644 --- a/internal/modules/master/areas/services/area.service.go +++ b/internal/modules/master/areas/services/area.service.go @@ -47,10 +47,21 @@ func (s areaService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.Ar return nil, 0, err } + scope, err := m.ResolveAreaScope(c, s.Repository.DB()) + if err != nil { + return nil, 0, err + } + offset := (params.Page - 1) * params.Limit areas, total, err := s.Repository.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.Where("id IN ?", scope.IDs) + } if params.Search != "" { return db.Where("name ILIKE ?", "%"+params.Search+"%") } @@ -65,7 +76,21 @@ func (s areaService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.Ar } func (s areaService) GetOne(c *fiber.Ctx, id uint) (*entity.Area, error) { - area, err := s.Repository.GetByID(c.Context(), id, s.withRelations) + scope, err := m.ResolveAreaScope(c, s.Repository.DB()) + if err != nil { + return nil, err + } + + area, err := s.Repository.GetByID(c.Context(), id, 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.Where("id IN ?", scope.IDs) + } + return db + }) if errors.Is(err, gorm.ErrRecordNotFound) { return nil, fiber.NewError(fiber.StatusNotFound, "Area not found") } diff --git a/internal/modules/master/kandangs/services/kandang.service.go b/internal/modules/master/kandangs/services/kandang.service.go index 9f83f0ce..b70577d6 100644 --- a/internal/modules/master/kandangs/services/kandang.service.go +++ b/internal/modules/master/kandangs/services/kandang.service.go @@ -49,10 +49,16 @@ func (s kandangService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity return nil, 0, err } + scope, err := m.ResolveLocationScope(c, s.Repository.DB()) + if err != nil { + return nil, 0, err + } + offset := (params.Page - 1) * params.Limit kandangs, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB { db = s.withRelations(db) + db = m.ApplyScopeFilter(db, scope, "location_id") if params.Search != "" { return db.Where("name ILIKE ?", "%"+params.Search+"%") } @@ -73,7 +79,16 @@ func (s kandangService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity } func (s kandangService) GetOne(c *fiber.Ctx, id uint) (*entity.Kandang, error) { - kandang, err := s.Repository.GetByID(c.Context(), id, s.withRelations) + scope, err := m.ResolveLocationScope(c, s.Repository.DB()) + if err != nil { + return nil, err + } + + kandang, err := s.Repository.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB { + db = s.withRelations(db) + db = m.ApplyScopeFilter(db, scope, "location_id") + return db + }) if errors.Is(err, gorm.ErrRecordNotFound) { return nil, fiber.NewError(fiber.StatusNotFound, "Kandang not found") } diff --git a/internal/modules/master/locations/services/location.service.go b/internal/modules/master/locations/services/location.service.go index 3a1d1e23..633d7419 100644 --- a/internal/modules/master/locations/services/location.service.go +++ b/internal/modules/master/locations/services/location.service.go @@ -47,10 +47,21 @@ func (s locationService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entit return nil, 0, err } + scope, err := m.ResolveLocationScope(c, s.Repository.DB()) + if err != nil { + return nil, 0, err + } + offset := (params.Page - 1) * params.Limit locations, total, err := s.Repository.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.Where("id IN ?", scope.IDs) + } if params.Search != "" { db = db.Where("name ILIKE ?", "%"+params.Search+"%") } @@ -68,7 +79,21 @@ func (s locationService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entit } func (s locationService) GetOne(c *fiber.Ctx, id uint) (*entity.Location, error) { - location, err := s.Repository.GetByID(c.Context(), id, s.withRelations) + scope, err := m.ResolveLocationScope(c, s.Repository.DB()) + if err != nil { + return nil, err + } + + location, err := s.Repository.GetByID(c.Context(), id, 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.Where("id IN ?", scope.IDs) + } + return db + }) if errors.Is(err, gorm.ErrRecordNotFound) { return nil, fiber.NewError(fiber.StatusNotFound, "Location not found") } diff --git a/internal/modules/master/warehouses/services/warehouse.service.go b/internal/modules/master/warehouses/services/warehouse.service.go index 7eeaad3d..aaa5ca7e 100644 --- a/internal/modules/master/warehouses/services/warehouse.service.go +++ b/internal/modules/master/warehouses/services/warehouse.service.go @@ -48,10 +48,16 @@ func (s warehouseService) GetAll(c *fiber.Ctx, params *validation.Query) ([]enti return nil, 0, err } + scope, err := m.ResolveAreaScope(c, s.Repository.DB()) + if err != nil { + return nil, 0, err + } + offset := (params.Page - 1) * params.Limit warehouses, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB { db = s.withRelations(db) + db = m.ApplyScopeFilter(db, scope, "area_id") if params.Search != "" { db = db.Where("warehouses.name ILIKE ?", "%"+params.Search+"%") } @@ -86,7 +92,16 @@ func (s warehouseService) GetAll(c *fiber.Ctx, params *validation.Query) ([]enti } func (s warehouseService) GetOne(c *fiber.Ctx, id uint) (*entity.Warehouse, error) { - warehouse, err := s.Repository.GetByID(c.Context(), id, s.withRelations) + scope, err := m.ResolveAreaScope(c, s.Repository.DB()) + if err != nil { + return nil, err + } + + warehouse, err := s.Repository.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB { + db = s.withRelations(db) + db = m.ApplyScopeFilter(db, scope, "area_id") + return db + }) if errors.Is(err, gorm.ErrRecordNotFound) { return nil, fiber.NewError(fiber.StatusNotFound, "Warehouse not found") } 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 6f019ffa..61a593d5 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 @@ -88,9 +88,14 @@ func (s projectFlockKandangService) GetAll(c *fiber.Ctx, params *validation.Quer return nil, 0, err } + scope, err := m.ResolveLocationScope(c, s.Repository.DB()) + if err != nil { + return nil, 0, err + } + offset := (params.Page - 1) * params.Limit - projectFlockKandangs, total, err := s.Repository.GetAllWithFilters(c.Context(), offset, params.Limit, params) + projectFlockKandangs, total, err := s.Repository.GetAllWithFiltersScoped(c.Context(), offset, params.Limit, params, scope.IDs, scope.Restrict) if err != nil { s.Log.Errorf("Failed to get projectFlockKandangs: %+v", err) @@ -106,6 +111,28 @@ func (s projectFlockKandangService) GetAll(c *fiber.Ctx, params *validation.Quer } func (s projectFlockKandangService) GetOne(c *fiber.Ctx, id uint) (*entity.ProjectFlockKandang, map[uint]float64, []entity.ProductWarehouse, error) { + scope, err := m.ResolveLocationScope(c, s.Repository.DB()) + if err != nil { + return nil, nil, nil, err + } + if scope.Restrict { + if len(scope.IDs) == 0 { + return nil, nil, nil, fiber.NewError(fiber.StatusNotFound, "ProjectFlockKandang not found") + } + var count int64 + if err := s.Repository.DB().WithContext(c.Context()). + Table("project_flock_kandangs"). + Joins("JOIN project_flocks ON project_flocks.id = project_flock_kandangs.project_flock_id"). + Where("project_flock_kandangs.id = ?", id). + Where("project_flocks.location_id IN ?", scope.IDs). + Count(&count).Error; err != nil { + return nil, nil, nil, err + } + if count == 0 { + return nil, nil, nil, fiber.NewError(fiber.StatusNotFound, "ProjectFlockKandang not found") + } + } + projectFlockKandang, err := s.Repository.GetByID(c.Context(), id) if errors.Is(err, gorm.ErrRecordNotFound) { return nil, nil, nil, fiber.NewError(fiber.StatusNotFound, "ProjectFlockKandang not found") diff --git a/internal/modules/production/project_flocks/repositories/projectflock.repository.go b/internal/modules/production/project_flocks/repositories/projectflock.repository.go index e65dfb4a..346f2176 100644 --- a/internal/modules/production/project_flocks/repositories/projectflock.repository.go +++ b/internal/modules/production/project_flocks/repositories/projectflock.repository.go @@ -14,6 +14,7 @@ import ( type ProjectflockRepository interface { repository.BaseRepository[entity.ProjectFlock] GetAllWithFilters(ctx context.Context, offset, limit int, params *validation.Query) ([]entity.ProjectFlock, int64, error) + GetAllWithFiltersScoped(ctx context.Context, offset, limit int, params *validation.Query, locationIDs []uint, restrict bool) ([]entity.ProjectFlock, int64, error) WithDefaultRelations() func(*gorm.DB) *gorm.DB ExistsByFlockName(ctx context.Context, flockName string, excludeID *uint) (bool, error) GetNextPeriodsForKandangs(ctx context.Context, kandangIDs []uint) (map[uint]int, error) @@ -48,6 +49,19 @@ func (r *ProjectflockRepositoryImpl) GetAllWithFilters(ctx context.Context, offs }) } +func (r *ProjectflockRepositoryImpl) GetAllWithFiltersScoped(ctx context.Context, offset, limit int, params *validation.Query, locationIDs []uint, restrict bool) ([]entity.ProjectFlock, int64, error) { + return r.GetAll(ctx, offset, limit, func(db *gorm.DB) *gorm.DB { + db = r.applyQueryFilters(r.WithDefaultRelations()(db), params) + if restrict { + if len(locationIDs) == 0 { + return db.Where("1 = 0") + } + db = db.Where("project_flocks.location_id IN ?", locationIDs) + } + return db + }) +} + func (r *ProjectflockRepositoryImpl) WithDefaultRelations() func(*gorm.DB) *gorm.DB { return func(db *gorm.DB) *gorm.DB { return db. 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 474a53c2..24c4c083 100644 --- a/internal/modules/production/project_flocks/repositories/projectflock_kandang.repository.go +++ b/internal/modules/production/project_flocks/repositories/projectflock_kandang.repository.go @@ -20,6 +20,7 @@ type ProjectFlockKandangRepository interface { DeleteMany(ctx context.Context, projectFlockID uint, kandangIDs []uint) error GetAll(ctx context.Context, offset int, limit int, modifier func(*gorm.DB) *gorm.DB) ([]entity.ProjectFlockKandang, int64, error) GetAllWithFilters(ctx context.Context, offset int, limit int, params interface{}) ([]entity.ProjectFlockKandang, int64, error) + GetAllWithFiltersScoped(ctx context.Context, offset int, limit int, params interface{}, locationIDs []uint, restrict bool) ([]entity.ProjectFlockKandang, int64, error) GetByProjectFlockID(ctx context.Context, projectFlockID uint) ([]entity.ProjectFlockKandang, error) ListExistingKandangIDs(ctx context.Context, projectFlockID uint, kandangIDs []uint) ([]uint, error) HasKandangsLinkedToOtherProject(ctx context.Context, kandangIDs []uint, exceptProjectID *uint) (bool, error) @@ -196,6 +197,104 @@ func (r *projectFlockKandangRepositoryImpl) GetAllWithFilters(ctx context.Contex return records, total, nil } +func (r *projectFlockKandangRepositoryImpl) GetAllWithFiltersScoped(ctx context.Context, offset int, limit int, params interface{}, locationIDs []uint, restrict bool) ([]entity.ProjectFlockKandang, int64, error) { + var records []entity.ProjectFlockKandang + var total int64 + + query, ok := params.(*validation.Query) + + q := r.db.WithContext(ctx). + Joins("JOIN \"kandangs\" ON \"project_flock_kandangs\".\"kandang_id\" = \"kandangs\".\"id\""). + Joins("JOIN \"project_flocks\" ON \"project_flock_kandangs\".\"project_flock_id\" = \"project_flocks\".\"id\""). + Preload("ProjectFlock"). + Preload("ProjectFlock.Fcr"). + Preload("ProjectFlock.Area"). + Preload("ProjectFlock.Location"). + Preload("ProjectFlock.CreatedUser"). + Preload("ProjectFlock.Kandangs"). + Preload("ProjectFlock.KandangHistory"). + Preload("Kandang"). + Preload("Chickins"). + Preload("Chickins.CreatedUser"). + Preload("Chickins.ProductWarehouse") + + if restrict { + if len(locationIDs) == 0 { + return []entity.ProjectFlockKandang{}, 0, nil + } + q = q.Where("\"project_flocks\".\"location_id\" IN ?", locationIDs) + } + + if ok && query != nil && query.StepName != "" { + q = q.Where(` + EXISTS ( + SELECT 1 FROM "approvals" + WHERE "approvals"."approvable_id" = "project_flock_kandangs"."id" + AND "approvals"."approvable_type" = ? + AND LOWER("approvals"."step_name") = LOWER(?) + AND "approvals"."id" IN ( + SELECT "approvals"."id" FROM "approvals" + WHERE "approvals"."approvable_id" = "project_flock_kandangs"."id" + AND "approvals"."approvable_type" = ? + ORDER BY "approvals"."id" DESC + LIMIT 1 + ) + ) + `, "PROJECT_FLOCK_KANDANGS", query.StepName, "PROJECT_FLOCK_KANDANGS") + } + + if ok && query != nil { + if query.Search != "" { + escapedSearch := strings.NewReplacer("\\", "\\\\", "%", "\\%", "_", "\\_").Replace(query.Search) + q = q.Where( + r.db.Where("LOWER(\"kandangs\".\"name\") LIKE LOWER(?) ESCAPE '\\'", "%"+escapedSearch+"%"). + Or("LOWER(\"project_flocks\".\"flock_name\") LIKE LOWER(?) ESCAPE '\\'", "%"+escapedSearch+"%"), + ) + } + + if query.ProjectFlockId > 0 { + q = q.Where("\"project_flock_kandangs\".\"project_flock_id\" = ?", query.ProjectFlockId) + } + + if query.KandangId > 0 { + q = q.Where("\"project_flock_kandangs\".\"kandang_id\" = ?", query.KandangId) + } + + if query.Category != "" { + q = q.Where("\"project_flocks\".\"category\" = ?", query.Category) + } + + if query.AreaId > 0 { + q = q.Where("\"project_flocks\".\"area_id\" = ?", query.AreaId) + } + } + + if err := q.Model(&entity.ProjectFlockKandang{}).Count(&total).Error; err != nil { + return nil, 0, err + } + + sortBy := "\"project_flock_kandangs\".\"created_at\" DESC" + if ok && query != nil && query.SortBy != "" { + sortOrder := "DESC" + if query.SortOrder == "ASC" { + sortOrder = "ASC" + } + + switch query.SortBy { + case "created_at": + sortBy = "\"project_flock_kandangs\".\"created_at\" " + sortOrder + case "period": + sortBy = "\"project_flocks\".\"period\" " + sortOrder + } + } + + if err := q.Order(sortBy).Offset(offset).Limit(limit).Find(&records).Error; err != nil { + return nil, 0, err + } + + return records, total, nil +} + func (r *projectFlockKandangRepositoryImpl) WithTx(tx *gorm.DB) ProjectFlockKandangRepository { return &projectFlockKandangRepositoryImpl{db: tx} } diff --git a/internal/modules/production/project_flocks/services/projectflock.service.go b/internal/modules/production/project_flocks/services/projectflock.service.go index 3dbe3f4b..5bccfbf9 100644 --- a/internal/modules/production/project_flocks/services/projectflock.service.go +++ b/internal/modules/production/project_flocks/services/projectflock.service.go @@ -114,9 +114,14 @@ func (s projectflockService) GetAll(c *fiber.Ctx, params *validation.Query) ([]e return nil, 0, nil, err } + scope, err := m.ResolveLocationScope(c, s.Repository.DB()) + if err != nil { + return nil, 0, nil, err + } + offset := (params.Page - 1) * params.Limit - projectflocks, total, err := s.Repository.GetAllWithFilters(c.Context(), offset, params.Limit, params) + projectflocks, total, err := s.Repository.GetAllWithFiltersScoped(c.Context(), offset, params.Limit, params, scope.IDs, scope.Restrict) if err != nil { s.Log.Errorf("Failed to get projectflocks: %+v", err) @@ -190,7 +195,16 @@ func (s projectflockService) getOneEntityOnly(c *fiber.Ctx, id uint) (*entity.Pr } func (s projectflockService) GetOne(c *fiber.Ctx, id uint) (*entity.ProjectFlock, *flockDTO.FlockRelationDTO, error) { - projectflock, err := s.Repository.GetByID(c.Context(), id, s.Repository.WithDefaultRelations()) + scope, err := m.ResolveLocationScope(c, s.Repository.DB()) + if err != nil { + return nil, nil, err + } + + projectflock, err := s.Repository.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB { + db = s.Repository.WithDefaultRelations()(db) + db = m.ApplyScopeFilter(db, scope, "project_flocks.location_id") + return db + }) if errors.Is(err, gorm.ErrRecordNotFound) { return nil, nil, fiber.NewError(fiber.StatusNotFound, "Projectflock not found") } diff --git a/internal/modules/purchases/services/purchase.service.go b/internal/modules/purchases/services/purchase.service.go index 35ca2f75..6b9537cc 100644 --- a/internal/modules/purchases/services/purchase.service.go +++ b/internal/modules/purchases/services/purchase.service.go @@ -124,6 +124,11 @@ func (s *purchaseService) GetAll(c *fiber.Ctx, params *validation.Query) ([]enti return nil, 0, err } + scope, err := m.ResolveLocationScope(c, s.PurchaseRepo.DB()) + if err != nil { + return nil, 0, err + } + offset := (params.Page - 1) * params.Limit createdFrom, createdTo, err := utils.ParseDateRangeForQuery(params.CreatedFrom, params.CreatedTo) @@ -147,6 +152,21 @@ func (s *purchaseService) GetAll(c *fiber.Ctx, params *validation.Query) ([]enti db = db.Where("created_at < ?", *createdTo) } + if scope.Restrict { + if len(scope.IDs) == 0 { + return db.Where("1 = 0") + } + db = db.Where( + `EXISTS ( + SELECT 1 + FROM purchase_items pi + JOIN warehouses w ON w.id = pi.warehouse_id + WHERE pi.purchase_id = purchases.id AND w.location_id IN ? + )`, + scope.IDs, + ) + } + if params.AreaID > 0 { db = db.Where( `EXISTS ( @@ -201,7 +221,42 @@ func (s *purchaseService) GetAll(c *fiber.Ctx, params *validation.Query) ([]enti } func (s *purchaseService) GetOne(c *fiber.Ctx, id uint) (*entity.Purchase, error) { - return s.loadPurchase(c.Context(), id) + scope, err := m.ResolveLocationScope(c, s.PurchaseRepo.DB()) + if err != nil { + return nil, err + } + + purchase, err := s.PurchaseRepo.GetByID(c.Context(), id, 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.Where( + `EXISTS ( + SELECT 1 + FROM purchase_items pi + JOIN warehouses w ON w.id = pi.warehouse_id + WHERE pi.purchase_id = purchases.id AND w.location_id IN ? + )`, + scope.IDs, + ) + } + return db + }) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, utils.NotFound("Purchase not found") + } + s.Log.Errorf("Failed to get purchase %d: %+v", id, err) + return nil, utils.Internal("Failed to get purchase") + } + if err := s.attachLatestApproval(c.Context(), purchase); err != nil { + s.Log.Warnf("Unable to attach latest approval for purchase %d: %+v", id, err) + } + s.applyTravelDocumentURLs(c.Context(), purchase) + + return purchase, nil } func (s *purchaseService) CreateOne(c *fiber.Ctx, req *validation.CreatePurchaseRequest) (*entity.Purchase, error) { diff --git a/internal/modules/repports/controllers/repport.controller.go b/internal/modules/repports/controllers/repport.controller.go index 1d273af1..4ed7a855 100644 --- a/internal/modules/repports/controllers/repport.controller.go +++ b/internal/modules/repports/controllers/repport.controller.go @@ -5,6 +5,7 @@ import ( "strconv" "strings" + m "gitlab.com/mbugroup/lti-api.git/internal/middleware" "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/dto" service "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/services" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/validations" @@ -49,6 +50,21 @@ func (c *RepportController) GetExpense(ctx *fiber.Ctx) error { RealizationDate: ctx.Query("realization_date", ""), } + locationScope, err := m.ResolveLocationScope(ctx, c.RepportService.DB()) + if err != nil { + return err + } + areaScope, err := m.ResolveAreaScope(ctx, c.RepportService.DB()) + if err != nil { + return err + } + if locationScope.Restrict { + query.AllowedLocationIDs = toInt64Slice(locationScope.IDs) + } + if areaScope.Restrict { + query.AllowedAreaIDs = toInt64Slice(areaScope.IDs) + } + if query.Page < 1 || query.Limit < 1 { return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0") } @@ -130,6 +146,14 @@ func (c *RepportController) GetPurchaseSupplier(ctx *fiber.Ctx) error { FilterBy: ctx.Query("filter_by", ""), } + areaScope, err := m.ResolveAreaScope(ctx, c.RepportService.DB()) + if err != nil { + return err + } + if areaScope.Restrict { + query.AllowedAreaIDs = toInt64Slice(areaScope.IDs) + } + if query.Page < 1 || query.Limit < 1 { return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0") } @@ -306,3 +330,14 @@ func parseCommaSeparatedInt64s(raw string) ([]int64, error) { return result, nil } + +func toInt64Slice(ids []uint) []int64 { + if len(ids) == 0 { + return nil + } + out := make([]int64, 0, len(ids)) + for _, id := range ids { + out = append(out, int64(id)) + } + return out +} diff --git a/internal/modules/repports/repositories/purchase_supplier.repository.go b/internal/modules/repports/repositories/purchase_supplier.repository.go index 979623fc..048fe02c 100644 --- a/internal/modules/repports/repositories/purchase_supplier.repository.go +++ b/internal/modules/repports/repositories/purchase_supplier.repository.go @@ -53,10 +53,18 @@ func (r *purchaseSupplierRepositoryImpl) baseSupplierQuery(ctx context.Context, Where("products.product_category_id = ?", filters.ProductCategoryId) } - if filters.AreaId > 0 { - db = db. - Joins("JOIN warehouses ON warehouses.id = purchase_items.warehouse_id"). - Where("warehouses.area_id = ?", filters.AreaId) + if filters.AreaId > 0 || filters.AllowedAreaIDs != nil { + db = db.Joins("JOIN warehouses ON warehouses.id = purchase_items.warehouse_id") + if filters.AreaId > 0 { + db = db.Where("warehouses.area_id = ?", filters.AreaId) + } + if filters.AllowedAreaIDs != nil { + if len(filters.AllowedAreaIDs) == 0 { + db = db.Where("1 = 0") + } else { + db = db.Where("warehouses.area_id IN ?", filters.AllowedAreaIDs) + } + } } if filters.StartDate != "" { @@ -164,10 +172,18 @@ func (r *purchaseSupplierRepositoryImpl) GetItemsBySuppliers(ctx context.Context Where("products.product_category_id = ?", filters.ProductCategoryId) } - if filters.AreaId > 0 { - db = db. - Joins("JOIN warehouses ON warehouses.id = purchase_items.warehouse_id"). - Where("warehouses.area_id = ?", filters.AreaId) + if filters.AreaId > 0 || filters.AllowedAreaIDs != nil { + db = db.Joins("JOIN warehouses ON warehouses.id = purchase_items.warehouse_id") + if filters.AreaId > 0 { + db = db.Where("warehouses.area_id = ?", filters.AreaId) + } + if filters.AllowedAreaIDs != nil { + if len(filters.AllowedAreaIDs) == 0 { + db = db.Where("1 = 0") + } else { + db = db.Where("warehouses.area_id IN ?", filters.AllowedAreaIDs) + } + } } if filters.StartDate != "" { diff --git a/internal/modules/repports/services/repport.service.go b/internal/modules/repports/services/repport.service.go index c4883b72..0e3dbada 100644 --- a/internal/modules/repports/services/repport.service.go +++ b/internal/modules/repports/services/repport.service.go @@ -9,6 +9,7 @@ import ( "strings" "time" + m "gitlab.com/mbugroup/lti-api.git/internal/middleware" "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/dto" repportRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/repositories" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/validations" @@ -40,6 +41,7 @@ type RepportService interface { GetDebtSupplier(ctx *fiber.Ctx, params *validation.DebtSupplierQuery) ([]dto.DebtSupplierDTO, int64, error) GetHppPerKandang(ctx *fiber.Ctx) (*dto.HppPerKandangResponseData, *dto.HppPerKandangMetaDTO, error) GetProductionResult(ctx *fiber.Ctx, params *validation.ProductionResultQuery) ([]dto.ProductionResultDTO, int64, error) + DB() *gorm.DB } type repportService struct { @@ -95,6 +97,10 @@ func NewRepportService( } } +func (s *repportService) DB() *gorm.DB { + return s.ExpenseRealizationRepo.DB() +} + func (s *repportService) GetExpense(c *fiber.Ctx, params *validation.ExpenseQuery) ([]dto.RepportExpenseListDTO, int64, error) { if err := s.Validate.Struct(params); err != nil { return nil, 0, err @@ -1303,6 +1309,36 @@ func (s *repportService) parseHppPerKandangQuery(ctx *fiber.Ctx) (*validation.Hp return nil, dto.HppPerKandangFiltersDTO{}, fiber.NewError(fiber.StatusBadRequest, err.Error()) } + locationScope, err := m.ResolveLocationScope(ctx, s.ExpenseRealizationRepo.DB()) + if err != nil { + return nil, dto.HppPerKandangFiltersDTO{}, err + } + areaScope, err := m.ResolveAreaScope(ctx, s.ExpenseRealizationRepo.DB()) + if err != nil { + return nil, dto.HppPerKandangFiltersDTO{}, err + } + + if locationScope.Restrict { + allowed := toInt64Slice(locationScope.IDs) + if len(allowed) == 0 { + locationIDs = []int64{-1} + } else if len(locationIDs) > 0 { + locationIDs = intersectInt64(locationIDs, allowed) + } else { + locationIDs = allowed + } + } + if areaScope.Restrict { + allowed := toInt64Slice(areaScope.IDs) + if len(allowed) == 0 { + areaIDs = []int64{-1} + } else if len(areaIDs) > 0 { + areaIDs = intersectInt64(areaIDs, allowed) + } else { + areaIDs = allowed + } + } + weightMin, err := parseOptionalFloat64(rawWeightMin) if err != nil { return nil, dto.HppPerKandangFiltersDTO{}, fiber.NewError(fiber.StatusBadRequest, err.Error()) @@ -1366,6 +1402,34 @@ func parseCommaSeparatedInt64s(raw string) ([]int64, error) { return result, nil } +func toInt64Slice(ids []uint) []int64 { + if len(ids) == 0 { + return nil + } + out := make([]int64, 0, len(ids)) + for _, id := range ids { + out = append(out, int64(id)) + } + return out +} + +func intersectInt64(a, b []int64) []int64 { + if len(a) == 0 || len(b) == 0 { + return nil + } + set := make(map[int64]struct{}, len(b)) + for _, id := range b { + set[id] = struct{}{} + } + out := make([]int64, 0, len(a)) + for _, id := range a { + if _, ok := set[id]; ok { + out = append(out, id) + } + } + return out +} + func parseOptionalFloat64(raw string) (*float64, error) { raw = strings.TrimSpace(raw) if raw == "" { diff --git a/internal/modules/repports/validations/repport.validation.go b/internal/modules/repports/validations/repport.validation.go index 6d50f3e6..4c1d7356 100644 --- a/internal/modules/repports/validations/repport.validation.go +++ b/internal/modules/repports/validations/repport.validation.go @@ -13,6 +13,8 @@ type ExpenseQuery struct { AreaId int64 `query:"area_id" validate:"omitempty"` LocationId int64 `query:"location_id" validate:"omitempty"` RealizationDate string `query:"realization_date" validate:"omitempty"` + AllowedAreaIDs []int64 `query:"-"` + AllowedLocationIDs []int64 `query:"-"` } type MarketingQuery struct { @@ -42,6 +44,7 @@ type PurchaseSupplierQuery struct { EndDate string `query:"end_date" validate:"omitempty"` SortBy string `query:"sort_by" validate:"omitempty"` FilterBy string `query:"filter_by" validate:"omitempty"` + AllowedAreaIDs []int64 `query:"-"` } type DebtSupplierQuery struct { diff --git a/internal/modules/sso/controllers/master_data.controller.go b/internal/modules/sso/controllers/master_data.controller.go new file mode 100644 index 00000000..2364011f --- /dev/null +++ b/internal/modules/sso/controllers/master_data.controller.go @@ -0,0 +1,331 @@ +package controllers + +import ( + "context" + "crypto/hmac" + "crypto/sha256" + "encoding/base64" + "encoding/hex" + "errors" + "fmt" + "strconv" + "strings" + "sync" + "time" + + "github.com/gofiber/fiber/v2" + "github.com/redis/go-redis/v9" + "gorm.io/gorm" + + "gitlab.com/mbugroup/lti-api.git/internal/config" + entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + sso "gitlab.com/mbugroup/lti-api.git/internal/modules/sso/verifier" + "gitlab.com/mbugroup/lti-api.git/internal/response" + "gitlab.com/mbugroup/lti-api.git/internal/utils" +) + +type MasterDataController struct { + db *gorm.DB + redis *redis.Client + clients map[string]config.SSOClientConfig + drift time.Duration + nonceTTL time.Duration + localNonce sync.Map +} + +type masterArea struct { + ID uint `json:"id"` + Name string `json:"name"` +} + +type masterLocation struct { + ID uint `json:"id"` + Name string `json:"name"` + AreaID uint `json:"area_id"` +} + +func NewMasterDataController(db *gorm.DB, redis *redis.Client, clients map[string]config.SSOClientConfig) *MasterDataController { + normalized := make(map[string]config.SSOClientConfig, len(clients)) + for alias, cfg := range clients { + alias = strings.ToLower(strings.TrimSpace(alias)) + normalized[alias] = cfg + } + + drift := config.SSOUserSyncDrift + if drift <= 0 { + drift = 2 * time.Minute + } + + nonceTTL := config.SSOUserSyncNonceTTL + if nonceTTL <= 0 { + nonceTTL = 10 * time.Minute + } + + return &MasterDataController{ + db: db, + redis: redis, + clients: normalized, + drift: drift, + nonceTTL: nonceTTL, + } +} + +func (h *MasterDataController) GetAreas(c *fiber.Ctx) error { + if _, _, err := h.authenticate(c, nil); err != nil { + return err + } + + search := strings.TrimSpace(c.Query("search", "")) + ids := parseUintList(c.Query("ids", "")) + + query := h.db.WithContext(c.Context()). + Model(&entity.Area{}). + Where("deleted_at IS NULL") + if search != "" { + query = query.Where("name ILIKE ?", "%"+search+"%") + } + if len(ids) > 0 { + query = query.Where("id IN ?", ids) + } + + var areas []masterArea + if err := query.Order("name ASC").Find(&areas).Error; err != nil { + utils.Log.WithError(err).Error("failed to fetch areas for master data") + return fiber.NewError(fiber.StatusInternalServerError, "failed to fetch areas") + } + + return c.Status(fiber.StatusOK).JSON(response.Success{ + Code: fiber.StatusOK, + Status: "success", + Message: "Get areas successfully", + Data: areas, + }) +} + +func (h *MasterDataController) GetLocations(c *fiber.Ctx) error { + if _, _, err := h.authenticate(c, nil); err != nil { + return err + } + + search := strings.TrimSpace(c.Query("search", "")) + areaIDs := parseUintList(c.Query("area_ids", "")) + ids := parseUintList(c.Query("ids", "")) + + query := h.db.WithContext(c.Context()). + Model(&entity.Location{}). + Where("deleted_at IS NULL") + if search != "" { + query = query.Where("name ILIKE ?", "%"+search+"%") + } + if len(areaIDs) > 0 { + query = query.Where("area_id IN ?", areaIDs) + } + if len(ids) > 0 { + query = query.Where("id IN ?", ids) + } + + var locations []masterLocation + if err := query.Order("name ASC").Find(&locations).Error; err != nil { + utils.Log.WithError(err).Error("failed to fetch locations for master data") + return fiber.NewError(fiber.StatusInternalServerError, "failed to fetch locations") + } + + return c.Status(fiber.StatusOK).JSON(response.Success{ + Code: fiber.StatusOK, + Status: "success", + Message: "Get locations successfully", + Data: locations, + }) +} + +func (h *MasterDataController) authenticate(c *fiber.Ctx, body []byte) (string, config.SSOClientConfig, error) { + rawAlias := strings.TrimSpace(c.Get("X-Sync-Client")) + if rawAlias == "" { + return "", config.SSOClientConfig{}, fiber.NewError(fiber.StatusUnauthorized, "missing sync client header") + } + + aliasKey := strings.ToLower(rawAlias) + clientCfg, ok := h.clients[aliasKey] + if !ok { + return "", config.SSOClientConfig{}, fiber.NewError(fiber.StatusUnauthorized, "unknown sync client") + } + + if err := h.verifyAuthorization(c, aliasKey); err != nil { + return "", config.SSOClientConfig{}, err + } + + secret := strings.TrimSpace(clientCfg.SyncSecret) + if secret == "" { + return "", config.SSOClientConfig{}, fiber.NewError(fiber.StatusUnauthorized, "sync secret not configured") + } + + timestamp := strings.TrimSpace(c.Get("X-Sync-Timestamp")) + nonce := strings.TrimSpace(c.Get("X-Sync-Nonce")) + signature := strings.TrimSpace(c.Get("X-Sync-Signature")) + if timestamp == "" || nonce == "" || signature == "" { + return "", config.SSOClientConfig{}, fiber.NewError(fiber.StatusUnauthorized, "missing signature headers") + } + if len(nonce) < 16 { + return "", config.SSOClientConfig{}, fiber.NewError(fiber.StatusUnauthorized, "nonce too short") + } + + ts, err := strconv.ParseInt(timestamp, 10, 64) + if err != nil { + return "", config.SSOClientConfig{}, fiber.NewError(fiber.StatusBadRequest, "invalid timestamp") + } + + msgTime := time.Unix(ts, 0).UTC() + now := time.Now().UTC() + drift := now.Sub(msgTime) + if drift > h.drift || drift < -h.drift { + return "", config.SSOClientConfig{}, fiber.NewError(fiber.StatusUnauthorized, "timestamp outside allowed window") + } + + providedSig, err := decodeMasterSignature(signature) + if err != nil { + return "", config.SSOClientConfig{}, fiber.NewError(fiber.StatusUnauthorized, "invalid signature encoding") + } + + expectedSignature := calculateSignature(secret, rawAlias, timestamp, nonce, body) + if !hmac.Equal(providedSig, expectedSignature) { + return "", config.SSOClientConfig{}, fiber.NewError(fiber.StatusUnauthorized, "invalid signature") + } + + if err := h.registerNonce(c.Context(), aliasKey, nonce); err != nil { + return "", config.SSOClientConfig{}, err + } + + return aliasKey, clientCfg, nil +} + +func (h *MasterDataController) verifyAuthorization(c *fiber.Ctx, alias string) error { + authHeader := strings.TrimSpace(c.Get(fiber.HeaderAuthorization)) + if authHeader == "" { + return fiber.NewError(fiber.StatusUnauthorized, "missing authorization header") + } + parts := strings.SplitN(authHeader, " ", 2) + if len(parts) != 2 || !strings.EqualFold(parts[0], "Bearer") { + return fiber.NewError(fiber.StatusUnauthorized, "invalid authorization header") + } + + token := strings.TrimSpace(parts[1]) + if token == "" { + return fiber.NewError(fiber.StatusUnauthorized, "invalid authorization header") + } + + verification, err := sso.VerifyAccessToken(token) + if err != nil { + return fiber.NewError(fiber.StatusUnauthorized, "invalid access token") + } + + if verification.ServiceAlias == "" || verification.ServiceAlias != alias { + return fiber.NewError(fiber.StatusUnauthorized, "service subject mismatch") + } + if !hasAnyScope(verification.Claims.Scopes(), []string{"sync.master", "sync.users"}) { + return fiber.NewError(fiber.StatusForbidden, "missing sync scope") + } + return nil +} + +func (h *MasterDataController) registerNonce(ctx context.Context, alias, nonce string) error { + ttl := h.nonceTTL + if ttl <= 0 { + ttl = 10 * time.Minute + } + + key := fmt.Sprintf("sso:sync:%s:%s", alias, nonce) + if h.redis != nil { + stored, err := h.redis.SetNX(ctx, key, "1", ttl).Result() + if err == nil { + if !stored { + return fiber.NewError(fiber.StatusUnauthorized, "nonce already used") + } + return nil + } + utils.Log.WithError(err).Warn("store sync nonce failed") + } + + now := time.Now().UTC() + if expRaw, ok := h.localNonce.Load(key); ok { + if expTime, ok := expRaw.(time.Time); ok && expTime.After(now) { + return fiber.NewError(fiber.StatusUnauthorized, "nonce already used") + } + } + h.localNonce.Store(key, now.Add(ttl)) + return nil +} + +func calculateSignature(secret, alias, timestamp, nonce string, body []byte) []byte { + mac := hmac.New(sha256.New, []byte(secret)) + mac.Write([]byte(alias)) + mac.Write([]byte("\n")) + mac.Write([]byte(timestamp)) + mac.Write([]byte("\n")) + mac.Write([]byte(nonce)) + mac.Write([]byte("\n")) + if len(body) > 0 { + mac.Write(body) + } + return mac.Sum(nil) +} + +func decodeMasterSignature(sig string) ([]byte, error) { + sig = strings.TrimSpace(sig) + if sig == "" { + return nil, errors.New("empty signature") + } + if decoded, err := hex.DecodeString(sig); err == nil { + return decoded, nil + } + if decoded, err := base64.StdEncoding.DecodeString(sig); err == nil { + return decoded, nil + } + if decoded, err := base64.URLEncoding.DecodeString(sig); err == nil { + return decoded, nil + } + return nil, errors.New("unrecognized signature encoding") +} + +func parseUintList(raw string) []uint { + raw = strings.TrimSpace(raw) + if raw == "" { + return nil + } + parts := strings.Split(raw, ",") + out := make([]uint, 0, len(parts)) + seen := make(map[uint]struct{}, len(parts)) + for _, part := range parts { + part = strings.TrimSpace(part) + if part == "" { + continue + } + val, err := strconv.ParseUint(part, 10, 64) + if err != nil || val == 0 { + continue + } + if _, ok := seen[uint(val)]; ok { + continue + } + seen[uint(val)] = struct{}{} + out = append(out, uint(val)) + } + return out +} + +func hasAnyScope(scopes []string, targets []string) bool { + if len(scopes) == 0 || len(targets) == 0 { + return false + } + for _, scope := range scopes { + scope = strings.ToLower(strings.TrimSpace(scope)) + if scope == "" { + continue + } + for _, target := range targets { + if scope == strings.ToLower(strings.TrimSpace(target)) { + return true + } + } + } + return false +} diff --git a/internal/modules/sso/controllers/sso.controller.go b/internal/modules/sso/controllers/sso.controller.go index 410e9577..b49d73e5 100644 --- a/internal/modules/sso/controllers/sso.controller.go +++ b/internal/modules/sso/controllers/sso.controller.go @@ -16,7 +16,7 @@ import ( "gitlab.com/mbugroup/lti-api.git/internal/config" "gitlab.com/mbugroup/lti-api.git/internal/modules/sso/session" - "gitlab.com/mbugroup/lti-api.git/internal/sso" + sso "gitlab.com/mbugroup/lti-api.git/internal/modules/sso/verifier" "gitlab.com/mbugroup/lti-api.git/internal/utils" "gitlab.com/mbugroup/lti-api.git/internal/utils/secure" ) diff --git a/internal/modules/sso/controllers/user_sync.controller.go b/internal/modules/sso/controllers/user_sync.controller.go index 9aeb9555..72c7768a 100644 --- a/internal/modules/sso/controllers/user_sync.controller.go +++ b/internal/modules/sso/controllers/user_sync.controller.go @@ -9,23 +9,24 @@ import ( "encoding/json" "errors" "fmt" - "github.com/go-playground/validator/v10" - "github.com/gofiber/fiber/v2" - "github.com/redis/go-redis/v9" - "github.com/sirupsen/logrus" - "gorm.io/gorm" "strconv" "strings" "sync" "time" + "github.com/go-playground/validator/v10" + "github.com/gofiber/fiber/v2" + "github.com/redis/go-redis/v9" + "github.com/sirupsen/logrus" + "gorm.io/gorm" + "gitlab.com/mbugroup/lti-api.git/internal/config" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" "gitlab.com/mbugroup/lti-api.git/internal/modules/sso/session" + sso "gitlab.com/mbugroup/lti-api.git/internal/modules/sso/verifier" "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto" userRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories" "gitlab.com/mbugroup/lti-api.git/internal/response" - "gitlab.com/mbugroup/lti-api.git/internal/sso" "gitlab.com/mbugroup/lti-api.git/internal/utils" ) diff --git a/internal/modules/sso/route.go b/internal/modules/sso/route.go index 3f2a699e..a864611e 100644 --- a/internal/modules/sso/route.go +++ b/internal/modules/sso/route.go @@ -26,6 +26,7 @@ func Routes(router fiber.Router, db *gorm.DB, validate *validator.Validate) { ctrl := ssoController.NewController(&http.Client{Timeout: 10 * time.Second}, store, session.GetRevocationStore()) userRepo := userRepository.NewUserRepository(db) syncCtrl := ssoController.NewUserSyncController(validate, userRepo, cache.Redis(), config.SSOClients) + masterCtrl := ssoController.NewMasterDataController(db, cache.Redis(), config.SSOClients) group := router.Group("/sso") group.Get("/start", middleware.NewLimiter(30, time.Minute), ctrl.Start) @@ -34,4 +35,6 @@ func Routes(router fiber.Router, db *gorm.DB, validate *validator.Validate) { group.Post("/refresh", middleware.NewLimiter(60, time.Minute), ctrl.Refresh) group.Post("/logout", middleware.NewLimiter(60, time.Minute), ctrl.Logout) group.Post("/users/sync", middleware.NewLimiter(30, time.Minute), syncCtrl.Sync) + group.Get("/master/areas", middleware.NewLimiter(60, time.Minute), masterCtrl.GetAreas) + group.Get("/master/locations", middleware.NewLimiter(60, time.Minute), masterCtrl.GetLocations) } diff --git a/internal/sso/profile.go b/internal/modules/sso/verifier/profile.go similarity index 95% rename from internal/sso/profile.go rename to internal/modules/sso/verifier/profile.go index a211fc74..52b7ef5a 100644 --- a/internal/sso/profile.go +++ b/internal/modules/sso/verifier/profile.go @@ -49,6 +49,10 @@ type Role struct { ClientID uint ClientAlias string ClientName string + AllArea bool + AllLocation bool + AreaIDs []uint + LocationIDs []uint Permissions []Permission RawReference json.RawMessage `json:"-"` } @@ -162,6 +166,10 @@ func fetchProfileFromSSO(ctx context.Context, token string) (*UserProfile, error ClientAlias: strings.TrimSpace(r.Client.Alias), ClientName: strings.TrimSpace(r.Client.Name), ClientID: uint(r.Client.ID), + AllArea: r.AllArea, + AllLocation: r.AllLocation, + AreaIDs: r.AreaIDs, + LocationIDs: r.LocationIDs, } rolePerms := make([]Permission, 0, len(r.Permissions)) for _, p := range r.Permissions { @@ -288,6 +296,10 @@ type userInfoRole struct { ID int64 `json:"id"` Key string `json:"key"` Name string `json:"name"` + AllArea bool `json:"all_area"` + AllLocation bool `json:"all_location"` + AreaIDs []uint `json:"area_ids"` + LocationIDs []uint `json:"location_ids"` Client userInfoClient `json:"client"` Permissions []userInfoPermRaw `json:"permissions"` } diff --git a/internal/sso/verifier.go b/internal/modules/sso/verifier/verifier.go similarity index 100% rename from internal/sso/verifier.go rename to internal/modules/sso/verifier/verifier.go From 26bf7f165e2c4d5bede4f16771ae1b95e476a2c5 Mon Sep 17 00:00:00 2001 From: ragilap Date: Wed, 14 Jan 2026 13:30:48 +0700 Subject: [PATCH 2/7] Revert "[FIX/BE-US]add feature restrict by location and areas in roles" This reverts commit dff9e73ab104e9adf4ca3ab3fefec80769e55c63. --- cmd/api/main.go | 2 +- internal/middleware/auth.go | 2 +- internal/middleware/role_scope.go | 280 --------------- .../services/daily-checklist.service.go | 11 +- .../controllers/dashboard.controller.go | 32 -- .../dashboards/services/dashboard.service.go | 6 - .../expense_realization.repository.go | 23 +- .../expenses/services/expense.service.go | 17 +- .../services/product-stock.service.go | 56 +-- .../services/product_warehouse.service.go | 32 +- .../transfers/services/transfer.service.go | 36 -- .../master/areas/services/area.service.go | 27 +- .../kandangs/services/kandang.service.go | 17 +- .../locations/services/location.service.go | 27 +- .../warehouses/services/warehouse.service.go | 17 +- .../services/project_flock_kandang.service.go | 29 +- .../repositories/projectflock.repository.go | 14 - .../projectflock_kandang.repository.go | 99 ------ .../services/projectflock.service.go | 18 +- .../purchases/services/purchase.service.go | 57 +-- .../controllers/repport.controller.go | 35 -- .../purchase_supplier.repository.go | 32 +- .../repports/services/repport.service.go | 64 ---- .../validations/repport.validation.go | 3 - .../sso/controllers/master_data.controller.go | 331 ------------------ .../modules/sso/controllers/sso.controller.go | 2 +- .../sso/controllers/user_sync.controller.go | 11 +- internal/modules/sso/route.go | 3 - .../{modules/sso/verifier => sso}/profile.go | 12 - .../{modules/sso/verifier => sso}/verifier.go | 0 30 files changed, 37 insertions(+), 1258 deletions(-) delete mode 100644 internal/middleware/role_scope.go delete mode 100644 internal/modules/sso/controllers/master_data.controller.go rename internal/{modules/sso/verifier => sso}/profile.go (95%) rename internal/{modules/sso/verifier => sso}/verifier.go (100%) diff --git a/cmd/api/main.go b/cmd/api/main.go index 76de7729..a7c278d7 100644 --- a/cmd/api/main.go +++ b/cmd/api/main.go @@ -14,8 +14,8 @@ import ( "gitlab.com/mbugroup/lti-api.git/internal/database" "gitlab.com/mbugroup/lti-api.git/internal/middleware" "gitlab.com/mbugroup/lti-api.git/internal/modules/sso/session" - sso "gitlab.com/mbugroup/lti-api.git/internal/modules/sso/verifier" "gitlab.com/mbugroup/lti-api.git/internal/route" + "gitlab.com/mbugroup/lti-api.git/internal/sso" "gitlab.com/mbugroup/lti-api.git/internal/utils" "github.com/gofiber/fiber/v2" diff --git a/internal/middleware/auth.go b/internal/middleware/auth.go index 1b670c14..a831c25b 100644 --- a/internal/middleware/auth.go +++ b/internal/middleware/auth.go @@ -7,8 +7,8 @@ import ( "gitlab.com/mbugroup/lti-api.git/internal/config" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" "gitlab.com/mbugroup/lti-api.git/internal/modules/sso/session" - sso "gitlab.com/mbugroup/lti-api.git/internal/modules/sso/verifier" service "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" + "gitlab.com/mbugroup/lti-api.git/internal/sso" "gitlab.com/mbugroup/lti-api.git/internal/utils" ) diff --git a/internal/middleware/role_scope.go b/internal/middleware/role_scope.go deleted file mode 100644 index f00e12d9..00000000 --- a/internal/middleware/role_scope.go +++ /dev/null @@ -1,280 +0,0 @@ -package middleware - -import ( - "errors" - "strings" - - "github.com/gofiber/fiber/v2" - "gorm.io/gorm" - - "gitlab.com/mbugroup/lti-api.git/internal/config" - entity "gitlab.com/mbugroup/lti-api.git/internal/entities" -) - -type ScopeFilter struct { - IDs []uint - Restrict bool -} - -type roleScope struct { - allArea bool - allLocation bool - areaIDs []uint - locationIDs []uint - hasAnyScopes bool -} - -func ResolveAreaScope(c *fiber.Ctx, db *gorm.DB) (ScopeFilter, error) { - scope, err := collectRoleScope(c) - if err != nil || !scope.hasAnyScopes { - return ScopeFilter{}, err - } - - if scope.allArea || scope.allLocation { - return ScopeFilter{}, nil - } - - allowed := uniqueUint(scope.areaIDs) - if len(scope.locationIDs) > 0 { - derived, err := areaIDsByLocationIDs(db, scope.locationIDs) - if err != nil { - return ScopeFilter{}, err - } - allowed = uniqueUint(append(allowed, derived...)) - } - - if len(allowed) == 0 { - return ScopeFilter{Restrict: true}, nil - } - return ScopeFilter{IDs: allowed, Restrict: true}, nil -} - -func ResolveLocationScope(c *fiber.Ctx, db *gorm.DB) (ScopeFilter, error) { - scope, err := collectRoleScope(c) - if err != nil || !scope.hasAnyScopes { - return ScopeFilter{}, err - } - - if scope.allLocation || scope.allArea { - return ScopeFilter{}, nil - } - - areaIDs := uniqueUint(scope.areaIDs) - locationIDs := uniqueUint(scope.locationIDs) - - switch { - case len(locationIDs) > 0 && len(areaIDs) > 0: - filtered, err := filterLocationIDsByAreaIDs(db, locationIDs, areaIDs) - if err != nil { - return ScopeFilter{}, err - } - locationIDs = filtered - case len(locationIDs) == 0 && len(areaIDs) > 0: - derived, err := locationIDsByAreaIDs(db, areaIDs) - if err != nil { - return ScopeFilter{}, err - } - locationIDs = derived - } - - locationIDs = uniqueUint(locationIDs) - if len(locationIDs) == 0 { - return ScopeFilter{Restrict: true}, nil - } - return ScopeFilter{IDs: locationIDs, Restrict: true}, nil -} - -func collectRoleScope(c *fiber.Ctx) (roleScope, error) { - ctx, ok := AuthDetails(c) - if !ok || ctx == nil || len(ctx.Roles) == 0 { - return roleScope{}, nil - } - - clientAlias := resolveClientAlias(ctx) - - scope := roleScope{} - areaSet := make(map[uint]struct{}) - locationSet := make(map[uint]struct{}) - - for _, role := range ctx.Roles { - if clientAlias != "" && !strings.EqualFold(strings.TrimSpace(role.ClientAlias), clientAlias) { - continue - } - scope.hasAnyScopes = true - if role.AllArea { - scope.allArea = true - } - if role.AllLocation { - scope.allLocation = true - } - for _, id := range role.AreaIDs { - if id == 0 { - continue - } - areaSet[id] = struct{}{} - } - for _, id := range role.LocationIDs { - if id == 0 { - continue - } - locationSet[id] = struct{}{} - } - } - - scope.areaIDs = keysUint(areaSet) - scope.locationIDs = keysUint(locationSet) - - scope.hasAnyScopes = scope.hasAnyScopes && - (scope.allArea || scope.allLocation || len(scope.areaIDs) > 0 || len(scope.locationIDs) > 0) - - return scope, nil -} - -func areaIDsByLocationIDs(db *gorm.DB, locationIDs []uint) ([]uint, error) { - if db == nil { - return nil, errors.New("database not configured") - } - if len(locationIDs) == 0 { - return nil, nil - } - var areaIDs []uint - if err := db.Model(&entity.Location{}). - Where("deleted_at IS NULL"). - Where("id IN ?", locationIDs). - Distinct("area_id"). - Pluck("area_id", &areaIDs).Error; err != nil { - return nil, err - } - return areaIDs, nil -} - -func locationIDsByAreaIDs(db *gorm.DB, areaIDs []uint) ([]uint, error) { - if db == nil { - return nil, errors.New("database not configured") - } - if len(areaIDs) == 0 { - return nil, nil - } - var locationIDs []uint - if err := db.Model(&entity.Location{}). - Where("deleted_at IS NULL"). - Where("area_id IN ?", areaIDs). - Distinct("id"). - Pluck("id", &locationIDs).Error; err != nil { - return nil, err - } - return locationIDs, nil -} - -func filterLocationIDsByAreaIDs(db *gorm.DB, locationIDs, areaIDs []uint) ([]uint, error) { - if db == nil { - return nil, errors.New("database not configured") - } - if len(locationIDs) == 0 || len(areaIDs) == 0 { - return nil, nil - } - var filtered []uint - if err := db.Model(&entity.Location{}). - Where("deleted_at IS NULL"). - Where("id IN ?", locationIDs). - Where("area_id IN ?", areaIDs). - Distinct("id"). - Pluck("id", &filtered).Error; err != nil { - return nil, err - } - return filtered, nil -} - -func uniqueUint(ids []uint) []uint { - if len(ids) == 0 { - return nil - } - seen := make(map[uint]struct{}, len(ids)) - result := make([]uint, 0, len(ids)) - for _, id := range ids { - if id == 0 { - continue - } - if _, ok := seen[id]; ok { - continue - } - seen[id] = struct{}{} - result = append(result, id) - } - return result -} - -func keysUint(set map[uint]struct{}) []uint { - if len(set) == 0 { - return nil - } - out := make([]uint, 0, len(set)) - for id := range set { - out = append(out, id) - } - return out -} - -func resolveClientAlias(ctx *AuthContext) string { - if ctx == nil || ctx.Verification == nil || ctx.Verification.Claims == nil { - return "" - } - - scopes := ctx.Verification.Claims.Scopes() - if len(scopes) == 0 { - return "" - } - - seen := make(map[string]struct{}) - for _, scope := range scopes { - scope = strings.ToLower(strings.TrimSpace(scope)) - if scope == "" { - continue - } - prefix := scope - if idx := strings.IndexAny(prefix, ".:"); idx > 0 { - prefix = prefix[:idx] - } - prefix = strings.TrimSpace(prefix) - if prefix == "" { - continue - } - if alias := matchAlias(prefix); alias != "" { - seen[alias] = struct{}{} - } - } - - if len(seen) != 1 { - return "" - } - for alias := range seen { - return alias - } - return "" -} - -func matchAlias(alias string) string { - alias = strings.ToLower(strings.TrimSpace(alias)) - if alias == "" { - return "" - } - if _, ok := config.SSOClients[alias]; ok { - return alias - } - for key := range config.SSOClients { - if strings.EqualFold(key, alias) { - return strings.ToLower(strings.TrimSpace(key)) - } - } - return "" -} - -func ApplyScopeFilter(db *gorm.DB, scope ScopeFilter, column string) *gorm.DB { - if db == nil || !scope.Restrict { - return db - } - if len(scope.IDs) == 0 { - return db.Where("1 = 0") - } - return db.Where(column+" IN ?", scope.IDs) -} diff --git a/internal/modules/daily-checklists/services/daily-checklist.service.go b/internal/modules/daily-checklists/services/daily-checklist.service.go index 64802560..f306c74d 100644 --- a/internal/modules/daily-checklists/services/daily-checklist.service.go +++ b/internal/modules/daily-checklists/services/daily-checklist.service.go @@ -9,7 +9,7 @@ import ( "time" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" - m "gitlab.com/mbugroup/lti-api.git/internal/middleware" + middleware "gitlab.com/mbugroup/lti-api.git/internal/middleware" repository "gitlab.com/mbugroup/lti-api.git/internal/modules/daily-checklists/repositories" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/daily-checklists/validations" phaseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/phasess/repositories" @@ -456,7 +456,7 @@ func (s dailyChecklistService) UpdateOne(c *fiber.Ctx, req *validation.Update, i updateBody["reject_reason"] = *req.RejectReason } - actorID, err := m.ActorIDFromContext(c) + actorID, err := middleware.ActorIDFromContext(c) if err != nil { return &entity.DailyChecklist{}, fiber.NewError(fiber.StatusUnauthorized, "Failed to get actor ID from context") } @@ -946,11 +946,6 @@ func (s dailyChecklistService) GetReport(c *fiber.Ctx, params *validation.Report return nil, 0, err } - scope, err := m.ResolveLocationScope(c, s.Repository.DB()) - if err != nil { - return nil, 0, err - } - offset := (params.Page - 1) * params.Limit buildBase := func() *gorm.DB { @@ -967,8 +962,6 @@ func (s dailyChecklistService) GetReport(c *fiber.Ctx, params *validation.Report Where("EXTRACT(YEAR FROM dc.date) = ?", params.Year). Where("dc.status = ?", "APPROVED") - db = m.ApplyScopeFilter(db, scope, "loc.id") - if params.AreaID != nil { db = db.Where("a.id = ?", *params.AreaID) } diff --git a/internal/modules/dashboards/controllers/dashboard.controller.go b/internal/modules/dashboards/controllers/dashboard.controller.go index f74f31a7..bebad10f 100644 --- a/internal/modules/dashboards/controllers/dashboard.controller.go +++ b/internal/modules/dashboards/controllers/dashboard.controller.go @@ -6,7 +6,6 @@ import ( "strings" "time" - m "gitlab.com/mbugroup/lti-api.git/internal/middleware" "gitlab.com/mbugroup/lti-api.git/internal/modules/dashboards/dto" service "gitlab.com/mbugroup/lti-api.git/internal/modules/dashboards/services" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/dashboards/validations" @@ -82,20 +81,6 @@ func (u *DashboardController) GetAll(c *fiber.Ctx) error { return fiber.NewError(fiber.StatusBadRequest, "Invalid include") } - scope, err := m.ResolveLocationScope(c, u.DashboardService.DB()) - if err != nil { - return err - } - if scope.Restrict { - if len(scope.IDs) == 0 { - lokasiIds = []uint{} - } else if len(lokasiIds) > 0 { - lokasiIds = intersectUint(lokasiIds, scope.IDs) - } else { - lokasiIds = scope.IDs - } - } - analysisMode := strings.ToUpper(strings.TrimSpace(c.Query("analysis_mode", validation.AnalysisModeOverview))) metric := strings.ToLower(strings.TrimSpace(c.Query("metric", ""))) @@ -191,23 +176,6 @@ func defaultUintSlice(values []uint) []uint { return values } -func intersectUint(a, b []uint) []uint { - if len(a) == 0 || len(b) == 0 { - return nil - } - set := make(map[uint]struct{}, len(b)) - for _, id := range b { - set[id] = struct{}{} - } - out := make([]uint, 0, len(a)) - for _, id := range a { - if _, ok := set[id]; ok { - out = append(out, id) - } - } - return out -} - func parsePeriodDates(startDateRaw, endDateRaw string, location *time.Location) (time.Time, time.Time, time.Time, error) { now := time.Now().In(location) startDate := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, location) diff --git a/internal/modules/dashboards/services/dashboard.service.go b/internal/modules/dashboards/services/dashboard.service.go index afdbd1c1..b4635b2e 100644 --- a/internal/modules/dashboards/services/dashboard.service.go +++ b/internal/modules/dashboards/services/dashboard.service.go @@ -17,12 +17,10 @@ import ( "github.com/go-playground/validator/v10" "github.com/sirupsen/logrus" - "gorm.io/gorm" ) type DashboardService interface { GetAll(ctx context.Context, params *validation.Query) (dto.DashboardPerformanceOverviewDTO, int64, error) - DB() *gorm.DB } type dashboardService struct { @@ -39,10 +37,6 @@ func NewDashboardService(repo repository.DashboardRepository, validate *validato } } -func (s dashboardService) DB() *gorm.DB { - return s.Repository.DB() -} - func (s dashboardService) GetAll(ctx context.Context, params *validation.Query) (dto.DashboardPerformanceOverviewDTO, int64, error) { if err := s.Validate.Struct(params); err != nil { return dto.DashboardPerformanceOverviewDTO{}, 0, err diff --git a/internal/modules/expenses/repositories/expense_realization.repository.go b/internal/modules/expenses/repositories/expense_realization.repository.go index 504e65ad..60ec97a7 100644 --- a/internal/modules/expenses/repositories/expense_realization.repository.go +++ b/internal/modules/expenses/repositories/expense_realization.repository.go @@ -138,28 +138,9 @@ func (r *ExpenseRealizationRepositoryImpl) GetAllWithFilters(ctx context.Context locationID := filters.LocationId areaID := filters.AreaId - if filters.AllowedLocationIDs != nil || filters.AllowedAreaIDs != nil || locationID > 0 || areaID > 0 { - db = db.Joins("JOIN kandangs ON kandangs.id = expense_nonstocks.kandang_id") - } - - if filters.AllowedLocationIDs != nil { - if len(filters.AllowedLocationIDs) == 0 { - db = db.Where("1 = 0") - } else { - db = db.Where("kandangs.location_id IN ?", filters.AllowedLocationIDs) - } - } - - if filters.AllowedAreaIDs != nil { - if len(filters.AllowedAreaIDs) == 0 { - db = db.Where("1 = 0") - } else { - db = db.Joins("JOIN locations ON locations.id = kandangs.location_id"). - Where("locations.area_id IN ?", filters.AllowedAreaIDs) - } - } - if locationID > 0 || areaID > 0 { + db = db.Joins("JOIN kandangs ON kandangs.id = expense_nonstocks.kandang_id") + if locationID > 0 { db = db.Where("kandangs.location_id = ?", uint(locationID)) } diff --git a/internal/modules/expenses/services/expense.service.go b/internal/modules/expenses/services/expense.service.go index 27b4a07f..3bf2db55 100644 --- a/internal/modules/expenses/services/expense.service.go +++ b/internal/modules/expenses/services/expense.service.go @@ -87,16 +87,10 @@ func (s expenseService) GetAll(c *fiber.Ctx, params *validation.Query) ([]expens return nil, 0, err } - scope, err := middleware.ResolveLocationScope(c, s.Repository.DB()) - if err != nil { - return nil, 0, err - } - offset := (params.Page - 1) * params.Limit expenses, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB { db = s.withRelations(db) - db = middleware.ApplyScopeFilter(db, scope, "location_id") if params.Search != "" { return db.Where("category ILIKE ?", "%"+params.Search+"%") } @@ -123,16 +117,7 @@ func (s expenseService) GetAll(c *fiber.Ctx, params *validation.Query) ([]expens } func (s expenseService) GetOne(c *fiber.Ctx, id uint) (*expenseDto.ExpenseDetailDTO, error) { - scope, err := middleware.ResolveLocationScope(c, s.Repository.DB()) - if err != nil { - return nil, err - } - - expense, err := s.Repository.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB { - db = s.withRelations(db) - db = middleware.ApplyScopeFilter(db, scope, "location_id") - return db - }) + expense, err := s.Repository.GetByID(c.Context(), id, s.withRelations) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { diff --git a/internal/modules/inventory/product-stocks/services/product-stock.service.go b/internal/modules/inventory/product-stocks/services/product-stock.service.go index a4e404d6..11475109 100644 --- a/internal/modules/inventory/product-stocks/services/product-stock.service.go +++ b/internal/modules/inventory/product-stocks/services/product-stock.service.go @@ -4,7 +4,6 @@ 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" @@ -62,34 +61,15 @@ func (s productStockService) GetAll(c *fiber.Ctx, params *validation.Query) ([]e return nil, 0, err } - scope, err := m.ResolveLocationScope(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 { - if scope.Restrict { - if len(scope.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 w.location_id IN ? - )`, scope.IDs) - } else { - db = db.Where(`EXISTS ( - SELECT 1 - FROM product_warehouses pw - WHERE pw.product_id = products.id - AND pw.qty > 0 - )`) - } + db = db.Where(`EXISTS ( + SELECT 1 + FROM product_warehouses pw + WHERE pw.product_id = products.id + AND pw.qty > 0 + )`) db = s.withRelations(db) if params.Search != "" { @@ -106,30 +86,6 @@ func (s productStockService) GetAll(c *fiber.Ctx, params *validation.Query) ([]e } func (s productStockService) GetOne(c *fiber.Ctx, id uint) (*entity.Product, error) { - scope, err := m.ResolveLocationScope(c, s.ProductRepository.DB()) - if err != nil { - return nil, err - } - - if scope.Restrict { - if len(scope.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("w.location_id IN ?", scope.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, s.withRelations) if errors.Is(err, gorm.ErrRecordNotFound) { return nil, fiber.NewError(fiber.StatusNotFound, "Product not found") 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 5c162084..152bfa24 100644 --- a/internal/modules/inventory/product-warehouses/services/product_warehouse.service.go +++ b/internal/modules/inventory/product-warehouses/services/product_warehouse.service.go @@ -8,7 +8,7 @@ import ( 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" "gitlab.com/mbugroup/lti-api.git/internal/utils" - m "gitlab.com/mbugroup/lti-api.git/internal/middleware" + "github.com/go-playground/validator/v10" "github.com/gofiber/fiber/v2" "github.com/sirupsen/logrus" @@ -53,11 +53,6 @@ func (s productWarehouseService) GetAll(c *fiber.Ctx, params *validation.Query) return nil, 0, err } - 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 { @@ -95,14 +90,6 @@ 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) - if scope.Restrict { - if len(scope.IDs) == 0 { - return db.Where("1 = 0") - } - db = db.Joins("JOIN warehouses w_scope ON product_warehouses.warehouse_id = w_scope.id"). - Where("w_scope.location_id IN ?", scope.IDs) - } - if params.ProductId != 0 { db = db.Where("product_id = ?", params.ProductId) } @@ -129,22 +116,7 @@ func (s productWarehouseService) GetAll(c *fiber.Ctx, params *validation.Query) } func (s productWarehouseService) GetOne(c *fiber.Ctx, id uint) (*entity.ProductWarehouse, error) { - 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) - if scope.Restrict { - if len(scope.IDs) == 0 { - return db.Where("1 = 0") - } - db = db.Joins("JOIN warehouses w_scope ON product_warehouses.warehouse_id = w_scope.id"). - Where("w_scope.location_id IN ?", scope.IDs) - } - return db - }) + productWarehouse, err := s.Repository.GetByID(c.Context(), id, s.withRelations) if errors.Is(err, gorm.ErrRecordNotFound) { return nil, fiber.NewError(fiber.StatusNotFound, "ProductWarehouse not found") } diff --git a/internal/modules/inventory/transfers/services/transfer.service.go b/internal/modules/inventory/transfers/services/transfer.service.go index b4652a5d..3f12b444 100644 --- a/internal/modules/inventory/transfers/services/transfer.service.go +++ b/internal/modules/inventory/transfers/services/transfer.service.go @@ -94,24 +94,10 @@ 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 != "" { db = db.Where("movement_number ILIKE ?", "%"+strings.TrimSpace(params.Search)+"%") } @@ -126,28 +112,6 @@ 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) diff --git a/internal/modules/master/areas/services/area.service.go b/internal/modules/master/areas/services/area.service.go index 1ec30d8d..e6f9205c 100644 --- a/internal/modules/master/areas/services/area.service.go +++ b/internal/modules/master/areas/services/area.service.go @@ -47,21 +47,10 @@ func (s areaService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.Ar return nil, 0, err } - scope, err := m.ResolveAreaScope(c, s.Repository.DB()) - if err != nil { - return nil, 0, err - } - offset := (params.Page - 1) * params.Limit areas, total, err := s.Repository.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.Where("id IN ?", scope.IDs) - } if params.Search != "" { return db.Where("name ILIKE ?", "%"+params.Search+"%") } @@ -76,21 +65,7 @@ func (s areaService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.Ar } func (s areaService) GetOne(c *fiber.Ctx, id uint) (*entity.Area, error) { - scope, err := m.ResolveAreaScope(c, s.Repository.DB()) - if err != nil { - return nil, err - } - - area, err := s.Repository.GetByID(c.Context(), id, 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.Where("id IN ?", scope.IDs) - } - return db - }) + area, err := s.Repository.GetByID(c.Context(), id, s.withRelations) if errors.Is(err, gorm.ErrRecordNotFound) { return nil, fiber.NewError(fiber.StatusNotFound, "Area not found") } diff --git a/internal/modules/master/kandangs/services/kandang.service.go b/internal/modules/master/kandangs/services/kandang.service.go index b70577d6..9f83f0ce 100644 --- a/internal/modules/master/kandangs/services/kandang.service.go +++ b/internal/modules/master/kandangs/services/kandang.service.go @@ -49,16 +49,10 @@ func (s kandangService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity return nil, 0, err } - scope, err := m.ResolveLocationScope(c, s.Repository.DB()) - if err != nil { - return nil, 0, err - } - offset := (params.Page - 1) * params.Limit kandangs, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB { db = s.withRelations(db) - db = m.ApplyScopeFilter(db, scope, "location_id") if params.Search != "" { return db.Where("name ILIKE ?", "%"+params.Search+"%") } @@ -79,16 +73,7 @@ func (s kandangService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity } func (s kandangService) GetOne(c *fiber.Ctx, id uint) (*entity.Kandang, error) { - scope, err := m.ResolveLocationScope(c, s.Repository.DB()) - if err != nil { - return nil, err - } - - kandang, err := s.Repository.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB { - db = s.withRelations(db) - db = m.ApplyScopeFilter(db, scope, "location_id") - return db - }) + kandang, err := s.Repository.GetByID(c.Context(), id, s.withRelations) if errors.Is(err, gorm.ErrRecordNotFound) { return nil, fiber.NewError(fiber.StatusNotFound, "Kandang not found") } diff --git a/internal/modules/master/locations/services/location.service.go b/internal/modules/master/locations/services/location.service.go index 633d7419..3a1d1e23 100644 --- a/internal/modules/master/locations/services/location.service.go +++ b/internal/modules/master/locations/services/location.service.go @@ -47,21 +47,10 @@ func (s locationService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entit return nil, 0, err } - scope, err := m.ResolveLocationScope(c, s.Repository.DB()) - if err != nil { - return nil, 0, err - } - offset := (params.Page - 1) * params.Limit locations, total, err := s.Repository.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.Where("id IN ?", scope.IDs) - } if params.Search != "" { db = db.Where("name ILIKE ?", "%"+params.Search+"%") } @@ -79,21 +68,7 @@ func (s locationService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entit } func (s locationService) GetOne(c *fiber.Ctx, id uint) (*entity.Location, error) { - scope, err := m.ResolveLocationScope(c, s.Repository.DB()) - if err != nil { - return nil, err - } - - location, err := s.Repository.GetByID(c.Context(), id, 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.Where("id IN ?", scope.IDs) - } - return db - }) + location, err := s.Repository.GetByID(c.Context(), id, s.withRelations) if errors.Is(err, gorm.ErrRecordNotFound) { return nil, fiber.NewError(fiber.StatusNotFound, "Location not found") } diff --git a/internal/modules/master/warehouses/services/warehouse.service.go b/internal/modules/master/warehouses/services/warehouse.service.go index aaa5ca7e..7eeaad3d 100644 --- a/internal/modules/master/warehouses/services/warehouse.service.go +++ b/internal/modules/master/warehouses/services/warehouse.service.go @@ -48,16 +48,10 @@ func (s warehouseService) GetAll(c *fiber.Ctx, params *validation.Query) ([]enti return nil, 0, err } - scope, err := m.ResolveAreaScope(c, s.Repository.DB()) - if err != nil { - return nil, 0, err - } - offset := (params.Page - 1) * params.Limit warehouses, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB { db = s.withRelations(db) - db = m.ApplyScopeFilter(db, scope, "area_id") if params.Search != "" { db = db.Where("warehouses.name ILIKE ?", "%"+params.Search+"%") } @@ -92,16 +86,7 @@ func (s warehouseService) GetAll(c *fiber.Ctx, params *validation.Query) ([]enti } func (s warehouseService) GetOne(c *fiber.Ctx, id uint) (*entity.Warehouse, error) { - scope, err := m.ResolveAreaScope(c, s.Repository.DB()) - if err != nil { - return nil, err - } - - warehouse, err := s.Repository.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB { - db = s.withRelations(db) - db = m.ApplyScopeFilter(db, scope, "area_id") - return db - }) + warehouse, err := s.Repository.GetByID(c.Context(), id, s.withRelations) if errors.Is(err, gorm.ErrRecordNotFound) { return nil, fiber.NewError(fiber.StatusNotFound, "Warehouse not found") } 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 61a593d5..6f019ffa 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 @@ -88,14 +88,9 @@ func (s projectFlockKandangService) GetAll(c *fiber.Ctx, params *validation.Quer return nil, 0, err } - scope, err := m.ResolveLocationScope(c, s.Repository.DB()) - if err != nil { - return nil, 0, err - } - offset := (params.Page - 1) * params.Limit - projectFlockKandangs, total, err := s.Repository.GetAllWithFiltersScoped(c.Context(), offset, params.Limit, params, scope.IDs, scope.Restrict) + projectFlockKandangs, total, err := s.Repository.GetAllWithFilters(c.Context(), offset, params.Limit, params) if err != nil { s.Log.Errorf("Failed to get projectFlockKandangs: %+v", err) @@ -111,28 +106,6 @@ func (s projectFlockKandangService) GetAll(c *fiber.Ctx, params *validation.Quer } func (s projectFlockKandangService) GetOne(c *fiber.Ctx, id uint) (*entity.ProjectFlockKandang, map[uint]float64, []entity.ProductWarehouse, error) { - scope, err := m.ResolveLocationScope(c, s.Repository.DB()) - if err != nil { - return nil, nil, nil, err - } - if scope.Restrict { - if len(scope.IDs) == 0 { - return nil, nil, nil, fiber.NewError(fiber.StatusNotFound, "ProjectFlockKandang not found") - } - var count int64 - if err := s.Repository.DB().WithContext(c.Context()). - Table("project_flock_kandangs"). - Joins("JOIN project_flocks ON project_flocks.id = project_flock_kandangs.project_flock_id"). - Where("project_flock_kandangs.id = ?", id). - Where("project_flocks.location_id IN ?", scope.IDs). - Count(&count).Error; err != nil { - return nil, nil, nil, err - } - if count == 0 { - return nil, nil, nil, fiber.NewError(fiber.StatusNotFound, "ProjectFlockKandang not found") - } - } - projectFlockKandang, err := s.Repository.GetByID(c.Context(), id) if errors.Is(err, gorm.ErrRecordNotFound) { return nil, nil, nil, fiber.NewError(fiber.StatusNotFound, "ProjectFlockKandang not found") diff --git a/internal/modules/production/project_flocks/repositories/projectflock.repository.go b/internal/modules/production/project_flocks/repositories/projectflock.repository.go index 346f2176..e65dfb4a 100644 --- a/internal/modules/production/project_flocks/repositories/projectflock.repository.go +++ b/internal/modules/production/project_flocks/repositories/projectflock.repository.go @@ -14,7 +14,6 @@ import ( type ProjectflockRepository interface { repository.BaseRepository[entity.ProjectFlock] GetAllWithFilters(ctx context.Context, offset, limit int, params *validation.Query) ([]entity.ProjectFlock, int64, error) - GetAllWithFiltersScoped(ctx context.Context, offset, limit int, params *validation.Query, locationIDs []uint, restrict bool) ([]entity.ProjectFlock, int64, error) WithDefaultRelations() func(*gorm.DB) *gorm.DB ExistsByFlockName(ctx context.Context, flockName string, excludeID *uint) (bool, error) GetNextPeriodsForKandangs(ctx context.Context, kandangIDs []uint) (map[uint]int, error) @@ -49,19 +48,6 @@ func (r *ProjectflockRepositoryImpl) GetAllWithFilters(ctx context.Context, offs }) } -func (r *ProjectflockRepositoryImpl) GetAllWithFiltersScoped(ctx context.Context, offset, limit int, params *validation.Query, locationIDs []uint, restrict bool) ([]entity.ProjectFlock, int64, error) { - return r.GetAll(ctx, offset, limit, func(db *gorm.DB) *gorm.DB { - db = r.applyQueryFilters(r.WithDefaultRelations()(db), params) - if restrict { - if len(locationIDs) == 0 { - return db.Where("1 = 0") - } - db = db.Where("project_flocks.location_id IN ?", locationIDs) - } - return db - }) -} - func (r *ProjectflockRepositoryImpl) WithDefaultRelations() func(*gorm.DB) *gorm.DB { return func(db *gorm.DB) *gorm.DB { return db. 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 24c4c083..474a53c2 100644 --- a/internal/modules/production/project_flocks/repositories/projectflock_kandang.repository.go +++ b/internal/modules/production/project_flocks/repositories/projectflock_kandang.repository.go @@ -20,7 +20,6 @@ type ProjectFlockKandangRepository interface { DeleteMany(ctx context.Context, projectFlockID uint, kandangIDs []uint) error GetAll(ctx context.Context, offset int, limit int, modifier func(*gorm.DB) *gorm.DB) ([]entity.ProjectFlockKandang, int64, error) GetAllWithFilters(ctx context.Context, offset int, limit int, params interface{}) ([]entity.ProjectFlockKandang, int64, error) - GetAllWithFiltersScoped(ctx context.Context, offset int, limit int, params interface{}, locationIDs []uint, restrict bool) ([]entity.ProjectFlockKandang, int64, error) GetByProjectFlockID(ctx context.Context, projectFlockID uint) ([]entity.ProjectFlockKandang, error) ListExistingKandangIDs(ctx context.Context, projectFlockID uint, kandangIDs []uint) ([]uint, error) HasKandangsLinkedToOtherProject(ctx context.Context, kandangIDs []uint, exceptProjectID *uint) (bool, error) @@ -197,104 +196,6 @@ func (r *projectFlockKandangRepositoryImpl) GetAllWithFilters(ctx context.Contex return records, total, nil } -func (r *projectFlockKandangRepositoryImpl) GetAllWithFiltersScoped(ctx context.Context, offset int, limit int, params interface{}, locationIDs []uint, restrict bool) ([]entity.ProjectFlockKandang, int64, error) { - var records []entity.ProjectFlockKandang - var total int64 - - query, ok := params.(*validation.Query) - - q := r.db.WithContext(ctx). - Joins("JOIN \"kandangs\" ON \"project_flock_kandangs\".\"kandang_id\" = \"kandangs\".\"id\""). - Joins("JOIN \"project_flocks\" ON \"project_flock_kandangs\".\"project_flock_id\" = \"project_flocks\".\"id\""). - Preload("ProjectFlock"). - Preload("ProjectFlock.Fcr"). - Preload("ProjectFlock.Area"). - Preload("ProjectFlock.Location"). - Preload("ProjectFlock.CreatedUser"). - Preload("ProjectFlock.Kandangs"). - Preload("ProjectFlock.KandangHistory"). - Preload("Kandang"). - Preload("Chickins"). - Preload("Chickins.CreatedUser"). - Preload("Chickins.ProductWarehouse") - - if restrict { - if len(locationIDs) == 0 { - return []entity.ProjectFlockKandang{}, 0, nil - } - q = q.Where("\"project_flocks\".\"location_id\" IN ?", locationIDs) - } - - if ok && query != nil && query.StepName != "" { - q = q.Where(` - EXISTS ( - SELECT 1 FROM "approvals" - WHERE "approvals"."approvable_id" = "project_flock_kandangs"."id" - AND "approvals"."approvable_type" = ? - AND LOWER("approvals"."step_name") = LOWER(?) - AND "approvals"."id" IN ( - SELECT "approvals"."id" FROM "approvals" - WHERE "approvals"."approvable_id" = "project_flock_kandangs"."id" - AND "approvals"."approvable_type" = ? - ORDER BY "approvals"."id" DESC - LIMIT 1 - ) - ) - `, "PROJECT_FLOCK_KANDANGS", query.StepName, "PROJECT_FLOCK_KANDANGS") - } - - if ok && query != nil { - if query.Search != "" { - escapedSearch := strings.NewReplacer("\\", "\\\\", "%", "\\%", "_", "\\_").Replace(query.Search) - q = q.Where( - r.db.Where("LOWER(\"kandangs\".\"name\") LIKE LOWER(?) ESCAPE '\\'", "%"+escapedSearch+"%"). - Or("LOWER(\"project_flocks\".\"flock_name\") LIKE LOWER(?) ESCAPE '\\'", "%"+escapedSearch+"%"), - ) - } - - if query.ProjectFlockId > 0 { - q = q.Where("\"project_flock_kandangs\".\"project_flock_id\" = ?", query.ProjectFlockId) - } - - if query.KandangId > 0 { - q = q.Where("\"project_flock_kandangs\".\"kandang_id\" = ?", query.KandangId) - } - - if query.Category != "" { - q = q.Where("\"project_flocks\".\"category\" = ?", query.Category) - } - - if query.AreaId > 0 { - q = q.Where("\"project_flocks\".\"area_id\" = ?", query.AreaId) - } - } - - if err := q.Model(&entity.ProjectFlockKandang{}).Count(&total).Error; err != nil { - return nil, 0, err - } - - sortBy := "\"project_flock_kandangs\".\"created_at\" DESC" - if ok && query != nil && query.SortBy != "" { - sortOrder := "DESC" - if query.SortOrder == "ASC" { - sortOrder = "ASC" - } - - switch query.SortBy { - case "created_at": - sortBy = "\"project_flock_kandangs\".\"created_at\" " + sortOrder - case "period": - sortBy = "\"project_flocks\".\"period\" " + sortOrder - } - } - - if err := q.Order(sortBy).Offset(offset).Limit(limit).Find(&records).Error; err != nil { - return nil, 0, err - } - - return records, total, nil -} - func (r *projectFlockKandangRepositoryImpl) WithTx(tx *gorm.DB) ProjectFlockKandangRepository { return &projectFlockKandangRepositoryImpl{db: tx} } diff --git a/internal/modules/production/project_flocks/services/projectflock.service.go b/internal/modules/production/project_flocks/services/projectflock.service.go index 5bccfbf9..3dbe3f4b 100644 --- a/internal/modules/production/project_flocks/services/projectflock.service.go +++ b/internal/modules/production/project_flocks/services/projectflock.service.go @@ -114,14 +114,9 @@ func (s projectflockService) GetAll(c *fiber.Ctx, params *validation.Query) ([]e return nil, 0, nil, err } - scope, err := m.ResolveLocationScope(c, s.Repository.DB()) - if err != nil { - return nil, 0, nil, err - } - offset := (params.Page - 1) * params.Limit - projectflocks, total, err := s.Repository.GetAllWithFiltersScoped(c.Context(), offset, params.Limit, params, scope.IDs, scope.Restrict) + projectflocks, total, err := s.Repository.GetAllWithFilters(c.Context(), offset, params.Limit, params) if err != nil { s.Log.Errorf("Failed to get projectflocks: %+v", err) @@ -195,16 +190,7 @@ func (s projectflockService) getOneEntityOnly(c *fiber.Ctx, id uint) (*entity.Pr } func (s projectflockService) GetOne(c *fiber.Ctx, id uint) (*entity.ProjectFlock, *flockDTO.FlockRelationDTO, error) { - scope, err := m.ResolveLocationScope(c, s.Repository.DB()) - if err != nil { - return nil, nil, err - } - - projectflock, err := s.Repository.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB { - db = s.Repository.WithDefaultRelations()(db) - db = m.ApplyScopeFilter(db, scope, "project_flocks.location_id") - return db - }) + projectflock, err := s.Repository.GetByID(c.Context(), id, s.Repository.WithDefaultRelations()) if errors.Is(err, gorm.ErrRecordNotFound) { return nil, nil, fiber.NewError(fiber.StatusNotFound, "Projectflock not found") } diff --git a/internal/modules/purchases/services/purchase.service.go b/internal/modules/purchases/services/purchase.service.go index 6b9537cc..35ca2f75 100644 --- a/internal/modules/purchases/services/purchase.service.go +++ b/internal/modules/purchases/services/purchase.service.go @@ -124,11 +124,6 @@ func (s *purchaseService) GetAll(c *fiber.Ctx, params *validation.Query) ([]enti return nil, 0, err } - scope, err := m.ResolveLocationScope(c, s.PurchaseRepo.DB()) - if err != nil { - return nil, 0, err - } - offset := (params.Page - 1) * params.Limit createdFrom, createdTo, err := utils.ParseDateRangeForQuery(params.CreatedFrom, params.CreatedTo) @@ -152,21 +147,6 @@ func (s *purchaseService) GetAll(c *fiber.Ctx, params *validation.Query) ([]enti db = db.Where("created_at < ?", *createdTo) } - if scope.Restrict { - if len(scope.IDs) == 0 { - return db.Where("1 = 0") - } - db = db.Where( - `EXISTS ( - SELECT 1 - FROM purchase_items pi - JOIN warehouses w ON w.id = pi.warehouse_id - WHERE pi.purchase_id = purchases.id AND w.location_id IN ? - )`, - scope.IDs, - ) - } - if params.AreaID > 0 { db = db.Where( `EXISTS ( @@ -221,42 +201,7 @@ func (s *purchaseService) GetAll(c *fiber.Ctx, params *validation.Query) ([]enti } func (s *purchaseService) GetOne(c *fiber.Ctx, id uint) (*entity.Purchase, error) { - scope, err := m.ResolveLocationScope(c, s.PurchaseRepo.DB()) - if err != nil { - return nil, err - } - - purchase, err := s.PurchaseRepo.GetByID(c.Context(), id, 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.Where( - `EXISTS ( - SELECT 1 - FROM purchase_items pi - JOIN warehouses w ON w.id = pi.warehouse_id - WHERE pi.purchase_id = purchases.id AND w.location_id IN ? - )`, - scope.IDs, - ) - } - return db - }) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, utils.NotFound("Purchase not found") - } - s.Log.Errorf("Failed to get purchase %d: %+v", id, err) - return nil, utils.Internal("Failed to get purchase") - } - if err := s.attachLatestApproval(c.Context(), purchase); err != nil { - s.Log.Warnf("Unable to attach latest approval for purchase %d: %+v", id, err) - } - s.applyTravelDocumentURLs(c.Context(), purchase) - - return purchase, nil + return s.loadPurchase(c.Context(), id) } func (s *purchaseService) CreateOne(c *fiber.Ctx, req *validation.CreatePurchaseRequest) (*entity.Purchase, error) { diff --git a/internal/modules/repports/controllers/repport.controller.go b/internal/modules/repports/controllers/repport.controller.go index 4ed7a855..1d273af1 100644 --- a/internal/modules/repports/controllers/repport.controller.go +++ b/internal/modules/repports/controllers/repport.controller.go @@ -5,7 +5,6 @@ import ( "strconv" "strings" - m "gitlab.com/mbugroup/lti-api.git/internal/middleware" "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/dto" service "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/services" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/validations" @@ -50,21 +49,6 @@ func (c *RepportController) GetExpense(ctx *fiber.Ctx) error { RealizationDate: ctx.Query("realization_date", ""), } - locationScope, err := m.ResolveLocationScope(ctx, c.RepportService.DB()) - if err != nil { - return err - } - areaScope, err := m.ResolveAreaScope(ctx, c.RepportService.DB()) - if err != nil { - return err - } - if locationScope.Restrict { - query.AllowedLocationIDs = toInt64Slice(locationScope.IDs) - } - if areaScope.Restrict { - query.AllowedAreaIDs = toInt64Slice(areaScope.IDs) - } - if query.Page < 1 || query.Limit < 1 { return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0") } @@ -146,14 +130,6 @@ func (c *RepportController) GetPurchaseSupplier(ctx *fiber.Ctx) error { FilterBy: ctx.Query("filter_by", ""), } - areaScope, err := m.ResolveAreaScope(ctx, c.RepportService.DB()) - if err != nil { - return err - } - if areaScope.Restrict { - query.AllowedAreaIDs = toInt64Slice(areaScope.IDs) - } - if query.Page < 1 || query.Limit < 1 { return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0") } @@ -330,14 +306,3 @@ func parseCommaSeparatedInt64s(raw string) ([]int64, error) { return result, nil } - -func toInt64Slice(ids []uint) []int64 { - if len(ids) == 0 { - return nil - } - out := make([]int64, 0, len(ids)) - for _, id := range ids { - out = append(out, int64(id)) - } - return out -} diff --git a/internal/modules/repports/repositories/purchase_supplier.repository.go b/internal/modules/repports/repositories/purchase_supplier.repository.go index 048fe02c..979623fc 100644 --- a/internal/modules/repports/repositories/purchase_supplier.repository.go +++ b/internal/modules/repports/repositories/purchase_supplier.repository.go @@ -53,18 +53,10 @@ func (r *purchaseSupplierRepositoryImpl) baseSupplierQuery(ctx context.Context, Where("products.product_category_id = ?", filters.ProductCategoryId) } - if filters.AreaId > 0 || filters.AllowedAreaIDs != nil { - db = db.Joins("JOIN warehouses ON warehouses.id = purchase_items.warehouse_id") - if filters.AreaId > 0 { - db = db.Where("warehouses.area_id = ?", filters.AreaId) - } - if filters.AllowedAreaIDs != nil { - if len(filters.AllowedAreaIDs) == 0 { - db = db.Where("1 = 0") - } else { - db = db.Where("warehouses.area_id IN ?", filters.AllowedAreaIDs) - } - } + if filters.AreaId > 0 { + db = db. + Joins("JOIN warehouses ON warehouses.id = purchase_items.warehouse_id"). + Where("warehouses.area_id = ?", filters.AreaId) } if filters.StartDate != "" { @@ -172,18 +164,10 @@ func (r *purchaseSupplierRepositoryImpl) GetItemsBySuppliers(ctx context.Context Where("products.product_category_id = ?", filters.ProductCategoryId) } - if filters.AreaId > 0 || filters.AllowedAreaIDs != nil { - db = db.Joins("JOIN warehouses ON warehouses.id = purchase_items.warehouse_id") - if filters.AreaId > 0 { - db = db.Where("warehouses.area_id = ?", filters.AreaId) - } - if filters.AllowedAreaIDs != nil { - if len(filters.AllowedAreaIDs) == 0 { - db = db.Where("1 = 0") - } else { - db = db.Where("warehouses.area_id IN ?", filters.AllowedAreaIDs) - } - } + if filters.AreaId > 0 { + db = db. + Joins("JOIN warehouses ON warehouses.id = purchase_items.warehouse_id"). + Where("warehouses.area_id = ?", filters.AreaId) } if filters.StartDate != "" { diff --git a/internal/modules/repports/services/repport.service.go b/internal/modules/repports/services/repport.service.go index 0e3dbada..c4883b72 100644 --- a/internal/modules/repports/services/repport.service.go +++ b/internal/modules/repports/services/repport.service.go @@ -9,7 +9,6 @@ import ( "strings" "time" - m "gitlab.com/mbugroup/lti-api.git/internal/middleware" "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/dto" repportRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/repositories" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/validations" @@ -41,7 +40,6 @@ type RepportService interface { GetDebtSupplier(ctx *fiber.Ctx, params *validation.DebtSupplierQuery) ([]dto.DebtSupplierDTO, int64, error) GetHppPerKandang(ctx *fiber.Ctx) (*dto.HppPerKandangResponseData, *dto.HppPerKandangMetaDTO, error) GetProductionResult(ctx *fiber.Ctx, params *validation.ProductionResultQuery) ([]dto.ProductionResultDTO, int64, error) - DB() *gorm.DB } type repportService struct { @@ -97,10 +95,6 @@ func NewRepportService( } } -func (s *repportService) DB() *gorm.DB { - return s.ExpenseRealizationRepo.DB() -} - func (s *repportService) GetExpense(c *fiber.Ctx, params *validation.ExpenseQuery) ([]dto.RepportExpenseListDTO, int64, error) { if err := s.Validate.Struct(params); err != nil { return nil, 0, err @@ -1309,36 +1303,6 @@ func (s *repportService) parseHppPerKandangQuery(ctx *fiber.Ctx) (*validation.Hp return nil, dto.HppPerKandangFiltersDTO{}, fiber.NewError(fiber.StatusBadRequest, err.Error()) } - locationScope, err := m.ResolveLocationScope(ctx, s.ExpenseRealizationRepo.DB()) - if err != nil { - return nil, dto.HppPerKandangFiltersDTO{}, err - } - areaScope, err := m.ResolveAreaScope(ctx, s.ExpenseRealizationRepo.DB()) - if err != nil { - return nil, dto.HppPerKandangFiltersDTO{}, err - } - - if locationScope.Restrict { - allowed := toInt64Slice(locationScope.IDs) - if len(allowed) == 0 { - locationIDs = []int64{-1} - } else if len(locationIDs) > 0 { - locationIDs = intersectInt64(locationIDs, allowed) - } else { - locationIDs = allowed - } - } - if areaScope.Restrict { - allowed := toInt64Slice(areaScope.IDs) - if len(allowed) == 0 { - areaIDs = []int64{-1} - } else if len(areaIDs) > 0 { - areaIDs = intersectInt64(areaIDs, allowed) - } else { - areaIDs = allowed - } - } - weightMin, err := parseOptionalFloat64(rawWeightMin) if err != nil { return nil, dto.HppPerKandangFiltersDTO{}, fiber.NewError(fiber.StatusBadRequest, err.Error()) @@ -1402,34 +1366,6 @@ func parseCommaSeparatedInt64s(raw string) ([]int64, error) { return result, nil } -func toInt64Slice(ids []uint) []int64 { - if len(ids) == 0 { - return nil - } - out := make([]int64, 0, len(ids)) - for _, id := range ids { - out = append(out, int64(id)) - } - return out -} - -func intersectInt64(a, b []int64) []int64 { - if len(a) == 0 || len(b) == 0 { - return nil - } - set := make(map[int64]struct{}, len(b)) - for _, id := range b { - set[id] = struct{}{} - } - out := make([]int64, 0, len(a)) - for _, id := range a { - if _, ok := set[id]; ok { - out = append(out, id) - } - } - return out -} - func parseOptionalFloat64(raw string) (*float64, error) { raw = strings.TrimSpace(raw) if raw == "" { diff --git a/internal/modules/repports/validations/repport.validation.go b/internal/modules/repports/validations/repport.validation.go index 4c1d7356..6d50f3e6 100644 --- a/internal/modules/repports/validations/repport.validation.go +++ b/internal/modules/repports/validations/repport.validation.go @@ -13,8 +13,6 @@ type ExpenseQuery struct { AreaId int64 `query:"area_id" validate:"omitempty"` LocationId int64 `query:"location_id" validate:"omitempty"` RealizationDate string `query:"realization_date" validate:"omitempty"` - AllowedAreaIDs []int64 `query:"-"` - AllowedLocationIDs []int64 `query:"-"` } type MarketingQuery struct { @@ -44,7 +42,6 @@ type PurchaseSupplierQuery struct { EndDate string `query:"end_date" validate:"omitempty"` SortBy string `query:"sort_by" validate:"omitempty"` FilterBy string `query:"filter_by" validate:"omitempty"` - AllowedAreaIDs []int64 `query:"-"` } type DebtSupplierQuery struct { diff --git a/internal/modules/sso/controllers/master_data.controller.go b/internal/modules/sso/controllers/master_data.controller.go deleted file mode 100644 index 2364011f..00000000 --- a/internal/modules/sso/controllers/master_data.controller.go +++ /dev/null @@ -1,331 +0,0 @@ -package controllers - -import ( - "context" - "crypto/hmac" - "crypto/sha256" - "encoding/base64" - "encoding/hex" - "errors" - "fmt" - "strconv" - "strings" - "sync" - "time" - - "github.com/gofiber/fiber/v2" - "github.com/redis/go-redis/v9" - "gorm.io/gorm" - - "gitlab.com/mbugroup/lti-api.git/internal/config" - entity "gitlab.com/mbugroup/lti-api.git/internal/entities" - sso "gitlab.com/mbugroup/lti-api.git/internal/modules/sso/verifier" - "gitlab.com/mbugroup/lti-api.git/internal/response" - "gitlab.com/mbugroup/lti-api.git/internal/utils" -) - -type MasterDataController struct { - db *gorm.DB - redis *redis.Client - clients map[string]config.SSOClientConfig - drift time.Duration - nonceTTL time.Duration - localNonce sync.Map -} - -type masterArea struct { - ID uint `json:"id"` - Name string `json:"name"` -} - -type masterLocation struct { - ID uint `json:"id"` - Name string `json:"name"` - AreaID uint `json:"area_id"` -} - -func NewMasterDataController(db *gorm.DB, redis *redis.Client, clients map[string]config.SSOClientConfig) *MasterDataController { - normalized := make(map[string]config.SSOClientConfig, len(clients)) - for alias, cfg := range clients { - alias = strings.ToLower(strings.TrimSpace(alias)) - normalized[alias] = cfg - } - - drift := config.SSOUserSyncDrift - if drift <= 0 { - drift = 2 * time.Minute - } - - nonceTTL := config.SSOUserSyncNonceTTL - if nonceTTL <= 0 { - nonceTTL = 10 * time.Minute - } - - return &MasterDataController{ - db: db, - redis: redis, - clients: normalized, - drift: drift, - nonceTTL: nonceTTL, - } -} - -func (h *MasterDataController) GetAreas(c *fiber.Ctx) error { - if _, _, err := h.authenticate(c, nil); err != nil { - return err - } - - search := strings.TrimSpace(c.Query("search", "")) - ids := parseUintList(c.Query("ids", "")) - - query := h.db.WithContext(c.Context()). - Model(&entity.Area{}). - Where("deleted_at IS NULL") - if search != "" { - query = query.Where("name ILIKE ?", "%"+search+"%") - } - if len(ids) > 0 { - query = query.Where("id IN ?", ids) - } - - var areas []masterArea - if err := query.Order("name ASC").Find(&areas).Error; err != nil { - utils.Log.WithError(err).Error("failed to fetch areas for master data") - return fiber.NewError(fiber.StatusInternalServerError, "failed to fetch areas") - } - - return c.Status(fiber.StatusOK).JSON(response.Success{ - Code: fiber.StatusOK, - Status: "success", - Message: "Get areas successfully", - Data: areas, - }) -} - -func (h *MasterDataController) GetLocations(c *fiber.Ctx) error { - if _, _, err := h.authenticate(c, nil); err != nil { - return err - } - - search := strings.TrimSpace(c.Query("search", "")) - areaIDs := parseUintList(c.Query("area_ids", "")) - ids := parseUintList(c.Query("ids", "")) - - query := h.db.WithContext(c.Context()). - Model(&entity.Location{}). - Where("deleted_at IS NULL") - if search != "" { - query = query.Where("name ILIKE ?", "%"+search+"%") - } - if len(areaIDs) > 0 { - query = query.Where("area_id IN ?", areaIDs) - } - if len(ids) > 0 { - query = query.Where("id IN ?", ids) - } - - var locations []masterLocation - if err := query.Order("name ASC").Find(&locations).Error; err != nil { - utils.Log.WithError(err).Error("failed to fetch locations for master data") - return fiber.NewError(fiber.StatusInternalServerError, "failed to fetch locations") - } - - return c.Status(fiber.StatusOK).JSON(response.Success{ - Code: fiber.StatusOK, - Status: "success", - Message: "Get locations successfully", - Data: locations, - }) -} - -func (h *MasterDataController) authenticate(c *fiber.Ctx, body []byte) (string, config.SSOClientConfig, error) { - rawAlias := strings.TrimSpace(c.Get("X-Sync-Client")) - if rawAlias == "" { - return "", config.SSOClientConfig{}, fiber.NewError(fiber.StatusUnauthorized, "missing sync client header") - } - - aliasKey := strings.ToLower(rawAlias) - clientCfg, ok := h.clients[aliasKey] - if !ok { - return "", config.SSOClientConfig{}, fiber.NewError(fiber.StatusUnauthorized, "unknown sync client") - } - - if err := h.verifyAuthorization(c, aliasKey); err != nil { - return "", config.SSOClientConfig{}, err - } - - secret := strings.TrimSpace(clientCfg.SyncSecret) - if secret == "" { - return "", config.SSOClientConfig{}, fiber.NewError(fiber.StatusUnauthorized, "sync secret not configured") - } - - timestamp := strings.TrimSpace(c.Get("X-Sync-Timestamp")) - nonce := strings.TrimSpace(c.Get("X-Sync-Nonce")) - signature := strings.TrimSpace(c.Get("X-Sync-Signature")) - if timestamp == "" || nonce == "" || signature == "" { - return "", config.SSOClientConfig{}, fiber.NewError(fiber.StatusUnauthorized, "missing signature headers") - } - if len(nonce) < 16 { - return "", config.SSOClientConfig{}, fiber.NewError(fiber.StatusUnauthorized, "nonce too short") - } - - ts, err := strconv.ParseInt(timestamp, 10, 64) - if err != nil { - return "", config.SSOClientConfig{}, fiber.NewError(fiber.StatusBadRequest, "invalid timestamp") - } - - msgTime := time.Unix(ts, 0).UTC() - now := time.Now().UTC() - drift := now.Sub(msgTime) - if drift > h.drift || drift < -h.drift { - return "", config.SSOClientConfig{}, fiber.NewError(fiber.StatusUnauthorized, "timestamp outside allowed window") - } - - providedSig, err := decodeMasterSignature(signature) - if err != nil { - return "", config.SSOClientConfig{}, fiber.NewError(fiber.StatusUnauthorized, "invalid signature encoding") - } - - expectedSignature := calculateSignature(secret, rawAlias, timestamp, nonce, body) - if !hmac.Equal(providedSig, expectedSignature) { - return "", config.SSOClientConfig{}, fiber.NewError(fiber.StatusUnauthorized, "invalid signature") - } - - if err := h.registerNonce(c.Context(), aliasKey, nonce); err != nil { - return "", config.SSOClientConfig{}, err - } - - return aliasKey, clientCfg, nil -} - -func (h *MasterDataController) verifyAuthorization(c *fiber.Ctx, alias string) error { - authHeader := strings.TrimSpace(c.Get(fiber.HeaderAuthorization)) - if authHeader == "" { - return fiber.NewError(fiber.StatusUnauthorized, "missing authorization header") - } - parts := strings.SplitN(authHeader, " ", 2) - if len(parts) != 2 || !strings.EqualFold(parts[0], "Bearer") { - return fiber.NewError(fiber.StatusUnauthorized, "invalid authorization header") - } - - token := strings.TrimSpace(parts[1]) - if token == "" { - return fiber.NewError(fiber.StatusUnauthorized, "invalid authorization header") - } - - verification, err := sso.VerifyAccessToken(token) - if err != nil { - return fiber.NewError(fiber.StatusUnauthorized, "invalid access token") - } - - if verification.ServiceAlias == "" || verification.ServiceAlias != alias { - return fiber.NewError(fiber.StatusUnauthorized, "service subject mismatch") - } - if !hasAnyScope(verification.Claims.Scopes(), []string{"sync.master", "sync.users"}) { - return fiber.NewError(fiber.StatusForbidden, "missing sync scope") - } - return nil -} - -func (h *MasterDataController) registerNonce(ctx context.Context, alias, nonce string) error { - ttl := h.nonceTTL - if ttl <= 0 { - ttl = 10 * time.Minute - } - - key := fmt.Sprintf("sso:sync:%s:%s", alias, nonce) - if h.redis != nil { - stored, err := h.redis.SetNX(ctx, key, "1", ttl).Result() - if err == nil { - if !stored { - return fiber.NewError(fiber.StatusUnauthorized, "nonce already used") - } - return nil - } - utils.Log.WithError(err).Warn("store sync nonce failed") - } - - now := time.Now().UTC() - if expRaw, ok := h.localNonce.Load(key); ok { - if expTime, ok := expRaw.(time.Time); ok && expTime.After(now) { - return fiber.NewError(fiber.StatusUnauthorized, "nonce already used") - } - } - h.localNonce.Store(key, now.Add(ttl)) - return nil -} - -func calculateSignature(secret, alias, timestamp, nonce string, body []byte) []byte { - mac := hmac.New(sha256.New, []byte(secret)) - mac.Write([]byte(alias)) - mac.Write([]byte("\n")) - mac.Write([]byte(timestamp)) - mac.Write([]byte("\n")) - mac.Write([]byte(nonce)) - mac.Write([]byte("\n")) - if len(body) > 0 { - mac.Write(body) - } - return mac.Sum(nil) -} - -func decodeMasterSignature(sig string) ([]byte, error) { - sig = strings.TrimSpace(sig) - if sig == "" { - return nil, errors.New("empty signature") - } - if decoded, err := hex.DecodeString(sig); err == nil { - return decoded, nil - } - if decoded, err := base64.StdEncoding.DecodeString(sig); err == nil { - return decoded, nil - } - if decoded, err := base64.URLEncoding.DecodeString(sig); err == nil { - return decoded, nil - } - return nil, errors.New("unrecognized signature encoding") -} - -func parseUintList(raw string) []uint { - raw = strings.TrimSpace(raw) - if raw == "" { - return nil - } - parts := strings.Split(raw, ",") - out := make([]uint, 0, len(parts)) - seen := make(map[uint]struct{}, len(parts)) - for _, part := range parts { - part = strings.TrimSpace(part) - if part == "" { - continue - } - val, err := strconv.ParseUint(part, 10, 64) - if err != nil || val == 0 { - continue - } - if _, ok := seen[uint(val)]; ok { - continue - } - seen[uint(val)] = struct{}{} - out = append(out, uint(val)) - } - return out -} - -func hasAnyScope(scopes []string, targets []string) bool { - if len(scopes) == 0 || len(targets) == 0 { - return false - } - for _, scope := range scopes { - scope = strings.ToLower(strings.TrimSpace(scope)) - if scope == "" { - continue - } - for _, target := range targets { - if scope == strings.ToLower(strings.TrimSpace(target)) { - return true - } - } - } - return false -} diff --git a/internal/modules/sso/controllers/sso.controller.go b/internal/modules/sso/controllers/sso.controller.go index b49d73e5..410e9577 100644 --- a/internal/modules/sso/controllers/sso.controller.go +++ b/internal/modules/sso/controllers/sso.controller.go @@ -16,7 +16,7 @@ import ( "gitlab.com/mbugroup/lti-api.git/internal/config" "gitlab.com/mbugroup/lti-api.git/internal/modules/sso/session" - sso "gitlab.com/mbugroup/lti-api.git/internal/modules/sso/verifier" + "gitlab.com/mbugroup/lti-api.git/internal/sso" "gitlab.com/mbugroup/lti-api.git/internal/utils" "gitlab.com/mbugroup/lti-api.git/internal/utils/secure" ) diff --git a/internal/modules/sso/controllers/user_sync.controller.go b/internal/modules/sso/controllers/user_sync.controller.go index 72c7768a..9aeb9555 100644 --- a/internal/modules/sso/controllers/user_sync.controller.go +++ b/internal/modules/sso/controllers/user_sync.controller.go @@ -9,24 +9,23 @@ import ( "encoding/json" "errors" "fmt" - "strconv" - "strings" - "sync" - "time" - "github.com/go-playground/validator/v10" "github.com/gofiber/fiber/v2" "github.com/redis/go-redis/v9" "github.com/sirupsen/logrus" "gorm.io/gorm" + "strconv" + "strings" + "sync" + "time" "gitlab.com/mbugroup/lti-api.git/internal/config" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" "gitlab.com/mbugroup/lti-api.git/internal/modules/sso/session" - sso "gitlab.com/mbugroup/lti-api.git/internal/modules/sso/verifier" "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto" userRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories" "gitlab.com/mbugroup/lti-api.git/internal/response" + "gitlab.com/mbugroup/lti-api.git/internal/sso" "gitlab.com/mbugroup/lti-api.git/internal/utils" ) diff --git a/internal/modules/sso/route.go b/internal/modules/sso/route.go index a864611e..3f2a699e 100644 --- a/internal/modules/sso/route.go +++ b/internal/modules/sso/route.go @@ -26,7 +26,6 @@ func Routes(router fiber.Router, db *gorm.DB, validate *validator.Validate) { ctrl := ssoController.NewController(&http.Client{Timeout: 10 * time.Second}, store, session.GetRevocationStore()) userRepo := userRepository.NewUserRepository(db) syncCtrl := ssoController.NewUserSyncController(validate, userRepo, cache.Redis(), config.SSOClients) - masterCtrl := ssoController.NewMasterDataController(db, cache.Redis(), config.SSOClients) group := router.Group("/sso") group.Get("/start", middleware.NewLimiter(30, time.Minute), ctrl.Start) @@ -35,6 +34,4 @@ func Routes(router fiber.Router, db *gorm.DB, validate *validator.Validate) { group.Post("/refresh", middleware.NewLimiter(60, time.Minute), ctrl.Refresh) group.Post("/logout", middleware.NewLimiter(60, time.Minute), ctrl.Logout) group.Post("/users/sync", middleware.NewLimiter(30, time.Minute), syncCtrl.Sync) - group.Get("/master/areas", middleware.NewLimiter(60, time.Minute), masterCtrl.GetAreas) - group.Get("/master/locations", middleware.NewLimiter(60, time.Minute), masterCtrl.GetLocations) } diff --git a/internal/modules/sso/verifier/profile.go b/internal/sso/profile.go similarity index 95% rename from internal/modules/sso/verifier/profile.go rename to internal/sso/profile.go index 52b7ef5a..a211fc74 100644 --- a/internal/modules/sso/verifier/profile.go +++ b/internal/sso/profile.go @@ -49,10 +49,6 @@ type Role struct { ClientID uint ClientAlias string ClientName string - AllArea bool - AllLocation bool - AreaIDs []uint - LocationIDs []uint Permissions []Permission RawReference json.RawMessage `json:"-"` } @@ -166,10 +162,6 @@ func fetchProfileFromSSO(ctx context.Context, token string) (*UserProfile, error ClientAlias: strings.TrimSpace(r.Client.Alias), ClientName: strings.TrimSpace(r.Client.Name), ClientID: uint(r.Client.ID), - AllArea: r.AllArea, - AllLocation: r.AllLocation, - AreaIDs: r.AreaIDs, - LocationIDs: r.LocationIDs, } rolePerms := make([]Permission, 0, len(r.Permissions)) for _, p := range r.Permissions { @@ -296,10 +288,6 @@ type userInfoRole struct { ID int64 `json:"id"` Key string `json:"key"` Name string `json:"name"` - AllArea bool `json:"all_area"` - AllLocation bool `json:"all_location"` - AreaIDs []uint `json:"area_ids"` - LocationIDs []uint `json:"location_ids"` Client userInfoClient `json:"client"` Permissions []userInfoPermRaw `json:"permissions"` } diff --git a/internal/modules/sso/verifier/verifier.go b/internal/sso/verifier.go similarity index 100% rename from internal/modules/sso/verifier/verifier.go rename to internal/sso/verifier.go From 32772a63c8fd6e98ea4459e10e91427fc4207dfe Mon Sep 17 00:00:00 2001 From: ragilap Date: Wed, 14 Jan 2026 13:34:44 +0700 Subject: [PATCH 3/7] Revert "Revert "[FIX/BE-US]add feature restrict by location and areas in roles"" This reverts commit 26bf7f165e2c4d5bede4f16771ae1b95e476a2c5. --- cmd/api/main.go | 2 +- internal/middleware/auth.go | 2 +- internal/middleware/role_scope.go | 280 +++++++++++++++ .../services/daily-checklist.service.go | 11 +- .../controllers/dashboard.controller.go | 32 ++ .../dashboards/services/dashboard.service.go | 6 + .../expense_realization.repository.go | 21 +- .../expenses/services/expense.service.go | 17 +- .../services/product-stock.service.go | 56 ++- .../services/product_warehouse.service.go | 32 +- .../transfers/services/transfer.service.go | 36 ++ .../master/areas/services/area.service.go | 27 +- .../kandangs/services/kandang.service.go | 17 +- .../locations/services/location.service.go | 27 +- .../warehouses/services/warehouse.service.go | 17 +- .../services/project_flock_kandang.service.go | 29 +- .../repositories/projectflock.repository.go | 14 + .../projectflock_kandang.repository.go | 99 ++++++ .../services/projectflock.service.go | 18 +- .../purchases/services/purchase.service.go | 57 ++- .../controllers/repport.controller.go | 35 ++ .../purchase_supplier.repository.go | 32 +- .../repports/services/repport.service.go | 64 ++++ .../validations/repport.validation.go | 3 + .../sso/controllers/master_data.controller.go | 331 ++++++++++++++++++ .../modules/sso/controllers/sso.controller.go | 2 +- .../sso/controllers/user_sync.controller.go | 13 +- internal/modules/sso/route.go | 3 + .../{sso => modules/sso/verifier}/profile.go | 12 + .../{sso => modules/sso/verifier}/verifier.go | 0 30 files changed, 1258 insertions(+), 37 deletions(-) create mode 100644 internal/middleware/role_scope.go create mode 100644 internal/modules/sso/controllers/master_data.controller.go rename internal/{sso => modules/sso/verifier}/profile.go (95%) rename internal/{sso => modules/sso/verifier}/verifier.go (100%) diff --git a/cmd/api/main.go b/cmd/api/main.go index a7c278d7..76de7729 100644 --- a/cmd/api/main.go +++ b/cmd/api/main.go @@ -14,8 +14,8 @@ import ( "gitlab.com/mbugroup/lti-api.git/internal/database" "gitlab.com/mbugroup/lti-api.git/internal/middleware" "gitlab.com/mbugroup/lti-api.git/internal/modules/sso/session" + sso "gitlab.com/mbugroup/lti-api.git/internal/modules/sso/verifier" "gitlab.com/mbugroup/lti-api.git/internal/route" - "gitlab.com/mbugroup/lti-api.git/internal/sso" "gitlab.com/mbugroup/lti-api.git/internal/utils" "github.com/gofiber/fiber/v2" diff --git a/internal/middleware/auth.go b/internal/middleware/auth.go index a831c25b..1b670c14 100644 --- a/internal/middleware/auth.go +++ b/internal/middleware/auth.go @@ -7,8 +7,8 @@ import ( "gitlab.com/mbugroup/lti-api.git/internal/config" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" "gitlab.com/mbugroup/lti-api.git/internal/modules/sso/session" + sso "gitlab.com/mbugroup/lti-api.git/internal/modules/sso/verifier" service "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" - "gitlab.com/mbugroup/lti-api.git/internal/sso" "gitlab.com/mbugroup/lti-api.git/internal/utils" ) diff --git a/internal/middleware/role_scope.go b/internal/middleware/role_scope.go new file mode 100644 index 00000000..f00e12d9 --- /dev/null +++ b/internal/middleware/role_scope.go @@ -0,0 +1,280 @@ +package middleware + +import ( + "errors" + "strings" + + "github.com/gofiber/fiber/v2" + "gorm.io/gorm" + + "gitlab.com/mbugroup/lti-api.git/internal/config" + entity "gitlab.com/mbugroup/lti-api.git/internal/entities" +) + +type ScopeFilter struct { + IDs []uint + Restrict bool +} + +type roleScope struct { + allArea bool + allLocation bool + areaIDs []uint + locationIDs []uint + hasAnyScopes bool +} + +func ResolveAreaScope(c *fiber.Ctx, db *gorm.DB) (ScopeFilter, error) { + scope, err := collectRoleScope(c) + if err != nil || !scope.hasAnyScopes { + return ScopeFilter{}, err + } + + if scope.allArea || scope.allLocation { + return ScopeFilter{}, nil + } + + allowed := uniqueUint(scope.areaIDs) + if len(scope.locationIDs) > 0 { + derived, err := areaIDsByLocationIDs(db, scope.locationIDs) + if err != nil { + return ScopeFilter{}, err + } + allowed = uniqueUint(append(allowed, derived...)) + } + + if len(allowed) == 0 { + return ScopeFilter{Restrict: true}, nil + } + return ScopeFilter{IDs: allowed, Restrict: true}, nil +} + +func ResolveLocationScope(c *fiber.Ctx, db *gorm.DB) (ScopeFilter, error) { + scope, err := collectRoleScope(c) + if err != nil || !scope.hasAnyScopes { + return ScopeFilter{}, err + } + + if scope.allLocation || scope.allArea { + return ScopeFilter{}, nil + } + + areaIDs := uniqueUint(scope.areaIDs) + locationIDs := uniqueUint(scope.locationIDs) + + switch { + case len(locationIDs) > 0 && len(areaIDs) > 0: + filtered, err := filterLocationIDsByAreaIDs(db, locationIDs, areaIDs) + if err != nil { + return ScopeFilter{}, err + } + locationIDs = filtered + case len(locationIDs) == 0 && len(areaIDs) > 0: + derived, err := locationIDsByAreaIDs(db, areaIDs) + if err != nil { + return ScopeFilter{}, err + } + locationIDs = derived + } + + locationIDs = uniqueUint(locationIDs) + if len(locationIDs) == 0 { + return ScopeFilter{Restrict: true}, nil + } + return ScopeFilter{IDs: locationIDs, Restrict: true}, nil +} + +func collectRoleScope(c *fiber.Ctx) (roleScope, error) { + ctx, ok := AuthDetails(c) + if !ok || ctx == nil || len(ctx.Roles) == 0 { + return roleScope{}, nil + } + + clientAlias := resolveClientAlias(ctx) + + scope := roleScope{} + areaSet := make(map[uint]struct{}) + locationSet := make(map[uint]struct{}) + + for _, role := range ctx.Roles { + if clientAlias != "" && !strings.EqualFold(strings.TrimSpace(role.ClientAlias), clientAlias) { + continue + } + scope.hasAnyScopes = true + if role.AllArea { + scope.allArea = true + } + if role.AllLocation { + scope.allLocation = true + } + for _, id := range role.AreaIDs { + if id == 0 { + continue + } + areaSet[id] = struct{}{} + } + for _, id := range role.LocationIDs { + if id == 0 { + continue + } + locationSet[id] = struct{}{} + } + } + + scope.areaIDs = keysUint(areaSet) + scope.locationIDs = keysUint(locationSet) + + scope.hasAnyScopes = scope.hasAnyScopes && + (scope.allArea || scope.allLocation || len(scope.areaIDs) > 0 || len(scope.locationIDs) > 0) + + return scope, nil +} + +func areaIDsByLocationIDs(db *gorm.DB, locationIDs []uint) ([]uint, error) { + if db == nil { + return nil, errors.New("database not configured") + } + if len(locationIDs) == 0 { + return nil, nil + } + var areaIDs []uint + if err := db.Model(&entity.Location{}). + Where("deleted_at IS NULL"). + Where("id IN ?", locationIDs). + Distinct("area_id"). + Pluck("area_id", &areaIDs).Error; err != nil { + return nil, err + } + return areaIDs, nil +} + +func locationIDsByAreaIDs(db *gorm.DB, areaIDs []uint) ([]uint, error) { + if db == nil { + return nil, errors.New("database not configured") + } + if len(areaIDs) == 0 { + return nil, nil + } + var locationIDs []uint + if err := db.Model(&entity.Location{}). + Where("deleted_at IS NULL"). + Where("area_id IN ?", areaIDs). + Distinct("id"). + Pluck("id", &locationIDs).Error; err != nil { + return nil, err + } + return locationIDs, nil +} + +func filterLocationIDsByAreaIDs(db *gorm.DB, locationIDs, areaIDs []uint) ([]uint, error) { + if db == nil { + return nil, errors.New("database not configured") + } + if len(locationIDs) == 0 || len(areaIDs) == 0 { + return nil, nil + } + var filtered []uint + if err := db.Model(&entity.Location{}). + Where("deleted_at IS NULL"). + Where("id IN ?", locationIDs). + Where("area_id IN ?", areaIDs). + Distinct("id"). + Pluck("id", &filtered).Error; err != nil { + return nil, err + } + return filtered, nil +} + +func uniqueUint(ids []uint) []uint { + if len(ids) == 0 { + return nil + } + seen := make(map[uint]struct{}, len(ids)) + result := make([]uint, 0, len(ids)) + for _, id := range ids { + if id == 0 { + continue + } + if _, ok := seen[id]; ok { + continue + } + seen[id] = struct{}{} + result = append(result, id) + } + return result +} + +func keysUint(set map[uint]struct{}) []uint { + if len(set) == 0 { + return nil + } + out := make([]uint, 0, len(set)) + for id := range set { + out = append(out, id) + } + return out +} + +func resolveClientAlias(ctx *AuthContext) string { + if ctx == nil || ctx.Verification == nil || ctx.Verification.Claims == nil { + return "" + } + + scopes := ctx.Verification.Claims.Scopes() + if len(scopes) == 0 { + return "" + } + + seen := make(map[string]struct{}) + for _, scope := range scopes { + scope = strings.ToLower(strings.TrimSpace(scope)) + if scope == "" { + continue + } + prefix := scope + if idx := strings.IndexAny(prefix, ".:"); idx > 0 { + prefix = prefix[:idx] + } + prefix = strings.TrimSpace(prefix) + if prefix == "" { + continue + } + if alias := matchAlias(prefix); alias != "" { + seen[alias] = struct{}{} + } + } + + if len(seen) != 1 { + return "" + } + for alias := range seen { + return alias + } + return "" +} + +func matchAlias(alias string) string { + alias = strings.ToLower(strings.TrimSpace(alias)) + if alias == "" { + return "" + } + if _, ok := config.SSOClients[alias]; ok { + return alias + } + for key := range config.SSOClients { + if strings.EqualFold(key, alias) { + return strings.ToLower(strings.TrimSpace(key)) + } + } + return "" +} + +func ApplyScopeFilter(db *gorm.DB, scope ScopeFilter, column string) *gorm.DB { + if db == nil || !scope.Restrict { + return db + } + if len(scope.IDs) == 0 { + return db.Where("1 = 0") + } + return db.Where(column+" IN ?", scope.IDs) +} diff --git a/internal/modules/daily-checklists/services/daily-checklist.service.go b/internal/modules/daily-checklists/services/daily-checklist.service.go index f306c74d..64802560 100644 --- a/internal/modules/daily-checklists/services/daily-checklist.service.go +++ b/internal/modules/daily-checklists/services/daily-checklist.service.go @@ -9,7 +9,7 @@ import ( "time" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" - middleware "gitlab.com/mbugroup/lti-api.git/internal/middleware" + m "gitlab.com/mbugroup/lti-api.git/internal/middleware" repository "gitlab.com/mbugroup/lti-api.git/internal/modules/daily-checklists/repositories" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/daily-checklists/validations" phaseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/phasess/repositories" @@ -456,7 +456,7 @@ func (s dailyChecklistService) UpdateOne(c *fiber.Ctx, req *validation.Update, i updateBody["reject_reason"] = *req.RejectReason } - actorID, err := middleware.ActorIDFromContext(c) + actorID, err := m.ActorIDFromContext(c) if err != nil { return &entity.DailyChecklist{}, fiber.NewError(fiber.StatusUnauthorized, "Failed to get actor ID from context") } @@ -946,6 +946,11 @@ func (s dailyChecklistService) GetReport(c *fiber.Ctx, params *validation.Report return nil, 0, err } + scope, err := m.ResolveLocationScope(c, s.Repository.DB()) + if err != nil { + return nil, 0, err + } + offset := (params.Page - 1) * params.Limit buildBase := func() *gorm.DB { @@ -962,6 +967,8 @@ func (s dailyChecklistService) GetReport(c *fiber.Ctx, params *validation.Report Where("EXTRACT(YEAR FROM dc.date) = ?", params.Year). Where("dc.status = ?", "APPROVED") + db = m.ApplyScopeFilter(db, scope, "loc.id") + if params.AreaID != nil { db = db.Where("a.id = ?", *params.AreaID) } diff --git a/internal/modules/dashboards/controllers/dashboard.controller.go b/internal/modules/dashboards/controllers/dashboard.controller.go index bebad10f..f74f31a7 100644 --- a/internal/modules/dashboards/controllers/dashboard.controller.go +++ b/internal/modules/dashboards/controllers/dashboard.controller.go @@ -6,6 +6,7 @@ import ( "strings" "time" + m "gitlab.com/mbugroup/lti-api.git/internal/middleware" "gitlab.com/mbugroup/lti-api.git/internal/modules/dashboards/dto" service "gitlab.com/mbugroup/lti-api.git/internal/modules/dashboards/services" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/dashboards/validations" @@ -81,6 +82,20 @@ func (u *DashboardController) GetAll(c *fiber.Ctx) error { return fiber.NewError(fiber.StatusBadRequest, "Invalid include") } + scope, err := m.ResolveLocationScope(c, u.DashboardService.DB()) + if err != nil { + return err + } + if scope.Restrict { + if len(scope.IDs) == 0 { + lokasiIds = []uint{} + } else if len(lokasiIds) > 0 { + lokasiIds = intersectUint(lokasiIds, scope.IDs) + } else { + lokasiIds = scope.IDs + } + } + analysisMode := strings.ToUpper(strings.TrimSpace(c.Query("analysis_mode", validation.AnalysisModeOverview))) metric := strings.ToLower(strings.TrimSpace(c.Query("metric", ""))) @@ -176,6 +191,23 @@ func defaultUintSlice(values []uint) []uint { return values } +func intersectUint(a, b []uint) []uint { + if len(a) == 0 || len(b) == 0 { + return nil + } + set := make(map[uint]struct{}, len(b)) + for _, id := range b { + set[id] = struct{}{} + } + out := make([]uint, 0, len(a)) + for _, id := range a { + if _, ok := set[id]; ok { + out = append(out, id) + } + } + return out +} + func parsePeriodDates(startDateRaw, endDateRaw string, location *time.Location) (time.Time, time.Time, time.Time, error) { now := time.Now().In(location) startDate := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, location) diff --git a/internal/modules/dashboards/services/dashboard.service.go b/internal/modules/dashboards/services/dashboard.service.go index b4635b2e..afdbd1c1 100644 --- a/internal/modules/dashboards/services/dashboard.service.go +++ b/internal/modules/dashboards/services/dashboard.service.go @@ -17,10 +17,12 @@ import ( "github.com/go-playground/validator/v10" "github.com/sirupsen/logrus" + "gorm.io/gorm" ) type DashboardService interface { GetAll(ctx context.Context, params *validation.Query) (dto.DashboardPerformanceOverviewDTO, int64, error) + DB() *gorm.DB } type dashboardService struct { @@ -37,6 +39,10 @@ func NewDashboardService(repo repository.DashboardRepository, validate *validato } } +func (s dashboardService) DB() *gorm.DB { + return s.Repository.DB() +} + func (s dashboardService) GetAll(ctx context.Context, params *validation.Query) (dto.DashboardPerformanceOverviewDTO, int64, error) { if err := s.Validate.Struct(params); err != nil { return dto.DashboardPerformanceOverviewDTO{}, 0, err diff --git a/internal/modules/expenses/repositories/expense_realization.repository.go b/internal/modules/expenses/repositories/expense_realization.repository.go index 60ec97a7..504e65ad 100644 --- a/internal/modules/expenses/repositories/expense_realization.repository.go +++ b/internal/modules/expenses/repositories/expense_realization.repository.go @@ -138,9 +138,28 @@ func (r *ExpenseRealizationRepositoryImpl) GetAllWithFilters(ctx context.Context locationID := filters.LocationId areaID := filters.AreaId - if locationID > 0 || areaID > 0 { + if filters.AllowedLocationIDs != nil || filters.AllowedAreaIDs != nil || locationID > 0 || areaID > 0 { db = db.Joins("JOIN kandangs ON kandangs.id = expense_nonstocks.kandang_id") + } + if filters.AllowedLocationIDs != nil { + if len(filters.AllowedLocationIDs) == 0 { + db = db.Where("1 = 0") + } else { + db = db.Where("kandangs.location_id IN ?", filters.AllowedLocationIDs) + } + } + + if filters.AllowedAreaIDs != nil { + if len(filters.AllowedAreaIDs) == 0 { + db = db.Where("1 = 0") + } else { + db = db.Joins("JOIN locations ON locations.id = kandangs.location_id"). + Where("locations.area_id IN ?", filters.AllowedAreaIDs) + } + } + + if locationID > 0 || areaID > 0 { if locationID > 0 { db = db.Where("kandangs.location_id = ?", uint(locationID)) } diff --git a/internal/modules/expenses/services/expense.service.go b/internal/modules/expenses/services/expense.service.go index 3bf2db55..27b4a07f 100644 --- a/internal/modules/expenses/services/expense.service.go +++ b/internal/modules/expenses/services/expense.service.go @@ -87,10 +87,16 @@ func (s expenseService) GetAll(c *fiber.Ctx, params *validation.Query) ([]expens return nil, 0, err } + scope, err := middleware.ResolveLocationScope(c, s.Repository.DB()) + if err != nil { + return nil, 0, err + } + offset := (params.Page - 1) * params.Limit expenses, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB { db = s.withRelations(db) + db = middleware.ApplyScopeFilter(db, scope, "location_id") if params.Search != "" { return db.Where("category ILIKE ?", "%"+params.Search+"%") } @@ -117,7 +123,16 @@ func (s expenseService) GetAll(c *fiber.Ctx, params *validation.Query) ([]expens } func (s expenseService) GetOne(c *fiber.Ctx, id uint) (*expenseDto.ExpenseDetailDTO, error) { - expense, err := s.Repository.GetByID(c.Context(), id, s.withRelations) + scope, err := middleware.ResolveLocationScope(c, s.Repository.DB()) + if err != nil { + return nil, err + } + + expense, err := s.Repository.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB { + db = s.withRelations(db) + db = middleware.ApplyScopeFilter(db, scope, "location_id") + return db + }) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { diff --git a/internal/modules/inventory/product-stocks/services/product-stock.service.go b/internal/modules/inventory/product-stocks/services/product-stock.service.go index 11475109..a4e404d6 100644 --- a/internal/modules/inventory/product-stocks/services/product-stock.service.go +++ b/internal/modules/inventory/product-stocks/services/product-stock.service.go @@ -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" @@ -61,15 +62,34 @@ func (s productStockService) GetAll(c *fiber.Ctx, params *validation.Query) ([]e return nil, 0, err } + scope, err := m.ResolveLocationScope(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 scope.Restrict { + if len(scope.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 w.location_id IN ? + )`, scope.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) if params.Search != "" { @@ -86,6 +106,30 @@ func (s productStockService) GetAll(c *fiber.Ctx, params *validation.Query) ([]e } func (s productStockService) GetOne(c *fiber.Ctx, id uint) (*entity.Product, error) { + scope, err := m.ResolveLocationScope(c, s.ProductRepository.DB()) + if err != nil { + return nil, err + } + + if scope.Restrict { + if len(scope.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("w.location_id IN ?", scope.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, s.withRelations) if errors.Is(err, gorm.ErrRecordNotFound) { return nil, fiber.NewError(fiber.StatusNotFound, "Product not found") 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 152bfa24..5c162084 100644 --- a/internal/modules/inventory/product-warehouses/services/product_warehouse.service.go +++ b/internal/modules/inventory/product-warehouses/services/product_warehouse.service.go @@ -8,7 +8,7 @@ import ( 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" "gitlab.com/mbugroup/lti-api.git/internal/utils" - + m "gitlab.com/mbugroup/lti-api.git/internal/middleware" "github.com/go-playground/validator/v10" "github.com/gofiber/fiber/v2" "github.com/sirupsen/logrus" @@ -53,6 +53,11 @@ func (s productWarehouseService) GetAll(c *fiber.Ctx, params *validation.Query) return nil, 0, err } + 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 +95,14 @@ 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) + if scope.Restrict { + if len(scope.IDs) == 0 { + return db.Where("1 = 0") + } + db = db.Joins("JOIN warehouses w_scope ON product_warehouses.warehouse_id = w_scope.id"). + Where("w_scope.location_id IN ?", scope.IDs) + } + if params.ProductId != 0 { db = db.Where("product_id = ?", params.ProductId) } @@ -116,7 +129,22 @@ 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) + 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) + if scope.Restrict { + if len(scope.IDs) == 0 { + return db.Where("1 = 0") + } + db = db.Joins("JOIN warehouses w_scope ON product_warehouses.warehouse_id = w_scope.id"). + 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") } diff --git a/internal/modules/inventory/transfers/services/transfer.service.go b/internal/modules/inventory/transfers/services/transfer.service.go index 3f12b444..b4652a5d 100644 --- a/internal/modules/inventory/transfers/services/transfer.service.go +++ b/internal/modules/inventory/transfers/services/transfer.service.go @@ -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 != "" { db = db.Where("movement_number ILIKE ?", "%"+strings.TrimSpace(params.Search)+"%") } @@ -112,6 +126,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) diff --git a/internal/modules/master/areas/services/area.service.go b/internal/modules/master/areas/services/area.service.go index e6f9205c..1ec30d8d 100644 --- a/internal/modules/master/areas/services/area.service.go +++ b/internal/modules/master/areas/services/area.service.go @@ -47,10 +47,21 @@ func (s areaService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.Ar return nil, 0, err } + scope, err := m.ResolveAreaScope(c, s.Repository.DB()) + if err != nil { + return nil, 0, err + } + offset := (params.Page - 1) * params.Limit areas, total, err := s.Repository.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.Where("id IN ?", scope.IDs) + } if params.Search != "" { return db.Where("name ILIKE ?", "%"+params.Search+"%") } @@ -65,7 +76,21 @@ func (s areaService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.Ar } func (s areaService) GetOne(c *fiber.Ctx, id uint) (*entity.Area, error) { - area, err := s.Repository.GetByID(c.Context(), id, s.withRelations) + scope, err := m.ResolveAreaScope(c, s.Repository.DB()) + if err != nil { + return nil, err + } + + area, err := s.Repository.GetByID(c.Context(), id, 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.Where("id IN ?", scope.IDs) + } + return db + }) if errors.Is(err, gorm.ErrRecordNotFound) { return nil, fiber.NewError(fiber.StatusNotFound, "Area not found") } diff --git a/internal/modules/master/kandangs/services/kandang.service.go b/internal/modules/master/kandangs/services/kandang.service.go index 9f83f0ce..b70577d6 100644 --- a/internal/modules/master/kandangs/services/kandang.service.go +++ b/internal/modules/master/kandangs/services/kandang.service.go @@ -49,10 +49,16 @@ func (s kandangService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity return nil, 0, err } + scope, err := m.ResolveLocationScope(c, s.Repository.DB()) + if err != nil { + return nil, 0, err + } + offset := (params.Page - 1) * params.Limit kandangs, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB { db = s.withRelations(db) + db = m.ApplyScopeFilter(db, scope, "location_id") if params.Search != "" { return db.Where("name ILIKE ?", "%"+params.Search+"%") } @@ -73,7 +79,16 @@ func (s kandangService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity } func (s kandangService) GetOne(c *fiber.Ctx, id uint) (*entity.Kandang, error) { - kandang, err := s.Repository.GetByID(c.Context(), id, s.withRelations) + scope, err := m.ResolveLocationScope(c, s.Repository.DB()) + if err != nil { + return nil, err + } + + kandang, err := s.Repository.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB { + db = s.withRelations(db) + db = m.ApplyScopeFilter(db, scope, "location_id") + return db + }) if errors.Is(err, gorm.ErrRecordNotFound) { return nil, fiber.NewError(fiber.StatusNotFound, "Kandang not found") } diff --git a/internal/modules/master/locations/services/location.service.go b/internal/modules/master/locations/services/location.service.go index 3a1d1e23..633d7419 100644 --- a/internal/modules/master/locations/services/location.service.go +++ b/internal/modules/master/locations/services/location.service.go @@ -47,10 +47,21 @@ func (s locationService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entit return nil, 0, err } + scope, err := m.ResolveLocationScope(c, s.Repository.DB()) + if err != nil { + return nil, 0, err + } + offset := (params.Page - 1) * params.Limit locations, total, err := s.Repository.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.Where("id IN ?", scope.IDs) + } if params.Search != "" { db = db.Where("name ILIKE ?", "%"+params.Search+"%") } @@ -68,7 +79,21 @@ func (s locationService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entit } func (s locationService) GetOne(c *fiber.Ctx, id uint) (*entity.Location, error) { - location, err := s.Repository.GetByID(c.Context(), id, s.withRelations) + scope, err := m.ResolveLocationScope(c, s.Repository.DB()) + if err != nil { + return nil, err + } + + location, err := s.Repository.GetByID(c.Context(), id, 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.Where("id IN ?", scope.IDs) + } + return db + }) if errors.Is(err, gorm.ErrRecordNotFound) { return nil, fiber.NewError(fiber.StatusNotFound, "Location not found") } diff --git a/internal/modules/master/warehouses/services/warehouse.service.go b/internal/modules/master/warehouses/services/warehouse.service.go index 7eeaad3d..aaa5ca7e 100644 --- a/internal/modules/master/warehouses/services/warehouse.service.go +++ b/internal/modules/master/warehouses/services/warehouse.service.go @@ -48,10 +48,16 @@ func (s warehouseService) GetAll(c *fiber.Ctx, params *validation.Query) ([]enti return nil, 0, err } + scope, err := m.ResolveAreaScope(c, s.Repository.DB()) + if err != nil { + return nil, 0, err + } + offset := (params.Page - 1) * params.Limit warehouses, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB { db = s.withRelations(db) + db = m.ApplyScopeFilter(db, scope, "area_id") if params.Search != "" { db = db.Where("warehouses.name ILIKE ?", "%"+params.Search+"%") } @@ -86,7 +92,16 @@ func (s warehouseService) GetAll(c *fiber.Ctx, params *validation.Query) ([]enti } func (s warehouseService) GetOne(c *fiber.Ctx, id uint) (*entity.Warehouse, error) { - warehouse, err := s.Repository.GetByID(c.Context(), id, s.withRelations) + scope, err := m.ResolveAreaScope(c, s.Repository.DB()) + if err != nil { + return nil, err + } + + warehouse, err := s.Repository.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB { + db = s.withRelations(db) + db = m.ApplyScopeFilter(db, scope, "area_id") + return db + }) if errors.Is(err, gorm.ErrRecordNotFound) { return nil, fiber.NewError(fiber.StatusNotFound, "Warehouse not found") } 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 6f019ffa..61a593d5 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 @@ -88,9 +88,14 @@ func (s projectFlockKandangService) GetAll(c *fiber.Ctx, params *validation.Quer return nil, 0, err } + scope, err := m.ResolveLocationScope(c, s.Repository.DB()) + if err != nil { + return nil, 0, err + } + offset := (params.Page - 1) * params.Limit - projectFlockKandangs, total, err := s.Repository.GetAllWithFilters(c.Context(), offset, params.Limit, params) + projectFlockKandangs, total, err := s.Repository.GetAllWithFiltersScoped(c.Context(), offset, params.Limit, params, scope.IDs, scope.Restrict) if err != nil { s.Log.Errorf("Failed to get projectFlockKandangs: %+v", err) @@ -106,6 +111,28 @@ func (s projectFlockKandangService) GetAll(c *fiber.Ctx, params *validation.Quer } func (s projectFlockKandangService) GetOne(c *fiber.Ctx, id uint) (*entity.ProjectFlockKandang, map[uint]float64, []entity.ProductWarehouse, error) { + scope, err := m.ResolveLocationScope(c, s.Repository.DB()) + if err != nil { + return nil, nil, nil, err + } + if scope.Restrict { + if len(scope.IDs) == 0 { + return nil, nil, nil, fiber.NewError(fiber.StatusNotFound, "ProjectFlockKandang not found") + } + var count int64 + if err := s.Repository.DB().WithContext(c.Context()). + Table("project_flock_kandangs"). + Joins("JOIN project_flocks ON project_flocks.id = project_flock_kandangs.project_flock_id"). + Where("project_flock_kandangs.id = ?", id). + Where("project_flocks.location_id IN ?", scope.IDs). + Count(&count).Error; err != nil { + return nil, nil, nil, err + } + if count == 0 { + return nil, nil, nil, fiber.NewError(fiber.StatusNotFound, "ProjectFlockKandang not found") + } + } + projectFlockKandang, err := s.Repository.GetByID(c.Context(), id) if errors.Is(err, gorm.ErrRecordNotFound) { return nil, nil, nil, fiber.NewError(fiber.StatusNotFound, "ProjectFlockKandang not found") diff --git a/internal/modules/production/project_flocks/repositories/projectflock.repository.go b/internal/modules/production/project_flocks/repositories/projectflock.repository.go index e65dfb4a..346f2176 100644 --- a/internal/modules/production/project_flocks/repositories/projectflock.repository.go +++ b/internal/modules/production/project_flocks/repositories/projectflock.repository.go @@ -14,6 +14,7 @@ import ( type ProjectflockRepository interface { repository.BaseRepository[entity.ProjectFlock] GetAllWithFilters(ctx context.Context, offset, limit int, params *validation.Query) ([]entity.ProjectFlock, int64, error) + GetAllWithFiltersScoped(ctx context.Context, offset, limit int, params *validation.Query, locationIDs []uint, restrict bool) ([]entity.ProjectFlock, int64, error) WithDefaultRelations() func(*gorm.DB) *gorm.DB ExistsByFlockName(ctx context.Context, flockName string, excludeID *uint) (bool, error) GetNextPeriodsForKandangs(ctx context.Context, kandangIDs []uint) (map[uint]int, error) @@ -48,6 +49,19 @@ func (r *ProjectflockRepositoryImpl) GetAllWithFilters(ctx context.Context, offs }) } +func (r *ProjectflockRepositoryImpl) GetAllWithFiltersScoped(ctx context.Context, offset, limit int, params *validation.Query, locationIDs []uint, restrict bool) ([]entity.ProjectFlock, int64, error) { + return r.GetAll(ctx, offset, limit, func(db *gorm.DB) *gorm.DB { + db = r.applyQueryFilters(r.WithDefaultRelations()(db), params) + if restrict { + if len(locationIDs) == 0 { + return db.Where("1 = 0") + } + db = db.Where("project_flocks.location_id IN ?", locationIDs) + } + return db + }) +} + func (r *ProjectflockRepositoryImpl) WithDefaultRelations() func(*gorm.DB) *gorm.DB { return func(db *gorm.DB) *gorm.DB { return db. 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 474a53c2..24c4c083 100644 --- a/internal/modules/production/project_flocks/repositories/projectflock_kandang.repository.go +++ b/internal/modules/production/project_flocks/repositories/projectflock_kandang.repository.go @@ -20,6 +20,7 @@ type ProjectFlockKandangRepository interface { DeleteMany(ctx context.Context, projectFlockID uint, kandangIDs []uint) error GetAll(ctx context.Context, offset int, limit int, modifier func(*gorm.DB) *gorm.DB) ([]entity.ProjectFlockKandang, int64, error) GetAllWithFilters(ctx context.Context, offset int, limit int, params interface{}) ([]entity.ProjectFlockKandang, int64, error) + GetAllWithFiltersScoped(ctx context.Context, offset int, limit int, params interface{}, locationIDs []uint, restrict bool) ([]entity.ProjectFlockKandang, int64, error) GetByProjectFlockID(ctx context.Context, projectFlockID uint) ([]entity.ProjectFlockKandang, error) ListExistingKandangIDs(ctx context.Context, projectFlockID uint, kandangIDs []uint) ([]uint, error) HasKandangsLinkedToOtherProject(ctx context.Context, kandangIDs []uint, exceptProjectID *uint) (bool, error) @@ -196,6 +197,104 @@ func (r *projectFlockKandangRepositoryImpl) GetAllWithFilters(ctx context.Contex return records, total, nil } +func (r *projectFlockKandangRepositoryImpl) GetAllWithFiltersScoped(ctx context.Context, offset int, limit int, params interface{}, locationIDs []uint, restrict bool) ([]entity.ProjectFlockKandang, int64, error) { + var records []entity.ProjectFlockKandang + var total int64 + + query, ok := params.(*validation.Query) + + q := r.db.WithContext(ctx). + Joins("JOIN \"kandangs\" ON \"project_flock_kandangs\".\"kandang_id\" = \"kandangs\".\"id\""). + Joins("JOIN \"project_flocks\" ON \"project_flock_kandangs\".\"project_flock_id\" = \"project_flocks\".\"id\""). + Preload("ProjectFlock"). + Preload("ProjectFlock.Fcr"). + Preload("ProjectFlock.Area"). + Preload("ProjectFlock.Location"). + Preload("ProjectFlock.CreatedUser"). + Preload("ProjectFlock.Kandangs"). + Preload("ProjectFlock.KandangHistory"). + Preload("Kandang"). + Preload("Chickins"). + Preload("Chickins.CreatedUser"). + Preload("Chickins.ProductWarehouse") + + if restrict { + if len(locationIDs) == 0 { + return []entity.ProjectFlockKandang{}, 0, nil + } + q = q.Where("\"project_flocks\".\"location_id\" IN ?", locationIDs) + } + + if ok && query != nil && query.StepName != "" { + q = q.Where(` + EXISTS ( + SELECT 1 FROM "approvals" + WHERE "approvals"."approvable_id" = "project_flock_kandangs"."id" + AND "approvals"."approvable_type" = ? + AND LOWER("approvals"."step_name") = LOWER(?) + AND "approvals"."id" IN ( + SELECT "approvals"."id" FROM "approvals" + WHERE "approvals"."approvable_id" = "project_flock_kandangs"."id" + AND "approvals"."approvable_type" = ? + ORDER BY "approvals"."id" DESC + LIMIT 1 + ) + ) + `, "PROJECT_FLOCK_KANDANGS", query.StepName, "PROJECT_FLOCK_KANDANGS") + } + + if ok && query != nil { + if query.Search != "" { + escapedSearch := strings.NewReplacer("\\", "\\\\", "%", "\\%", "_", "\\_").Replace(query.Search) + q = q.Where( + r.db.Where("LOWER(\"kandangs\".\"name\") LIKE LOWER(?) ESCAPE '\\'", "%"+escapedSearch+"%"). + Or("LOWER(\"project_flocks\".\"flock_name\") LIKE LOWER(?) ESCAPE '\\'", "%"+escapedSearch+"%"), + ) + } + + if query.ProjectFlockId > 0 { + q = q.Where("\"project_flock_kandangs\".\"project_flock_id\" = ?", query.ProjectFlockId) + } + + if query.KandangId > 0 { + q = q.Where("\"project_flock_kandangs\".\"kandang_id\" = ?", query.KandangId) + } + + if query.Category != "" { + q = q.Where("\"project_flocks\".\"category\" = ?", query.Category) + } + + if query.AreaId > 0 { + q = q.Where("\"project_flocks\".\"area_id\" = ?", query.AreaId) + } + } + + if err := q.Model(&entity.ProjectFlockKandang{}).Count(&total).Error; err != nil { + return nil, 0, err + } + + sortBy := "\"project_flock_kandangs\".\"created_at\" DESC" + if ok && query != nil && query.SortBy != "" { + sortOrder := "DESC" + if query.SortOrder == "ASC" { + sortOrder = "ASC" + } + + switch query.SortBy { + case "created_at": + sortBy = "\"project_flock_kandangs\".\"created_at\" " + sortOrder + case "period": + sortBy = "\"project_flocks\".\"period\" " + sortOrder + } + } + + if err := q.Order(sortBy).Offset(offset).Limit(limit).Find(&records).Error; err != nil { + return nil, 0, err + } + + return records, total, nil +} + func (r *projectFlockKandangRepositoryImpl) WithTx(tx *gorm.DB) ProjectFlockKandangRepository { return &projectFlockKandangRepositoryImpl{db: tx} } diff --git a/internal/modules/production/project_flocks/services/projectflock.service.go b/internal/modules/production/project_flocks/services/projectflock.service.go index 3dbe3f4b..5bccfbf9 100644 --- a/internal/modules/production/project_flocks/services/projectflock.service.go +++ b/internal/modules/production/project_flocks/services/projectflock.service.go @@ -114,9 +114,14 @@ func (s projectflockService) GetAll(c *fiber.Ctx, params *validation.Query) ([]e return nil, 0, nil, err } + scope, err := m.ResolveLocationScope(c, s.Repository.DB()) + if err != nil { + return nil, 0, nil, err + } + offset := (params.Page - 1) * params.Limit - projectflocks, total, err := s.Repository.GetAllWithFilters(c.Context(), offset, params.Limit, params) + projectflocks, total, err := s.Repository.GetAllWithFiltersScoped(c.Context(), offset, params.Limit, params, scope.IDs, scope.Restrict) if err != nil { s.Log.Errorf("Failed to get projectflocks: %+v", err) @@ -190,7 +195,16 @@ func (s projectflockService) getOneEntityOnly(c *fiber.Ctx, id uint) (*entity.Pr } func (s projectflockService) GetOne(c *fiber.Ctx, id uint) (*entity.ProjectFlock, *flockDTO.FlockRelationDTO, error) { - projectflock, err := s.Repository.GetByID(c.Context(), id, s.Repository.WithDefaultRelations()) + scope, err := m.ResolveLocationScope(c, s.Repository.DB()) + if err != nil { + return nil, nil, err + } + + projectflock, err := s.Repository.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB { + db = s.Repository.WithDefaultRelations()(db) + db = m.ApplyScopeFilter(db, scope, "project_flocks.location_id") + return db + }) if errors.Is(err, gorm.ErrRecordNotFound) { return nil, nil, fiber.NewError(fiber.StatusNotFound, "Projectflock not found") } diff --git a/internal/modules/purchases/services/purchase.service.go b/internal/modules/purchases/services/purchase.service.go index 35ca2f75..6b9537cc 100644 --- a/internal/modules/purchases/services/purchase.service.go +++ b/internal/modules/purchases/services/purchase.service.go @@ -124,6 +124,11 @@ func (s *purchaseService) GetAll(c *fiber.Ctx, params *validation.Query) ([]enti return nil, 0, err } + scope, err := m.ResolveLocationScope(c, s.PurchaseRepo.DB()) + if err != nil { + return nil, 0, err + } + offset := (params.Page - 1) * params.Limit createdFrom, createdTo, err := utils.ParseDateRangeForQuery(params.CreatedFrom, params.CreatedTo) @@ -147,6 +152,21 @@ func (s *purchaseService) GetAll(c *fiber.Ctx, params *validation.Query) ([]enti db = db.Where("created_at < ?", *createdTo) } + if scope.Restrict { + if len(scope.IDs) == 0 { + return db.Where("1 = 0") + } + db = db.Where( + `EXISTS ( + SELECT 1 + FROM purchase_items pi + JOIN warehouses w ON w.id = pi.warehouse_id + WHERE pi.purchase_id = purchases.id AND w.location_id IN ? + )`, + scope.IDs, + ) + } + if params.AreaID > 0 { db = db.Where( `EXISTS ( @@ -201,7 +221,42 @@ func (s *purchaseService) GetAll(c *fiber.Ctx, params *validation.Query) ([]enti } func (s *purchaseService) GetOne(c *fiber.Ctx, id uint) (*entity.Purchase, error) { - return s.loadPurchase(c.Context(), id) + scope, err := m.ResolveLocationScope(c, s.PurchaseRepo.DB()) + if err != nil { + return nil, err + } + + purchase, err := s.PurchaseRepo.GetByID(c.Context(), id, 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.Where( + `EXISTS ( + SELECT 1 + FROM purchase_items pi + JOIN warehouses w ON w.id = pi.warehouse_id + WHERE pi.purchase_id = purchases.id AND w.location_id IN ? + )`, + scope.IDs, + ) + } + return db + }) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, utils.NotFound("Purchase not found") + } + s.Log.Errorf("Failed to get purchase %d: %+v", id, err) + return nil, utils.Internal("Failed to get purchase") + } + if err := s.attachLatestApproval(c.Context(), purchase); err != nil { + s.Log.Warnf("Unable to attach latest approval for purchase %d: %+v", id, err) + } + s.applyTravelDocumentURLs(c.Context(), purchase) + + return purchase, nil } func (s *purchaseService) CreateOne(c *fiber.Ctx, req *validation.CreatePurchaseRequest) (*entity.Purchase, error) { diff --git a/internal/modules/repports/controllers/repport.controller.go b/internal/modules/repports/controllers/repport.controller.go index 1d273af1..4ed7a855 100644 --- a/internal/modules/repports/controllers/repport.controller.go +++ b/internal/modules/repports/controllers/repport.controller.go @@ -5,6 +5,7 @@ import ( "strconv" "strings" + m "gitlab.com/mbugroup/lti-api.git/internal/middleware" "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/dto" service "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/services" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/validations" @@ -49,6 +50,21 @@ func (c *RepportController) GetExpense(ctx *fiber.Ctx) error { RealizationDate: ctx.Query("realization_date", ""), } + locationScope, err := m.ResolveLocationScope(ctx, c.RepportService.DB()) + if err != nil { + return err + } + areaScope, err := m.ResolveAreaScope(ctx, c.RepportService.DB()) + if err != nil { + return err + } + if locationScope.Restrict { + query.AllowedLocationIDs = toInt64Slice(locationScope.IDs) + } + if areaScope.Restrict { + query.AllowedAreaIDs = toInt64Slice(areaScope.IDs) + } + if query.Page < 1 || query.Limit < 1 { return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0") } @@ -130,6 +146,14 @@ func (c *RepportController) GetPurchaseSupplier(ctx *fiber.Ctx) error { FilterBy: ctx.Query("filter_by", ""), } + areaScope, err := m.ResolveAreaScope(ctx, c.RepportService.DB()) + if err != nil { + return err + } + if areaScope.Restrict { + query.AllowedAreaIDs = toInt64Slice(areaScope.IDs) + } + if query.Page < 1 || query.Limit < 1 { return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0") } @@ -306,3 +330,14 @@ func parseCommaSeparatedInt64s(raw string) ([]int64, error) { return result, nil } + +func toInt64Slice(ids []uint) []int64 { + if len(ids) == 0 { + return nil + } + out := make([]int64, 0, len(ids)) + for _, id := range ids { + out = append(out, int64(id)) + } + return out +} diff --git a/internal/modules/repports/repositories/purchase_supplier.repository.go b/internal/modules/repports/repositories/purchase_supplier.repository.go index 979623fc..048fe02c 100644 --- a/internal/modules/repports/repositories/purchase_supplier.repository.go +++ b/internal/modules/repports/repositories/purchase_supplier.repository.go @@ -53,10 +53,18 @@ func (r *purchaseSupplierRepositoryImpl) baseSupplierQuery(ctx context.Context, Where("products.product_category_id = ?", filters.ProductCategoryId) } - if filters.AreaId > 0 { - db = db. - Joins("JOIN warehouses ON warehouses.id = purchase_items.warehouse_id"). - Where("warehouses.area_id = ?", filters.AreaId) + if filters.AreaId > 0 || filters.AllowedAreaIDs != nil { + db = db.Joins("JOIN warehouses ON warehouses.id = purchase_items.warehouse_id") + if filters.AreaId > 0 { + db = db.Where("warehouses.area_id = ?", filters.AreaId) + } + if filters.AllowedAreaIDs != nil { + if len(filters.AllowedAreaIDs) == 0 { + db = db.Where("1 = 0") + } else { + db = db.Where("warehouses.area_id IN ?", filters.AllowedAreaIDs) + } + } } if filters.StartDate != "" { @@ -164,10 +172,18 @@ func (r *purchaseSupplierRepositoryImpl) GetItemsBySuppliers(ctx context.Context Where("products.product_category_id = ?", filters.ProductCategoryId) } - if filters.AreaId > 0 { - db = db. - Joins("JOIN warehouses ON warehouses.id = purchase_items.warehouse_id"). - Where("warehouses.area_id = ?", filters.AreaId) + if filters.AreaId > 0 || filters.AllowedAreaIDs != nil { + db = db.Joins("JOIN warehouses ON warehouses.id = purchase_items.warehouse_id") + if filters.AreaId > 0 { + db = db.Where("warehouses.area_id = ?", filters.AreaId) + } + if filters.AllowedAreaIDs != nil { + if len(filters.AllowedAreaIDs) == 0 { + db = db.Where("1 = 0") + } else { + db = db.Where("warehouses.area_id IN ?", filters.AllowedAreaIDs) + } + } } if filters.StartDate != "" { diff --git a/internal/modules/repports/services/repport.service.go b/internal/modules/repports/services/repport.service.go index c4883b72..0e3dbada 100644 --- a/internal/modules/repports/services/repport.service.go +++ b/internal/modules/repports/services/repport.service.go @@ -9,6 +9,7 @@ import ( "strings" "time" + m "gitlab.com/mbugroup/lti-api.git/internal/middleware" "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/dto" repportRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/repositories" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/validations" @@ -40,6 +41,7 @@ type RepportService interface { GetDebtSupplier(ctx *fiber.Ctx, params *validation.DebtSupplierQuery) ([]dto.DebtSupplierDTO, int64, error) GetHppPerKandang(ctx *fiber.Ctx) (*dto.HppPerKandangResponseData, *dto.HppPerKandangMetaDTO, error) GetProductionResult(ctx *fiber.Ctx, params *validation.ProductionResultQuery) ([]dto.ProductionResultDTO, int64, error) + DB() *gorm.DB } type repportService struct { @@ -95,6 +97,10 @@ func NewRepportService( } } +func (s *repportService) DB() *gorm.DB { + return s.ExpenseRealizationRepo.DB() +} + func (s *repportService) GetExpense(c *fiber.Ctx, params *validation.ExpenseQuery) ([]dto.RepportExpenseListDTO, int64, error) { if err := s.Validate.Struct(params); err != nil { return nil, 0, err @@ -1303,6 +1309,36 @@ func (s *repportService) parseHppPerKandangQuery(ctx *fiber.Ctx) (*validation.Hp return nil, dto.HppPerKandangFiltersDTO{}, fiber.NewError(fiber.StatusBadRequest, err.Error()) } + locationScope, err := m.ResolveLocationScope(ctx, s.ExpenseRealizationRepo.DB()) + if err != nil { + return nil, dto.HppPerKandangFiltersDTO{}, err + } + areaScope, err := m.ResolveAreaScope(ctx, s.ExpenseRealizationRepo.DB()) + if err != nil { + return nil, dto.HppPerKandangFiltersDTO{}, err + } + + if locationScope.Restrict { + allowed := toInt64Slice(locationScope.IDs) + if len(allowed) == 0 { + locationIDs = []int64{-1} + } else if len(locationIDs) > 0 { + locationIDs = intersectInt64(locationIDs, allowed) + } else { + locationIDs = allowed + } + } + if areaScope.Restrict { + allowed := toInt64Slice(areaScope.IDs) + if len(allowed) == 0 { + areaIDs = []int64{-1} + } else if len(areaIDs) > 0 { + areaIDs = intersectInt64(areaIDs, allowed) + } else { + areaIDs = allowed + } + } + weightMin, err := parseOptionalFloat64(rawWeightMin) if err != nil { return nil, dto.HppPerKandangFiltersDTO{}, fiber.NewError(fiber.StatusBadRequest, err.Error()) @@ -1366,6 +1402,34 @@ func parseCommaSeparatedInt64s(raw string) ([]int64, error) { return result, nil } +func toInt64Slice(ids []uint) []int64 { + if len(ids) == 0 { + return nil + } + out := make([]int64, 0, len(ids)) + for _, id := range ids { + out = append(out, int64(id)) + } + return out +} + +func intersectInt64(a, b []int64) []int64 { + if len(a) == 0 || len(b) == 0 { + return nil + } + set := make(map[int64]struct{}, len(b)) + for _, id := range b { + set[id] = struct{}{} + } + out := make([]int64, 0, len(a)) + for _, id := range a { + if _, ok := set[id]; ok { + out = append(out, id) + } + } + return out +} + func parseOptionalFloat64(raw string) (*float64, error) { raw = strings.TrimSpace(raw) if raw == "" { diff --git a/internal/modules/repports/validations/repport.validation.go b/internal/modules/repports/validations/repport.validation.go index 6d50f3e6..4c1d7356 100644 --- a/internal/modules/repports/validations/repport.validation.go +++ b/internal/modules/repports/validations/repport.validation.go @@ -13,6 +13,8 @@ type ExpenseQuery struct { AreaId int64 `query:"area_id" validate:"omitempty"` LocationId int64 `query:"location_id" validate:"omitempty"` RealizationDate string `query:"realization_date" validate:"omitempty"` + AllowedAreaIDs []int64 `query:"-"` + AllowedLocationIDs []int64 `query:"-"` } type MarketingQuery struct { @@ -42,6 +44,7 @@ type PurchaseSupplierQuery struct { EndDate string `query:"end_date" validate:"omitempty"` SortBy string `query:"sort_by" validate:"omitempty"` FilterBy string `query:"filter_by" validate:"omitempty"` + AllowedAreaIDs []int64 `query:"-"` } type DebtSupplierQuery struct { diff --git a/internal/modules/sso/controllers/master_data.controller.go b/internal/modules/sso/controllers/master_data.controller.go new file mode 100644 index 00000000..2364011f --- /dev/null +++ b/internal/modules/sso/controllers/master_data.controller.go @@ -0,0 +1,331 @@ +package controllers + +import ( + "context" + "crypto/hmac" + "crypto/sha256" + "encoding/base64" + "encoding/hex" + "errors" + "fmt" + "strconv" + "strings" + "sync" + "time" + + "github.com/gofiber/fiber/v2" + "github.com/redis/go-redis/v9" + "gorm.io/gorm" + + "gitlab.com/mbugroup/lti-api.git/internal/config" + entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + sso "gitlab.com/mbugroup/lti-api.git/internal/modules/sso/verifier" + "gitlab.com/mbugroup/lti-api.git/internal/response" + "gitlab.com/mbugroup/lti-api.git/internal/utils" +) + +type MasterDataController struct { + db *gorm.DB + redis *redis.Client + clients map[string]config.SSOClientConfig + drift time.Duration + nonceTTL time.Duration + localNonce sync.Map +} + +type masterArea struct { + ID uint `json:"id"` + Name string `json:"name"` +} + +type masterLocation struct { + ID uint `json:"id"` + Name string `json:"name"` + AreaID uint `json:"area_id"` +} + +func NewMasterDataController(db *gorm.DB, redis *redis.Client, clients map[string]config.SSOClientConfig) *MasterDataController { + normalized := make(map[string]config.SSOClientConfig, len(clients)) + for alias, cfg := range clients { + alias = strings.ToLower(strings.TrimSpace(alias)) + normalized[alias] = cfg + } + + drift := config.SSOUserSyncDrift + if drift <= 0 { + drift = 2 * time.Minute + } + + nonceTTL := config.SSOUserSyncNonceTTL + if nonceTTL <= 0 { + nonceTTL = 10 * time.Minute + } + + return &MasterDataController{ + db: db, + redis: redis, + clients: normalized, + drift: drift, + nonceTTL: nonceTTL, + } +} + +func (h *MasterDataController) GetAreas(c *fiber.Ctx) error { + if _, _, err := h.authenticate(c, nil); err != nil { + return err + } + + search := strings.TrimSpace(c.Query("search", "")) + ids := parseUintList(c.Query("ids", "")) + + query := h.db.WithContext(c.Context()). + Model(&entity.Area{}). + Where("deleted_at IS NULL") + if search != "" { + query = query.Where("name ILIKE ?", "%"+search+"%") + } + if len(ids) > 0 { + query = query.Where("id IN ?", ids) + } + + var areas []masterArea + if err := query.Order("name ASC").Find(&areas).Error; err != nil { + utils.Log.WithError(err).Error("failed to fetch areas for master data") + return fiber.NewError(fiber.StatusInternalServerError, "failed to fetch areas") + } + + return c.Status(fiber.StatusOK).JSON(response.Success{ + Code: fiber.StatusOK, + Status: "success", + Message: "Get areas successfully", + Data: areas, + }) +} + +func (h *MasterDataController) GetLocations(c *fiber.Ctx) error { + if _, _, err := h.authenticate(c, nil); err != nil { + return err + } + + search := strings.TrimSpace(c.Query("search", "")) + areaIDs := parseUintList(c.Query("area_ids", "")) + ids := parseUintList(c.Query("ids", "")) + + query := h.db.WithContext(c.Context()). + Model(&entity.Location{}). + Where("deleted_at IS NULL") + if search != "" { + query = query.Where("name ILIKE ?", "%"+search+"%") + } + if len(areaIDs) > 0 { + query = query.Where("area_id IN ?", areaIDs) + } + if len(ids) > 0 { + query = query.Where("id IN ?", ids) + } + + var locations []masterLocation + if err := query.Order("name ASC").Find(&locations).Error; err != nil { + utils.Log.WithError(err).Error("failed to fetch locations for master data") + return fiber.NewError(fiber.StatusInternalServerError, "failed to fetch locations") + } + + return c.Status(fiber.StatusOK).JSON(response.Success{ + Code: fiber.StatusOK, + Status: "success", + Message: "Get locations successfully", + Data: locations, + }) +} + +func (h *MasterDataController) authenticate(c *fiber.Ctx, body []byte) (string, config.SSOClientConfig, error) { + rawAlias := strings.TrimSpace(c.Get("X-Sync-Client")) + if rawAlias == "" { + return "", config.SSOClientConfig{}, fiber.NewError(fiber.StatusUnauthorized, "missing sync client header") + } + + aliasKey := strings.ToLower(rawAlias) + clientCfg, ok := h.clients[aliasKey] + if !ok { + return "", config.SSOClientConfig{}, fiber.NewError(fiber.StatusUnauthorized, "unknown sync client") + } + + if err := h.verifyAuthorization(c, aliasKey); err != nil { + return "", config.SSOClientConfig{}, err + } + + secret := strings.TrimSpace(clientCfg.SyncSecret) + if secret == "" { + return "", config.SSOClientConfig{}, fiber.NewError(fiber.StatusUnauthorized, "sync secret not configured") + } + + timestamp := strings.TrimSpace(c.Get("X-Sync-Timestamp")) + nonce := strings.TrimSpace(c.Get("X-Sync-Nonce")) + signature := strings.TrimSpace(c.Get("X-Sync-Signature")) + if timestamp == "" || nonce == "" || signature == "" { + return "", config.SSOClientConfig{}, fiber.NewError(fiber.StatusUnauthorized, "missing signature headers") + } + if len(nonce) < 16 { + return "", config.SSOClientConfig{}, fiber.NewError(fiber.StatusUnauthorized, "nonce too short") + } + + ts, err := strconv.ParseInt(timestamp, 10, 64) + if err != nil { + return "", config.SSOClientConfig{}, fiber.NewError(fiber.StatusBadRequest, "invalid timestamp") + } + + msgTime := time.Unix(ts, 0).UTC() + now := time.Now().UTC() + drift := now.Sub(msgTime) + if drift > h.drift || drift < -h.drift { + return "", config.SSOClientConfig{}, fiber.NewError(fiber.StatusUnauthorized, "timestamp outside allowed window") + } + + providedSig, err := decodeMasterSignature(signature) + if err != nil { + return "", config.SSOClientConfig{}, fiber.NewError(fiber.StatusUnauthorized, "invalid signature encoding") + } + + expectedSignature := calculateSignature(secret, rawAlias, timestamp, nonce, body) + if !hmac.Equal(providedSig, expectedSignature) { + return "", config.SSOClientConfig{}, fiber.NewError(fiber.StatusUnauthorized, "invalid signature") + } + + if err := h.registerNonce(c.Context(), aliasKey, nonce); err != nil { + return "", config.SSOClientConfig{}, err + } + + return aliasKey, clientCfg, nil +} + +func (h *MasterDataController) verifyAuthorization(c *fiber.Ctx, alias string) error { + authHeader := strings.TrimSpace(c.Get(fiber.HeaderAuthorization)) + if authHeader == "" { + return fiber.NewError(fiber.StatusUnauthorized, "missing authorization header") + } + parts := strings.SplitN(authHeader, " ", 2) + if len(parts) != 2 || !strings.EqualFold(parts[0], "Bearer") { + return fiber.NewError(fiber.StatusUnauthorized, "invalid authorization header") + } + + token := strings.TrimSpace(parts[1]) + if token == "" { + return fiber.NewError(fiber.StatusUnauthorized, "invalid authorization header") + } + + verification, err := sso.VerifyAccessToken(token) + if err != nil { + return fiber.NewError(fiber.StatusUnauthorized, "invalid access token") + } + + if verification.ServiceAlias == "" || verification.ServiceAlias != alias { + return fiber.NewError(fiber.StatusUnauthorized, "service subject mismatch") + } + if !hasAnyScope(verification.Claims.Scopes(), []string{"sync.master", "sync.users"}) { + return fiber.NewError(fiber.StatusForbidden, "missing sync scope") + } + return nil +} + +func (h *MasterDataController) registerNonce(ctx context.Context, alias, nonce string) error { + ttl := h.nonceTTL + if ttl <= 0 { + ttl = 10 * time.Minute + } + + key := fmt.Sprintf("sso:sync:%s:%s", alias, nonce) + if h.redis != nil { + stored, err := h.redis.SetNX(ctx, key, "1", ttl).Result() + if err == nil { + if !stored { + return fiber.NewError(fiber.StatusUnauthorized, "nonce already used") + } + return nil + } + utils.Log.WithError(err).Warn("store sync nonce failed") + } + + now := time.Now().UTC() + if expRaw, ok := h.localNonce.Load(key); ok { + if expTime, ok := expRaw.(time.Time); ok && expTime.After(now) { + return fiber.NewError(fiber.StatusUnauthorized, "nonce already used") + } + } + h.localNonce.Store(key, now.Add(ttl)) + return nil +} + +func calculateSignature(secret, alias, timestamp, nonce string, body []byte) []byte { + mac := hmac.New(sha256.New, []byte(secret)) + mac.Write([]byte(alias)) + mac.Write([]byte("\n")) + mac.Write([]byte(timestamp)) + mac.Write([]byte("\n")) + mac.Write([]byte(nonce)) + mac.Write([]byte("\n")) + if len(body) > 0 { + mac.Write(body) + } + return mac.Sum(nil) +} + +func decodeMasterSignature(sig string) ([]byte, error) { + sig = strings.TrimSpace(sig) + if sig == "" { + return nil, errors.New("empty signature") + } + if decoded, err := hex.DecodeString(sig); err == nil { + return decoded, nil + } + if decoded, err := base64.StdEncoding.DecodeString(sig); err == nil { + return decoded, nil + } + if decoded, err := base64.URLEncoding.DecodeString(sig); err == nil { + return decoded, nil + } + return nil, errors.New("unrecognized signature encoding") +} + +func parseUintList(raw string) []uint { + raw = strings.TrimSpace(raw) + if raw == "" { + return nil + } + parts := strings.Split(raw, ",") + out := make([]uint, 0, len(parts)) + seen := make(map[uint]struct{}, len(parts)) + for _, part := range parts { + part = strings.TrimSpace(part) + if part == "" { + continue + } + val, err := strconv.ParseUint(part, 10, 64) + if err != nil || val == 0 { + continue + } + if _, ok := seen[uint(val)]; ok { + continue + } + seen[uint(val)] = struct{}{} + out = append(out, uint(val)) + } + return out +} + +func hasAnyScope(scopes []string, targets []string) bool { + if len(scopes) == 0 || len(targets) == 0 { + return false + } + for _, scope := range scopes { + scope = strings.ToLower(strings.TrimSpace(scope)) + if scope == "" { + continue + } + for _, target := range targets { + if scope == strings.ToLower(strings.TrimSpace(target)) { + return true + } + } + } + return false +} diff --git a/internal/modules/sso/controllers/sso.controller.go b/internal/modules/sso/controllers/sso.controller.go index 410e9577..b49d73e5 100644 --- a/internal/modules/sso/controllers/sso.controller.go +++ b/internal/modules/sso/controllers/sso.controller.go @@ -16,7 +16,7 @@ import ( "gitlab.com/mbugroup/lti-api.git/internal/config" "gitlab.com/mbugroup/lti-api.git/internal/modules/sso/session" - "gitlab.com/mbugroup/lti-api.git/internal/sso" + sso "gitlab.com/mbugroup/lti-api.git/internal/modules/sso/verifier" "gitlab.com/mbugroup/lti-api.git/internal/utils" "gitlab.com/mbugroup/lti-api.git/internal/utils/secure" ) diff --git a/internal/modules/sso/controllers/user_sync.controller.go b/internal/modules/sso/controllers/user_sync.controller.go index 9aeb9555..72c7768a 100644 --- a/internal/modules/sso/controllers/user_sync.controller.go +++ b/internal/modules/sso/controllers/user_sync.controller.go @@ -9,23 +9,24 @@ import ( "encoding/json" "errors" "fmt" - "github.com/go-playground/validator/v10" - "github.com/gofiber/fiber/v2" - "github.com/redis/go-redis/v9" - "github.com/sirupsen/logrus" - "gorm.io/gorm" "strconv" "strings" "sync" "time" + "github.com/go-playground/validator/v10" + "github.com/gofiber/fiber/v2" + "github.com/redis/go-redis/v9" + "github.com/sirupsen/logrus" + "gorm.io/gorm" + "gitlab.com/mbugroup/lti-api.git/internal/config" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" "gitlab.com/mbugroup/lti-api.git/internal/modules/sso/session" + sso "gitlab.com/mbugroup/lti-api.git/internal/modules/sso/verifier" "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto" userRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories" "gitlab.com/mbugroup/lti-api.git/internal/response" - "gitlab.com/mbugroup/lti-api.git/internal/sso" "gitlab.com/mbugroup/lti-api.git/internal/utils" ) diff --git a/internal/modules/sso/route.go b/internal/modules/sso/route.go index 3f2a699e..a864611e 100644 --- a/internal/modules/sso/route.go +++ b/internal/modules/sso/route.go @@ -26,6 +26,7 @@ func Routes(router fiber.Router, db *gorm.DB, validate *validator.Validate) { ctrl := ssoController.NewController(&http.Client{Timeout: 10 * time.Second}, store, session.GetRevocationStore()) userRepo := userRepository.NewUserRepository(db) syncCtrl := ssoController.NewUserSyncController(validate, userRepo, cache.Redis(), config.SSOClients) + masterCtrl := ssoController.NewMasterDataController(db, cache.Redis(), config.SSOClients) group := router.Group("/sso") group.Get("/start", middleware.NewLimiter(30, time.Minute), ctrl.Start) @@ -34,4 +35,6 @@ func Routes(router fiber.Router, db *gorm.DB, validate *validator.Validate) { group.Post("/refresh", middleware.NewLimiter(60, time.Minute), ctrl.Refresh) group.Post("/logout", middleware.NewLimiter(60, time.Minute), ctrl.Logout) group.Post("/users/sync", middleware.NewLimiter(30, time.Minute), syncCtrl.Sync) + group.Get("/master/areas", middleware.NewLimiter(60, time.Minute), masterCtrl.GetAreas) + group.Get("/master/locations", middleware.NewLimiter(60, time.Minute), masterCtrl.GetLocations) } diff --git a/internal/sso/profile.go b/internal/modules/sso/verifier/profile.go similarity index 95% rename from internal/sso/profile.go rename to internal/modules/sso/verifier/profile.go index a211fc74..52b7ef5a 100644 --- a/internal/sso/profile.go +++ b/internal/modules/sso/verifier/profile.go @@ -49,6 +49,10 @@ type Role struct { ClientID uint ClientAlias string ClientName string + AllArea bool + AllLocation bool + AreaIDs []uint + LocationIDs []uint Permissions []Permission RawReference json.RawMessage `json:"-"` } @@ -162,6 +166,10 @@ func fetchProfileFromSSO(ctx context.Context, token string) (*UserProfile, error ClientAlias: strings.TrimSpace(r.Client.Alias), ClientName: strings.TrimSpace(r.Client.Name), ClientID: uint(r.Client.ID), + AllArea: r.AllArea, + AllLocation: r.AllLocation, + AreaIDs: r.AreaIDs, + LocationIDs: r.LocationIDs, } rolePerms := make([]Permission, 0, len(r.Permissions)) for _, p := range r.Permissions { @@ -288,6 +296,10 @@ type userInfoRole struct { ID int64 `json:"id"` Key string `json:"key"` Name string `json:"name"` + AllArea bool `json:"all_area"` + AllLocation bool `json:"all_location"` + AreaIDs []uint `json:"area_ids"` + LocationIDs []uint `json:"location_ids"` Client userInfoClient `json:"client"` Permissions []userInfoPermRaw `json:"permissions"` } diff --git a/internal/sso/verifier.go b/internal/modules/sso/verifier/verifier.go similarity index 100% rename from internal/sso/verifier.go rename to internal/modules/sso/verifier/verifier.go From a3e9017e29219b58bf3e84aec18c6a31a813e3b5 Mon Sep 17 00:00:00 2001 From: ragilap Date: Thu, 15 Jan 2026 14:43:59 +0700 Subject: [PATCH 4/7] [FIX/BE-US] changes role to user and query --- internal/middleware/auth.go | 32 +- internal/middleware/role_scope.go | 501 ++++++++++++++---- .../closings/services/closing.service.go | 57 +- .../services/adjustment.service.go | 28 +- .../salesorder_delivery_product.repository.go | 9 + .../services/deliveryorder.service.go | 32 ++ .../marketing/services/salesorder.service.go | 34 +- .../kandangs/services/kandang.service.go | 15 + .../warehouses/services/warehouse.service.go | 32 ++ .../recordings/services/recording.service.go | 40 ++ .../services/transfer_laying.service.go | 34 ++ .../repositories/uniformity.repository.go | 12 +- .../services/uniformity.service.go | 37 +- .../controllers/repport.controller.go | 16 + .../validations/repport.validation.go | 1 + internal/modules/sso/verifier/profile.go | 72 ++- 16 files changed, 805 insertions(+), 147 deletions(-) diff --git a/internal/middleware/auth.go b/internal/middleware/auth.go index 1b670c14..b7229382 100644 --- a/internal/middleware/auth.go +++ b/internal/middleware/auth.go @@ -24,6 +24,10 @@ type AuthContext struct { User *entity.User Roles []sso.Role Permissions map[string]struct{} + UserAreaIDs []uint + UserLocationIDs []uint + UserAllArea bool + UserAllLocation bool } // Auth validates the incoming request against the central SSO access token and @@ -67,15 +71,19 @@ func Auth(userService service.UserService, requiredScopes ...string) fiber.Handl var roles []sso.Role permissions := make(map[string]struct{}) + var profile *sso.UserProfile if verification.UserID != 0 { - if profile, err := sso.FetchProfile(c.Context(), token, verification); err != nil { + if p, err := sso.FetchProfile(c.Context(), token, verification); err != nil { utils.Log.WithError(err).Warn("auth: failed to fetch sso profile") - } else if profile != nil { - roles = profile.Roles - for _, perm := range profile.PermissionNames() { - if perm != "" { - permissions[perm] = struct{}{} - } + } else { + profile = p + } + } + if profile != nil { + roles = profile.Roles + for _, perm := range profile.PermissionNames() { + if perm != "" { + permissions[perm] = struct{}{} } } } @@ -86,6 +94,16 @@ func Auth(userService service.UserService, requiredScopes ...string) fiber.Handl User: user, Roles: roles, Permissions: permissions, + UserAreaIDs: nil, + UserLocationIDs: nil, + UserAllArea: false, + UserAllLocation: false, + } + if profile != nil { + ctx.UserAreaIDs = profile.AreaIDs + ctx.UserLocationIDs = profile.LocationIDs + ctx.UserAllArea = profile.AllArea + ctx.UserAllLocation = profile.AllLocation } c.Locals(authContextLocalsKey, ctx) diff --git a/internal/middleware/role_scope.go b/internal/middleware/role_scope.go index f00e12d9..155d2e6a 100644 --- a/internal/middleware/role_scope.go +++ b/internal/middleware/role_scope.go @@ -2,12 +2,10 @@ package middleware import ( "errors" - "strings" "github.com/gofiber/fiber/v2" "gorm.io/gorm" - "gitlab.com/mbugroup/lti-api.git/internal/config" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" ) @@ -86,48 +84,24 @@ func ResolveLocationScope(c *fiber.Ctx, db *gorm.DB) (ScopeFilter, error) { func collectRoleScope(c *fiber.Ctx) (roleScope, error) { ctx, ok := AuthDetails(c) - if !ok || ctx == nil || len(ctx.Roles) == 0 { + if !ok || ctx == nil { return roleScope{}, nil } - clientAlias := resolveClientAlias(ctx) - - scope := roleScope{} - areaSet := make(map[uint]struct{}) - locationSet := make(map[uint]struct{}) - - for _, role := range ctx.Roles { - if clientAlias != "" && !strings.EqualFold(strings.TrimSpace(role.ClientAlias), clientAlias) { - continue - } - scope.hasAnyScopes = true - if role.AllArea { - scope.allArea = true - } - if role.AllLocation { - scope.allLocation = true - } - for _, id := range role.AreaIDs { - if id == 0 { - continue - } - areaSet[id] = struct{}{} - } - for _, id := range role.LocationIDs { - if id == 0 { - continue - } - locationSet[id] = struct{}{} - } + userAreaIDs := uniqueUint(ctx.UserAreaIDs) + userLocationIDs := uniqueUint(ctx.UserLocationIDs) + userScope := roleScope{ + allArea: ctx.UserAllArea, + allLocation: ctx.UserAllLocation, + areaIDs: userAreaIDs, + locationIDs: userLocationIDs, + hasAnyScopes: ctx.UserAllArea || ctx.UserAllLocation || len(userAreaIDs) > 0 || len(userLocationIDs) > 0, + } + if userScope.hasAnyScopes { + return userScope, nil } - scope.areaIDs = keysUint(areaSet) - scope.locationIDs = keysUint(locationSet) - - scope.hasAnyScopes = scope.hasAnyScopes && - (scope.allArea || scope.allLocation || len(scope.areaIDs) > 0 || len(scope.locationIDs) > 0) - - return scope, nil + return roleScope{}, nil } func areaIDsByLocationIDs(db *gorm.DB, locationIDs []uint) ([]uint, error) { @@ -204,70 +178,7 @@ func uniqueUint(ids []uint) []uint { return result } -func keysUint(set map[uint]struct{}) []uint { - if len(set) == 0 { - return nil - } - out := make([]uint, 0, len(set)) - for id := range set { - out = append(out, id) - } - return out -} -func resolveClientAlias(ctx *AuthContext) string { - if ctx == nil || ctx.Verification == nil || ctx.Verification.Claims == nil { - return "" - } - - scopes := ctx.Verification.Claims.Scopes() - if len(scopes) == 0 { - return "" - } - - seen := make(map[string]struct{}) - for _, scope := range scopes { - scope = strings.ToLower(strings.TrimSpace(scope)) - if scope == "" { - continue - } - prefix := scope - if idx := strings.IndexAny(prefix, ".:"); idx > 0 { - prefix = prefix[:idx] - } - prefix = strings.TrimSpace(prefix) - if prefix == "" { - continue - } - if alias := matchAlias(prefix); alias != "" { - seen[alias] = struct{}{} - } - } - - if len(seen) != 1 { - return "" - } - for alias := range seen { - return alias - } - return "" -} - -func matchAlias(alias string) string { - alias = strings.ToLower(strings.TrimSpace(alias)) - if alias == "" { - return "" - } - if _, ok := config.SSOClients[alias]; ok { - return alias - } - for key := range config.SSOClients { - if strings.EqualFold(key, alias) { - return strings.ToLower(strings.TrimSpace(key)) - } - } - return "" -} func ApplyScopeFilter(db *gorm.DB, scope ScopeFilter, column string) *gorm.DB { if db == nil || !scope.Restrict { @@ -278,3 +189,389 @@ func ApplyScopeFilter(db *gorm.DB, scope ScopeFilter, column string) *gorm.DB { } return db.Where(column+" IN ?", scope.IDs) } + +func EnsureWarehouseAccess(c *fiber.Ctx, db *gorm.DB, warehouseID uint) error { + if warehouseID == 0 { + return fiber.NewError(fiber.StatusBadRequest, "Invalid warehouse id") + } + if db == nil { + return fiber.NewError(fiber.StatusInternalServerError, "Database not configured") + } + + scope, err := ResolveLocationScope(c, db) + if err != nil || !scope.Restrict { + return err + } + if len(scope.IDs) == 0 { + return fiber.NewError(fiber.StatusNotFound, "Warehouse not found") + } + + var count int64 + if err := ApplyScopeFilter( + db.WithContext(c.Context()). + Model(&entity.Warehouse{}). + Where("id = ?", warehouseID), + scope, + "warehouses.location_id", + ).Count(&count).Error; err != nil { + return err + } + if count == 0 { + return fiber.NewError(fiber.StatusNotFound, "Warehouse not found") + } + return nil +} + +func EnsureAreaAccess(c *fiber.Ctx, db *gorm.DB, areaID uint) error { + if areaID == 0 { + return fiber.NewError(fiber.StatusBadRequest, "Invalid area id") + } + if db == nil { + return fiber.NewError(fiber.StatusInternalServerError, "Database not configured") + } + + scope, err := ResolveAreaScope(c, db) + if err != nil || !scope.Restrict { + return err + } + if len(scope.IDs) == 0 { + return fiber.NewError(fiber.StatusNotFound, "Area not found") + } + + var count int64 + if err := ApplyScopeFilter( + db.WithContext(c.Context()). + Model(&entity.Area{}). + Where("id = ?", areaID), + scope, + "areas.id", + ).Count(&count).Error; err != nil { + return err + } + if count == 0 { + return fiber.NewError(fiber.StatusNotFound, "Area not found") + } + return nil +} + +func EnsureLocationAccess(c *fiber.Ctx, db *gorm.DB, locationID uint) error { + if locationID == 0 { + return fiber.NewError(fiber.StatusBadRequest, "Invalid location id") + } + if db == nil { + return fiber.NewError(fiber.StatusInternalServerError, "Database not configured") + } + + scope, err := ResolveLocationScope(c, db) + if err != nil || !scope.Restrict { + return err + } + if len(scope.IDs) == 0 { + return fiber.NewError(fiber.StatusNotFound, "Location not found") + } + + var count int64 + if err := ApplyScopeFilter( + db.WithContext(c.Context()). + Model(&entity.Location{}). + Where("id = ?", locationID), + scope, + "locations.id", + ).Count(&count).Error; err != nil { + return err + } + if count == 0 { + return fiber.NewError(fiber.StatusNotFound, "Location not found") + } + return nil +} + +func EnsureKandangAccess(c *fiber.Ctx, db *gorm.DB, kandangID uint) error { + if kandangID == 0 { + return fiber.NewError(fiber.StatusBadRequest, "Invalid kandang id") + } + if db == nil { + return fiber.NewError(fiber.StatusInternalServerError, "Database not configured") + } + + scope, err := ResolveLocationScope(c, db) + if err != nil || !scope.Restrict { + return err + } + if len(scope.IDs) == 0 { + return fiber.NewError(fiber.StatusNotFound, "Kandang not found") + } + + var count int64 + if err := ApplyScopeFilter( + db.WithContext(c.Context()). + Model(&entity.Kandang{}). + Where("id = ?", kandangID), + scope, + "kandangs.location_id", + ).Count(&count).Error; err != nil { + return err + } + if count == 0 { + return fiber.NewError(fiber.StatusNotFound, "Kandang not found") + } + return nil +} + +func EnsureProductWarehouseAccess(c *fiber.Ctx, db *gorm.DB, productWarehouseID uint) error { + if productWarehouseID == 0 { + return fiber.NewError(fiber.StatusBadRequest, "Invalid product warehouse id") + } + if db == nil { + return fiber.NewError(fiber.StatusInternalServerError, "Database not configured") + } + + scope, err := ResolveLocationScope(c, db) + if err != nil || !scope.Restrict { + return err + } + if len(scope.IDs) == 0 { + return fiber.NewError(fiber.StatusNotFound, "Product warehouse not found") + } + + var count int64 + q := db.WithContext(c.Context()). + Table("product_warehouses pw"). + Joins("JOIN warehouses w ON w.id = pw.warehouse_id"). + Where("pw.id = ?", productWarehouseID) + q = ApplyScopeFilter(q, scope, "w.location_id") + if err := q.Count(&count).Error; err != nil { + return err + } + if count == 0 { + return fiber.NewError(fiber.StatusNotFound, "Product warehouse not found") + } + return nil +} + +func EnsureStockLogAccess(c *fiber.Ctx, db *gorm.DB, stockLogID uint) error { + if stockLogID == 0 { + return fiber.NewError(fiber.StatusBadRequest, "Invalid stock log id") + } + if db == nil { + return fiber.NewError(fiber.StatusInternalServerError, "Database not configured") + } + + scope, err := ResolveLocationScope(c, db) + if err != nil || !scope.Restrict { + return err + } + if len(scope.IDs) == 0 { + return fiber.NewError(fiber.StatusNotFound, "Stock log not found") + } + + var count int64 + q := db.WithContext(c.Context()). + Table("stock_logs sl"). + Joins("JOIN product_warehouses pw ON pw.id = sl.product_warehouse_id"). + Joins("JOIN warehouses w ON w.id = pw.warehouse_id"). + Where("sl.id = ?", stockLogID) + q = ApplyScopeFilter(q, scope, "w.location_id") + if err := q.Count(&count).Error; err != nil { + return err + } + if count == 0 { + return fiber.NewError(fiber.StatusNotFound, "Stock log not found") + } + return nil +} + +func EnsureMarketingAccess(c *fiber.Ctx, db *gorm.DB, marketingID uint) error { + if marketingID == 0 { + return fiber.NewError(fiber.StatusBadRequest, "Invalid marketing id") + } + if db == nil { + return fiber.NewError(fiber.StatusInternalServerError, "Database not configured") + } + + scope, err := ResolveLocationScope(c, db) + if err != nil || !scope.Restrict { + return err + } + if len(scope.IDs) == 0 { + return fiber.NewError(fiber.StatusNotFound, "Marketing not found") + } + + var count int64 + q := db.WithContext(c.Context()). + Table("marketings m"). + Joins("JOIN marketing_products mp ON mp.marketing_id = m.id"). + Joins("JOIN product_warehouses pw ON pw.id = mp.product_warehouse_id"). + Joins("JOIN warehouses w ON w.id = pw.warehouse_id"). + Where("m.id = ?", marketingID) + q = ApplyScopeFilter(q, scope, "w.location_id") + if err := q.Count(&count).Error; err != nil { + return err + } + if count == 0 { + return fiber.NewError(fiber.StatusNotFound, "Marketing not found") + } + return nil +} + +func EnsureRecordingAccess(c *fiber.Ctx, db *gorm.DB, recordingID uint) error { + if recordingID == 0 { + return fiber.NewError(fiber.StatusBadRequest, "Invalid recording id") + } + if db == nil { + return fiber.NewError(fiber.StatusInternalServerError, "Database not configured") + } + + scope, err := ResolveLocationScope(c, db) + if err != nil || !scope.Restrict { + return err + } + if len(scope.IDs) == 0 { + return fiber.NewError(fiber.StatusNotFound, "Recording not found") + } + + var count int64 + q := db.WithContext(c.Context()). + Table("recordings r"). + Joins("JOIN project_flock_kandangs pfk ON pfk.id = r.project_flock_kandangs_id"). + Joins("JOIN project_flocks pf ON pf.id = pfk.project_flock_id"). + Where("r.id = ?", recordingID) + q = ApplyScopeFilter(q, scope, "pf.location_id") + if err := q.Count(&count).Error; err != nil { + return err + } + if count == 0 { + return fiber.NewError(fiber.StatusNotFound, "Recording not found") + } + return nil +} + +func EnsureUniformityAccess(c *fiber.Ctx, db *gorm.DB, uniformityID uint) error { + if uniformityID == 0 { + return fiber.NewError(fiber.StatusBadRequest, "Invalid uniformity id") + } + if db == nil { + return fiber.NewError(fiber.StatusInternalServerError, "Database not configured") + } + + scope, err := ResolveLocationScope(c, db) + if err != nil || !scope.Restrict { + return err + } + if len(scope.IDs) == 0 { + return fiber.NewError(fiber.StatusNotFound, "Uniformity not found") + } + + var count int64 + q := db.WithContext(c.Context()). + Table("project_flock_kandang_uniformities u"). + Joins("JOIN project_flock_kandangs pfk ON pfk.id = u.project_flock_kandang_id"). + Joins("JOIN project_flocks pf ON pf.id = pfk.project_flock_id"). + Where("u.id = ?", uniformityID) + q = ApplyScopeFilter(q, scope, "pf.location_id") + if err := q.Count(&count).Error; err != nil { + return err + } + if count == 0 { + return fiber.NewError(fiber.StatusNotFound, "Uniformity not found") + } + return nil +} + +func EnsureLayingTransferAccess(c *fiber.Ctx, db *gorm.DB, transferID uint) error { + if transferID == 0 { + return fiber.NewError(fiber.StatusBadRequest, "Invalid transfer id") + } + if db == nil { + return fiber.NewError(fiber.StatusInternalServerError, "Database not configured") + } + + scope, err := ResolveLocationScope(c, db) + if err != nil || !scope.Restrict { + return err + } + if len(scope.IDs) == 0 { + return fiber.NewError(fiber.StatusNotFound, "Transfer not found") + } + + var count int64 + q := db.WithContext(c.Context()). + Table("laying_transfers lt"). + Joins("JOIN project_flocks pf_from ON pf_from.id = lt.from_project_flock_id"). + Joins("JOIN project_flocks pf_to ON pf_to.id = lt.to_project_flock_id"). + Where("lt.id = ?", transferID). + Where("(pf_from.location_id IN ? OR pf_to.location_id IN ?)", scope.IDs, scope.IDs) + if err := q.Count(&count).Error; err != nil { + return err + } + if count == 0 { + return fiber.NewError(fiber.StatusNotFound, "Transfer not found") + } + return nil +} + +func EnsureProjectFlockAccess(c *fiber.Ctx, db *gorm.DB, projectFlockID uint) error { + if projectFlockID == 0 { + return fiber.NewError(fiber.StatusBadRequest, "Invalid project flock id") + } + if db == nil { + return fiber.NewError(fiber.StatusInternalServerError, "Database not configured") + } + + scope, err := ResolveLocationScope(c, db) + if err != nil || !scope.Restrict { + return err + } + if len(scope.IDs) == 0 { + return fiber.NewError(fiber.StatusNotFound, "Project Flock not found") + } + + var count int64 + if err := ApplyScopeFilter( + db.WithContext(c.Context()). + Model(&entity.ProjectFlock{}). + Where("id = ?", projectFlockID), + scope, + "project_flocks.location_id", + ).Count(&count).Error; err != nil { + return err + } + if count == 0 { + return fiber.NewError(fiber.StatusNotFound, "Project Flock not found") + } + return nil +} + +func EnsureProjectFlockKandangAccess(c *fiber.Ctx, db *gorm.DB, projectFlockID, projectFlockKandangID uint) error { + if projectFlockKandangID == 0 { + return fiber.NewError(fiber.StatusBadRequest, "Invalid project flock kandang id") + } + if db == nil { + return fiber.NewError(fiber.StatusInternalServerError, "Database not configured") + } + + scope, err := ResolveLocationScope(c, db) + if err != nil || !scope.Restrict { + return err + } + if len(scope.IDs) == 0 { + return fiber.NewError(fiber.StatusNotFound, "Project Flock Kandang not found") + } + + var count int64 + q := db.WithContext(c.Context()). + Table("project_flock_kandangs"). + Joins("JOIN project_flocks ON project_flocks.id = project_flock_kandangs.project_flock_id"). + Where("project_flock_kandangs.id = ?", projectFlockKandangID) + if projectFlockID > 0 { + q = q.Where("project_flock_kandangs.project_flock_id = ?", projectFlockID) + } + q = ApplyScopeFilter(q, scope, "project_flocks.location_id") + if err := q.Count(&count).Error; err != nil { + return err + } + if count == 0 { + return fiber.NewError(fiber.StatusNotFound, "Project Flock Kandang not found") + } + return nil +} diff --git a/internal/modules/closings/services/closing.service.go b/internal/modules/closings/services/closing.service.go index 38529b0d..16a889b5 100644 --- a/internal/modules/closings/services/closing.service.go +++ b/internal/modules/closings/services/closing.service.go @@ -12,6 +12,7 @@ import ( commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + m "gitlab.com/mbugroup/lti-api.git/internal/middleware" "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/dto" repository "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/repositories" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/validations" @@ -98,10 +99,16 @@ func (s closingService) GetAll(c *fiber.Ctx, params *validation.Query) ([]dto.Cl return nil, 0, err } + scope, err := m.ResolveLocationScope(c, s.Repository.DB()) + if err != nil { + return nil, 0, err + } + offset := (params.Page - 1) * params.Limit closings, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB { db = s.withClosingRelations(db) + db = m.ApplyScopeFilter(db, scope, "project_flocks.location_id") if params.Search != "" { return db.Where("flock_name ILIKE ?", "%"+params.Search+"%") } @@ -128,6 +135,10 @@ func (s closingService) GetAll(c *fiber.Ctx, params *validation.Query) ([]dto.Cl } func (s closingService) GetProjectFlockByID(c *fiber.Ctx, id uint) (*entity.ProjectFlock, error) { + if err := m.EnsureProjectFlockAccess(c, s.Repository.DB(), id); err != nil { + return nil, err + } + projectFlock, err := s.ProjectFlockRepo.GetByID(c.Context(), id, s.withRelations) if errors.Is(err, gorm.ErrRecordNotFound) { return nil, fiber.NewError(fiber.StatusNotFound, "Project Flock not found") @@ -139,6 +150,13 @@ func (s closingService) GetProjectFlockByID(c *fiber.Ctx, id uint) (*entity.Proj } func (s closingService) GetPenjualan(c *fiber.Ctx, projectFlockID uint, projectFlockKandangID *uint) ([]entity.MarketingDeliveryProduct, error) { + if projectFlockKandangID != nil { + if err := m.EnsureProjectFlockKandangAccess(c, s.Repository.DB(), projectFlockID, *projectFlockKandangID); err != nil { + return nil, err + } + } else if err := m.EnsureProjectFlockAccess(c, s.Repository.DB(), projectFlockID); err != nil { + return nil, err + } realisasi, err := s.MarketingDeliveryProductRepo.GetClosingPenjualan(c.Context(), projectFlockID, projectFlockKandangID) if err != nil { @@ -152,8 +170,8 @@ func (s closingService) GetPenjualan(c *fiber.Ctx, projectFlockID uint, projectF } func (s closingService) GetClosingSummary(c *fiber.Ctx, projectFlockID uint, kandangID *uint) (any, error) { - if projectFlockID == 0 { - return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid project flock id") + if err := m.EnsureProjectFlockAccess(c, s.Repository.DB(), projectFlockID); err != nil { + return nil, err } if kandangID != nil { @@ -299,8 +317,8 @@ func (s closingService) getClosingSummaryByKandang(ctx context.Context, projectF } func (s closingService) GetClosingSapronak(c *fiber.Ctx, projectFlockID uint, params *validation.ClosingSapronakQuery) ([]dto.ClosingSapronakItemDTO, int64, error) { - if projectFlockID == 0 { - return nil, 0, fiber.NewError(fiber.StatusBadRequest, "Invalid project flock id") + if err := m.EnsureProjectFlockAccess(c, s.Repository.DB(), projectFlockID); err != nil { + return nil, 0, err } if params == nil { @@ -322,14 +340,6 @@ func (s closingService) GetClosingSapronak(c *fiber.Ctx, projectFlockID uint, pa return nil, 0, fiber.NewError(fiber.StatusBadRequest, "type must be either incoming or outgoing") } - if _, err := s.Repository.GetByID(c.Context(), projectFlockID, nil); err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, 0, fiber.NewError(fiber.StatusNotFound, "Project flock tidak ditemukan") - } - s.Log.Errorf("Failed get project flock %d for sapronak closing: %+v", projectFlockID, err) - return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock") - } - warehouseIDs, err := s.getWarehouseIDsByProjectFlock(c.Context(), projectFlockID) if err != nil { s.Log.Errorf("Failed to fetch warehouses for project flock %d: %+v", projectFlockID, err) @@ -490,6 +500,14 @@ func (s closingService) getApprovalStatuses(ctx context.Context, projectFlockID } func (s closingService) GetOverhead(c *fiber.Ctx, projectFlockID uint, projectFlockKandangID *uint) (*dto.OverheadListDTO, error) { + if projectFlockKandangID != nil { + if err := m.EnsureProjectFlockKandangAccess(c, s.Repository.DB(), projectFlockID, *projectFlockKandangID); err != nil { + return nil, err + } + } else if err := m.EnsureProjectFlockAccess(c, s.Repository.DB(), projectFlockID); err != nil { + return nil, err + } + budgets, err := s.ProjectBudgetRepo.GetByProjectFlockID(c.Context(), projectFlockID) if err != nil { return nil, err @@ -578,6 +596,9 @@ func (s closingService) GetOverhead(c *fiber.Ctx, projectFlockID uint, projectFl } func (s closingService) GetClosingKeuangan(c *fiber.Ctx, projectFlockID uint) (*dto.ReportResponse, error) { + if err := m.EnsureProjectFlockAccess(c, s.Repository.DB(), projectFlockID); err != nil { + return nil, err + } if err := commonSvc.EnsureRelations(c.Context(), commonSvc.RelationCheck{Name: "Project Flock", ID: &projectFlockID, Exists: s.ProjectFlockRepo.IdExists}, @@ -654,8 +675,12 @@ func (s closingService) GetClosingKeuangan(c *fiber.Ctx, projectFlockID uint) (* } func (s closingService) GetExpeditionHPP(c *fiber.Ctx, projectFlockID uint, projectFlockKandangID *uint) (*dto.ExpeditionHPPDTO, error) { - if projectFlockID == 0 { - return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid project flock id") + if projectFlockKandangID != nil { + if err := m.EnsureProjectFlockKandangAccess(c, s.Repository.DB(), projectFlockID, *projectFlockKandangID); err != nil { + return nil, err + } + } else if err := m.EnsureProjectFlockAccess(c, s.Repository.DB(), projectFlockID); err != nil { + return nil, err } rows, err := s.Repository.GetExpeditionHPP(c.Context(), projectFlockID, projectFlockKandangID) @@ -686,8 +711,8 @@ func (s closingService) GetExpeditionHPP(c *fiber.Ctx, projectFlockID uint, proj } func (s closingService) GetClosingDataProduksi(c *fiber.Ctx, projectFlockID uint, kandangID *uint) (*dto.ClosingProductionReportDTO, error) { - if projectFlockID == 0 { - return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid project flock id") + if err := m.EnsureProjectFlockAccess(c, s.Repository.DB(), projectFlockID); err != nil { + return nil, err } projectFlockKandangIDs, err := s.getProjectFlockKandangIDs(c.Context(), projectFlockID, kandangID) diff --git a/internal/modules/inventory/adjustments/services/adjustment.service.go b/internal/modules/inventory/adjustments/services/adjustment.service.go index 71b985c2..4347fd6c 100644 --- a/internal/modules/inventory/adjustments/services/adjustment.service.go +++ b/internal/modules/inventory/adjustments/services/adjustment.service.go @@ -74,6 +74,10 @@ func (s *adjustmentService) withRelations(db *gorm.DB) *gorm.DB { } func (s *adjustmentService) GetOne(c *fiber.Ctx, id uint) (*entity.StockLog, error) { + if err := m.EnsureStockLogAccess(c, s.StockLogsRepository.DB(), id); err != nil { + return nil, err + } + stockLog, err := s.StockLogsRepository.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB { return s.withRelations(db).Preload("ProductWarehouse.Product.ProductCategory") }) @@ -101,6 +105,11 @@ 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}, @@ -279,6 +288,11 @@ func (s *adjustmentService) AdjustmentHistory(c *fiber.Ctx, query *validation.Qu if query.WarehouseID > 0 && !isWarehousesExist { return nil, 0, fiber.NewError(fiber.StatusNotFound, "Warehouse not found") } + if query.WarehouseID > 0 { + if err := m.EnsureWarehouseAccess(c, s.WarehouseRepo.DB(), uint(query.WarehouseID)); err != nil { + return nil, 0, err + } + } isProductsExist, err := s.ProductRepo.IdExists(c.Context(), uint(query.ProductID)) if err != nil { @@ -290,7 +304,19 @@ func (s *adjustmentService) AdjustmentHistory(c *fiber.Ctx, query *validation.Qu } stockLogs, total, err := s.StockLogsRepository.GetAll(c.Context(), offset, query.Limit, func(db *gorm.DB) *gorm.DB { - + scope, err := m.ResolveLocationScope(c, s.StockLogsRepository.DB()) + if err != nil { + return db.Where("1 = 0") + } + if scope.Restrict { + if len(scope.IDs) == 0 { + return db.Where("1 = 0") + } + 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") + db = m.ApplyScopeFilter(db, scope, "w.location_id") + } db = s.withRelations(db) db = db.Where("loggable_type = ?", string(utils.StockLogTypeAdjustment)) diff --git a/internal/modules/marketing/repositories/salesorder_delivery_product.repository.go b/internal/modules/marketing/repositories/salesorder_delivery_product.repository.go index f14988b1..4998f82b 100644 --- a/internal/modules/marketing/repositories/salesorder_delivery_product.repository.go +++ b/internal/modules/marketing/repositories/salesorder_delivery_product.repository.go @@ -140,6 +140,15 @@ func (r *MarketingDeliveryProductRepositoryImpl) GetAllWithFilters(ctx context.C Joins("JOIN marketings ON marketings.id = marketing_products.marketing_id"). Where("marketing_delivery_products.delivery_date IS NOT NULL") + if len(filters.AllowedLocationIDs) > 0 { + if !containsJoin(db, "product_warehouses") { + db = db.Joins("JOIN product_warehouses ON product_warehouses.id = marketing_products.product_warehouse_id") + } + db = db.Joins("JOIN project_flock_kandangs ON project_flock_kandangs.id = product_warehouses.project_flock_kandang_id"). + Joins("JOIN project_flocks ON project_flocks.id = project_flock_kandangs.project_flock_id"). + Where("project_flocks.location_id IN ?", filters.AllowedLocationIDs) + } + if filters.ProductId > 0 || filters.WarehouseId > 0 || filters.Search != "" || filters.MarketingType != "" { db = db.Joins("LEFT JOIN product_warehouses ON product_warehouses.id = marketing_products.product_warehouse_id") } diff --git a/internal/modules/marketing/services/deliveryorder.service.go b/internal/modules/marketing/services/deliveryorder.service.go index a1f4e1dd..9c197234 100644 --- a/internal/modules/marketing/services/deliveryorder.service.go +++ b/internal/modules/marketing/services/deliveryorder.service.go @@ -67,6 +67,10 @@ func (s deliveryOrdersService) withRelations(db *gorm.DB) *gorm.DB { } func (s deliveryOrdersService) getMarketingWithDeliveries(c *fiber.Ctx, marketingId uint) (*dto.MarketingDetailDTO, error) { + if err := m.EnsureMarketingAccess(c, s.MarketingRepo.DB(), marketingId); err != nil { + return nil, err + } + marketing, err := s.MarketingRepo.GetByID(c.Context(), marketingId, s.withRelations) if err != nil { return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch marketing") @@ -91,6 +95,11 @@ func (s deliveryOrdersService) GetAll(c *fiber.Ctx, params *validation.DeliveryO return nil, 0, err } + scope, err := m.ResolveLocationScope(c, s.MarketingRepo.DB()) + if err != nil { + return nil, 0, err + } + offset := (params.Page - 1) * params.Limit marketings, total, err := s.MarketingRepo.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB { @@ -102,6 +111,18 @@ func (s deliveryOrdersService) GetAll(c *fiber.Ctx, params *validation.DeliveryO Preload("Products.ProductWarehouse.Warehouse"). Preload("Products.DeliveryProduct") + if scope.Restrict { + if len(scope.IDs) == 0 { + return db.Where("1 = 0") + } + db = db. + Joins("JOIN marketing_products mp ON mp.marketing_id = marketings.id"). + Joins("JOIN product_warehouses pw ON pw.id = mp.product_warehouse_id"). + Joins("JOIN warehouses w ON w.id = pw.warehouse_id"). + Where("w.location_id IN ?", scope.IDs). + Distinct("marketings.*") + } + if params.MarketingId != 0 { return db.Where("id = ?", params.MarketingId) } @@ -130,6 +151,9 @@ func (s deliveryOrdersService) GetAll(c *fiber.Ctx, params *validation.DeliveryO } func (s deliveryOrdersService) GetOne(c *fiber.Ctx, id uint) (*dto.MarketingDetailDTO, error) { + if err := m.EnsureMarketingAccess(c, s.MarketingRepo.DB(), id); err != nil { + return nil, err + } marketing, err := s.MarketingRepo.GetByID(c.Context(), id, s.withRelations) if errors.Is(err, gorm.ErrRecordNotFound) { @@ -169,6 +193,10 @@ func (s *deliveryOrdersService) CreateOne(c *fiber.Ctx, req *validation.Delivery return nil, err } + if err := m.EnsureMarketingAccess(c, s.MarketingRepo.DB(), req.MarketingId); err != nil { + return nil, err + } + if err := commonSvc.EnsureRelations(c.Context(), commonSvc.RelationCheck{Name: "Marketing", ID: &req.MarketingId, Exists: s.MarketingRepo.IdExists}, ); err != nil { @@ -303,6 +331,10 @@ func (s deliveryOrdersService) UpdateOne(c *fiber.Ctx, req *validation.DeliveryO return nil, err } + if err := m.EnsureMarketingAccess(c, s.MarketingRepo.DB(), id); err != nil { + return nil, err + } + if err := commonSvc.EnsureRelations(c.Context(), commonSvc.RelationCheck{Name: "Marketing", ID: &id, Exists: s.MarketingRepo.IdExists}, ); err != nil { diff --git a/internal/modules/marketing/services/salesorder.service.go b/internal/modules/marketing/services/salesorder.service.go index d57b323e..e65fa9bb 100644 --- a/internal/modules/marketing/services/salesorder.service.go +++ b/internal/modules/marketing/services/salesorder.service.go @@ -70,6 +70,10 @@ func (s salesOrdersService) withRelations(db *gorm.DB) *gorm.DB { } func (s salesOrdersService) getOne(c *fiber.Ctx, id uint) (*entity.Marketing, error) { + if err := m.EnsureMarketingAccess(c, s.MarketingRepo.DB(), id); err != nil { + return nil, err + } + marketing, err := s.MarketingRepo.GetByID(c.Context(), id, s.withRelations) if errors.Is(err, gorm.ErrRecordNotFound) { return nil, fiber.NewError(fiber.StatusNotFound, "SalesOrders not found") @@ -108,6 +112,9 @@ func (s *salesOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) (*e } for _, item := range req.MarketingProducts { + if err := m.EnsureProductWarehouseAccess(c, s.MarketingRepo.DB(), item.ProductWarehouseId); err != nil { + return nil, err + } if err := commonSvc.EnsureRelations(c.Context(), commonSvc.RelationCheck{Name: "ProductWarehouse", ID: &item.ProductWarehouseId, Exists: s.ProductWarehouseRepo.IdExists}, ); err != nil { @@ -197,6 +204,10 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u return nil, err } + if err := m.EnsureMarketingAccess(c, s.MarketingRepo.DB(), id); err != nil { + return nil, err + } + actorID, err := m.ActorIDFromContext(c) if err != nil { return nil, err @@ -219,11 +230,14 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u } if len(req.MarketingProducts) > 0 { - for _, item := range req.MarketingProducts { - if err := commonSvc.EnsureRelations(c.Context(), - commonSvc.RelationCheck{Name: "ProductWarehouse", ID: &item.ProductWarehouseId, Exists: s.ProductWarehouseRepo.IdExists}, - ); err != nil { - return nil, err + for _, item := range req.MarketingProducts { + if err := m.EnsureProductWarehouseAccess(c, s.MarketingRepo.DB(), item.ProductWarehouseId); err != nil { + return nil, err + } + if err := commonSvc.EnsureRelations(c.Context(), + commonSvc.RelationCheck{Name: "ProductWarehouse", ID: &item.ProductWarehouseId, Exists: s.ProductWarehouseRepo.IdExists}, + ); err != nil { + return nil, err } } } @@ -391,6 +405,10 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u } func (s salesOrdersService) DeleteOne(c *fiber.Ctx, id uint) error { + if err := m.EnsureMarketingAccess(c, s.MarketingRepo.DB(), id); err != nil { + return err + } + marketing, err := s.MarketingRepo.GetByID(c.Context(), id, s.withRelations) if errors.Is(err, gorm.ErrRecordNotFound) { @@ -455,6 +473,12 @@ func (s salesOrdersService) Approval(c *fiber.Ctx, req *validation.Approve) ([]e return nil, err } + for _, id := range req.ApprovableIds { + if err := m.EnsureMarketingAccess(c, s.MarketingRepo.DB(), id); err != nil { + return nil, err + } + } + actorID, err := m.ActorIDFromContext(c) if err != nil { return nil, err diff --git a/internal/modules/master/kandangs/services/kandang.service.go b/internal/modules/master/kandangs/services/kandang.service.go index b70577d6..d5b297e5 100644 --- a/internal/modules/master/kandangs/services/kandang.service.go +++ b/internal/modules/master/kandangs/services/kandang.service.go @@ -103,6 +103,9 @@ func (s *kandangService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entit if err := s.Validate.Struct(req); err != nil { return nil, err } + if err := m.EnsureLocationAccess(c, s.Repository.DB(), req.LocationId); err != nil { + return nil, err + } if exists, err := s.Repository.NameExists(c.Context(), req.Name, nil); err != nil { s.Log.Errorf("Failed to check kandang name: %+v", err) @@ -177,6 +180,14 @@ func (s kandangService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) if err := s.Validate.Struct(req); err != nil { return nil, err } + if err := m.EnsureKandangAccess(c, s.Repository.DB(), id); err != nil { + return nil, err + } + if req.LocationId != nil { + if err := m.EnsureLocationAccess(c, s.Repository.DB(), *req.LocationId); err != nil { + return nil, err + } + } existing, err := s.Repository.GetByID(c.Context(), id, nil) if errors.Is(err, gorm.ErrRecordNotFound) { @@ -268,6 +279,10 @@ func (s kandangService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) } func (s kandangService) DeleteOne(c *fiber.Ctx, id uint) error { + if err := m.EnsureKandangAccess(c, s.Repository.DB(), id); err != nil { + return err + } + if err := s.Repository.DeleteOne(c.Context(), id); err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return fiber.NewError(fiber.StatusNotFound, "Kandang not found") diff --git a/internal/modules/master/warehouses/services/warehouse.service.go b/internal/modules/master/warehouses/services/warehouse.service.go index aaa5ca7e..29cf402f 100644 --- a/internal/modules/master/warehouses/services/warehouse.service.go +++ b/internal/modules/master/warehouses/services/warehouse.service.go @@ -128,6 +128,19 @@ func (s *warehouseService) CreateOne(c *fiber.Ctx, req *validation.Create) (*ent if err := validateWarehouseTypeRequirements(typ, &req.AreaId, req.LocationId, req.KandangId); err != nil { return nil, err } + if err := m.EnsureAreaAccess(c, s.Repository.DB(), req.AreaId); err != nil { + return nil, err + } + if req.LocationId != nil { + if err := m.EnsureLocationAccess(c, s.Repository.DB(), *req.LocationId); err != nil { + return nil, err + } + } + if req.KandangId != nil { + if err := m.EnsureKandangAccess(c, s.Repository.DB(), *req.KandangId); err != nil { + return nil, err + } + } //? Check relation area, location, and kandang if err := common.EnsureRelations(c.Context(), @@ -166,6 +179,21 @@ func (s warehouseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin if err := s.Validate.Struct(req); err != nil { return nil, err } + if req.AreaId != nil { + if err := m.EnsureAreaAccess(c, s.Repository.DB(), *req.AreaId); err != nil { + return nil, err + } + } + if req.LocationId != nil { + if err := m.EnsureLocationAccess(c, s.Repository.DB(), *req.LocationId); err != nil { + return nil, err + } + } + if req.KandangId != nil { + if err := m.EnsureKandangAccess(c, s.Repository.DB(), *req.KandangId); err != nil { + return nil, err + } + } existing, err := s.GetOne(c, id) if err != nil { @@ -243,6 +271,10 @@ func (s warehouseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin } func (s warehouseService) DeleteOne(c *fiber.Ctx, id uint) error { + if err := m.EnsureWarehouseAccess(c, s.Repository.DB(), id); err != nil { + return err + } + if err := s.Repository.DeleteOne(c.Context(), id); err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return fiber.NewError(fiber.StatusNotFound, "Warehouse not found") diff --git a/internal/modules/production/recordings/services/recording.service.go b/internal/modules/production/recordings/services/recording.service.go index 88ed4cf7..e52aaddc 100644 --- a/internal/modules/production/recordings/services/recording.service.go +++ b/internal/modules/production/recordings/services/recording.service.go @@ -101,6 +101,16 @@ func (s recordingService) GetAll(c *fiber.Ctx, params *validation.Query) ([]enti return nil, 0, err } + scope, err := m.ResolveLocationScope(c, s.Repository.DB()) + if err != nil { + return nil, 0, err + } + if params.ProjectFlockKandangId != 0 { + if err := m.EnsureProjectFlockKandangAccess(c, s.Repository.DB(), 0, params.ProjectFlockKandangId); err != nil { + return nil, 0, err + } + } + limit := params.Limit if limit == 0 { limit = 10 @@ -113,6 +123,15 @@ func (s recordingService) GetAll(c *fiber.Ctx, params *validation.Query) ([]enti recordings, total, err := s.Repository.GetAll(c.Context(), offset, limit, func(db *gorm.DB) *gorm.DB { db = s.Repository.WithRelations(db) + if scope.Restrict { + if len(scope.IDs) == 0 { + return db.Where("1 = 0") + } + db = db. + Joins("JOIN project_flock_kandangs pfk ON pfk.id = recordings.project_flock_kandangs_id"). + Joins("JOIN project_flocks pf ON pf.id = pfk.project_flock_id") + db = m.ApplyScopeFilter(db, scope, "pf.location_id") + } if params.ProjectFlockKandangId != 0 { db = db.Where("project_flock_kandangs_id = ?", params.ProjectFlockKandangId) } @@ -133,6 +152,10 @@ func (s recordingService) GetAll(c *fiber.Ctx, params *validation.Query) ([]enti } func (s recordingService) GetOne(c *fiber.Ctx, id uint) (*entity.Recording, error) { + if err := m.EnsureRecordingAccess(c, s.Repository.DB(), id); err != nil { + return nil, err + } + recording, err := s.Repository.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB { return s.Repository.WithRelations(db) }) @@ -156,6 +179,9 @@ func (s recordingService) GetNextDay(c *fiber.Ctx, projectFlockKandangId uint) ( if projectFlockKandangId == 0 { return 0, fiber.NewError(fiber.StatusBadRequest, "project_flock_kandang_id is required") } + if err := m.EnsureProjectFlockKandangAccess(c, s.Repository.DB(), 0, projectFlockKandangId); err != nil { + return 0, err + } db := s.Repository.DB().WithContext(c.Context()) next, err := s.Repository.GenerateNextDay(db, projectFlockKandangId) @@ -171,6 +197,9 @@ func (s *recordingService) CreateOne(c *fiber.Ctx, req *validation.Create) (*ent if err := s.Validate.Struct(req); err != nil { return nil, err } + if err := m.EnsureProjectFlockKandangAccess(c, s.Repository.DB(), 0, req.ProjectFlockKandangId); err != nil { + return nil, err + } ctx := c.Context() recordTime := time.Now().UTC() @@ -320,6 +349,9 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin if err := s.Validate.Struct(req); err != nil { return nil, err } + if err := m.EnsureRecordingAccess(c, s.Repository.DB(), id); err != nil { + return nil, err + } if req.Stocks == nil && req.Depletions == nil && req.Eggs == nil { return s.GetOne(c, id) @@ -535,6 +567,11 @@ func (s recordingService) Approval(c *fiber.Ctx, req *validation.Approve) ([]ent if err := s.Validate.Struct(req); err != nil { return nil, err } + for _, id := range req.ApprovableIds { + if err := m.EnsureRecordingAccess(c, s.Repository.DB(), id); err != nil { + return nil, err + } + } actionValue := strings.ToUpper(strings.TrimSpace(req.Action)) var action entity.ApprovalAction @@ -612,6 +649,9 @@ func (s recordingService) Approval(c *fiber.Ctx, req *validation.Approve) ([]ent } func (s recordingService) DeleteOne(c *fiber.Ctx, id uint) error { + if err := m.EnsureRecordingAccess(c, s.Repository.DB(), id); err != nil { + return err + } ctx := c.Context() return s.Repository.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error { 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 9732ad75..bcafd489 100644 --- a/internal/modules/production/transfer_layings/services/transfer_laying.service.go +++ b/internal/modules/production/transfer_layings/services/transfer_laying.service.go @@ -107,10 +107,25 @@ func (s transferLayingService) GetAll(c *fiber.Ctx, params *validation.Query) ([ return nil, 0, err } + scope, err := m.ResolveLocationScope(c, s.Repository.DB()) + if 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 scope.Restrict { + if len(scope.IDs) == 0 { + return db.Where("1 = 0") + } + db = db. + Joins("JOIN project_flocks pf_from ON pf_from.id = laying_transfers.from_project_flock_id"). + Joins("JOIN project_flocks pf_to ON pf_to.id = laying_transfers.to_project_flock_id"). + Where("(pf_from.location_id IN ? OR pf_to.location_id IN ?)", scope.IDs, scope.IDs). + Distinct("laying_transfers.*") + } db = db.Order("created_at DESC") return db }) @@ -134,6 +149,10 @@ func (s transferLayingService) GetAll(c *fiber.Ctx, params *validation.Query) ([ } func (s transferLayingService) GetOne(c *fiber.Ctx, id uint) (*entity.LayingTransfer, error) { + if err := m.EnsureLayingTransferAccess(c, s.Repository.DB(), id); err != nil { + return nil, err + } + 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") @@ -167,6 +186,12 @@ func (s *transferLayingService) CreateOne(c *fiber.Ctx, req *validation.Create) if err := s.Validate.Struct(req); err != nil { return nil, err } + if err := m.EnsureProjectFlockAccess(c, s.Repository.DB(), req.SourceProjectFlockId); err != nil { + return nil, err + } + if err := m.EnsureProjectFlockAccess(c, s.Repository.DB(), req.TargetProjectFlockId); err != nil { + return nil, err + } actorID, err := m.ActorIDFromContext(c) if err != nil { @@ -375,6 +400,15 @@ func (s *transferLayingService) UpdateOne(c *fiber.Ctx, req *validation.Update, if err := s.Validate.Struct(req); err != nil { return nil, err } + if err := m.EnsureLayingTransferAccess(c, s.Repository.DB(), id); err != nil { + return nil, err + } + if err := m.EnsureProjectFlockAccess(c, s.Repository.DB(), req.SourceProjectFlockId); err != nil { + return nil, err + } + if err := m.EnsureProjectFlockAccess(c, s.Repository.DB(), req.TargetProjectFlockId); err != nil { + return nil, err + } existingTransfer, err := s.Repository.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB { return db.Preload("Sources.ProductWarehouse").Preload("Targets") diff --git a/internal/modules/production/uniformities/repositories/uniformity.repository.go b/internal/modules/production/uniformities/repositories/uniformity.repository.go index 9641c650..8453bf84 100644 --- a/internal/modules/production/uniformities/repositories/uniformity.repository.go +++ b/internal/modules/production/uniformities/repositories/uniformity.repository.go @@ -12,7 +12,7 @@ import ( type UniformityRepository interface { repository.BaseRepository[entity.ProjectFlockKandangUniformity] - GetAllWithFilters(ctx context.Context, offset, limit int, params *validation.Query) ([]entity.ProjectFlockKandangUniformity, int64, error) + GetAllWithFilters(ctx context.Context, offset, limit int, params *validation.Query, modifiers ...func(*gorm.DB) *gorm.DB) ([]entity.ProjectFlockKandangUniformity, int64, error) WithDefaultRelations() func(*gorm.DB) *gorm.DB DeleteByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) error } @@ -27,9 +27,15 @@ func NewUniformityRepository(db *gorm.DB) UniformityRepository { } } -func (r *UniformityRepositoryImpl) GetAllWithFilters(ctx context.Context, offset, limit int, params *validation.Query) ([]entity.ProjectFlockKandangUniformity, int64, error) { +func (r *UniformityRepositoryImpl) GetAllWithFilters(ctx context.Context, offset, limit int, params *validation.Query, modifiers ...func(*gorm.DB) *gorm.DB) ([]entity.ProjectFlockKandangUniformity, int64, error) { return r.GetAll(ctx, offset, limit, func(db *gorm.DB) *gorm.DB { - return r.applyQueryFilters(r.WithDefaultRelations()(db), params) + db = r.applyQueryFilters(r.WithDefaultRelations()(db), params) + for _, modifier := range modifiers { + if modifier != nil { + db = modifier(db) + } + } + return db }) } diff --git a/internal/modules/production/uniformities/services/uniformity.service.go b/internal/modules/production/uniformities/services/uniformity.service.go index 92db84a3..51ea29a7 100644 --- a/internal/modules/production/uniformities/services/uniformity.service.go +++ b/internal/modules/production/uniformities/services/uniformity.service.go @@ -87,8 +87,24 @@ func (s uniformityService) GetAll(c *fiber.Ctx, params *validation.Query) ([]ent return nil, 0, err } + scope, err := m.ResolveLocationScope(c, s.Repository.DB()) + if err != nil { + return nil, 0, err + } + offset := (params.Page - 1) * params.Limit - uniformitys, total, err := s.Repository.GetAllWithFilters(c.Context(), offset, params.Limit, params) + uniformitys, total, err := s.Repository.GetAllWithFilters(c.Context(), offset, params.Limit, params, func(db *gorm.DB) *gorm.DB { + if scope.Restrict { + if len(scope.IDs) == 0 { + return db.Where("1 = 0") + } + db = db. + Joins("JOIN project_flock_kandangs pfk ON pfk.id = project_flock_kandang_uniformities.project_flock_kandang_id"). + Joins("JOIN project_flocks pf ON pf.id = pfk.project_flock_id") + db = m.ApplyScopeFilter(db, scope, "pf.location_id") + } + return db + }) if err != nil { s.Log.Errorf("Failed to get uniformitys: %+v", err) @@ -101,6 +117,10 @@ func (s uniformityService) GetAll(c *fiber.Ctx, params *validation.Query) ([]ent } func (s uniformityService) GetOne(c *fiber.Ctx, id uint) (*entity.ProjectFlockKandangUniformity, error) { + if err := m.EnsureUniformityAccess(c, s.Repository.DB(), id); err != nil { + return nil, err + } + uniformity, err := s.Repository.GetByID(c.Context(), id, s.Repository.WithDefaultRelations()) if errors.Is(err, gorm.ErrRecordNotFound) { return nil, fiber.NewError(fiber.StatusNotFound, "Uniformity not found") @@ -326,6 +346,9 @@ func (s *uniformityService) CreateOne(c *fiber.Ctx, req *validation.Create, file if err := s.Validate.Struct(req); err != nil { return nil, err } + if err := m.EnsureProjectFlockKandangAccess(c, s.Repository.DB(), 0, req.ProjectFlockKandangId); err != nil { + return nil, err + } if s.ProjectFlockKandangRepo == nil { return nil, fiber.NewError(fiber.StatusInternalServerError, "Project flock kandang repository not available") } @@ -439,6 +462,9 @@ func (s uniformityService) UpdateOne(c *fiber.Ctx, req *validation.Update, id ui if err := s.Validate.Struct(req); err != nil { return nil, err } + if err := m.EnsureUniformityAccess(c, s.Repository.DB(), id); err != nil { + return nil, err + } updateBody := make(map[string]any) var uniformDate *time.Time @@ -627,6 +653,10 @@ func (s *uniformityService) ensureUniqueUniformity(ctx context.Context, id uint, } func (s uniformityService) DeleteOne(c *fiber.Ctx, id uint) error { + if err := m.EnsureUniformityAccess(c, s.Repository.DB(), id); err != nil { + return err + } + if err := s.Repository.DeleteOne(c.Context(), id); err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return fiber.NewError(fiber.StatusNotFound, "Uniformity not found") @@ -657,6 +687,11 @@ func (s uniformityService) Approval(c *fiber.Ctx, req *validation.Approve) ([]en if len(ids) == 0 { return nil, fiber.NewError(fiber.StatusBadRequest, "approvable_ids must contain at least one id") } + for _, id := range ids { + if err := m.EnsureUniformityAccess(c, s.Repository.DB(), id); err != nil { + return nil, err + } + } step := utils.UniformityStepPengajuan if action == entity.ApprovalActionApproved { diff --git a/internal/modules/repports/controllers/repport.controller.go b/internal/modules/repports/controllers/repport.controller.go index 4ed7a855..3d0468d4 100644 --- a/internal/modules/repports/controllers/repport.controller.go +++ b/internal/modules/repports/controllers/repport.controller.go @@ -106,6 +106,18 @@ func (c *RepportController) GetMarketing(ctx *fiber.Ctx) error { SortOrder: ctx.Query("sort_order", ""), } + locationScope, err := m.ResolveLocationScope(ctx, c.RepportService.DB()) + if err != nil { + return err + } + if locationScope.Restrict { + allowed := toInt64Slice(locationScope.IDs) + if len(allowed) == 0 { + allowed = []int64{-1} + } + query.AllowedLocationIDs = allowed + } + if query.Page < 1 || query.Limit < 1 { return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0") } @@ -283,6 +295,10 @@ func (c *RepportController) GetProductionResult(ctx *fiber.Ctx) error { ProjectFlockKandangID: uint(projectFlockKandangID), } + if err := m.EnsureProjectFlockKandangAccess(ctx, c.RepportService.DB(), 0, query.ProjectFlockKandangID); err != nil { + return err + } + 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/repports/validations/repport.validation.go b/internal/modules/repports/validations/repport.validation.go index 4c1d7356..c5572edb 100644 --- a/internal/modules/repports/validations/repport.validation.go +++ b/internal/modules/repports/validations/repport.validation.go @@ -31,6 +31,7 @@ type MarketingQuery struct { EndDate string `query:"end_date" validate:"omitempty,datetime=2006-01-02"` SortBy string `query:"sort_by" validate:"omitempty,oneof=so_date realization_date customer warehouse product sales_person vehicle_number sales_amount hpp_amount qty average_weight total_weight sales_price hpp_price aging_days"` SortOrder string `query:"sort_order" validate:"omitempty,oneof=asc desc"` + AllowedLocationIDs []int64 `query:"-"` } type PurchaseSupplierQuery struct { diff --git a/internal/modules/sso/verifier/profile.go b/internal/modules/sso/verifier/profile.go index 52b7ef5a..e3cd40ca 100644 --- a/internal/modules/sso/verifier/profile.go +++ b/internal/modules/sso/verifier/profile.go @@ -39,6 +39,10 @@ type UserProfile struct { UserID uint Roles []Role Permissions []Permission + AreaIDs []uint + LocationIDs []uint + AllArea bool + AllLocation bool } // Role describes a role assignment from the SSO profile response. @@ -49,10 +53,6 @@ type Role struct { ClientID uint ClientAlias string ClientName string - AllArea bool - AllLocation bool - AreaIDs []uint - LocationIDs []uint Permissions []Permission RawReference json.RawMessage `json:"-"` } @@ -149,6 +149,10 @@ func fetchProfileFromSSO(ctx context.Context, token string) (*UserProfile, error } roles := envelope.getRoles() + areaIDs := envelope.getAreaIDs() + locationIDs := envelope.getLocationIDs() + allArea := envelope.getAllArea() + allLocation := envelope.getAllLocation() profile := &UserProfile{} // Attempt to infer user id if provided. @@ -166,10 +170,6 @@ func fetchProfileFromSSO(ctx context.Context, token string) (*UserProfile, error ClientAlias: strings.TrimSpace(r.Client.Alias), ClientName: strings.TrimSpace(r.Client.Name), ClientID: uint(r.Client.ID), - AllArea: r.AllArea, - AllLocation: r.AllLocation, - AreaIDs: r.AreaIDs, - LocationIDs: r.LocationIDs, } rolePerms := make([]Permission, 0, len(r.Permissions)) for _, p := range r.Permissions { @@ -191,6 +191,10 @@ func fetchProfileFromSSO(ctx context.Context, token string) (*UserProfile, error } profile.Roles = convertedRoles profile.Permissions = perms + profile.AreaIDs = areaIDs + profile.LocationIDs = locationIDs + profile.AllArea = allArea + profile.AllLocation = allLocation return profile, nil } @@ -268,9 +272,17 @@ func canonicalPermissionName(name string) string { // userInfoEnvelope handles the varying shapes returned by the SSO userinfo endpoint. type userInfoEnvelope struct { Roles []userInfoRole `json:"roles"` + AreaIDs []uint `json:"area_ids"` + LocationIDs []uint `json:"location_ids"` + AllArea bool `json:"all_area"` + AllLocation bool `json:"all_location"` Data *struct { ID int64 `json:"id"` Roles []userInfoRole `json:"roles"` + AreaIDs []uint `json:"area_ids"` + LocationIDs []uint `json:"location_ids"` + AllArea bool `json:"all_area"` + AllLocation bool `json:"all_location"` } `json:"data"` User *struct { ID int64 `json:"id"` @@ -292,14 +304,50 @@ func (e *userInfoEnvelope) getRoles() []userInfoRole { return nil } +func (e *userInfoEnvelope) getAreaIDs() []uint { + if len(e.AreaIDs) > 0 { + return e.AreaIDs + } + if e.Data != nil && len(e.Data.AreaIDs) > 0 { + return e.Data.AreaIDs + } + return nil +} + +func (e *userInfoEnvelope) getLocationIDs() []uint { + if len(e.LocationIDs) > 0 { + return e.LocationIDs + } + if e.Data != nil && len(e.Data.LocationIDs) > 0 { + return e.Data.LocationIDs + } + return nil +} + +func (e *userInfoEnvelope) getAllArea() bool { + if e.AllArea { + return true + } + if e.Data != nil && e.Data.AllArea { + return true + } + return false +} + +func (e *userInfoEnvelope) getAllLocation() bool { + if e.AllLocation { + return true + } + if e.Data != nil && e.Data.AllLocation { + return true + } + return false +} + type userInfoRole struct { ID int64 `json:"id"` Key string `json:"key"` Name string `json:"name"` - AllArea bool `json:"all_area"` - AllLocation bool `json:"all_location"` - AreaIDs []uint `json:"area_ids"` - LocationIDs []uint `json:"location_ids"` Client userInfoClient `json:"client"` Permissions []userInfoPermRaw `json:"permissions"` } From 3a457ceee53feedc0302d075c71b388bd1e30632 Mon Sep 17 00:00:00 2001 From: ragilap Date: Thu, 22 Jan 2026 09:27:11 +0700 Subject: [PATCH 5/7] [FIX/BE-US] adjustment sso-location --- internal/middleware/role_scope.go | 26 ++++++++++++-- .../expenses/services/expense.service.go | 20 +++++------ .../services/adjustment.service.go | 21 +++++------ .../master/areas/services/area.service.go | 30 ++++++---------- .../kandangs/services/kandang.service.go | 20 +++++------ .../locations/services/location.service.go | 30 ++++++---------- .../warehouses/services/warehouse.service.go | 20 +++++------ .../controllers/projectflock.controller.go | 19 +++++++--- .../dto/projectflock_kandang.dto.go | 36 ++++++++++--------- .../services/projectflock.service.go | 26 ++++++++++++++ .../recordings/services/recording.service.go | 22 +++++------- .../services/uniformity.service.go | 21 +++++------ 12 files changed, 158 insertions(+), 133 deletions(-) diff --git a/internal/middleware/role_scope.go b/internal/middleware/role_scope.go index 155d2e6a..c46a7fab 100644 --- a/internal/middleware/role_scope.go +++ b/internal/middleware/role_scope.go @@ -178,8 +178,6 @@ func uniqueUint(ids []uint) []uint { return result } - - func ApplyScopeFilter(db *gorm.DB, scope ScopeFilter, column string) *gorm.DB { if db == nil || !scope.Restrict { return db @@ -190,6 +188,30 @@ func ApplyScopeFilter(db *gorm.DB, scope ScopeFilter, column string) *gorm.DB { return db.Where(column+" IN ?", scope.IDs) } +func ApplyLocationScope(c *fiber.Ctx, db *gorm.DB, column string) (*gorm.DB, error) { + scopeDB := db + if db != nil { + scopeDB = db.Session(&gorm.Session{NewDB: true}) + } + scope, err := ResolveLocationScope(c, scopeDB) + if err != nil { + return db, err + } + return ApplyScopeFilter(db, scope, column), nil +} + +func ApplyAreaScope(c *fiber.Ctx, db *gorm.DB, column string) (*gorm.DB, error) { + scopeDB := db + if db != nil { + scopeDB = db.Session(&gorm.Session{NewDB: true}) + } + scope, err := ResolveAreaScope(c, scopeDB) + if err != nil { + return db, err + } + return ApplyScopeFilter(db, scope, column), nil +} + func EnsureWarehouseAccess(c *fiber.Ctx, db *gorm.DB, warehouseID uint) error { if warehouseID == 0 { return fiber.NewError(fiber.StatusBadRequest, "Invalid warehouse id") diff --git a/internal/modules/expenses/services/expense.service.go b/internal/modules/expenses/services/expense.service.go index 27b4a07f..61a7db50 100644 --- a/internal/modules/expenses/services/expense.service.go +++ b/internal/modules/expenses/services/expense.service.go @@ -87,22 +87,22 @@ func (s expenseService) GetAll(c *fiber.Ctx, params *validation.Query) ([]expens return nil, 0, err } - scope, err := middleware.ResolveLocationScope(c, s.Repository.DB()) - if err != nil { - return nil, 0, err - } + var scopeErr error offset := (params.Page - 1) * params.Limit expenses, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB { db = s.withRelations(db) - db = middleware.ApplyScopeFilter(db, scope, "location_id") + db, scopeErr = middleware.ApplyLocationScope(c, db, "expenses.location_id") if params.Search != "" { return db.Where("category ILIKE ?", "%"+params.Search+"%") } return db.Order("created_at DESC").Order("updated_at DESC") }) + if scopeErr != nil { + return nil, 0, scopeErr + } if err != nil { return nil, 0, err @@ -123,16 +123,16 @@ func (s expenseService) GetAll(c *fiber.Ctx, params *validation.Query) ([]expens } func (s expenseService) GetOne(c *fiber.Ctx, id uint) (*expenseDto.ExpenseDetailDTO, error) { - scope, err := middleware.ResolveLocationScope(c, s.Repository.DB()) - if err != nil { - return nil, err - } + var scopeErr error expense, err := s.Repository.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB { db = s.withRelations(db) - db = middleware.ApplyScopeFilter(db, scope, "location_id") + db, scopeErr = middleware.ApplyLocationScope(c, db, "expenses.location_id") return db }) + if scopeErr != nil { + return nil, scopeErr + } if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { diff --git a/internal/modules/inventory/adjustments/services/adjustment.service.go b/internal/modules/inventory/adjustments/services/adjustment.service.go index 4347fd6c..7bb9fb6a 100644 --- a/internal/modules/inventory/adjustments/services/adjustment.service.go +++ b/internal/modules/inventory/adjustments/services/adjustment.service.go @@ -303,20 +303,12 @@ func (s *adjustmentService) AdjustmentHistory(c *fiber.Ctx, query *validation.Qu return nil, 0, fiber.NewError(fiber.StatusNotFound, "Product not found") } + var scopeErr error stockLogs, total, err := s.StockLogsRepository.GetAll(c.Context(), offset, query.Limit, func(db *gorm.DB) *gorm.DB { - scope, err := m.ResolveLocationScope(c, s.StockLogsRepository.DB()) - if err != nil { - return db.Where("1 = 0") - } - if scope.Restrict { - if len(scope.IDs) == 0 { - return db.Where("1 = 0") - } - 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") - db = m.ApplyScopeFilter(db, scope, "w.location_id") - } + 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") + db, scopeErr = m.ApplyLocationScope(c, db, "w.location_id") db = s.withRelations(db) db = db.Where("loggable_type = ?", string(utils.StockLogTypeAdjustment)) @@ -329,6 +321,9 @@ func (s *adjustmentService) AdjustmentHistory(c *fiber.Ctx, query *validation.Qu return db.Order("created_at DESC") }) + if scopeErr != nil { + return nil, 0, scopeErr + } if err != nil { s.Log.Errorf("Failed to get adjustments: %+v", err) return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to get adjustment history") diff --git a/internal/modules/master/areas/services/area.service.go b/internal/modules/master/areas/services/area.service.go index 1ec30d8d..6110aaef 100644 --- a/internal/modules/master/areas/services/area.service.go +++ b/internal/modules/master/areas/services/area.service.go @@ -47,27 +47,22 @@ func (s areaService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.Ar return nil, 0, err } - scope, err := m.ResolveAreaScope(c, s.Repository.DB()) - if err != nil { - return nil, 0, err - } + var scopeErr error offset := (params.Page - 1) * params.Limit areas, total, err := s.Repository.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.Where("id IN ?", scope.IDs) - } + db, scopeErr = m.ApplyAreaScope(c, db, "id") if params.Search != "" { return db.Where("name ILIKE ?", "%"+params.Search+"%") } return db.Order("created_at DESC").Order("updated_at DESC") }) + if scopeErr != nil { + return nil, 0, scopeErr + } if err != nil { s.Log.Errorf("Failed to get areas: %+v", err) return nil, 0, err @@ -76,21 +71,16 @@ func (s areaService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.Ar } func (s areaService) GetOne(c *fiber.Ctx, id uint) (*entity.Area, error) { - scope, err := m.ResolveAreaScope(c, s.Repository.DB()) - if err != nil { - return nil, err - } + var scopeErr error area, err := s.Repository.GetByID(c.Context(), id, 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.Where("id IN ?", scope.IDs) - } + db, scopeErr = m.ApplyAreaScope(c, db, "id") return db }) + if scopeErr != nil { + return nil, scopeErr + } if errors.Is(err, gorm.ErrRecordNotFound) { return nil, fiber.NewError(fiber.StatusNotFound, "Area not found") } diff --git a/internal/modules/master/kandangs/services/kandang.service.go b/internal/modules/master/kandangs/services/kandang.service.go index d5b297e5..159bc410 100644 --- a/internal/modules/master/kandangs/services/kandang.service.go +++ b/internal/modules/master/kandangs/services/kandang.service.go @@ -49,16 +49,13 @@ func (s kandangService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity return nil, 0, err } - scope, err := m.ResolveLocationScope(c, s.Repository.DB()) - if err != nil { - return nil, 0, err - } + var scopeErr error offset := (params.Page - 1) * params.Limit kandangs, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB { db = s.withRelations(db) - db = m.ApplyScopeFilter(db, scope, "location_id") + db, scopeErr = m.ApplyLocationScope(c, db, "kandangs.location_id") if params.Search != "" { return db.Where("name ILIKE ?", "%"+params.Search+"%") } @@ -71,6 +68,9 @@ func (s kandangService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity return db.Order("created_at DESC").Order("updated_at DESC") }) + if scopeErr != nil { + return nil, 0, scopeErr + } if err != nil { s.Log.Errorf("Failed to get kandangs: %+v", err) return nil, 0, err @@ -79,16 +79,16 @@ func (s kandangService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity } func (s kandangService) GetOne(c *fiber.Ctx, id uint) (*entity.Kandang, error) { - scope, err := m.ResolveLocationScope(c, s.Repository.DB()) - if err != nil { - return nil, err - } + var scopeErr error kandang, err := s.Repository.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB { db = s.withRelations(db) - db = m.ApplyScopeFilter(db, scope, "location_id") + db, scopeErr = m.ApplyLocationScope(c, db, "kandangs.location_id") return db }) + if scopeErr != nil { + return nil, scopeErr + } if errors.Is(err, gorm.ErrRecordNotFound) { return nil, fiber.NewError(fiber.StatusNotFound, "Kandang not found") } diff --git a/internal/modules/master/locations/services/location.service.go b/internal/modules/master/locations/services/location.service.go index 633d7419..03f6cf45 100644 --- a/internal/modules/master/locations/services/location.service.go +++ b/internal/modules/master/locations/services/location.service.go @@ -47,21 +47,13 @@ func (s locationService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entit return nil, 0, err } - scope, err := m.ResolveLocationScope(c, s.Repository.DB()) - if err != nil { - return nil, 0, err - } + var scopeErr error offset := (params.Page - 1) * params.Limit locations, total, err := s.Repository.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.Where("id IN ?", scope.IDs) - } + db, scopeErr = m.ApplyLocationScope(c, db, "locations.id") if params.Search != "" { db = db.Where("name ILIKE ?", "%"+params.Search+"%") } @@ -71,6 +63,9 @@ func (s locationService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entit return db.Order("created_at DESC").Order("updated_at DESC") }) + if scopeErr != nil { + return nil, 0, scopeErr + } if err != nil { s.Log.Errorf("Failed to get locations: %+v", err) return nil, 0, err @@ -79,21 +74,16 @@ func (s locationService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entit } func (s locationService) GetOne(c *fiber.Ctx, id uint) (*entity.Location, error) { - scope, err := m.ResolveLocationScope(c, s.Repository.DB()) - if err != nil { - return nil, err - } + var scopeErr error location, err := s.Repository.GetByID(c.Context(), id, 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.Where("id IN ?", scope.IDs) - } + db, scopeErr = m.ApplyLocationScope(c, db, "locations.id") return db }) + if scopeErr != nil { + return nil, scopeErr + } if errors.Is(err, gorm.ErrRecordNotFound) { return nil, fiber.NewError(fiber.StatusNotFound, "Location not found") } diff --git a/internal/modules/master/warehouses/services/warehouse.service.go b/internal/modules/master/warehouses/services/warehouse.service.go index 29cf402f..89e4467d 100644 --- a/internal/modules/master/warehouses/services/warehouse.service.go +++ b/internal/modules/master/warehouses/services/warehouse.service.go @@ -48,16 +48,13 @@ func (s warehouseService) GetAll(c *fiber.Ctx, params *validation.Query) ([]enti return nil, 0, err } - scope, err := m.ResolveAreaScope(c, s.Repository.DB()) - if err != nil { - return nil, 0, err - } + var scopeErr error offset := (params.Page - 1) * params.Limit warehouses, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB { db = s.withRelations(db) - db = m.ApplyScopeFilter(db, scope, "area_id") + db, scopeErr = m.ApplyAreaScope(c, db, "warehouses.area_id") if params.Search != "" { db = db.Where("warehouses.name ILIKE ?", "%"+params.Search+"%") } @@ -84,6 +81,9 @@ func (s warehouseService) GetAll(c *fiber.Ctx, params *validation.Query) ([]enti return db.Order("created_at DESC").Order("updated_at DESC") }) + if scopeErr != nil { + return nil, 0, scopeErr + } if err != nil { s.Log.Errorf("Failed to get warehouses: %+v", err) return nil, 0, err @@ -92,16 +92,16 @@ func (s warehouseService) GetAll(c *fiber.Ctx, params *validation.Query) ([]enti } func (s warehouseService) GetOne(c *fiber.Ctx, id uint) (*entity.Warehouse, error) { - scope, err := m.ResolveAreaScope(c, s.Repository.DB()) - if err != nil { - return nil, err - } + var scopeErr error warehouse, err := s.Repository.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB { db = s.withRelations(db) - db = m.ApplyScopeFilter(db, scope, "area_id") + db, scopeErr = m.ApplyAreaScope(c, db, "warehouses.area_id") return db }) + if scopeErr != nil { + return nil, scopeErr + } if errors.Is(err, gorm.ErrRecordNotFound) { return nil, fiber.NewError(fiber.StatusNotFound, "Warehouse not found") } diff --git a/internal/modules/production/project_flocks/controllers/projectflock.controller.go b/internal/modules/production/project_flocks/controllers/projectflock.controller.go index 4315b948..e82d3af5 100644 --- a/internal/modules/production/project_flocks/controllers/projectflock.controller.go +++ b/internal/modules/production/project_flocks/controllers/projectflock.controller.go @@ -7,6 +7,7 @@ import ( "strconv" "strings" + warehouseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/dto" "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/dto" service "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/services" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/validations" @@ -278,14 +279,22 @@ func (u *ProjectflockController) LookupProjectFlockKandang(c *fiber.Ctx) error { if err != nil { return err } + _ = availableStock dtoResult := dto.ToProjectFlockKandangDTO(*result) - dtoResult.AvailableQuantity = float64(availableStock) + if population, err := u.ProjectflockService.GetProjectFlockKandangPopulation(c, result.Id); err != nil { + return err + } else { + dtoResult.AvailableQuantity = population + } + if warehouse, werr := u.ProjectflockService.GetWarehouseByKandangID(c, result.KandangId); werr != nil { + return werr + } else if warehouse != nil { + mapped := warehouseDTO.ToWarehouseRelationDTO(*warehouse) + dtoResult.Warehouse = &mapped + } if withPopulation { - population, err := u.ProjectflockService.GetProjectFlockKandangPopulation(c, result.Id) - if err != nil { - return err - } + population := dtoResult.AvailableQuantity dtoResult.Population = &population } diff --git a/internal/modules/production/project_flocks/dto/projectflock_kandang.dto.go b/internal/modules/production/project_flocks/dto/projectflock_kandang.dto.go index 8dedaf15..c18f3f65 100644 --- a/internal/modules/production/project_flocks/dto/projectflock_kandang.dto.go +++ b/internal/modules/production/project_flocks/dto/projectflock_kandang.dto.go @@ -7,6 +7,7 @@ import ( kandangDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/dto" locationDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/locations/dto" productionStandardDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/production-standards/dto" + warehouseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/dto" userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto" ) @@ -17,24 +18,26 @@ type KandangWithPivotDTO struct { type ProjectFlockWithPivotDTO struct { ProjectFlockRelationDTO - Area *areaDTO.AreaRelationDTO `json:"area,omitempty"` - Category string `json:"category"` - Fcr *fcrDTO.FcrRelationDTO `json:"fcr,omitempty"` - ProductionStandard *productionStandardDTO.ProductionStandardRelationDTO `json:"production_standard,omitempty"` - Location *locationDTO.LocationRelationDTO `json:"location,omitempty"` - Kandangs []KandangWithPivotDTO `json:"kandangs,omitempty"` - CreatedUser *userDTO.UserRelationDTO `json:"created_user,omitempty"` + Area *areaDTO.AreaRelationDTO `json:"area,omitempty"` + Category string `json:"category"` + Fcr *fcrDTO.FcrRelationDTO `json:"fcr,omitempty"` + ProductionStandard *productionStandardDTO.ProductionStandardRelationDTO `json:"production_standard,omitempty"` + ProductionStandardId uint `json:"production_standard_id"` + Location *locationDTO.LocationRelationDTO `json:"location,omitempty"` + Kandangs []KandangWithPivotDTO `json:"kandangs,omitempty"` + CreatedUser *userDTO.UserRelationDTO `json:"created_user,omitempty"` } type ProjectFlockKandangDTO struct { - Id uint `json:"id"` - ProjectFlockKandangId uint `json:"project_flock_kandang_id"` - ProjectFlockId uint `json:"project_flock_id"` - KandangId uint `json:"kandang_id"` - Kandang *kandangDTO.KandangRelationDTO `json:"kandang,omitempty"` - ProjectFlock *ProjectFlockWithPivotDTO `json:"project_flock,omitempty"` - AvailableQuantity float64 `json:"available_quantity"` - Population *float64 `json:"population,omitempty"` + Id uint `json:"id"` + ProjectFlockKandangId uint `json:"project_flock_kandang_id"` + ProjectFlockId uint `json:"project_flock_id"` + KandangId uint `json:"kandang_id"` + Kandang *kandangDTO.KandangRelationDTO `json:"kandang,omitempty"` + Warehouse *warehouseDTO.WarehouseRelationDTO `json:"warehouse,omitempty"` + ProjectFlock *ProjectFlockWithPivotDTO `json:"project_flock,omitempty"` + AvailableQuantity float64 `json:"available_quantity"` + Population *float64 `json:"population,omitempty"` } func ToProjectFlockKandangDTO(e entity.ProjectFlockKandang) ProjectFlockKandangDTO { @@ -53,7 +56,8 @@ func ToProjectFlockKandangDTO(e entity.ProjectFlockKandang) ProjectFlockKandangD Period: e.Period, FlockName: e.ProjectFlock.FlockName, }, - Category: e.ProjectFlock.Category, + Category: e.ProjectFlock.Category, + ProductionStandardId: e.ProjectFlock.ProductionStandardId, } if e.ProjectFlock.Area.Id != 0 { diff --git a/internal/modules/production/project_flocks/services/projectflock.service.go b/internal/modules/production/project_flocks/services/projectflock.service.go index 5bccfbf9..166c7e0b 100644 --- a/internal/modules/production/project_flocks/services/projectflock.service.go +++ b/internal/modules/production/project_flocks/services/projectflock.service.go @@ -38,6 +38,7 @@ type ProjectflockService interface { GetOne(ctx *fiber.Ctx, id uint) (*entity.ProjectFlock, *flockDTO.FlockRelationDTO, error) CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.ProjectFlock, error) GetAvailableDocQuantity(ctx *fiber.Ctx, kandangID uint) (float64, error) + GetWarehouseByKandangID(ctx *fiber.Ctx, kandangID uint) (*entity.Warehouse, error) DeleteOne(ctx *fiber.Ctx, id uint) error GetProjectFlockKandangByProjectAndKandang(ctx *fiber.Ctx, projectFlockID uint, kandangID uint) (*entity.ProjectFlockKandang, float64, error) GetProjectFlockKandangPopulation(ctx *fiber.Ctx, projectFlockKandangID uint) (float64, error) @@ -532,6 +533,31 @@ func (s projectflockService) GetAvailableDocQuantity(ctx *fiber.Ctx, kandangID u return total, nil } +func (s projectflockService) GetWarehouseByKandangID(ctx *fiber.Ctx, kandangID uint) (*entity.Warehouse, error) { + if kandangID == 0 || s.WarehouseRepo == nil { + return nil, nil + } + + var warehouse entity.Warehouse + err := s.WarehouseRepo.DB().WithContext(ctx.Context()). + Preload("Area"). + Preload("Location"). + Preload("Kandang"). + Where("kandang_id = ?", kandangID). + Where("deleted_at IS NULL"). + Order("id DESC"). + First(&warehouse).Error + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, nil + } + if err != nil { + s.Log.Errorf("Failed to fetch warehouse for kandang %d: %+v", kandangID, err) + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch warehouse") + } + + return &warehouse, nil +} + func (s projectflockService) GetProjectPeriods(c *fiber.Ctx, projectIDs []uint) (map[uint]int, error) { if len(projectIDs) == 0 { return map[uint]int{}, nil diff --git a/internal/modules/production/recordings/services/recording.service.go b/internal/modules/production/recordings/services/recording.service.go index e52aaddc..378e72df 100644 --- a/internal/modules/production/recordings/services/recording.service.go +++ b/internal/modules/production/recordings/services/recording.service.go @@ -101,10 +101,7 @@ func (s recordingService) GetAll(c *fiber.Ctx, params *validation.Query) ([]enti return nil, 0, err } - scope, err := m.ResolveLocationScope(c, s.Repository.DB()) - if err != nil { - return nil, 0, err - } + var scopeErr error if params.ProjectFlockKandangId != 0 { if err := m.EnsureProjectFlockKandangAccess(c, s.Repository.DB(), 0, params.ProjectFlockKandangId); err != nil { return nil, 0, err @@ -123,21 +120,19 @@ func (s recordingService) GetAll(c *fiber.Ctx, params *validation.Query) ([]enti recordings, total, err := s.Repository.GetAll(c.Context(), offset, limit, func(db *gorm.DB) *gorm.DB { db = s.Repository.WithRelations(db) - if scope.Restrict { - if len(scope.IDs) == 0 { - return db.Where("1 = 0") - } - db = db. - Joins("JOIN project_flock_kandangs pfk ON pfk.id = recordings.project_flock_kandangs_id"). - Joins("JOIN project_flocks pf ON pf.id = pfk.project_flock_id") - db = m.ApplyScopeFilter(db, scope, "pf.location_id") - } + db = db. + Joins("JOIN project_flock_kandangs pfk ON pfk.id = recordings.project_flock_kandangs_id"). + Joins("JOIN project_flocks pf ON pf.id = pfk.project_flock_id") + db, scopeErr = m.ApplyLocationScope(c, db, "pf.location_id") if params.ProjectFlockKandangId != 0 { db = db.Where("project_flock_kandangs_id = ?", params.ProjectFlockKandangId) } return db.Order("record_datetime DESC").Order("created_at DESC") }) + if scopeErr != nil { + return nil, 0, scopeErr + } if err != nil { s.Log.Errorf("Failed to get recordings: %+v", err) return nil, 0, err @@ -962,7 +957,6 @@ type eggTotals struct { Weight float64 } - func stocksMatch(existing []entity.RecordingStock, incoming []validation.Stock) bool { hasPending := false for _, item := range incoming { diff --git a/internal/modules/production/uniformities/services/uniformity.service.go b/internal/modules/production/uniformities/services/uniformity.service.go index 51ea29a7..f10ae25c 100644 --- a/internal/modules/production/uniformities/services/uniformity.service.go +++ b/internal/modules/production/uniformities/services/uniformity.service.go @@ -87,25 +87,20 @@ func (s uniformityService) GetAll(c *fiber.Ctx, params *validation.Query) ([]ent return nil, 0, err } - scope, err := m.ResolveLocationScope(c, s.Repository.DB()) - if err != nil { - return nil, 0, err - } + var scopeErr error offset := (params.Page - 1) * params.Limit uniformitys, total, err := s.Repository.GetAllWithFilters(c.Context(), offset, params.Limit, params, func(db *gorm.DB) *gorm.DB { - if scope.Restrict { - if len(scope.IDs) == 0 { - return db.Where("1 = 0") - } - db = db. - Joins("JOIN project_flock_kandangs pfk ON pfk.id = project_flock_kandang_uniformities.project_flock_kandang_id"). - Joins("JOIN project_flocks pf ON pf.id = pfk.project_flock_id") - db = m.ApplyScopeFilter(db, scope, "pf.location_id") - } + db = db. + Joins("JOIN project_flock_kandangs pfk ON pfk.id = project_flock_kandang_uniformities.project_flock_kandang_id"). + Joins("JOIN project_flocks pf ON pf.id = pfk.project_flock_id") + db, scopeErr = m.ApplyLocationScope(c, db, "pf.location_id") return db }) + if scopeErr != nil { + return nil, 0, scopeErr + } if err != nil { s.Log.Errorf("Failed to get uniformitys: %+v", err) return nil, 0, err From 00cdfb692b6407bf56bdb8aff5a938308750062e Mon Sep 17 00:00:00 2001 From: ragilap Date: Tue, 27 Jan 2026 10:34:25 +0700 Subject: [PATCH 6/7] [FIX/BE-US] feat adjustment location and area --- internal/middleware/role_scope.go | 39 +- .../closings/services/closing.service.go | 10 + .../services/daily-checklist.service.go | 138 +++++- .../services/adjustment.service.go | 20 + .../services/product-stock.service.go | 71 ++- .../product_warehouse.controller.go | 14 +- .../services/product_warehouse.service.go | 56 ++- .../product_warehouse.validation.go | 13 +- .../salesorder_delivery_product.repository.go | 20 +- .../services/deliveryorder.service.go | 17 +- .../employees/services/employees.service.go | 89 +++- .../controllers/warehouse.controller.go | 31 ++ .../warehouses/services/warehouse.service.go | 14 +- .../validations/warehouse.validation.go | 2 + .../controllers/projectflock.controller.go | 2 + .../services/projectflock.service.go | 6 + .../validations/projectflock.validation.go | 21 +- .../services/transfer_laying.service.go | 54 ++- .../services/uniformity.service.go | 2 +- .../controllers/repport.controller.go | 26 + .../customer_payment.repository.go | 40 +- .../repositories/debt_supplier.repository.go | 34 ++ .../repports/services/repport.service.go | 87 +++- .../validations/repport.validation.go | 91 ++-- internal/utils/constant.go | 10 + .../recording_fifo_integration_test.go | 446 ------------------ 26 files changed, 753 insertions(+), 600 deletions(-) delete mode 100644 test/integration/production/recordings/recording_fifo_integration_test.go diff --git a/internal/middleware/role_scope.go b/internal/middleware/role_scope.go index c46a7fab..8430b6fc 100644 --- a/internal/middleware/role_scope.go +++ b/internal/middleware/role_scope.go @@ -82,6 +82,18 @@ func ResolveLocationScope(c *fiber.Ctx, db *gorm.DB) (ScopeFilter, error) { return ScopeFilter{IDs: locationIDs, Restrict: true}, nil } +func ResolveLocationAreaScopes(c *fiber.Ctx, db *gorm.DB) (ScopeFilter, ScopeFilter, error) { + locationScope, err := ResolveLocationScope(c, db) + if err != nil { + return ScopeFilter{}, ScopeFilter{}, err + } + areaScope, err := ResolveAreaScope(c, db) + if err != nil { + return ScopeFilter{}, ScopeFilter{}, err + } + return locationScope, areaScope, nil +} + func collectRoleScope(c *fiber.Ctx) (roleScope, error) { ctx, ok := AuthDetails(c) if !ok || ctx == nil { @@ -212,6 +224,31 @@ func ApplyAreaScope(c *fiber.Ctx, db *gorm.DB, column string) (*gorm.DB, error) return ApplyScopeFilter(db, scope, column), nil } +func ApplyLocationAreaScope(c *fiber.Ctx, db *gorm.DB, locationColumn, areaColumn string) (*gorm.DB, error) { + scopeDB := db + if db != nil { + scopeDB = db.Session(&gorm.Session{NewDB: true}) + } + + if locationColumn != "" { + locationScope, err := ResolveLocationScope(c, scopeDB) + if err != nil { + return db, err + } + db = ApplyScopeFilter(db, locationScope, locationColumn) + } + + if areaColumn != "" { + areaScope, err := ResolveAreaScope(c, scopeDB) + if err != nil { + return db, err + } + db = ApplyScopeFilter(db, areaScope, areaColumn) + } + + return db, nil +} + func EnsureWarehouseAccess(c *fiber.Ctx, db *gorm.DB, warehouseID uint) error { if warehouseID == 0 { return fiber.NewError(fiber.StatusBadRequest, "Invalid warehouse id") @@ -486,7 +523,7 @@ func EnsureUniformityAccess(c *fiber.Ctx, db *gorm.DB, uniformityID uint) error var count int64 q := db.WithContext(c.Context()). - Table("project_flock_kandang_uniformities u"). + Table("project_flock_kandang_uniformity u"). Joins("JOIN project_flock_kandangs pfk ON pfk.id = u.project_flock_kandang_id"). Joins("JOIN project_flocks pf ON pf.id = pfk.project_flock_id"). Where("u.id = ?", uniformityID) diff --git a/internal/modules/closings/services/closing.service.go b/internal/modules/closings/services/closing.service.go index caa3fc24..10ba9c9b 100644 --- a/internal/modules/closings/services/closing.service.go +++ b/internal/modules/closings/services/closing.service.go @@ -99,6 +99,10 @@ func (s closingService) GetAll(c *fiber.Ctx, params *validation.Query) ([]dto.Cl return nil, 0, err } + scope, err := m.ResolveLocationScope(c, s.Repository.DB()) + if err != nil { + return nil, 0, err + } offset := (params.Page - 1) * params.Limit statusFilter := "" @@ -113,6 +117,12 @@ func (s closingService) GetAll(c *fiber.Ctx, params *validation.Query) ([]dto.Cl closings, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB { db = s.withClosingRelations(db) + if scope.Restrict { + if len(scope.IDs) == 0 { + return db.Where("1 = 0") + } + db = m.ApplyScopeFilter(db, scope, "project_flocks.location_id") + } if params.LocationID != nil { db = db.Where("location_id = ?", *params.LocationID) } diff --git a/internal/modules/daily-checklists/services/daily-checklist.service.go b/internal/modules/daily-checklists/services/daily-checklist.service.go index 64802560..e2974039 100644 --- a/internal/modules/daily-checklists/services/daily-checklist.service.go +++ b/internal/modules/daily-checklists/services/daily-checklist.service.go @@ -134,6 +134,87 @@ func (s dailyChecklistService) withRelations(db *gorm.DB) *gorm.DB { return db.Preload("Kandang") } +func (s dailyChecklistService) ensureChecklistAccess(c *fiber.Ctx, checklistID uint) error { + if checklistID == 0 { + return fiber.NewError(fiber.StatusBadRequest, "Invalid checklist id") + } + + db := s.Repository.DB().WithContext(c.Context()). + Table("daily_checklists dc"). + Joins("JOIN kandangs k ON k.id = dc.kandang_id"). + Joins("JOIN locations loc ON loc.id = k.location_id"). + Joins("JOIN areas a ON a.id = loc.area_id"). + Where("dc.id = ?", checklistID) + + scopedDB, err := m.ApplyLocationAreaScope(c, db, "loc.id", "a.id") + if err != nil { + return err + } + + var count int64 + if err := scopedDB.Count(&count).Error; err != nil { + return err + } + if count == 0 { + return fiber.NewError(fiber.StatusNotFound, "DailyChecklist not found") + } + return nil +} + +func (s dailyChecklistService) ensureKandangAccess(c *fiber.Ctx, kandangID uint) error { + if kandangID == 0 { + return fiber.NewError(fiber.StatusBadRequest, "Invalid kandang id") + } + + db := s.Repository.DB().WithContext(c.Context()). + Table("kandangs k"). + Joins("JOIN locations loc ON loc.id = k.location_id"). + Joins("JOIN areas a ON a.id = loc.area_id"). + Where("k.id = ?", kandangID) + + scopedDB, err := m.ApplyLocationAreaScope(c, db, "loc.id", "a.id") + if err != nil { + return err + } + + var count int64 + if err := scopedDB.Count(&count).Error; err != nil { + return err + } + if count == 0 { + return fiber.NewError(fiber.StatusNotFound, "Kandang not found") + } + return nil +} + +func (s dailyChecklistService) ensureTaskAccess(c *fiber.Ctx, taskID uint) error { + if taskID == 0 { + return fiber.NewError(fiber.StatusBadRequest, "Invalid task id") + } + + db := s.Repository.DB().WithContext(c.Context()). + Table("daily_checklist_activity_tasks t"). + Joins("JOIN daily_checklists dc ON dc.id = t.checklist_id"). + Joins("JOIN kandangs k ON k.id = dc.kandang_id"). + Joins("JOIN locations loc ON loc.id = k.location_id"). + Joins("JOIN areas a ON a.id = loc.area_id"). + Where("t.id = ?", taskID) + + scopedDB, err := m.ApplyLocationAreaScope(c, db, "loc.id", "a.id") + if err != nil { + return err + } + + var count int64 + if err := scopedDB.Count(&count).Error; err != nil { + return err + } + if count == 0 { + return fiber.NewError(fiber.StatusNotFound, "Task not found") + } + return nil +} + func (s dailyChecklistService) GetAll(c *fiber.Ctx, params *validation.Query) ([]DailyChecklistListItem, int64, error) { if err := s.Validate.Struct(params); err != nil { return nil, 0, err @@ -143,7 +224,15 @@ func (s dailyChecklistService) GetAll(c *fiber.Ctx, params *validation.Query) ([ db := s.Repository.DB().WithContext(c.Context()). Table("daily_checklists dc"). - Joins("JOIN kandangs k ON k.id = dc.kandang_id") + Joins("JOIN kandangs k ON k.id = dc.kandang_id"). + Joins("JOIN locations loc ON loc.id = k.location_id"). + Joins("JOIN areas a ON a.id = loc.area_id") + + var scopeErr error + db, scopeErr = m.ApplyLocationAreaScope(c, db, "loc.id", "a.id") + if scopeErr != nil { + return nil, 0, scopeErr + } if params.DateFrom != "" { dateFrom, err := time.Parse("2006-01-02", params.DateFrom) @@ -294,6 +383,9 @@ func (s dailyChecklistService) GetAll(c *fiber.Ctx, params *validation.Query) ([ } func (s dailyChecklistService) GetOne(c *fiber.Ctx, id uint) (*entity.DailyChecklist, error) { + if err := s.ensureChecklistAccess(c, id); err != nil { + return nil, err + } dailyChecklist, err := s.Repository.GetByID(c.Context(), id, s.withRelations) if errors.Is(err, gorm.ErrRecordNotFound) { return nil, fiber.NewError(fiber.StatusNotFound, "DailyChecklist not found") @@ -399,6 +491,9 @@ func (s *dailyChecklistService) CreateOne(c *fiber.Ctx, req *validation.Create) if err := s.Validate.Struct(req); err != nil { return nil, err } + if err := s.ensureKandangAccess(c, req.KandangId); err != nil { + return nil, err + } date, err := time.Parse("2006-01-02", req.Date) if err != nil { @@ -431,6 +526,9 @@ func (s dailyChecklistService) UpdateOne(c *fiber.Ctx, req *validation.Update, i if err := s.Validate.Struct(req); err != nil { return nil, err } + if err := s.ensureChecklistAccess(c, id); err != nil { + return nil, err + } deletedIDs := make([]uint, 0) if req.DeletedDocumentIDs != nil { @@ -502,6 +600,9 @@ func (s dailyChecklistService) UpdateOne(c *fiber.Ctx, req *validation.Update, i } func (s dailyChecklistService) DeleteOne(c *fiber.Ctx, id uint) error { + if err := s.ensureChecklistAccess(c, id); err != nil { + return err + } if err := s.Repository.DeleteOne(c.Context(), id); err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return fiber.NewError(fiber.StatusNotFound, "DailyChecklist not found") @@ -516,6 +617,9 @@ func (s dailyChecklistService) AssignPhases(c *fiber.Ctx, id uint, req *validati if err := s.Validate.Struct(req); err != nil { return err } + if err := s.ensureChecklistAccess(c, id); err != nil { + return err + } if _, err := s.Repository.GetByID(c.Context(), id, nil); err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { @@ -597,6 +701,9 @@ func (s dailyChecklistService) AssignPhases(c *fiber.Ctx, id uint, req *validati } func (s dailyChecklistService) RemoveAssignment(c *fiber.Ctx, id uint, employeeID uint) error { + if err := s.ensureChecklistAccess(c, id); err != nil { + return err + } if _, err := s.Repository.GetByID(c.Context(), id, nil); err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return fiber.NewError(fiber.StatusNotFound, "DailyChecklist not found") @@ -634,6 +741,9 @@ func (s dailyChecklistService) GetTasks(c *fiber.Ctx, checklistID uint) ([]entit if checklistID == 0 { return nil, fiber.NewError(fiber.StatusBadRequest, "checklist_id is required") } + if err := s.ensureChecklistAccess(c, checklistID); err != nil { + return nil, err + } if _, err := s.Repository.GetByID(c.Context(), checklistID, nil); err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { @@ -658,6 +768,9 @@ func (s dailyChecklistService) GetChecklistPhaseIDs(c *fiber.Ctx, checklistID ui if checklistID == 0 { return nil, fiber.NewError(fiber.StatusBadRequest, "checklist_id is required") } + if err := s.ensureChecklistAccess(c, checklistID); err != nil { + return nil, err + } if _, err := s.Repository.GetByID(c.Context(), checklistID, nil); err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { @@ -687,6 +800,9 @@ func (s dailyChecklistService) UpdateAssignment(c *fiber.Ctx, req *validation.Up if err := s.Validate.Struct(req); err != nil { return err } + if err := s.ensureTaskAccess(c, req.TaskID); err != nil { + return err + } task := new(entity.DailyChecklistActivityTask) if err := s.Repository.DB().WithContext(c.Context()).First(task, req.TaskID).Error; err != nil { @@ -808,6 +924,9 @@ func (s dailyChecklistService) AssignTasks(c *fiber.Ctx, id uint, req *validatio if err := s.Validate.Struct(req); err != nil { return err } + if err := s.ensureChecklistAccess(c, id); err != nil { + return err + } employeeIDs, err := parseIDs(req.EmployeeIDs) if err != nil { @@ -900,8 +1019,16 @@ func (s dailyChecklistService) GetSummary(c *fiber.Ctx, params *validation.Summa Joins("JOIN daily_checklists d ON d.id = t.checklist_id"). Joins("JOIN kandangs k ON k.id = d.kandang_id"). Joins("JOIN employees e ON e.id = a.employee_id"). + Joins("JOIN locations loc ON loc.id = k.location_id"). + Joins("JOIN areas ar ON ar.id = loc.area_id"). Where("d.date BETWEEN ? AND ? AND d.status = ?", dateFrom, dateTo, "APPROVED") + var scopeErr error + db, scopeErr = m.ApplyLocationAreaScope(c, db, "loc.id", "ar.id") + if scopeErr != nil { + return nil, scopeErr + } + if params.Category != "" { db = db.Where("d.category = ?", params.Category) } @@ -946,7 +1073,11 @@ func (s dailyChecklistService) GetReport(c *fiber.Ctx, params *validation.Report return nil, 0, err } - scope, err := m.ResolveLocationScope(c, s.Repository.DB()) + locationScope, err := m.ResolveLocationScope(c, s.Repository.DB()) + if err != nil { + return nil, 0, err + } + areaScope, err := m.ResolveAreaScope(c, s.Repository.DB()) if err != nil { return nil, 0, err } @@ -967,7 +1098,8 @@ func (s dailyChecklistService) GetReport(c *fiber.Ctx, params *validation.Report Where("EXTRACT(YEAR FROM dc.date) = ?", params.Year). Where("dc.status = ?", "APPROVED") - db = m.ApplyScopeFilter(db, scope, "loc.id") + db = m.ApplyScopeFilter(db, locationScope, "loc.id") + db = m.ApplyScopeFilter(db, areaScope, "a.id") if params.AreaID != nil { db = db.Where("a.id = ?", *params.AreaID) diff --git a/internal/modules/inventory/adjustments/services/adjustment.service.go b/internal/modules/inventory/adjustments/services/adjustment.service.go index bec0ef74..737a4e86 100644 --- a/internal/modules/inventory/adjustments/services/adjustment.service.go +++ b/internal/modules/inventory/adjustments/services/adjustment.service.go @@ -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.GetByStockLogID(c.Context(), id) 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.Product"). Preload("ProductWarehouse.Warehouse") + 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 stock_logs ON stock_logs.id = adjustment_stocks.stock_log_id"). Joins("JOIN product_warehouses ON product_warehouses.id = stock_logs.product_warehouse_id"). diff --git a/internal/modules/inventory/product-stocks/services/product-stock.service.go b/internal/modules/inventory/product-stocks/services/product-stock.service.go index a4e404d6..63ae97ac 100644 --- a/internal/modules/inventory/product-stocks/services/product-stock.service.go +++ b/internal/modules/inventory/product-stocks/services/product-stock.service.go @@ -37,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 { @@ -62,7 +92,7 @@ func (s productStockService) GetAll(c *fiber.Ctx, params *validation.Query) ([]e return nil, 0, err } - scope, err := m.ResolveLocationScope(c, s.ProductRepository.DB()) + locationScope, areaScope, err := m.ResolveLocationAreaScopes(c, s.ProductRepository.DB()) if err != nil { return nil, 0, err } @@ -70,8 +100,8 @@ func (s productStockService) GetAll(c *fiber.Ctx, params *validation.Query) ([]e offset := (params.Page - 1) * params.Limit productStocks, total, err := s.ProductRepository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB { - if scope.Restrict { - if len(scope.IDs) == 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 ( @@ -80,8 +110,12 @@ func (s productStockService) GetAll(c *fiber.Ctx, params *validation.Query) ([]e JOIN warehouses w ON w.id = pw.warehouse_id WHERE pw.product_id = products.id AND pw.qty > 0 - AND w.location_id IN ? - )`, scope.IDs) + 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 @@ -91,7 +125,7 @@ func (s productStockService) GetAll(c *fiber.Ctx, params *validation.Query) ([]e )`) } - db = s.withRelations(db) + db = s.withRelations(db, locationScope, areaScope) if params.Search != "" { db = db.Where("products.name ILIKE ?", "%"+params.Search+"%") } @@ -106,13 +140,13 @@ func (s productStockService) GetAll(c *fiber.Ctx, params *validation.Query) ([]e } func (s productStockService) GetOne(c *fiber.Ctx, id uint) (*entity.Product, error) { - scope, err := m.ResolveLocationScope(c, s.ProductRepository.DB()) + locationScope, areaScope, err := m.ResolveLocationAreaScopes(c, s.ProductRepository.DB()) if err != nil { return nil, err } - if scope.Restrict { - if len(scope.IDs) == 0 { + 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 @@ -121,7 +155,8 @@ func (s productStockService) GetOne(c *fiber.Ctx, id uint) (*entity.Product, err Joins("JOIN warehouses w ON w.id = pw.warehouse_id"). Where("pw.product_id = ?", id). Where("pw.qty > 0"). - Where("w.location_id IN ?", scope.IDs). + 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 } @@ -130,7 +165,9 @@ func (s productStockService) GetOne(c *fiber.Ctx, id uint) (*entity.Product, err } } - product, err := s.ProductRepository.GetByID(c.Context(), id, s.withRelations) + 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") } 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 671d964b..47d85a65 100644 --- a/internal/modules/inventory/product-warehouses/controllers/product_warehouse.controller.go +++ b/internal/modules/inventory/product-warehouses/controllers/product_warehouse.controller.go @@ -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 { 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 ec1992ef..5bb3f692 100644 --- a/internal/modules/inventory/product-warehouses/services/product_warehouse.service.go +++ b/internal/modules/inventory/product-warehouses/services/product_warehouse.service.go @@ -7,11 +7,11 @@ 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" "gitlab.com/mbugroup/lti-api.git/internal/utils" - m "gitlab.com/mbugroup/lti-api.git/internal/middleware" "gorm.io/gorm" ) @@ -54,9 +54,17 @@ func (s productWarehouseService) GetAll(c *fiber.Ctx, params *validation.Query) return nil, 0, err } - scope, err := m.ResolveLocationScope(c, s.Repository.DB()) - if err != nil { - 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 { @@ -96,12 +104,15 @@ 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) - if scope.Restrict { - if len(scope.IDs) == 0 { - return db.Where("1 = 0") + 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) } - db = db.Joins("JOIN warehouses w_scope ON product_warehouses.warehouse_id = w_scope.id"). - Where("w_scope.location_id IN ?", scope.IDs) } if params.ProductId != 0 { @@ -130,19 +141,30 @@ func (s productWarehouseService) GetAll(c *fiber.Ctx, params *validation.Query) } func (s productWarehouseService) GetOne(c *fiber.Ctx, id uint) (*entity.ProductWarehouse, error) { - scope, err := m.ResolveLocationScope(c, s.Repository.DB()) - if err != nil { - return nil, err + 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) - if scope.Restrict { - if len(scope.IDs) == 0 { - return db.Where("1 = 0") + 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) } - db = db.Joins("JOIN warehouses w_scope ON product_warehouses.warehouse_id = w_scope.id"). - Where("w_scope.location_id IN ?", scope.IDs) } return db }) 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 322d0a00..61a41ad0 100644 --- a/internal/modules/inventory/product-warehouses/validations/product_warehouse.validation.go +++ b/internal/modules/inventory/product-warehouses/validations/product_warehouse.validation.go @@ -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"` } diff --git a/internal/modules/marketing/repositories/salesorder_delivery_product.repository.go b/internal/modules/marketing/repositories/salesorder_delivery_product.repository.go index 1ec0bddf..b6ab83c9 100644 --- a/internal/modules/marketing/repositories/salesorder_delivery_product.repository.go +++ b/internal/modules/marketing/repositories/salesorder_delivery_product.repository.go @@ -140,7 +140,7 @@ func (r *MarketingDeliveryProductRepositoryImpl) GetAllWithFilters(ctx context.C Joins("JOIN marketings ON marketings.id = marketing_products.marketing_id"). Where("marketing_delivery_products.delivery_date IS NOT NULL") - if filters.ProductId > 0 || filters.WarehouseId > 0 || filters.AreaId > 0 || filters.LocationId > 0 || filters.Search != "" || filters.MarketingType != "" { + if filters.ProductId > 0 || filters.WarehouseId > 0 || filters.AreaId > 0 || filters.LocationId > 0 || filters.AllowedAreaIDs != nil || filters.AllowedLocationIDs != nil || filters.Search != "" || filters.MarketingType != "" { db = db.Joins("LEFT JOIN product_warehouses ON product_warehouses.id = marketing_products.product_warehouse_id") } @@ -190,7 +190,7 @@ func (r *MarketingDeliveryProductRepositoryImpl) GetAllWithFilters(ctx context.C db = db.Where("product_warehouses.warehouse_id = ?", filters.WarehouseId) } - if filters.AreaId > 0 || filters.LocationId > 0 { + if filters.AreaId > 0 || filters.LocationId > 0 || filters.AllowedAreaIDs != nil || filters.AllowedLocationIDs != nil { db = db.Joins("LEFT JOIN project_flock_kandangs ON project_flock_kandangs.id = product_warehouses.project_flock_kandang_id"). Joins("LEFT JOIN project_flocks ON project_flocks.id = project_flock_kandangs.project_flock_id") @@ -201,6 +201,22 @@ func (r *MarketingDeliveryProductRepositoryImpl) GetAllWithFilters(ctx context.C if filters.LocationId > 0 { db = db.Where("project_flocks.location_id = ?", filters.LocationId) } + + if filters.AllowedAreaIDs != nil { + if len(filters.AllowedAreaIDs) == 0 { + db = db.Where("1 = 0") + } else { + db = db.Where("project_flocks.area_id IN ?", filters.AllowedAreaIDs) + } + } + + if filters.AllowedLocationIDs != nil { + if len(filters.AllowedLocationIDs) == 0 { + db = db.Where("1 = 0") + } else { + db = db.Where("project_flocks.location_id IN ?", filters.AllowedLocationIDs) + } + } } if filters.MarketingType != "" { diff --git a/internal/modules/marketing/services/deliveryorder.service.go b/internal/modules/marketing/services/deliveryorder.service.go index 2a6a6fee..2022cc78 100644 --- a/internal/modules/marketing/services/deliveryorder.service.go +++ b/internal/modules/marketing/services/deliveryorder.service.go @@ -119,12 +119,17 @@ func (s deliveryOrdersService) GetAll(c *fiber.Ctx, params *validation.DeliveryO if len(scope.IDs) == 0 { return db.Where("1 = 0") } - db = db. - Joins("JOIN marketing_products mp ON mp.marketing_id = marketings.id"). - Joins("JOIN product_warehouses pw ON pw.id = mp.product_warehouse_id"). - Joins("JOIN warehouses w ON w.id = pw.warehouse_id"). - Where("w.location_id IN ?", scope.IDs). - Distinct("marketings.*") + db = db.Where( + `EXISTS ( + SELECT 1 + FROM marketing_products mp + JOIN product_warehouses pw ON pw.id = mp.product_warehouse_id + JOIN warehouses w ON w.id = pw.warehouse_id + WHERE mp.marketing_id = marketings.id + AND w.location_id IN ? + )`, + scope.IDs, + ) } if params.MarketingId != 0 { diff --git a/internal/modules/master/employees/services/employees.service.go b/internal/modules/master/employees/services/employees.service.go index b3673eaf..2817a9fa 100644 --- a/internal/modules/master/employees/services/employees.service.go +++ b/internal/modules/master/employees/services/employees.service.go @@ -5,6 +5,7 @@ import ( "strings" 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/master/employees/repositories" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/master/employees/validations" "gitlab.com/mbugroup/lti-api.git/internal/utils" @@ -43,6 +44,61 @@ func (s employeesService) withRelations(db *gorm.DB) *gorm.DB { Where("employees.deleted_at IS NULL") } +func (s employeesService) ensureEmployeeAccess(c *fiber.Ctx, employeeID uint) error { + if employeeID == 0 { + return fiber.NewError(fiber.StatusBadRequest, "Invalid employee id") + } + + db := s.Repository.DB().WithContext(c.Context()). + Table("employees e"). + Joins("JOIN employee_kandangs ek ON ek.employee_id = e.id"). + Joins("JOIN kandangs k ON k.id = ek.kandang_id"). + Joins("JOIN locations loc ON loc.id = k.location_id"). + Joins("JOIN areas a ON a.id = loc.area_id"). + Where("e.id = ?", employeeID). + Where("e.deleted_at IS NULL") + + scopedDB, err := m.ApplyLocationAreaScope(c, db, "loc.id", "a.id") + if err != nil { + return err + } + + var count int64 + if err := scopedDB.Count(&count).Error; err != nil { + return err + } + if count == 0 { + return fiber.NewError(fiber.StatusNotFound, "Employees not found") + } + return nil +} + +func (s employeesService) ensureKandangIDsAccess(c *fiber.Ctx, kandangIDs []uint) error { + if len(kandangIDs) == 0 { + return fiber.NewError(fiber.StatusBadRequest, "kandang_ids must contain at least one valid id") + } + + db := s.Repository.DB().WithContext(c.Context()). + Table("kandangs k"). + Joins("JOIN locations loc ON loc.id = k.location_id"). + Joins("JOIN areas a ON a.id = loc.area_id"). + Where("k.id IN ?", kandangIDs) + + scopedDB, err := m.ApplyLocationAreaScope(c, db, "loc.id", "a.id") + if err != nil { + return err + } + + var count int64 + if err := scopedDB.Count(&count).Error; err != nil { + return err + } + if count != int64(len(kandangIDs)) { + return fiber.NewError(fiber.StatusNotFound, "Kandang not found") + } + return nil +} + func (s employeesService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.Employees, int64, error) { if err := s.Validate.Struct(params); err != nil { return nil, 0, err @@ -52,17 +108,29 @@ func (s employeesService) GetAll(c *fiber.Ctx, params *validation.Query) ([]enti employeess, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB { db = s.withRelations(db) + db = db.Joins("JOIN employee_kandangs ek ON ek.employee_id = employees.id"). + Joins("JOIN kandangs k ON k.id = ek.kandang_id"). + Joins("JOIN locations loc ON loc.id = k.location_id"). + Joins("JOIN areas a ON a.id = loc.area_id") + var scopeErr error + db, scopeErr = m.ApplyLocationAreaScope(c, db, "loc.id", "a.id") + if scopeErr != nil { + return db.Where("1 = 0") + } if params.Search != "" { db = db.Where("employees.name ILIKE ?", "%"+params.Search+"%") } if params.KandangId != nil { - db = db.Joins("JOIN employee_kandangs ek ON ek.employee_id = employees.id"). - Where("ek.kandang_id = ?", *params.KandangId) + db = db.Where("ek.kandang_id = ?", *params.KandangId) } if params.IsActive != nil { db = db.Where("employees.is_active = ?", *params.IsActive) } - return db.Order("employees.created_at DESC").Order("employees.updated_at DESC") + return db. + Select("employees.id, employees.name, employees.is_active, employees.created_at, employees.updated_at"). + Group("employees.id, employees.name, employees.is_active, employees.created_at, employees.updated_at"). + Order("employees.created_at DESC"). + Order("employees.updated_at DESC") }) if err != nil { @@ -73,6 +141,9 @@ func (s employeesService) GetAll(c *fiber.Ctx, params *validation.Query) ([]enti } func (s employeesService) GetOne(c *fiber.Ctx, id uint) (*entity.Employees, error) { + if err := s.ensureEmployeeAccess(c, id); err != nil { + return nil, err + } employees, err := s.Repository.GetByID(c.Context(), id, s.withRelations) if errors.Is(err, gorm.ErrRecordNotFound) { return nil, fiber.NewError(fiber.StatusNotFound, "Employees not found") @@ -98,6 +169,9 @@ func (s *employeesService) CreateOne(c *fiber.Ctx, req *validation.Create) (*ent if len(kandangIDs) == 0 { return nil, fiber.NewError(fiber.StatusBadRequest, "kandang_ids must contain at least one valid id") } + if err := s.ensureKandangIDsAccess(c, kandangIDs); err != nil { + return nil, err + } if _, err := s.Repository.First(c.Context(), func(db *gorm.DB) *gorm.DB { return db.Where("LOWER(name) = ?", strings.ToLower(name)) @@ -147,6 +221,9 @@ func (s employeesService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin if err := s.Validate.Struct(req); err != nil { return nil, err } + if err := s.ensureEmployeeAccess(c, id); err != nil { + return nil, err + } updateBody := make(map[string]any) var ( @@ -181,6 +258,9 @@ func (s employeesService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin if len(ids) == 0 { return nil, fiber.NewError(fiber.StatusBadRequest, "kandang_ids must contain at least one valid id") } + if err := s.ensureKandangIDsAccess(c, ids); err != nil { + return nil, err + } kandangIDs = ids needKandangUpdate = true @@ -234,6 +314,9 @@ func (s employeesService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin } func (s employeesService) DeleteOne(c *fiber.Ctx, id uint) error { + if err := s.ensureEmployeeAccess(c, id); err != nil { + return err + } if err := s.Repository.DeleteOne(c.Context(), id); err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return fiber.NewError(fiber.StatusNotFound, "Employees not found") diff --git a/internal/modules/master/warehouses/controllers/warehouse.controller.go b/internal/modules/master/warehouses/controllers/warehouse.controller.go index 4e93cb52..8d1572fa 100644 --- a/internal/modules/master/warehouses/controllers/warehouse.controller.go +++ b/internal/modules/master/warehouses/controllers/warehouse.controller.go @@ -3,11 +3,13 @@ package controller import ( "math" "strconv" + "strings" "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/dto" service "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/services" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/validations" "gitlab.com/mbugroup/lti-api.git/internal/response" + "gitlab.com/mbugroup/lti-api.git/internal/utils" "github.com/gofiber/fiber/v2" ) @@ -23,6 +25,11 @@ func NewWarehouseController(warehouseService service.WarehouseService) *Warehous } func (u *WarehouseController) GetAll(c *fiber.Ctx) error { + excludeIDs, err := parseCommaSeparatedUint(c.Query("exclude_id", "")) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, err.Error()) + } + query := &validation.Query{ Page: c.QueryInt("page", 1), Limit: c.QueryInt("limit", 10), @@ -30,6 +37,8 @@ func (u *WarehouseController) GetAll(c *fiber.Ctx) error { AreaId: c.QueryInt("area_id", 0), LocationId: c.QueryInt("location_id", 0), ActiveProjectFlockOnly: c.QueryBool("active_project_flock", false), + TransferContext: c.Query(utils.TransferContextKey, ""), + ExcludeIDs: excludeIDs, } if query.Page < 1 || query.Limit < 1 { @@ -56,6 +65,28 @@ func (u *WarehouseController) GetAll(c *fiber.Ctx) error { }) } +func parseCommaSeparatedUint(raw string) ([]uint, error) { + raw = strings.TrimSpace(raw) + if raw == "" { + return nil, nil + } + + parts := strings.Split(raw, ",") + out := make([]uint, 0, len(parts)) + for _, part := range parts { + part = strings.TrimSpace(part) + if part == "" { + continue + } + value, err := strconv.ParseUint(part, 10, 64) + if err != nil { + return nil, fiber.NewError(fiber.StatusBadRequest, "invalid exclude_id") + } + out = append(out, uint(value)) + } + return out, nil +} + func (u *WarehouseController) GetOne(c *fiber.Ctx) error { param := c.Params("id") diff --git a/internal/modules/master/warehouses/services/warehouse.service.go b/internal/modules/master/warehouses/services/warehouse.service.go index 63c32b86..9d6321b5 100644 --- a/internal/modules/master/warehouses/services/warehouse.service.go +++ b/internal/modules/master/warehouses/services/warehouse.service.go @@ -54,7 +54,14 @@ func (s warehouseService) GetAll(c *fiber.Ctx, params *validation.Query) ([]enti warehouses, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB { db = s.withRelations(db) - db, scopeErr = m.ApplyAreaScope(c, db, "warehouses.area_id") + applyScope := true + if params.TransferContext == utils.TransferContextInventoryTransfer { + applyScope = !m.HasPermission(c, m.P_TransferCreateOne) + } + + if applyScope { + db, scopeErr = m.ApplyLocationAreaScope(c, db, "warehouses.location_id", "warehouses.area_id") + } if params.Search != "" { db = db.Where("warehouses.name ILIKE ?", "%"+params.Search+"%") } @@ -81,6 +88,9 @@ func (s warehouseService) GetAll(c *fiber.Ctx, params *validation.Query) ([]enti ) `, "Aktif") } + if len(params.ExcludeIDs) > 0 { + db = db.Where("warehouses.id NOT IN ?", params.ExcludeIDs) + } return db.Order("created_at DESC").Order("updated_at DESC") }) @@ -99,7 +109,7 @@ func (s warehouseService) GetOne(c *fiber.Ctx, id uint) (*entity.Warehouse, erro warehouse, err := s.Repository.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB { db = s.withRelations(db) - db, scopeErr = m.ApplyAreaScope(c, db, "warehouses.area_id") + db, scopeErr = m.ApplyLocationAreaScope(c, db, "warehouses.location_id", "warehouses.area_id") return db }) if scopeErr != nil { diff --git a/internal/modules/master/warehouses/validations/warehouse.validation.go b/internal/modules/master/warehouses/validations/warehouse.validation.go index be796082..2a2a9f87 100644 --- a/internal/modules/master/warehouses/validations/warehouse.validation.go +++ b/internal/modules/master/warehouses/validations/warehouse.validation.go @@ -23,4 +23,6 @@ type Query struct { AreaId int `query:"area_id" validate:"omitempty,number,gt=0"` LocationId int `query:"location_id" validate:"omitempty,number,gt=0"` ActiveProjectFlockOnly bool `query:"active_project_flock"` + ExcludeIDs []uint `query:"-" validate:"omitempty,dive,gt=0"` + TransferContext string `query:"transfer_context" validate:"omitempty,oneof=inventory_transfer"` } diff --git a/internal/modules/production/project_flocks/controllers/projectflock.controller.go b/internal/modules/production/project_flocks/controllers/projectflock.controller.go index 8c5a9298..3df6ad45 100644 --- a/internal/modules/production/project_flocks/controllers/projectflock.controller.go +++ b/internal/modules/production/project_flocks/controllers/projectflock.controller.go @@ -12,6 +12,7 @@ import ( service "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/services" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/validations" "gitlab.com/mbugroup/lti-api.git/internal/response" + "gitlab.com/mbugroup/lti-api.git/internal/utils" "github.com/gofiber/fiber/v2" ) @@ -76,6 +77,7 @@ func (u *ProjectflockController) GetAll(c *fiber.Ctx) error { query.Category = category } + query.TransferContext = c.Query(utils.TransferContextKey, "") if kandangRaw := c.Query("kandang_id", c.Query("kandang_ids", "")); kandangRaw != "" { ids, err := parseUintList(kandangRaw) diff --git a/internal/modules/production/project_flocks/services/projectflock.service.go b/internal/modules/production/project_flocks/services/projectflock.service.go index ec271c55..96e4b6b0 100644 --- a/internal/modules/production/project_flocks/services/projectflock.service.go +++ b/internal/modules/production/project_flocks/services/projectflock.service.go @@ -121,6 +121,12 @@ func (s projectflockService) GetAll(c *fiber.Ctx, params *validation.Query) ([]e if err != nil { return nil, 0, nil, err } + if params.TransferContext == utils.TransferContextTransferToLaying { + if m.HasPermission(c, m.P_TransferToLaying_CreateOne) || m.HasPermission(c, m.P_TransferToLaying_UpdateOne) { + scope.Restrict = false + scope.IDs = nil + } + } offset := (params.Page - 1) * params.Limit diff --git a/internal/modules/production/project_flocks/validations/projectflock.validation.go b/internal/modules/production/project_flocks/validations/projectflock.validation.go index 5b2a9407..1fb48abe 100644 --- a/internal/modules/production/project_flocks/validations/projectflock.validation.go +++ b/internal/modules/production/project_flocks/validations/projectflock.validation.go @@ -12,16 +12,17 @@ type Create 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,max=50"` - SortBy string `query:"sort_by" validate:"omitempty"` - SortOrder string `query:"sort_order" validate:"omitempty,oneof=asc desc"` - AreaId uint `query:"area_id" validate:"omitempty,number,gt=0"` - LocationId uint `query:"location_id" validate:"omitempty,number,gt=0"` - Period int `query:"period" validate:"omitempty,number,gt=0"` - Category string `query:"category" validate:"omitempty"` - KandangIds []uint `query:"kandang_id" validate:"omitempty,dive,gt=0"` + 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,max=50"` + SortBy string `query:"sort_by" validate:"omitempty"` + SortOrder string `query:"sort_order" validate:"omitempty,oneof=asc desc"` + AreaId uint `query:"area_id" validate:"omitempty,number,gt=0"` + LocationId uint `query:"location_id" validate:"omitempty,number,gt=0"` + Period int `query:"period" validate:"omitempty,number,gt=0"` + Category string `query:"category" validate:"omitempty"` + KandangIds []uint `query:"kandang_id" validate:"omitempty,dive,gt=0"` + TransferContext string `query:"transfer_context" validate:"omitempty,oneof=transfer_to_laying"` } type Approve struct { 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 b8c71568..2f11e9a1 100644 --- a/internal/modules/production/transfer_layings/services/transfer_laying.service.go +++ b/internal/modules/production/transfer_layings/services/transfer_laying.service.go @@ -106,16 +106,32 @@ func (s transferLayingService) GetAll(c *fiber.Ctx, params *validation.Query) ([ return nil, 0, err } + scope, err := m.ResolveLocationScope(c, s.Repository.DB()) + if 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 { + needFlockJoin := scope.Restrict || params.Search != "" + if needFlockJoin { + db = db.Joins("LEFT JOIN project_flocks AS pf_from ON laying_transfers.from_project_flock_id = pf_from.id"). + Joins("LEFT JOIN project_flocks AS pf_to ON laying_transfers.to_project_flock_id = pf_to.id") + } + + if scope.Restrict { + if len(scope.IDs) == 0 { + return db.Where("1 = 0") + } + db = db.Where("(pf_from.location_id IN ? OR pf_to.location_id IN ?)", scope.IDs, scope.IDs) + } + // Apply search and filters if params.Search != "" { searchPattern := "%" + params.Search + "%" - db = db.Joins("LEFT JOIN project_flocks AS pf_from ON laying_transfers.from_project_flock_id = pf_from.id"). - Joins("LEFT JOIN project_flocks AS pf_to ON laying_transfers.to_project_flock_id = pf_to.id"). - Where("laying_transfers.transfer_number ILIKE ? OR laying_transfers.notes ILIKE ? OR pf_from.flock_name ILIKE ? OR pf_to.flock_name ILIKE ?", - searchPattern, searchPattern, searchPattern, searchPattern) + db = db.Where("laying_transfers.transfer_number ILIKE ? OR laying_transfers.notes ILIKE ? OR pf_from.flock_name ILIKE ? OR pf_to.flock_name ILIKE ?", + searchPattern, searchPattern, searchPattern, searchPattern) } if params.TransferDate != "" { @@ -181,11 +197,10 @@ func (s *transferLayingService) CreateOne(c *fiber.Ctx, req *validation.Create) if err := s.Validate.Struct(req); err != nil { return nil, err } - if err := m.EnsureProjectFlockAccess(c, s.Repository.DB(), req.SourceProjectFlockId); err != nil { - return nil, err - } - if err := m.EnsureProjectFlockAccess(c, s.Repository.DB(), req.TargetProjectFlockId); err != nil { - return nil, err + if !m.HasPermission(c, m.P_TransferToLaying_CreateOne) { + if err := m.EnsureProjectFlockAccess(c, s.Repository.DB(), req.TargetProjectFlockId); err != nil { + return nil, err + } } actorID, err := m.ActorIDFromContext(c) @@ -415,14 +430,10 @@ func (s *transferLayingService) UpdateOne(c *fiber.Ctx, req *validation.Update, if err := s.Validate.Struct(req); err != nil { return nil, err } - if err := m.EnsureLayingTransferAccess(c, s.Repository.DB(), id); err != nil { - return nil, err - } - if err := m.EnsureProjectFlockAccess(c, s.Repository.DB(), req.SourceProjectFlockId); err != nil { - return nil, err - } - if err := m.EnsureProjectFlockAccess(c, s.Repository.DB(), req.TargetProjectFlockId); err != nil { - return nil, err + if !m.HasPermission(c, m.P_TransferToLaying_UpdateOne) { + if err := m.EnsureProjectFlockAccess(c, s.Repository.DB(), req.TargetProjectFlockId); err != nil { + return nil, err + } } existingTransfer, err := s.Repository.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB { @@ -599,6 +610,9 @@ func (s *transferLayingService) UpdateOne(c *fiber.Ctx, req *validation.Update, } func (s transferLayingService) DeleteOne(c *fiber.Ctx, id uint) error { + if err := m.EnsureLayingTransferAccess(c, s.Repository.DB(), id); err != nil { + return err + } _, err := s.Repository.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB { return db.Preload("Sources.ProductWarehouse").Preload("Targets") @@ -669,6 +683,12 @@ func (s transferLayingService) Approval(c *fiber.Ctx, req *validation.Approve) ( return nil, fiber.NewError(fiber.StatusBadRequest, "approvable_ids must contain at least one id") } + for _, approvableID := range approvableIDs { + if err := m.EnsureLayingTransferAccess(c, s.Repository.DB(), approvableID); err != nil { + return nil, err + } + } + step := utils.TransferToLayingStepPengajuan if action == entity.ApprovalActionApproved { step = utils.TransferToLayingStepDisetujui diff --git a/internal/modules/production/uniformities/services/uniformity.service.go b/internal/modules/production/uniformities/services/uniformity.service.go index e7671713..1e4ccbd5 100644 --- a/internal/modules/production/uniformities/services/uniformity.service.go +++ b/internal/modules/production/uniformities/services/uniformity.service.go @@ -92,7 +92,7 @@ func (s uniformityService) GetAll(c *fiber.Ctx, params *validation.Query) ([]ent offset := (params.Page - 1) * params.Limit uniformitys, total, err := s.Repository.GetAllWithFilters(c.Context(), offset, params.Limit, params, func(db *gorm.DB) *gorm.DB { db = db. - Joins("JOIN project_flock_kandangs pfk ON pfk.id = project_flock_kandang_uniformities.project_flock_kandang_id"). + Joins("JOIN project_flock_kandangs pfk ON pfk.id = project_flock_kandang_uniformity.project_flock_kandang_id"). Joins("JOIN project_flocks pf ON pf.id = pfk.project_flock_id") db, scopeErr = m.ApplyLocationScope(c, db, "pf.location_id") return db diff --git a/internal/modules/repports/controllers/repport.controller.go b/internal/modules/repports/controllers/repport.controller.go index 54e04442..9becdf87 100644 --- a/internal/modules/repports/controllers/repport.controller.go +++ b/internal/modules/repports/controllers/repport.controller.go @@ -112,6 +112,10 @@ func (c *RepportController) GetMarketing(ctx *fiber.Ctx) error { if err != nil { return err } + areaScope, err := m.ResolveAreaScope(ctx, c.RepportService.DB()) + if err != nil { + return err + } if locationScope.Restrict { allowed := toInt64Slice(locationScope.IDs) if len(allowed) == 0 { @@ -119,6 +123,13 @@ func (c *RepportController) GetMarketing(ctx *fiber.Ctx) error { } query.AllowedLocationIDs = allowed } + if areaScope.Restrict { + allowed := toInt64Slice(areaScope.IDs) + if len(allowed) == 0 { + allowed = []int64{-1} + } + query.AllowedAreaIDs = allowed + } if query.Page < 1 || query.Limit < 1 { return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0") @@ -220,6 +231,21 @@ func (c *RepportController) GetDebtSupplier(ctx *fiber.Ctx) error { SortOrder: ctx.Query("sort_order", ""), } + locationScope, err := m.ResolveLocationScope(ctx, c.RepportService.DB()) + if err != nil { + return err + } + areaScope, err := m.ResolveAreaScope(ctx, c.RepportService.DB()) + if err != nil { + return err + } + if locationScope.Restrict { + query.AllowedLocationIDs = toInt64Slice(locationScope.IDs) + } + if areaScope.Restrict { + query.AllowedAreaIDs = toInt64Slice(areaScope.IDs) + } + 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/repports/repositories/customer_payment.repository.go b/internal/modules/repports/repositories/customer_payment.repository.go index 8a5747aa..9004882b 100644 --- a/internal/modules/repports/repositories/customer_payment.repository.go +++ b/internal/modules/repports/repositories/customer_payment.repository.go @@ -30,7 +30,7 @@ type CustomerPaymentTransaction struct { type CustomerPaymentRepository interface { GetCustomerPaymentTransactions(ctx context.Context, customerID *uint) ([]CustomerPaymentTransaction, error) GetInitialBalanceByCustomer(ctx context.Context, customerID uint) (float64, error) - GetCustomerIDsWithTransactions(ctx context.Context, limit, offset int) ([]uint, int64, error) + GetCustomerIDsWithTransactions(ctx context.Context, limit, offset int, allowedCustomerIDs []uint) ([]uint, int64, error) } type customerPaymentRepositoryImpl struct { @@ -146,7 +146,7 @@ func (r *customerPaymentRepositoryImpl) GetInitialBalanceByCustomer(ctx context. return result.Nominal, nil } -func (r *customerPaymentRepositoryImpl) GetCustomerIDsWithTransactions(ctx context.Context, limit, offset int) ([]uint, int64, error) { +func (r *customerPaymentRepositoryImpl) GetCustomerIDsWithTransactions(ctx context.Context, limit, offset int, allowedCustomerIDs []uint) ([]uint, int64, error) { subQuery := r.db.WithContext(ctx). Table("(" + "SELECT DISTINCT c.id as customer_id FROM marketing_delivery_products mdp " + @@ -161,26 +161,36 @@ func (r *customerPaymentRepositoryImpl) GetCustomerIDsWithTransactions(ctx conte "AND p.transaction_type = 'PENJUALAN' AND p.deleted_at IS NULL AND c.deleted_at IS NULL" + ") as customer_ids") + if len(allowedCustomerIDs) > 0 { + subQuery = subQuery.Where("customer_id IN ?", allowedCustomerIDs) + } + var total int64 if err := subQuery.Count(&total).Error; err != nil { return nil, 0, err } var customerIDs []uint - err := r.db.WithContext(ctx). - Table("("+ - "SELECT DISTINCT c.id as customer_id FROM marketing_delivery_products mdp "+ - "INNER JOIN marketing_products mp ON mp.id = mdp.marketing_product_id "+ - "INNER JOIN marketings m ON m.id = mp.marketing_id "+ - "INNER JOIN customers c ON c.id = m.customer_id "+ - "WHERE mdp.delivery_date IS NOT NULL AND m.deleted_at IS NULL AND c.deleted_at IS NULL "+ - "UNION "+ - "SELECT DISTINCT c.id as customer_id FROM payments p "+ - "INNER JOIN customers c ON c.id = p.party_id "+ - "WHERE p.party_type = 'CUSTOMER' AND p.direction = 'IN' "+ - "AND p.transaction_type = 'PENJUALAN' AND p.deleted_at IS NULL AND c.deleted_at IS NULL"+ + query := r.db.WithContext(ctx). + Table("(" + + "SELECT DISTINCT c.id as customer_id FROM marketing_delivery_products mdp " + + "INNER JOIN marketing_products mp ON mp.id = mdp.marketing_product_id " + + "INNER JOIN marketings m ON m.id = mp.marketing_id " + + "INNER JOIN customers c ON c.id = m.customer_id " + + "WHERE mdp.delivery_date IS NOT NULL AND m.deleted_at IS NULL AND c.deleted_at IS NULL " + + "UNION " + + "SELECT DISTINCT c.id as customer_id FROM payments p " + + "INNER JOIN customers c ON c.id = p.party_id " + + "WHERE p.party_type = 'CUSTOMER' AND p.direction = 'IN' " + + "AND p.transaction_type = 'PENJUALAN' AND p.deleted_at IS NULL AND c.deleted_at IS NULL" + ") as customer_ids"). - Select("customer_id"). + Select("customer_id") + + if len(allowedCustomerIDs) > 0 { + query = query.Where("customer_id IN ?", allowedCustomerIDs) + } + + err := query. Order("customer_id ASC"). Limit(limit). Offset(offset). diff --git a/internal/modules/repports/repositories/debt_supplier.repository.go b/internal/modules/repports/repositories/debt_supplier.repository.go index 74039ebf..fefcbade 100644 --- a/internal/modules/repports/repositories/debt_supplier.repository.go +++ b/internal/modules/repports/repositories/debt_supplier.repository.go @@ -70,6 +70,7 @@ func (r *debtSupplierRepositoryImpl) baseSupplierQuery(ctx context.Context, filt Model(&entity.Supplier{}). Joins("JOIN purchases ON purchases.supplier_id = suppliers.id"). Joins("JOIN purchase_items ON purchase_items.purchase_id = purchases.id"). + Joins("JOIN warehouses w ON w.id = purchase_items.warehouse_id"). Joins("JOIN (?) AS la ON la.approvable_id = purchases.id", r.latestPurchaseApproval(ctx)). Where("la.step_number >= ?", uint16(utils.PurchaseStepReceiving)). Where("(la.action IS NULL OR la.action != ?)", string(entity.ApprovalActionRejected)). @@ -79,6 +80,22 @@ func (r *debtSupplierRepositoryImpl) baseSupplierQuery(ctx context.Context, filt db = db.Where("suppliers.id IN ?", filters.SupplierIDs) } + if filters.AllowedAreaIDs != nil { + if len(filters.AllowedAreaIDs) == 0 { + db = db.Where("1 = 0") + } else { + db = db.Where("w.area_id IN ?", filters.AllowedAreaIDs) + } + } + + if filters.AllowedLocationIDs != nil { + if len(filters.AllowedLocationIDs) == 0 { + db = db.Where("1 = 0") + } else { + db = db.Where("w.location_id IN ?", filters.AllowedLocationIDs) + } + } + if filters.StartDate != "" { if dateFrom, err := utils.ParseDateString(filters.StartDate); err == nil { db = db.Where(fmt.Sprintf("DATE(%s) >= ?", dateColumn), dateFrom) @@ -226,12 +243,29 @@ func (r *debtSupplierRepositoryImpl) getPurchaseIDs(ctx context.Context, supplie Table("purchases"). Select("DISTINCT purchases.id"). Joins("JOIN purchase_items ON purchase_items.purchase_id = purchases.id"). + Joins("JOIN warehouses w ON w.id = purchase_items.warehouse_id"). Joins("JOIN (?) AS la ON la.approvable_id = purchases.id", r.latestPurchaseApproval(ctx)). Where("purchases.supplier_id IN ?", supplierIDs). Where("la.step_number >= ?", uint16(utils.PurchaseStepReceiving)). Where("(la.action IS NULL OR la.action != ?)", string(entity.ApprovalActionRejected)). Where("purchase_items.received_date IS NOT NULL") + if filters.AllowedAreaIDs != nil { + if len(filters.AllowedAreaIDs) == 0 { + db = db.Where("1 = 0") + } else { + db = db.Where("w.area_id IN ?", filters.AllowedAreaIDs) + } + } + + if filters.AllowedLocationIDs != nil { + if len(filters.AllowedLocationIDs) == 0 { + db = db.Where("1 = 0") + } else { + db = db.Where("w.location_id IN ?", filters.AllowedLocationIDs) + } + } + if filters.StartDate != "" { if dateFrom, err := utils.ParseDateString(filters.StartDate); err == nil { db = db.Where(fmt.Sprintf("DATE(%s) >= ?", dateColumn), dateFrom) diff --git a/internal/modules/repports/services/repport.service.go b/internal/modules/repports/services/repport.service.go index 1ab5e7ba..531b9394 100644 --- a/internal/modules/repports/services/repport.service.go +++ b/internal/modules/repports/services/repport.service.go @@ -46,12 +46,13 @@ type RepportService interface { GetHppPerKandang(ctx *fiber.Ctx) (*dto.HppPerKandangResponseData, *dto.HppPerKandangMetaDTO, error) GetProductionResult(ctx *fiber.Ctx, params *validation.ProductionResultQuery) ([]dto.ProductionResultDTO, int64, error) GetCustomerPayment(ctx *fiber.Ctx, params *validation.CustomerPaymentQuery) ([]dto.CustomerPaymentReportItem, int64, error) + DB() *gorm.DB } type repportService struct { Log *logrus.Logger Validate *validator.Validate - DB *gorm.DB + db *gorm.DB ExpenseRealizationRepo expenseRepo.ExpenseRealizationRepository MarketingDeliveryRepo marketingRepo.MarketingDeliveryProductRepository PurchaseRepo purchaseRepo.PurchaseRepository @@ -100,7 +101,7 @@ func NewRepportService( return &repportService{ Log: utils.Log, Validate: validate, - DB: db, + db: db, ExpenseRealizationRepo: expenseRealizationRepo, MarketingDeliveryRepo: marketingDeliveryRepo, PurchaseRepo: purchaseRepo, @@ -119,6 +120,9 @@ func NewRepportService( } } +func (s *repportService) DB() *gorm.DB { + return s.db +} func (s *repportService) GetExpense(c *fiber.Ctx, params *validation.ExpenseQuery) ([]dto.RepportExpenseListDTO, int64, error) { if err := s.Validate.Struct(params); err != nil { @@ -407,11 +411,38 @@ func (s *repportService) GetCustomerPayment(ctx *fiber.Ctx, params *validation.C return nil, 0, err } + locationScope, err := m.ResolveLocationScope(ctx, s.DB()) + if err != nil { + return nil, 0, err + } + areaScope, err := m.ResolveAreaScope(ctx, s.DB()) + if err != nil { + return nil, 0, err + } + + restrictScope := locationScope.Restrict || areaScope.Restrict + var allowedCustomerIDs []uint + if restrictScope { + if (locationScope.Restrict && len(locationScope.IDs) == 0) || (areaScope.Restrict && len(areaScope.IDs) == 0) { + return []dto.CustomerPaymentReportItem{}, 0, nil + } + allowedCustomerIDs, err = s.getCustomerIDsByScope(ctx.Context(), locationScope.IDs, areaScope.IDs) + if err != nil { + return nil, 0, err + } + if len(allowedCustomerIDs) == 0 { + return []dto.CustomerPaymentReportItem{}, 0, nil + } + } + var customerIDs []uint var totalCustomers int64 if len(params.CustomerIDs) > 0 { customerIDs = params.CustomerIDs + if restrictScope { + customerIDs = intersectUint(customerIDs, allowedCustomerIDs) + } totalCustomers = int64(len(customerIDs)) if len(customerIDs) == 0 { @@ -430,7 +461,7 @@ func (s *repportService) GetCustomerPayment(ctx *fiber.Ctx, params *validation.C offset := (page - 1) * limit var err error - customerIDs, totalCustomers, err = s.CustomerPaymentRepo.GetCustomerIDsWithTransactions(ctx.Context(), limit, offset) + customerIDs, totalCustomers, err = s.CustomerPaymentRepo.GetCustomerIDsWithTransactions(ctx.Context(), limit, offset, allowedCustomerIDs) if err != nil { return nil, 0, err } @@ -456,6 +487,37 @@ func (s *repportService) GetCustomerPayment(ctx *fiber.Ctx, params *validation.C return result, totalCustomers, nil } +func (s *repportService) getCustomerIDsByScope(ctx context.Context, locationIDs, areaIDs []uint) ([]uint, error) { + if len(locationIDs) == 0 && len(areaIDs) == 0 { + return []uint{}, nil + } + + db := s.db.WithContext(ctx). + Table("customers c"). + Select("DISTINCT c.id"). + Joins("JOIN marketings m ON m.customer_id = c.id"). + Joins("JOIN marketing_products mp ON mp.marketing_id = m.id"). + Joins("JOIN marketing_delivery_products mdp ON mdp.marketing_product_id = mp.id"). + Joins("JOIN product_warehouses pw ON pw.id = mdp.product_warehouse_id"). + Joins("JOIN warehouses w ON w.id = pw.warehouse_id"). + Where("mdp.delivery_date IS NOT NULL"). + Where("m.deleted_at IS NULL"). + Where("c.deleted_at IS NULL") + + if len(locationIDs) > 0 { + db = db.Where("w.location_id IN ?", locationIDs) + } + if len(areaIDs) > 0 { + db = db.Where("w.area_id IN ?", areaIDs) + } + + var customerIDs []uint + if err := db.Pluck("c.id", &customerIDs).Error; err != nil { + return nil, err + } + return customerIDs, nil +} + func (s *repportService) processCustomerPayment(ctx context.Context, customerID uint, params *validation.CustomerPaymentQuery) (dto.CustomerPaymentReportItem, error) { customer, err := s.CustomerRepo.GetByID(ctx, customerID, nil) @@ -803,7 +865,7 @@ func (s *repportService) getUniformityByWeek(ctx context.Context, projectFlockKa } var rows []entity.ProjectFlockKandangUniformity - if err := s.DB.WithContext(ctx). + if err := s.db.WithContext(ctx). Model(&entity.ProjectFlockKandangUniformity{}). Select("week, uniformity, uniform_date, id, chart_data"). Where("project_flock_kandang_id = ?", projectFlockKandangID). @@ -2007,6 +2069,23 @@ func intersectInt64(a, b []int64) []int64 { return out } +func intersectUint(a, b []uint) []uint { + if len(a) == 0 || len(b) == 0 { + return nil + } + set := make(map[uint]struct{}, len(b)) + for _, id := range b { + set[id] = struct{}{} + } + out := make([]uint, 0, len(a)) + for _, id := range a { + if _, ok := set[id]; ok { + out = append(out, id) + } + } + return out +} + func parseOptionalFloat64(raw string) (*float64, error) { raw = strings.TrimSpace(raw) if raw == "" { diff --git a/internal/modules/repports/validations/repport.validation.go b/internal/modules/repports/validations/repport.validation.go index de68e467..37c581d9 100644 --- a/internal/modules/repports/validations/repport.validation.go +++ b/internal/modules/repports/validations/repport.validation.go @@ -1,63 +1,66 @@ package validation type ExpenseQuery struct { - Page int `query:"page" validate:"omitempty,min=1,gt=0"` - Limit int `query:"limit" validate:"omitempty,min=1,max=100,gt=0"` - Search string `query:"search" validate:"omitempty,max=100"` - Category string `query:"category" validate:"omitempty,oneof=BOP NON-BOP"` - SupplierId int64 `query:"supplier_id" validate:"omitempty"` - KandangId int64 `query:"kandang_id" validate:"omitempty"` - ProjectFlockKandangId int64 `query:"project_flock_kandang_id" validate:"omitempty"` - ProjectFlockId int64 `query:"project_flock_id" validate:"omitempty"` - NonstockId int64 `query:"nonstock_id" validate:"omitempty"` - AreaId int64 `query:"area_id" validate:"omitempty"` - LocationId int64 `query:"location_id" validate:"omitempty"` - RealizationDate string `query:"realization_date" validate:"omitempty"` + Page int `query:"page" validate:"omitempty,min=1,gt=0"` + Limit int `query:"limit" validate:"omitempty,min=1,max=100,gt=0"` + Search string `query:"search" validate:"omitempty,max=100"` + Category string `query:"category" validate:"omitempty,oneof=BOP NON-BOP"` + SupplierId int64 `query:"supplier_id" validate:"omitempty"` + KandangId int64 `query:"kandang_id" validate:"omitempty"` + ProjectFlockKandangId int64 `query:"project_flock_kandang_id" validate:"omitempty"` + ProjectFlockId int64 `query:"project_flock_id" validate:"omitempty"` + NonstockId int64 `query:"nonstock_id" validate:"omitempty"` + AreaId int64 `query:"area_id" validate:"omitempty"` + LocationId int64 `query:"location_id" validate:"omitempty"` + RealizationDate string `query:"realization_date" validate:"omitempty"` AllowedAreaIDs []int64 `query:"-"` AllowedLocationIDs []int64 `query:"-"` } type MarketingQuery struct { - Page int `query:"page" validate:"omitempty,min=1,gt=0"` - Limit int `query:"limit" validate:"omitempty,min=1,gt=0"` - Search string `query:"search" validate:"omitempty,max=100"` - CustomerId int64 `query:"customer_id" validate:"omitempty"` - ProductId int64 `query:"product_id" validate:"omitempty"` - WarehouseId int64 `query:"warehouse_id" validate:"omitempty"` - SalesPersonId int64 `query:"sales_person_id" validate:"omitempty"` - AreaId int64 `query:"area_id" validate:"omitempty"` - LocationId int64 `query:"location_id" validate:"omitempty"` - MarketingType string `query:"marketing_type" validate:"omitempty,oneof=ayam telur trading"` - FilterBy string `query:"filter_by" validate:"omitempty,oneof= so_date realization_date"` - StartDate string `query:"start_date" validate:"omitempty,datetime=2006-01-02"` - EndDate string `query:"end_date" validate:"omitempty,datetime=2006-01-02"` - SortBy string `query:"sort_by" validate:"omitempty,oneof=so_date realization_date customer warehouse product sales_person vehicle_number sales_amount hpp_amount qty average_weight total_weight sales_price hpp_price aging_days"` - SortOrder string `query:"sort_order" validate:"omitempty,oneof=asc desc"` + Page int `query:"page" validate:"omitempty,min=1,gt=0"` + Limit int `query:"limit" validate:"omitempty,min=1,gt=0"` + Search string `query:"search" validate:"omitempty,max=100"` + CustomerId int64 `query:"customer_id" validate:"omitempty"` + ProductId int64 `query:"product_id" validate:"omitempty"` + WarehouseId int64 `query:"warehouse_id" validate:"omitempty"` + SalesPersonId int64 `query:"sales_person_id" validate:"omitempty"` + AreaId int64 `query:"area_id" validate:"omitempty"` + LocationId int64 `query:"location_id" validate:"omitempty"` + MarketingType string `query:"marketing_type" validate:"omitempty,oneof=ayam telur trading"` + FilterBy string `query:"filter_by" validate:"omitempty,oneof= so_date realization_date"` + StartDate string `query:"start_date" validate:"omitempty,datetime=2006-01-02"` + EndDate string `query:"end_date" validate:"omitempty,datetime=2006-01-02"` + SortBy string `query:"sort_by" validate:"omitempty,oneof=so_date realization_date customer warehouse product sales_person vehicle_number sales_amount hpp_amount qty average_weight total_weight sales_price hpp_price aging_days"` + SortOrder string `query:"sort_order" validate:"omitempty,oneof=asc desc"` + AllowedAreaIDs []int64 `query:"-"` AllowedLocationIDs []int64 `query:"-"` } type PurchaseSupplierQuery struct { - Page int `query:"page" validate:"omitempty,min=1,gt=0"` - Limit int `query:"limit" validate:"omitempty,min=1,gt=0"` - AreaId int64 `query:"area_id" validate:"omitempty"` - SupplierId int64 `query:"supplier_id" validate:"omitempty"` - ProductId int64 `query:"product_id" validate:"omitempty"` - ProductCategoryId int64 `query:"product_category_id" validate:"omitempty"` - StartDate string `query:"start_date" validate:"omitempty"` - EndDate string `query:"end_date" validate:"omitempty"` - SortBy string `query:"sort_by" validate:"omitempty"` - FilterBy string `query:"filter_by" validate:"omitempty"` + Page int `query:"page" validate:"omitempty,min=1,gt=0"` + Limit int `query:"limit" validate:"omitempty,min=1,gt=0"` + AreaId int64 `query:"area_id" validate:"omitempty"` + SupplierId int64 `query:"supplier_id" validate:"omitempty"` + ProductId int64 `query:"product_id" validate:"omitempty"` + ProductCategoryId int64 `query:"product_category_id" validate:"omitempty"` + StartDate string `query:"start_date" validate:"omitempty"` + EndDate string `query:"end_date" validate:"omitempty"` + SortBy string `query:"sort_by" validate:"omitempty"` + FilterBy string `query:"filter_by" validate:"omitempty"` AllowedAreaIDs []int64 `query:"-"` } type DebtSupplierQuery struct { - Page int `query:"page" validate:"omitempty,min=1,gt=0"` - Limit int `query:"limit" validate:"omitempty,min=1,gt=0"` - SupplierIDs []int64 `query:"-" validate:"omitempty,dive,gt=0"` - StartDate string `query:"start_date" validate:"omitempty,datetime=2006-01-02"` - EndDate string `query:"end_date" validate:"omitempty,datetime=2006-01-02"` - FilterBy string `query:"filter_by" validate:"omitempty,oneof=received_date po_date"` - SortOrder string `query:"sort_order" validate:"omitempty,oneof=asc desc"` + Page int `query:"page" validate:"omitempty,min=1,gt=0"` + Limit int `query:"limit" validate:"omitempty,min=1,gt=0"` + SupplierIDs []int64 `query:"-" validate:"omitempty,dive,gt=0"` + StartDate string `query:"start_date" validate:"omitempty,datetime=2006-01-02"` + EndDate string `query:"end_date" validate:"omitempty,datetime=2006-01-02"` + FilterBy string `query:"filter_by" validate:"omitempty,oneof=received_date po_date"` + SortOrder string `query:"sort_order" validate:"omitempty,oneof=asc desc"` + AllowedAreaIDs []int64 `query:"-"` + AllowedLocationIDs []int64 `query:"-"` } type HppPerKandangQuery struct { diff --git a/internal/utils/constant.go b/internal/utils/constant.go index d27b07ef..d204ca97 100644 --- a/internal/utils/constant.go +++ b/internal/utils/constant.go @@ -117,6 +117,16 @@ const ( StockLogTypeRecording StockLogType = "RECORDING" ) +// ------------------------------------------------------------------- +// Transfer context +// ------------------------------------------------------------------- + +const ( + TransferContextKey = "transfer_context" + TransferContextInventoryTransfer = "inventory_transfer" + TransferContextTransferToLaying = "transfer_to_laying" +) + // ------------------------------------------------------------------- // WarehouseType // ------------------------------------------------------------------- diff --git a/test/integration/production/recordings/recording_fifo_integration_test.go b/test/integration/production/recordings/recording_fifo_integration_test.go deleted file mode 100644 index dd5f7d53..00000000 --- a/test/integration/production/recordings/recording_fifo_integration_test.go +++ /dev/null @@ -1,446 +0,0 @@ -package test - -import ( - "context" - "fmt" - "strings" - "testing" - "time" - - "github.com/glebarez/sqlite" - "gorm.io/gorm" - "gorm.io/gorm/logger" - - 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" - recordingRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories" - servicePkg "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/services" - "gitlab.com/mbugroup/lti-api.git/internal/utils" - "gitlab.com/mbugroup/lti-api.git/internal/utils/fifo" -) - -func TestRecordingFIFO_CreatePendingWithoutStock(t *testing.T) { - db, svc, _, _ := setupRecordingFIFOTableTest(t) - ctx := context.Background() - - recordingID := uint(1) - productWarehouse := createProductWarehouseRow(t, db, 0) - stock := createRecordingStockRow(t, db, recordingID, productWarehouse.Id, 10) - - if err := svc.ConsumeRecordingStocks(ctx, db, []entity.RecordingStock{stock}); err != nil { - t.Fatalf("consumeRecordingStocks (pending) failed: %v", err) - } - - updated := fetchRecordingStock(t, db, stock.Id) - assertFloatEqual(t, 0, updated.UsageQty, "usage_qty should remain zero when no stock is available") - assertFloatEqual(t, 10, updated.PendingQty, "pending_qty should capture the entire request") - assertWarehouseQuantity(t, db, productWarehouse.Id, 0) - assertAllocationCount(t, db, 0) - - assertAllocationCount(t, db, 0) -} - -func TestRecordingFIFO_EditReallocatesUsage(t *testing.T) { - db, svc, fifoSvc, stockableKey := setupRecordingFIFOTableTest(t) - ctx := context.Background() - - recordingID := uint(1) - productWarehouse := createProductWarehouseRow(t, db, 0) - stock := createRecordingStockRow(t, db, recordingID, productWarehouse.Id, 10) - lot := createStockLot(t, db, productWarehouse.Id) - - if _, err := fifoSvc.Replenish(ctx, commonSvc.StockReplenishRequest{ - StockableKey: stockableKey, - StockableID: lot.Id, - ProductWarehouseID: productWarehouse.Id, - Quantity: 12, - }); err != nil { - t.Fatalf("replenish failed: %v", err) - } - - if err := svc.ConsumeRecordingStocks(ctx, db, []entity.RecordingStock{stock}); err != nil { - t.Fatalf("consumeRecordingStocks (initial) failed: %v", err) - } - - assertWarehouseQuantity(t, db, productWarehouse.Id, 2) - - desired := 4.0 - stock.UsageQty = &desired - - if err := svc.ConsumeRecordingStocks(ctx, db, []entity.RecordingStock{stock}); err != nil { - t.Fatalf("consumeRecordingStocks (edit) failed: %v", err) - } - - updated := fetchRecordingStock(t, db, stock.Id) - assertFloatEqual(t, 4, updated.UsageQty, "usage_qty should reflect edited request") - assertFloatEqual(t, 0, updated.PendingQty, "pending_qty should remain zero after downsize") - assertWarehouseQuantity(t, db, productWarehouse.Id, 8) - - alloc := fetchSingleAllocation(t, db, stock.Id) - if alloc.Status != entity.StockAllocationStatusActive { - t.Fatalf("expected ACTIVE allocation, got %s", alloc.Status) - } - if mathAbs(alloc.Qty-4) > 1e-6 { - t.Fatalf("expected allocation qty 4, got %.3f", alloc.Qty) - } -} - -func TestRecordingFIFO_DeleteReleasesStock(t *testing.T) { - db, svc, fifoSvc, stockableKey := setupRecordingFIFOTableTest(t) - ctx := context.Background() - - recordingID := uint(1) - productWarehouse := createProductWarehouseRow(t, db, 0) - stock := createRecordingStockRow(t, db, recordingID, productWarehouse.Id, 10) - lot := createStockLot(t, db, productWarehouse.Id) - - if _, err := fifoSvc.Replenish(ctx, commonSvc.StockReplenishRequest{ - StockableKey: stockableKey, - StockableID: lot.Id, - ProductWarehouseID: productWarehouse.Id, - Quantity: 10, - }); err != nil { - t.Fatalf("replenish failed: %v", err) - } - if err := svc.ConsumeRecordingStocks(ctx, db, []entity.RecordingStock{stock}); err != nil { - t.Fatalf("consumeRecordingStocks failed: %v", err) - } - - if err := svc.ReleaseRecordingStocks(ctx, db, []entity.RecordingStock{stock}); err != nil { - t.Fatalf("releaseRecordingStocks failed: %v", err) - } - - updated := fetchRecordingStock(t, db, stock.Id) - assertFloatEqual(t, 0, updated.UsageQty, "usage_qty should be cleared after delete") - assertFloatEqual(t, 0, updated.PendingQty, "pending_qty should be cleared after delete") - assertWarehouseQuantity(t, db, productWarehouse.Id, 10) - - alloc := fetchSingleAllocation(t, db, stock.Id) - if alloc.Status != entity.StockAllocationStatusReleased { - t.Fatalf("expected allocation to be released, got %s", alloc.Status) - } -} - -// --- helpers ---------------------------------------------------------------- - -type recordingStockTable struct { - Id uint `gorm:"primaryKey"` - RecordingId uint `gorm:"column:recording_id;not null"` - ProductWarehouseId uint `gorm:"column:product_warehouse_id;not null"` - UsageQty *float64 `gorm:"column:usage_qty"` - PendingQty *float64 `gorm:"column:pending_qty"` - CreatedAt time.Time - UpdatedAt time.Time -} - -func (recordingStockTable) TableName() string { return "recording_stocks" } - -type productWarehouseTable struct { - Id uint `gorm:"primaryKey"` - ProductId uint `gorm:"column:product_id"` - WarehouseId uint `gorm:"column:warehouse_id"` - Quantity float64 `gorm:"column:quantity"` - CreatedBy uint `gorm:"column:created_by"` - CreatedAt time.Time - UpdatedAt time.Time - DeletedAt gorm.DeletedAt `gorm:"index"` -} - -func (productWarehouseTable) TableName() string { return "product_warehouses" } - -type stockAllocationTable struct { - Id uint `gorm:"primaryKey"` - ProductWarehouseId uint `gorm:"not null"` - StockableType string `gorm:"size:100"` - StockableId uint - UsableType string `gorm:"size:100"` - UsableId uint - Qty float64 `gorm:"column:qty"` - Status string `gorm:"size:20"` - Note *string `gorm:"type:text"` - CreatedAt time.Time - UpdatedAt time.Time - ReleasedAt *time.Time - DeletedAt gorm.DeletedAt `gorm:"index"` -} - -func (stockAllocationTable) TableName() string { return "stock_allocations" } - -type testStockSource struct { - Id uint `gorm:"primaryKey"` - ProductWarehouseId uint `gorm:"column:product_warehouse_id;not null"` - TotalQty float64 `gorm:"column:total_qty"` - TotalUsedQty float64 `gorm:"column:total_used_qty"` - CreatedAt time.Time `gorm:"column:created_at"` - UpdatedAt time.Time -} - -func (testStockSource) TableName() string { return "test_fifo_stockables" } - -func setupRecordingFIFOTableTest(t *testing.T) (*gorm.DB, servicePkg.RecordingFIFOIntegrationService, commonSvc.FifoService, fifo.StockableKey) { - t.Helper() - - db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{ - Logger: logger.Default.LogMode(logger.Silent), - }) - if err != nil { - t.Fatalf("open sqlite: %v", err) - } - - if err := db.AutoMigrate( - &recordingStockTable{}, - &productWarehouseTable{}, - &stockAllocationTable{}, - &testStockSource{}, - ); err != nil { - t.Fatalf("auto migrate: %v", err) - } - - if err := db.AutoMigrate( - &entity.ProductWarehouse{}, - &entity.StockAllocation{}, - &entity.RecordingStock{}, - ); err != nil { - t.Fatalf("auto migrate entities: %v", err) - } - - stockAllocRepo := newFifoTestStockAllocationRepo(db) - productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db) - fifoSvc := commonSvc.NewFifoService(db, stockAllocRepo, productWarehouseRepo, utils.Log) - - registerRecordingUsable(t, fifoSvc) - - key := fifo.StockableKey(fmt.Sprintf("TEST_STOCKABLE_%s_%d", sanitizeKey(t.Name()), time.Now().UnixNano())) - if err := fifoSvc.RegisterStockable(fifo.StockableConfig{ - Key: key, - Table: "test_fifo_stockables", - Columns: fifo.StockableColumns{ - ID: "id", - ProductWarehouseID: "product_warehouse_id", - TotalQuantity: "total_qty", - TotalUsedQuantity: "total_used_qty", - CreatedAt: "created_at", - }, - }); err != nil { - t.Fatalf("register stockable: %v", err) - } - - svc := servicePkg.NewRecordingFIFOIntegrationService( - recordingRepo.NewRecordingRepository(db), - productWarehouseRepo, - fifoSvc, - ) - - return db, svc, fifoSvc, key -} - -func registerRecordingUsable(t *testing.T, fifoSvc commonSvc.FifoService) { - t.Helper() - err := fifoSvc.RegisterUsable(fifo.UsableConfig{ - Key: fifo.UsableKeyRecordingStock, - Table: "recording_stocks", - Columns: fifo.UsableColumns{ - ID: "id", - ProductWarehouseID: "product_warehouse_id", - UsageQuantity: "usage_qty", - PendingQuantity: "pending_qty", - CreatedAt: "created_at", - }, - }) - if err != nil && !strings.Contains(strings.ToLower(err.Error()), "already registered") { - t.Fatalf("register usable: %v", err) - } - if _, ok := fifo.Usable(fifo.UsableKeyRecordingStock); !ok { - t.Fatal("recording stock usable key not registered") - } -} - -func createProductWarehouseRow(t *testing.T, db *gorm.DB, qty float64) entity.ProductWarehouse { - t.Helper() - pw := entity.ProductWarehouse{ - ProductId: 1, - WarehouseId: 1, - Quantity: qty, - // CreatedBy: 1, - } - if err := db.Create(&pw).Error; err != nil { - t.Fatalf("create product warehouse: %v", err) - } - return pw -} - -func createRecordingStockRow(t *testing.T, db *gorm.DB, recordingID, productWarehouseID uint, desired float64) entity.RecordingStock { - t.Helper() - stock := entity.RecordingStock{ - RecordingId: recordingID, - ProductWarehouseId: productWarehouseID, - UsageQty: floatPtr(0), - PendingQty: floatPtr(0), - } - if err := db.Create(&stock).Error; err != nil { - t.Fatalf("create recording stock: %v", err) - } - stock.UsageQty = floatPtr(desired) - return stock -} - -func createStockLot(t *testing.T, db *gorm.DB, productWarehouseID uint) testStockSource { - t.Helper() - lot := testStockSource{ - ProductWarehouseId: productWarehouseID, - CreatedAt: time.Now(), - } - if err := db.Create(&lot).Error; err != nil { - t.Fatalf("create stock lot: %v", err) - } - return lot -} - -func fetchRecordingStock(t *testing.T, db *gorm.DB, id uint) entity.RecordingStock { - t.Helper() - var stock entity.RecordingStock - if err := db.First(&stock, id).Error; err != nil { - t.Fatalf("fetch recording stock: %v", err) - } - return stock -} - -func fetchSingleAllocation(t *testing.T, db *gorm.DB, usableID uint) entity.StockAllocation { - t.Helper() - var alloc entity.StockAllocation - if err := db.Where("usable_id = ?", usableID).Order("created_at ASC").First(&alloc).Error; err != nil { - t.Fatalf("fetch allocation: %v", err) - } - return alloc -} - -func assertAllocationCount(t *testing.T, db *gorm.DB, expected int64) { - t.Helper() - var count int64 - if err := db.Model(&entity.StockAllocation{}).Count(&count).Error; err != nil { - t.Fatalf("count allocations: %v", err) - } - if count != expected { - t.Fatalf("expected %d allocations, got %d", expected, count) - } -} - -func assertWarehouseQuantity(t *testing.T, db *gorm.DB, id uint, expected float64) { - t.Helper() - var pw entity.ProductWarehouse - if err := db.First(&pw, id).Error; err != nil { - t.Fatalf("fetch product warehouse: %v", err) - } - if mathAbs(pw.Quantity-expected) > 1e-6 { - t.Fatalf("expected warehouse quantity %.3f, got %.3f", expected, pw.Quantity) - } -} - -func assertFloatEqual(t *testing.T, expected float64, value *float64, msg string) { - t.Helper() - if value == nil { - t.Fatalf("expected %s %.3f, got nil", msg, expected) - } - if mathAbs(*value-expected) > 1e-6 { - t.Fatalf("%s: expected %.3f, got %.3f", msg, expected, *value) - } -} - -func floatPtr(v float64) *float64 { - p := new(float64) - *p = v - return p -} - -func mathAbs(v float64) float64 { - if v < 0 { - return -v - } - return v -} - -func sanitizeKey(name string) string { - if name == "" { - return "CASE" - } - clean := strings.Map(func(r rune) rune { - if (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') { - return r - } - if r >= 'a' && r <= 'z' { - return r - 32 - } - return '_' - }, name) - return clean -} - -type fifoTestStockAllocationRepo struct { - commonRepo.StockAllocationRepository - db *gorm.DB -} - -func newFifoTestStockAllocationRepo(db *gorm.DB) commonRepo.StockAllocationRepository { - return &fifoTestStockAllocationRepo{ - StockAllocationRepository: commonRepo.NewStockAllocationRepository(db), - db: db, - } -} - -func (r *fifoTestStockAllocationRepo) PatchOne( - ctx context.Context, - id uint, - updates map[string]any, - modifier func(*gorm.DB) *gorm.DB, -) error { - base := r.db - - setClauses := make([]string, 0, len(updates)) - args := make([]any, 0, len(updates)+1) - for column, value := range updates { - colName := column - if strings.EqualFold(column, "quantity") { - colName = "qty" - } - setClauses = append(setClauses, fmt.Sprintf("%s = ?", colName)) - args = append(args, value) - } - args = append(args, id) - sql := fmt.Sprintf("UPDATE stock_allocations SET %s WHERE id = ?", strings.Join(setClauses, ", ")) - - result := base.Exec(sql, args...) - if result.Error != nil { - return result.Error - } - if result.RowsAffected == 0 { - return gorm.ErrRecordNotFound - } - return nil -} - -func (r *fifoTestStockAllocationRepo) ReleaseByUsable( - ctx context.Context, - usableType string, - usableID uint, - note *string, - modifier func(*gorm.DB) *gorm.DB, -) error { - base := r.db - - setClause := "status = ?, released_at = ?" - args := []any{entity.StockAllocationStatusReleased, time.Now()} - if note != nil { - setClause += ", note = ?" - args = append(args, *note) - } - args = append(args, usableType, usableID, entity.StockAllocationStatusActive) - sql := fmt.Sprintf( - "UPDATE stock_allocations SET %s WHERE usable_type = ? AND usable_id = ? AND status = ?", - setClause, - ) - - result := base.Exec(sql, args...) - return result.Error -} From 8410573ee6c28b90a3da568be76e0d72dbcb3ae8 Mon Sep 17 00:00:00 2001 From: ragilap Date: Tue, 27 Jan 2026 22:28:14 +0700 Subject: [PATCH 7/7] [FIX/BE-US] add transfer to laying filter location and area --- .../laying_transfer.repository.go | 21 +++++++++++++++++++ .../services/transfer_laying.service.go | 12 ++++++----- 2 files changed, 28 insertions(+), 5 deletions(-) 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 14fa4118..ebf63252 100644 --- a/internal/modules/production/transfer_layings/repositories/laying_transfer.repository.go +++ b/internal/modules/production/transfer_layings/repositories/laying_transfer.repository.go @@ -52,6 +52,8 @@ type GetAllFilterParams struct { FlockSource []uint FlockDestination []uint Status []string + LocationIDs []uint + LocationRestrict bool } func (r *TransferLayingRepositoryImpl) GetAllWithFilters(ctx context.Context, offset int, limit int, params *GetAllFilterParams) ([]entity.LayingTransfer, int64, error) { @@ -110,6 +112,25 @@ func (r *TransferLayingRepositoryImpl) GetAllWithFilters(ctx context.Context, of } } + if params.LocationRestrict { + if len(params.LocationIDs) == 0 { + q = q.Where("1 = 0") + } else { + q = q.Where( + `EXISTS ( + SELECT 1 FROM project_flocks pf + WHERE pf.id = laying_transfers.from_project_flock_id + AND pf.location_id IN ? + ) OR EXISTS ( + SELECT 1 FROM project_flocks pf + WHERE pf.id = laying_transfers.to_project_flock_id + AND pf.location_id IN ? + )`, + params.LocationIDs, params.LocationIDs, + ) + } + } + if err := q.Count(&total).Error; err != nil { return nil, 0, err } 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 60096547..9c586064 100644 --- a/internal/modules/production/transfer_layings/services/transfer_laying.service.go +++ b/internal/modules/production/transfer_layings/services/transfer_laying.service.go @@ -107,6 +107,11 @@ func (s transferLayingService) GetAll(c *fiber.Ctx, params *validation.Query) ([ return nil, 0, err } + scope, err := m.ResolveLocationScope(c, s.Repository.DB()) + if err != nil { + return nil, 0, err + } + offset := (params.Page - 1) * params.Limit filterParams := &repository.GetAllFilterParams{ @@ -116,6 +121,8 @@ func (s transferLayingService) GetAll(c *fiber.Ctx, params *validation.Query) ([ FlockSource: params.FlockSource, FlockDestination: params.FlockDestination, Status: params.Status, + LocationIDs: scope.IDs, + LocationRestrict: scope.Restrict, } transferLayings, total, err := s.Repository.GetAllWithFilters(c.Context(), offset, params.Limit, filterParams) @@ -124,11 +131,6 @@ func (s transferLayingService) GetAll(c *fiber.Ctx, params *validation.Query) ([ return nil, 0, err } - if err != nil { - s.Log.Errorf("Failed to get transferLayings: %+v", err) - return nil, 0, err - } - approvalRepo := commonRepo.NewApprovalRepository(s.Repository.DB()) for i, transfer := range transferLayings { latestApproval, err := approvalRepo.LatestByTarget(c.Context(), string(utils.ApprovalWorkflowTransferToLaying), transfer.Id, func(db *gorm.DB) *gorm.DB {