From 00cdfb692b6407bf56bdb8aff5a938308750062e Mon Sep 17 00:00:00 2001 From: ragilap Date: Tue, 27 Jan 2026 10:34:25 +0700 Subject: [PATCH] [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 -}