From 730fb22cc2801c9395ec76b4e6a73c187ccc0817 Mon Sep 17 00:00:00 2001 From: giovanni-ce Date: Wed, 3 Dec 2025 19:47:12 +0700 Subject: [PATCH] Feat[BE-297]: Create sub module inventory product stock; create api list product stock and api get one product stock --- .../controllers/product-stock.controller.go | 77 +++++++ .../product-stocks/dto/product-stock.dto.go | 202 ++++++++++++++++++ .../inventory/product-stocks/module.go | 25 +++ .../repositories/product-stock.repository.go | 21 ++ .../modules/inventory/product-stocks/route.go | 25 +++ .../services/product-stock.service.go | 89 ++++++++ .../validations/product-stock.validation.go | 15 ++ internal/modules/inventory/route.go | 2 + 8 files changed, 456 insertions(+) create mode 100644 internal/modules/inventory/product-stocks/controllers/product-stock.controller.go create mode 100644 internal/modules/inventory/product-stocks/dto/product-stock.dto.go create mode 100644 internal/modules/inventory/product-stocks/module.go create mode 100644 internal/modules/inventory/product-stocks/repositories/product-stock.repository.go create mode 100644 internal/modules/inventory/product-stocks/route.go create mode 100644 internal/modules/inventory/product-stocks/services/product-stock.service.go create mode 100644 internal/modules/inventory/product-stocks/validations/product-stock.validation.go diff --git a/internal/modules/inventory/product-stocks/controllers/product-stock.controller.go b/internal/modules/inventory/product-stocks/controllers/product-stock.controller.go new file mode 100644 index 00000000..430941ae --- /dev/null +++ b/internal/modules/inventory/product-stocks/controllers/product-stock.controller.go @@ -0,0 +1,77 @@ +package controller + +import ( + "math" + "strconv" + + "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-stocks/dto" + service "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-stocks/services" + validation "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-stocks/validations" + "gitlab.com/mbugroup/lti-api.git/internal/response" + + "github.com/gofiber/fiber/v2" + // entity "gitlab.com/mbugroup/lti-api.git/internal/entities" +) + +type ProductStockController struct { + ProductStockService service.ProductStockService +} + +func NewProductStockController(productStockService service.ProductStockService) *ProductStockController { + return &ProductStockController{ + ProductStockService: productStockService, + } +} + +func (u *ProductStockController) GetAll(c *fiber.Ctx) error { + query := &validation.Query{ + Page: c.QueryInt("page", 1), + Limit: c.QueryInt("limit", 10), + Search: c.Query("search", ""), + } + + if query.Page < 1 || query.Limit < 1 { + return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0") + } + + result, totalResults, err := u.ProductStockService.GetAll(c, query) + if err != nil { + return err + } + + return c.Status(fiber.StatusOK). + JSON(response.SuccessWithPaginate[dto.ProductStockListDTO]{ + Code: fiber.StatusOK, + Status: "success", + Message: "Get all productStocks successfully", + Meta: response.Meta{ + Page: query.Page, + Limit: query.Limit, + TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))), + TotalResults: totalResults, + }, + Data: dto.ToProductStockListDTOs(result), + }) +} + +func (u *ProductStockController) GetOne(c *fiber.Ctx) error { + param := c.Params("id") + + id, err := strconv.Atoi(param) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, "Invalid Id") + } + + res, err := u.ProductStockService.GetOne(c, uint(id)) + if err != nil { + return err + } + + return c.Status(fiber.StatusOK). + JSON(response.Success{ + Code: fiber.StatusOK, + Status: "success", + Message: "Retrieved product successfully", + Data: dto.ToProductStockDetailDTO(*res), + }) +} diff --git a/internal/modules/inventory/product-stocks/dto/product-stock.dto.go b/internal/modules/inventory/product-stocks/dto/product-stock.dto.go new file mode 100644 index 00000000..aa9f98c2 --- /dev/null +++ b/internal/modules/inventory/product-stocks/dto/product-stock.dto.go @@ -0,0 +1,202 @@ +package dto + +import ( + "time" + + entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + productCategoryDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/product-categories/dto" + uomDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/uoms/dto" + userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto" +) + +// === DTO Structs === + +type ProductStockRelationDTO struct { + Id uint `json:"id"` + Name string `json:"name"` +} + +type ProductStockListDTO struct { + Id uint `json:"id"` + Name string `json:"name"` + Brand string `json:"brand"` + Sku *string `json:"sku,omitempty"` + ProductPrice float64 `json:"product_price"` + SellingPrice *float64 `json:"selling_price,omitempty"` + Tax *float64 `json:"tax,omitempty"` + ExpiryPeriod *int `json:"expiry_period,omitempty"` + Flags []string `json:"flags"` + Uom *uomDTO.UomRelationDTO `json:"uom,omitempty"` + ProductCategory *productCategoryDTO.ProductCategoryRelationDTO `json:"product_category,omitempty"` + CreatedUser *userDTO.UserRelationDTO `json:"created_user"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + Suppliers []SupplierDTO `json:"suppliers,omitempty"` + ProductWarehouses []ProductWarehouseDTO `json:"product_warehouses,omitempty"` +} + +type ProductStockDetailDTO struct { + ProductStockListDTO +} + +type SupplierDTO struct { + Id uint `json:"id"` + Name string `json:"name"` + Alias string `json:"alias"` + Category string `json:"category"` +} + +type ProductWarehouseDTO struct { + Id uint `json:"id"` + ProductId uint `json:"product_id"` + WarehouseId uint `json:"warehouse_id"` + WarehouseName string `json:"warehouse_name"` + Location string `json:"location"` + CurrentStock float64 `json:"current_stock"` + StockLogs []StockLogDetailDTO `json:"stock_logs"` +} + +type StockLogDetailDTO struct { + Id uint `json:"id"` + Increase float64 `json:"increase"` + Decrease float64 `json:"decrease"` + LoggableType string `json:"loggable_type"` + LoggableId uint `json:"loggable_id"` + Notes *string `json:"notes"` + ProductWarehouseId uint `json:"product_warehouse_id"` + CreatedBy uint `json:"created_by"` + CreatedAt time.Time `json:"created_at"` +} + +// === Mapper Functions === +func ToProductStockListDTO(e entity.Product) ProductStockListDTO { + var createdUser *userDTO.UserRelationDTO + if e.CreatedUser.Id != 0 { + mapped := userDTO.ToUserRelationDTO(e.CreatedUser) + createdUser = &mapped + } + + var categoryRef *productCategoryDTO.ProductCategoryRelationDTO + if e.ProductCategory.Id != 0 { + mapped := productCategoryDTO.ToProductCategoryRelationDTO(e.ProductCategory) + categoryRef = &mapped + } + + flags := make([]string, len(e.Flags)) + for i, f := range e.Flags { + flags[i] = f.Name + } + + var uomRef *uomDTO.UomRelationDTO + if e.Uom.Id != 0 { + mapped := uomDTO.ToUomRelationDTO(e.Uom) + uomRef = &mapped + } + + return ProductStockListDTO{ + Id: e.Id, + Name: e.Name, + Flags: flags, + Uom: uomRef, + Brand: e.Brand, + Sku: e.Sku, + ProductPrice: e.ProductPrice, + SellingPrice: e.SellingPrice, + Tax: e.Tax, + ExpiryPeriod: e.ExpiryPeriod, + CreatedAt: e.CreatedAt, + UpdatedAt: e.UpdatedAt, + CreatedUser: createdUser, + ProductCategory: categoryRef, + Suppliers: mapSupplierDTOs(e.ProductSuppliers), + } +} + +func ToProductStockListDTOs(e []entity.Product) []ProductStockListDTO { + result := make([]ProductStockListDTO, len(e)) + for i, r := range e { + result[i] = ToProductStockListDTO(r) + } + return result +} + +func ToProductStockDetailDTO(e entity.Product) ProductStockDetailDTO { + base := ToProductStockListDTO(e) + base.ProductWarehouses = mapProductWarehouseDTOs(e.ProductWarehouses) + + return ProductStockDetailDTO{ + ProductStockListDTO: base, + } +} + +// --- helpers --- + +func mapSupplierDTOs(src []entity.ProductSupplier) []SupplierDTO { + if len(src) == 0 { + return nil + } + result := make([]SupplierDTO, 0, len(src)) + for _, ps := range src { + if ps.Supplier.Id == 0 { + continue + } + result = append(result, SupplierDTO{ + Id: ps.Supplier.Id, + Name: ps.Supplier.Name, + Alias: ps.Supplier.Alias, + Category: ps.Supplier.Category, + }) + } + return result +} + +func mapProductWarehouseDTOs(src []entity.ProductWarehouse) []ProductWarehouseDTO { + if len(src) == 0 { + return []ProductWarehouseDTO{} + } + result := make([]ProductWarehouseDTO, 0, len(src)) + for _, pw := range src { + dto := ProductWarehouseDTO{ + Id: pw.Id, + ProductId: pw.ProductId, + WarehouseId: pw.WarehouseId, + CurrentStock: pw.Quantity, + StockLogs: mapStockLogs(pw.StockLogs), + } + if pw.Warehouse.Id != 0 { + dto.WarehouseName = pw.Warehouse.Name + if pw.Warehouse.Location != nil { + dto.Location = pw.Warehouse.Location.Name + } + } + result = append(result, dto) + } + return result +} + +func mapStockLogs(src []entity.StockLog) []StockLogDetailDTO { + if len(src) == 0 { + return []StockLogDetailDTO{} + } + result := make([]StockLogDetailDTO, 0, len(src)) + for _, log := range src { + var notes *string + if log.Notes != "" { + n := log.Notes + notes = &n + } + + result = append(result, StockLogDetailDTO{ + Id: log.Id, + Increase: log.Increase, + Decrease: log.Decrease, + LoggableType: log.LoggableType, + LoggableId: log.LoggableId, + Notes: notes, + ProductWarehouseId: log.ProductWarehouseId, + CreatedBy: log.CreatedBy, + CreatedAt: log.CreatedAt, + }) + } + return result +} diff --git a/internal/modules/inventory/product-stocks/module.go b/internal/modules/inventory/product-stocks/module.go new file mode 100644 index 00000000..43bcd1be --- /dev/null +++ b/internal/modules/inventory/product-stocks/module.go @@ -0,0 +1,25 @@ +package productStocks + +import ( + "github.com/go-playground/validator/v10" + "github.com/gofiber/fiber/v2" + "gorm.io/gorm" + + sProductStock "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-stocks/services" + + rProduct "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/repositories" + rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories" + sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" +) + +type ProductStockModule struct{} + +func (ProductStockModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) { + productRepo := rProduct.NewProductRepository(db) + userRepo := rUser.NewUserRepository(db) + + productStockService := sProductStock.NewProductStockService(productRepo, validate) + userService := sUser.NewUserService(userRepo, validate) + + ProductStockRoutes(router, userService, productStockService) +} diff --git a/internal/modules/inventory/product-stocks/repositories/product-stock.repository.go b/internal/modules/inventory/product-stocks/repositories/product-stock.repository.go new file mode 100644 index 00000000..d6e5368d --- /dev/null +++ b/internal/modules/inventory/product-stocks/repositories/product-stock.repository.go @@ -0,0 +1,21 @@ +package repository + +// import ( +// entity "gitlab.com/mbugroup/lti-api.git/internal/entities" +// "gitlab.com/mbugroup/lti-api.git/internal/common/repository" +// "gorm.io/gorm" +// ) + +// type ProductStockRepository interface { +// repository.BaseRepository[entity.ProductStock] +// } + +// type ProductStockRepositoryImpl struct { +// *repository.BaseRepositoryImpl[entity.ProductStock] +// } + +// func NewProductStockRepository(db *gorm.DB) ProductStockRepository { +// return &ProductStockRepositoryImpl{ +// BaseRepositoryImpl: repository.NewBaseRepository[entity.ProductStock](db), +// } +// } diff --git a/internal/modules/inventory/product-stocks/route.go b/internal/modules/inventory/product-stocks/route.go new file mode 100644 index 00000000..c7bb37f8 --- /dev/null +++ b/internal/modules/inventory/product-stocks/route.go @@ -0,0 +1,25 @@ +package productStocks + +import ( + // m "gitlab.com/mbugroup/lti-api.git/internal/middleware" + controller "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-stocks/controllers" + productStock "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-stocks/services" + user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" + + "github.com/gofiber/fiber/v2" +) + +func ProductStockRoutes(v1 fiber.Router, u user.UserService, s productStock.ProductStockService) { + ctrl := controller.NewProductStockController(s) + + route := v1.Group("/product-stocks") + + // route.Get("/", m.Auth(u), ctrl.GetAll) + // route.Post("/", m.Auth(u), ctrl.CreateOne) + // route.Get("/:id", m.Auth(u), ctrl.GetOne) + // route.Patch("/:id", m.Auth(u), ctrl.UpdateOne) + // route.Delete("/:id", m.Auth(u), ctrl.DeleteOne) + + route.Get("/", ctrl.GetAll) + route.Get("/:id", ctrl.GetOne) +} diff --git a/internal/modules/inventory/product-stocks/services/product-stock.service.go b/internal/modules/inventory/product-stocks/services/product-stock.service.go new file mode 100644 index 00000000..5cf6ec17 --- /dev/null +++ b/internal/modules/inventory/product-stocks/services/product-stock.service.go @@ -0,0 +1,89 @@ +package service + +import ( + "errors" + + entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + 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" + + "github.com/go-playground/validator/v10" + "github.com/gofiber/fiber/v2" + "github.com/sirupsen/logrus" + "gorm.io/gorm" +) + +type ProductStockService interface { + GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.Product, int64, error) + GetOne(ctx *fiber.Ctx, id uint) (*entity.Product, error) +} + +type productStockService struct { + Log *logrus.Logger + Validate *validator.Validate + ProductRepository productRepository.ProductRepository +} + +func NewProductStockService( + productRepo productRepository.ProductRepository, + validate *validator.Validate, +) ProductStockService { + return &productStockService{ + Log: utils.Log, + Validate: validate, + ProductRepository: productRepo, + } +} + +func (s productStockService) withRelations(db *gorm.DB) *gorm.DB { + return db. + Preload("CreatedUser"). + Preload("Uom"). + Preload("ProductCategory"). + Preload("Flags"). + Preload("ProductWarehouses"). + Preload("ProductWarehouses.Warehouse"). + Preload("ProductWarehouses.Warehouse.Location"). + Preload("ProductWarehouses.StockLogs", func(db *gorm.DB) *gorm.DB { + return db.Order("created_at ASC") + }). + Preload("ProductSuppliers"). + Preload("ProductSuppliers.Supplier", func(db *gorm.DB) *gorm.DB { + return db.Order("suppliers.name ASC") + }) +} + +func (s productStockService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.Product, int64, error) { + if err := s.Validate.Struct(params); 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 = s.withRelations(db) + if params.Search != "" { + return db.Where("name ILIKE ?", "%"+params.Search+"%") + } + return db.Order("created_at DESC").Order("updated_at DESC") + }) + + if err != nil { + s.Log.Errorf("Failed to get productStocks: %+v", err) + return nil, 0, err + } + return productStocks, total, nil +} + +func (s productStockService) GetOne(c *fiber.Ctx, id uint) (*entity.Product, error) { + 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") + } + if err != nil { + s.Log.Errorf("Failed get product by id: %+v", err) + return nil, err + } + return product, nil +} diff --git a/internal/modules/inventory/product-stocks/validations/product-stock.validation.go b/internal/modules/inventory/product-stocks/validations/product-stock.validation.go new file mode 100644 index 00000000..7d16d3ee --- /dev/null +++ b/internal/modules/inventory/product-stocks/validations/product-stock.validation.go @@ -0,0 +1,15 @@ +package validation + +type Create struct { + Name string `json:"name" validate:"required_strict,min=3"` +} + +type Update struct { + Name *string `json:"name,omitempty" validate:"omitempty"` +} + +type Query struct { + Page int `query:"page" validate:"omitempty,number,min=1,gt=0"` + Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"` + Search string `query:"search" validate:"omitempty,max=50"` +} diff --git a/internal/modules/inventory/route.go b/internal/modules/inventory/route.go index a0e98154..0d4d2f4b 100644 --- a/internal/modules/inventory/route.go +++ b/internal/modules/inventory/route.go @@ -8,6 +8,7 @@ import ( "gorm.io/gorm" adjustments "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/adjustments" + productStocks "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-stocks" productWarehouses "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses" transfers "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/transfers" // MODULE IMPORTS @@ -21,6 +22,7 @@ func RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Valida adjustments.AdjustmentModule{}, transfers.TransferModule{}, + productStocks.ProductStockModule{}, // MODULE REGISTRY }