From 2ef6052a2c47aeb83fe0bf77092ffdf6dd39155d Mon Sep 17 00:00:00 2001 From: Habib Fatkhul Rohman Date: Tue, 25 Nov 2025 15:57:08 +0700 Subject: [PATCH] feat: Add OnComplete functionality to Inventory Receipt - Implemented OnComplete method in InventoryReceiptController to handle completion of inventory receipts. - Added success and failure messages in dto for completing inventory receipts. - Updated routes to include the new OnComplete endpoint. - Enhanced InventoryReceiptService to manage the completion logic, including updating inventory storage. - Introduced new methods in InventoryStorageRepository for bulk creation and retrieval of inventory storages. - Added methods in ProductService to fetch inventory storages and transactions by product and client. - Updated ProductController to handle new endpoints for inventory storages and transactions. - Enhanced Product DTO to include inventory storage and transaction responses. - Refactored ProductRepository to retrieve cross-references and preloaded relationships for inventory storages and transactions. - Updated core dependency injection to include new repositories and services. --- .../inventory_receipt_controller.go | 14 + .../dto/inventory_receipt_dto.go | 10 +- modules/inventory_receipt/routes.go | 1 + .../service/inventory_receipt_service.go | 111 ++++++- .../dto/inventory_request_dto.go | 9 +- .../service/inventory_request_service.go | 1 + .../dto/inventory_storage_dto.go | 4 +- .../inventory_storage_repository.go | 55 ++++ .../service/inventory_storage_service.go | 26 +- .../inventory_transaction_repository.go | 21 ++ .../product/controller/product_controller.go | 46 +++ modules/product/dto/product_dto.go | 296 ++++++++++-------- .../product/repository/product_repository.go | 21 ++ modules/product/routes.go | 3 + modules/product/service/product_service.go | 58 +++- providers/core.go | 4 +- 16 files changed, 521 insertions(+), 159 deletions(-) diff --git a/modules/inventory_receipt/controller/inventory_receipt_controller.go b/modules/inventory_receipt/controller/inventory_receipt_controller.go index b8d12c1..e6eb016 100644 --- a/modules/inventory_receipt/controller/inventory_receipt_controller.go +++ b/modules/inventory_receipt/controller/inventory_receipt_controller.go @@ -22,6 +22,7 @@ type InventoryReceiptController interface { CreateLine(ctx *gin.Context) UpdateLine(ctx *gin.Context) DeleteLine(ctx *gin.Context) + OnComplete(ctx *gin.Context) } type inventoryReceiptController struct { @@ -29,6 +30,19 @@ type inventoryReceiptController struct { db *gorm.DB } +// OnComplete implements InventoryReceiptController. +func (c *inventoryReceiptController) OnComplete(ctx *gin.Context) { + id := ctx.Param("id") + updated, err := c.receiptService.OnComplete(ctx, id) + if err != nil { + res := utils.BuildResponseFailed(dto.MESSAGE_FAILED_COMPLETE_INVENTORY_RECEIPT, err.Error(), nil) + ctx.JSON(http.StatusInternalServerError, res) + return + } + res := utils.BuildResponseSuccess(dto.MESSAGE_SUCCESS_COMPLETE_INVENTORY_RECEIPT, updated) + ctx.JSON(http.StatusOK, res) +} + // DeleteLine implements InventoryReceiptController. func (c *inventoryReceiptController) DeleteLine(ctx *gin.Context) { id := ctx.Param("id") diff --git a/modules/inventory_receipt/dto/inventory_receipt_dto.go b/modules/inventory_receipt/dto/inventory_receipt_dto.go index 1189881..287f6ae 100644 --- a/modules/inventory_receipt/dto/inventory_receipt_dto.go +++ b/modules/inventory_receipt/dto/inventory_receipt_dto.go @@ -18,6 +18,8 @@ const ( MESSAGE_SUCCESS_DELETE_INVENTORY_RECEIPT = "success delete inventory receipt" MESSAGE_SUCCESS_DELETE_INVENTORY_RECEIPT_LINE = "success delete inventory receipt line" MESSAGE_FAILED_GET_DATA_FROM_BODY = "failed get data from body" + MESSAGE_SUCCESS_COMPLETE_INVENTORY_RECEIPT = "success complete inventory receipt" + MESSAGE_FAILED_COMPLETE_INVENTORY_RECEIPT = "failed complete inventory receipt" ) type InventoryReceiptCreateRequest struct { @@ -34,10 +36,10 @@ type InventoryReceiptLineCreateRequest struct { Quantity float64 `json:"quantity"` BatchNumber string `json:"batch_number"` RepackingSuggestion string `json:"repacking_suggestion"` - RepackUomID string `json:"repack_uom_id"` - RepackUomCode string `json:"repack_uom_code"` - ProductID string `json:"product_id"` - ProductCode string `json:"product_code"` + RepackUomID string `json:"repack_uom_id"` + RepackUomCode string `json:"repack_uom_code"` + ProductID string `json:"product_id"` + ProductCode string `json:"product_code"` ClientID string `json:"client_id"` } diff --git a/modules/inventory_receipt/routes.go b/modules/inventory_receipt/routes.go index e25e733..ee7aede 100644 --- a/modules/inventory_receipt/routes.go +++ b/modules/inventory_receipt/routes.go @@ -23,5 +23,6 @@ func RegisterRoutes(server *gin.Engine, injector *do.Injector) { receiptRoutes.POST(":id/lines", middlewares.Authenticate(jwtService), receiptController.CreateLine) receiptRoutes.PUT("lines/:id", middlewares.Authenticate(jwtService), receiptController.UpdateLine) receiptRoutes.DELETE("lines/:id", middlewares.Authenticate(jwtService), receiptController.DeleteLine) + receiptRoutes.POST(":id/complete", middlewares.Authenticate(jwtService), receiptController.OnComplete) } } diff --git a/modules/inventory_receipt/service/inventory_receipt_service.go b/modules/inventory_receipt/service/inventory_receipt_service.go index a3d0857..b537742 100644 --- a/modules/inventory_receipt/service/inventory_receipt_service.go +++ b/modules/inventory_receipt/service/inventory_receipt_service.go @@ -2,13 +2,16 @@ package service import ( "context" + "errors" "github.com/Caknoooo/go-gin-clean-starter/database/entities" dtodomain "github.com/Caknoooo/go-gin-clean-starter/modules/inventory_receipt/dto" "github.com/Caknoooo/go-gin-clean-starter/modules/inventory_receipt/query" "github.com/Caknoooo/go-gin-clean-starter/modules/inventory_receipt/repository" + invstoragerepository "github.com/Caknoooo/go-gin-clean-starter/modules/inventory_storage/repository" productrepository "github.com/Caknoooo/go-gin-clean-starter/modules/product/repository" uomrepository "github.com/Caknoooo/go-gin-clean-starter/modules/uom/repository" + "github.com/Caknoooo/go-gin-clean-starter/pkg/constants" pkgdto "github.com/Caknoooo/go-gin-clean-starter/pkg/dto" "github.com/Caknoooo/go-gin-clean-starter/pkg/utils" "github.com/google/uuid" @@ -24,14 +27,75 @@ type InventoryReceiptService interface { CreateLine(ctx context.Context, receiptId string, req dtodomain.InventoryReceiptLineCreateRequest) (dtodomain.InventoryReceiptLineResponse, error) UpdateLine(ctx context.Context, lineId string, req dtodomain.InventoryReceiptLineUpdateRequest) (dtodomain.InventoryReceiptLineResponse, error) DeleteLine(ctx context.Context, lineId string) error + OnComplete(ctx context.Context, id string) (dtodomain.InventoryReceiptResponse, error) } type inventoryReceiptService struct { - db *gorm.DB - receiptRepo repository.InventoryReceiptRepository - receiptLineRepo repository.InventoryReceiptLineRepository - productRepo productrepository.ProductRepository - uomRepo uomrepository.UomRepository + db *gorm.DB + receiptRepo repository.InventoryReceiptRepository + receiptLineRepo repository.InventoryReceiptLineRepository + productRepo productrepository.ProductRepository + uomRepo uomrepository.UomRepository + invStorageRepository invstoragerepository.InventoryStorageRepository +} + +func (s *inventoryReceiptService) OnComplete(ctx context.Context, id string) (dtodomain.InventoryReceiptResponse, error) { + tx := s.db.Begin() + defer func() { + if r := recover(); r != nil { + tx.Rollback() + } + }() + receipt, err := s.receiptRepo.GetById(ctx, tx, id) + if err != nil { + tx.Rollback() + return dtodomain.InventoryReceiptResponse{}, err + } + receipt.Status = constants.COMPLETED + + for _, line := range receipt.ReceiptLines { + product, err := s.productRepo.GetById(ctx, tx, line.ProductID.String()) + if err != nil { + tx.Rollback() + return dtodomain.InventoryReceiptResponse{}, err + } + existing, err := s.invStorageRepository.GetByProductAndClient(ctx, tx, product.ID.String(), receipt.ClientID.String()) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + tx.Rollback() + return dtodomain.InventoryReceiptResponse{}, err + } + + if existing.ID != uuid.Nil { + existing.OnHandQuantity += line.Quantity + existing.AvailableQuantity += line.Quantity + _, err = s.invStorageRepository.Update(ctx, tx, existing) + if err != nil { + tx.Rollback() + return dtodomain.InventoryReceiptResponse{}, err + } + } else { + newStorage := entities.InventoryStorageEntity{ + ProductID: line.ProductID, + UomID: *product.UomID, + OnHandQuantity: line.Quantity, + AvailableQuantity: line.Quantity, + InvReceiptID: receipt.ID, + ClientID: receipt.ClientID, + } + _, err = s.invStorageRepository.Create(ctx, tx, newStorage) + if err != nil { + tx.Rollback() + return dtodomain.InventoryReceiptResponse{}, err + } + } + } + + if _, err := s.receiptRepo.Update(ctx, tx, receipt); err != nil { + tx.Rollback() + return dtodomain.InventoryReceiptResponse{}, err + } + tx.Commit() + return toInventoryReceiptResponse(receipt), nil } // DeleteLine implements InventoryReceiptService. @@ -235,6 +299,7 @@ func (s *inventoryReceiptService) Create(ctx context.Context, req dtodomain.Inve } // Bulk create lines var lines []entities.TInventoryReceiptLineEntity + // var invStorages []entities.InventoryStorageEntity for _, lineReq := range req.ReceiptLines { var productUUID uuid.UUID if lineReq.ProductID != "" { @@ -285,6 +350,21 @@ func (s *inventoryReceiptService) Create(ctx context.Context, req dtodomain.Inve ProductID: productUUID, ClientID: clientLineUUID, }) + + // // Prepare inventory storage entity + // product, err := s.productRepo.GetById(ctx, tx, productUUID.String()) + // if err != nil { + // tx.Rollback() + // return dtodomain.InventoryReceiptResponse{}, err + // } + // invStorages = append(invStorages, entities.InventoryStorageEntity{ + // ProductID: productUUID, + // UomID: *product.UomID, + // OnHandQuantity: 0, + // AvailableQuantity: 0, + // InvReceiptID: created.ID, + // ClientID: clientUUID, + // }) } if len(lines) > 0 { err = s.receiptLineRepo.BulkCreate(ctx, tx, lines) @@ -293,6 +373,13 @@ func (s *inventoryReceiptService) Create(ctx context.Context, req dtodomain.Inve return dtodomain.InventoryReceiptResponse{}, err } } + // if len(invStorages) > 0 { + // err = s.invStorageRepository.BulkCreate(ctx, tx, invStorages) + // if err != nil { + // tx.Rollback() + // return dtodomain.InventoryReceiptResponse{}, err + // } + // } tx.Commit() result, err := s.receiptRepo.GetById(ctx, nil, created.ID.String()) if err != nil { @@ -453,12 +540,14 @@ func NewInventoryReceiptService(db *gorm.DB, receiptRepo repository.InventoryReceiptRepository, receiptLineRepo repository.InventoryReceiptLineRepository, productRepo productrepository.ProductRepository, - uomRepo uomrepository.UomRepository) InventoryReceiptService { + uomRepo uomrepository.UomRepository, + invStorageRepository invstoragerepository.InventoryStorageRepository) InventoryReceiptService { return &inventoryReceiptService{ - db: db, - receiptRepo: receiptRepo, - receiptLineRepo: receiptLineRepo, - productRepo: productRepo, - uomRepo: uomRepo, + db: db, + receiptRepo: receiptRepo, + receiptLineRepo: receiptLineRepo, + productRepo: productRepo, + uomRepo: uomRepo, + invStorageRepository: invStorageRepository, } } diff --git a/modules/inventory_request/dto/inventory_request_dto.go b/modules/inventory_request/dto/inventory_request_dto.go index 537c155..97acfbd 100644 --- a/modules/inventory_request/dto/inventory_request_dto.go +++ b/modules/inventory_request/dto/inventory_request_dto.go @@ -75,10 +75,11 @@ type AssignmentUserResponse struct { } type InventoryRequestLineResponse struct { - ID string `json:"id"` - Quantity float64 `json:"quantity"` - Product InventoryRequestLineProductResponse `json:"product"` - ClientID string `json:"client_id"` + ID string `json:"id"` + Quantity float64 `json:"quantity"` + CurrentStock float64 `json:"current_stock"` + Product InventoryRequestLineProductResponse `json:"product"` + ClientID string `json:"client_id"` } type InventoryRequestLineProductResponse struct { diff --git a/modules/inventory_request/service/inventory_request_service.go b/modules/inventory_request/service/inventory_request_service.go index 1df0c7f..4d837c8 100644 --- a/modules/inventory_request/service/inventory_request_service.go +++ b/modules/inventory_request/service/inventory_request_service.go @@ -121,6 +121,7 @@ func toInventoryRequestResponse(e entities.TInventoryRequestEntity) dtodomain.In lines = append(lines, dtodomain.InventoryRequestLineResponse{ ID: line.ID.String(), Quantity: line.Quantity, + // CurrentStock: line.Product.CurrentStock, Product: product, ClientID: line.ClientID.String(), }) diff --git a/modules/inventory_storage/dto/inventory_storage_dto.go b/modules/inventory_storage/dto/inventory_storage_dto.go index 3f69012..94becae 100644 --- a/modules/inventory_storage/dto/inventory_storage_dto.go +++ b/modules/inventory_storage/dto/inventory_storage_dto.go @@ -33,8 +33,8 @@ type InventoryStorageCreateRequest struct { ClientID string `json:"client_id" binding:"required"` OnHandQuantity float64 `json:"on_hand_quantity"` AvailableQuantity float64 `json:"available_quantity"` - InvRequestID string `json:"inv_request_id"` - InvReceiptID string `json:"inv_receipt_id"` + InvRequestID *string `json:"inv_request_id"` + InvReceiptID *string `json:"inv_receipt_id"` } type InventoryStorageUpdateRequest struct { diff --git a/modules/inventory_storage/repository/inventory_storage_repository.go b/modules/inventory_storage/repository/inventory_storage_repository.go index 4c348dc..e39fa5b 100644 --- a/modules/inventory_storage/repository/inventory_storage_repository.go +++ b/modules/inventory_storage/repository/inventory_storage_repository.go @@ -6,6 +6,7 @@ import ( "github.com/Caknoooo/go-gin-clean-starter/database/entities" "github.com/Caknoooo/go-gin-clean-starter/modules/inventory_storage/query" "gorm.io/gorm" + "gorm.io/gorm/clause" ) type InventoryStorageRepository interface { @@ -14,12 +15,66 @@ type InventoryStorageRepository interface { GetAll(ctx context.Context, filter query.InventoryStorageFilter) ([]entities.InventoryStorageEntity, int64, error) Update(ctx context.Context, tx *gorm.DB, inventoryStorage entities.InventoryStorageEntity) (entities.InventoryStorageEntity, error) Delete(ctx context.Context, tx *gorm.DB, inventoryStorageId string) error + BulkCreate(ctx context.Context, tx *gorm.DB, inventoryStorages []entities.InventoryStorageEntity) error + GetByProductAndClient(ctx context.Context, tx *gorm.DB, productId string, clientId string) (entities.InventoryStorageEntity, error) + GetStoragesByProductAndClient(ctx context.Context, tx *gorm.DB, productId string, clientId string) ([]entities.InventoryStorageEntity, error) } type inventoryStorageRepository struct { db *gorm.DB } +// GetStoragesByProductAndClient implements InventoryStorageRepository. +func (r *inventoryStorageRepository) GetStoragesByProductAndClient(ctx context.Context, tx *gorm.DB, productId string, clientId string) ([]entities.InventoryStorageEntity, error) { + if tx == nil { + tx = r.db + } + var inventoryStorages []entities.InventoryStorageEntity + if err := tx.WithContext(ctx). + Preload("Product"). + Preload("Aisle"). + Preload("Uom"). + Preload("Client"). + Preload("InvRequest"). + Preload("InvReceipt"). + Where("product_id = ? AND client_id = ?", productId, clientId). + Find(&inventoryStorages).Error; err != nil { + return nil, err + } + return inventoryStorages, nil +} + +// GetByProductAndClient implements InventoryStorageRepository. +func (r *inventoryStorageRepository) GetByProductAndClient(ctx context.Context, tx *gorm.DB, productId string, clientId string) (entities.InventoryStorageEntity, error) { + if tx == nil { + tx = r.db + } + var inventoryStorage entities.InventoryStorageEntity + if err := tx.WithContext(ctx). + Preload("Product"). + Preload("Aisle"). + Preload("Uom"). + Preload("Client"). + Preload("InvRequest"). + Preload("InvReceipt"). + Order("created_at DESC"). + First(&inventoryStorage, "product_id = ? AND client_id = ?", productId, clientId).Error; err != nil { + return inventoryStorage, err + } + return inventoryStorage, nil +} + +// BulkCreate implements InventoryStorageRepository. +func (r *inventoryStorageRepository) BulkCreate(ctx context.Context, tx *gorm.DB, inventoryStorages []entities.InventoryStorageEntity) error { + if tx == nil { + tx = r.db + } + if err := tx.WithContext(ctx).Clauses(clause.OnConflict{DoNothing: true}).Create(&inventoryStorages).Error; err != nil { + return err + } + return nil +} + func NewInventoryStorageRepository(db *gorm.DB) InventoryStorageRepository { return &inventoryStorageRepository{db: db} } diff --git a/modules/inventory_storage/service/inventory_storage_service.go b/modules/inventory_storage/service/inventory_storage_service.go index 04f0a6b..051ff50 100644 --- a/modules/inventory_storage/service/inventory_storage_service.go +++ b/modules/inventory_storage/service/inventory_storage_service.go @@ -51,16 +51,26 @@ func (s *inventoryStorageService) Create(ctx context.Context, req dtodomain.Inve tx.Rollback() return dtodomain.InventoryStorageResponse{}, err } - InvRequestID, err := uuid.Parse(req.InvRequestID) - if err != nil { - tx.Rollback() - return dtodomain.InventoryStorageResponse{}, err + var InvRequestID, InvReceiptID uuid.UUID + if req.InvRequestID != nil && *req.InvRequestID != "" { + InvRequestID, err = uuid.Parse(*req.InvRequestID) + if err != nil { + tx.Rollback() + return dtodomain.InventoryStorageResponse{}, err + } + } else { + InvRequestID = uuid.Nil } - InvReceiptID, err := uuid.Parse(req.InvReceiptID) - if err != nil { - tx.Rollback() - return dtodomain.InventoryStorageResponse{}, err + if req.InvReceiptID != nil && *req.InvReceiptID != "" { + InvReceiptID, err = uuid.Parse(*req.InvReceiptID) + if err != nil { + tx.Rollback() + return dtodomain.InventoryStorageResponse{}, err + } + } else { + InvReceiptID = uuid.Nil } + inventoryStorage := entities.InventoryStorageEntity{ ProductID: productID, AisleID: aisleID, diff --git a/modules/inventory_transaction/repository/inventory_transaction_repository.go b/modules/inventory_transaction/repository/inventory_transaction_repository.go index 3119955..4a0589d 100644 --- a/modules/inventory_transaction/repository/inventory_transaction_repository.go +++ b/modules/inventory_transaction/repository/inventory_transaction_repository.go @@ -14,12 +14,33 @@ type InventoryTransactionRepository interface { GetAll(ctx context.Context, filter query.InventoryTransactionFilter) ([]entities.InventoryTransactionEntity, int64, error) Update(ctx context.Context, tx *gorm.DB, inventoryTransaction entities.InventoryTransactionEntity) (entities.InventoryTransactionEntity, error) Delete(ctx context.Context, tx *gorm.DB, inventoryTransactionId string) error + GetByProductAndClient(ctx context.Context, tx *gorm.DB, productId string, clientId string) ([]entities.InventoryTransactionEntity, error) } type inventoryTransactionRepository struct { db *gorm.DB } +// GetByProductAndClient implements InventoryTransactionRepository. +func (r *inventoryTransactionRepository) GetByProductAndClient(ctx context.Context, tx *gorm.DB, productId string, clientId string) ([]entities.InventoryTransactionEntity, error) { + if tx == nil { + tx = r.db + } + var inventoryTransactions []entities.InventoryTransactionEntity + if err := tx.WithContext(ctx). + Preload("Client"). + Preload("Product"). + Preload("Aisle"). + Preload("InvReceipt"). + Preload("InvIssue"). + Preload("InvMove"). + Where("product_id = ? AND client_id = ?", productId, clientId). + Find(&inventoryTransactions).Error; err != nil { + return nil, err + } + return inventoryTransactions, nil +} + func NewInventoryTransactionRepository(db *gorm.DB) InventoryTransactionRepository { return &inventoryTransactionRepository{db: db} } diff --git a/modules/product/controller/product_controller.go b/modules/product/controller/product_controller.go index 3c14131..cd555c3 100644 --- a/modules/product/controller/product_controller.go +++ b/modules/product/controller/product_controller.go @@ -22,6 +22,9 @@ type ( GetAll(ctx *gin.Context) AssignCrossReference(ctx *gin.Context) RemoveCrossReference(ctx *gin.Context) + GetInvStoragesByProductAndClient(ctx *gin.Context) + GetInvTransactionsByProductAndClient(ctx *gin.Context) + GetCrossReferencesByProductAndClient(ctx *gin.Context) } productController struct { @@ -30,6 +33,49 @@ type ( } ) +// GetCrossReferencesByProductAndClient implements ProductController. +func (c *productController) GetCrossReferencesByProductAndClient(ctx *gin.Context) { + id := ctx.Param("id") + crossReferences, err := c.productService.GetCrossReferencesByProduct(ctx, id) + if err != nil { + res := utils.BuildResponseFailed(dto.MESSAGE_FAILED_GET_CROSS_REFERENCES, err.Error(), nil) + ctx.JSON(http.StatusInternalServerError, res) + return + } + res := utils.BuildResponseSuccess(dto.MESSAGE_SUCCESS_GET_CROSS_REFERENCES, crossReferences) + ctx.JSON(http.StatusOK, res) +} + +// GetInvStoragesByProductAndClient implements ProductController. +func (c *productController) GetInvStoragesByProductAndClient(ctx *gin.Context) { + id := ctx.Param("id") + clientId := ctx.MustGet("client_id").(string) + + invStorages, err := c.productService.GetInvStoragesByProductAndClient(ctx, id, clientId) + if err != nil { + res := utils.BuildResponseFailed(dto.MESSAGE_FAILED_GET_INV_STORAGES, err.Error(), nil) + ctx.JSON(http.StatusInternalServerError, res) + return + } + res := utils.BuildResponseSuccess(dto.MESSAGE_SUCCESS_GET_INV_STORAGES, invStorages) + ctx.JSON(http.StatusOK, res) +} + +// GetInvTransactionsByProductAndClient implements ProductController. +func (c *productController) GetInvTransactionsByProductAndClient(ctx *gin.Context) { + id := ctx.Param("id") + clientId := ctx.MustGet("client_id").(string) + + invTransactions, err := c.productService.GetInvTransactionsByProductAndClient(ctx, id, clientId) + if err != nil { + res := utils.BuildResponseFailed(dto.MESSAGE_FAILED_GET_INV_TRANSACTIONS, err.Error(), nil) + ctx.JSON(http.StatusInternalServerError, res) + return + } + res := utils.BuildResponseSuccess(dto.MESSAGE_SUCCESS_GET_INV_TRANSACTIONS, invTransactions) + ctx.JSON(http.StatusOK, res) +} + func NewProductController(i *do.Injector, productService service.ProductService) ProductController { db := do.MustInvokeNamed[*gorm.DB](i, constants.DB) return &productController{ diff --git a/modules/product/dto/product_dto.go b/modules/product/dto/product_dto.go index 250eb09..94e17bd 100644 --- a/modules/product/dto/product_dto.go +++ b/modules/product/dto/product_dto.go @@ -11,19 +11,25 @@ import ( ) const ( - MESSAGE_FAILED_CREATE_PRODUCT = "failed create product" - MESSAGE_SUCCESS_CREATE_PRODUCT = "success create product" - MESSAGE_FAILED_GET_PRODUCT = "failed get product" - MESSAGE_SUCCESS_GET_PRODUCT = "success get product" - MESSAGE_FAILED_UPDATE_PRODUCT = "failed update product" - MESSAGE_SUCCESS_UPDATE_PRODUCT = "success update product" - MESSAGE_FAILED_DELETE_PRODUCT = "failed delete product" - MESSAGE_SUCCESS_DELETE_PRODUCT = "success delete product" - MESSAGE_FAILED_GET_DATA_FROM_BODY = "failed get data from body" - MESSAGE_FAILED_ASSIGN_CROSS_REF = "failed assign cross reference" - MESSAGE_SUCCESS_ASSIGN_CROSS_REF = "success assign cross reference" - MESSAGE_FAILED_REMOVE_CROSS_REF = "failed remove cross reference" - MESSAGE_SUCCESS_REMOVE_CROSS_REF = "success remove cross reference" + MESSAGE_FAILED_CREATE_PRODUCT = "failed create product" + MESSAGE_SUCCESS_CREATE_PRODUCT = "success create product" + MESSAGE_FAILED_GET_PRODUCT = "failed get product" + MESSAGE_SUCCESS_GET_PRODUCT = "success get product" + MESSAGE_FAILED_UPDATE_PRODUCT = "failed update product" + MESSAGE_SUCCESS_UPDATE_PRODUCT = "success update product" + MESSAGE_FAILED_DELETE_PRODUCT = "failed delete product" + MESSAGE_SUCCESS_DELETE_PRODUCT = "success delete product" + MESSAGE_FAILED_GET_DATA_FROM_BODY = "failed get data from body" + MESSAGE_FAILED_ASSIGN_CROSS_REF = "failed assign cross reference" + MESSAGE_SUCCESS_ASSIGN_CROSS_REF = "success assign cross reference" + MESSAGE_FAILED_REMOVE_CROSS_REF = "failed remove cross reference" + MESSAGE_SUCCESS_REMOVE_CROSS_REF = "success remove cross reference" + MESSAGE_FAILED_GET_INV_STORAGES = "failed get inventory storages by product and client" + MESSAGE_SUCCESS_GET_INV_STORAGES = "success get inventory storages by product and client" + MESSAGE_FAILED_GET_INV_TRANSACTIONS = "failed get inventory transactions by product and client" + MESSAGE_SUCCESS_GET_INV_TRANSACTIONS = "success get inventory transactions by product and client" + MESSAGE_SUCCESS_GET_CROSS_REFERENCES = "success get cross references by product and client" + MESSAGE_FAILED_GET_CROSS_REFERENCES = "failed get cross references by product and client" ) var ( @@ -107,41 +113,42 @@ type ( } ProductResponse struct { - ID string `json:"id"` - Name string `json:"name"` - RefNumber string `json:"ref_number"` - SKU string `json:"sku"` - Description string `json:"description"` - Status string `json:"status"` - IsReturnable bool `json:"is_returnable"` - DimLength float64 `json:"dim_length"` - DimWidth float64 `json:"dim_width"` - DimHeight float64 `json:"dim_height"` - Weight float64 `json:"weight"` - Volume float64 `json:"volume"` - MaxStackHeight int `json:"max_stack_height"` - Temperature string `json:"temperature"` - IsHazardous bool `json:"is_hazardous"` - MinStock int `json:"min_stock"` - MaxStock int `json:"max_stock"` - ReplenishType string `json:"replenish_type"` - CycleCount string `json:"cycle_count"` - LotRules string `json:"lot_rules"` - LeadTime int `json:"lead_time"` - MultiplyRate string `json:"multiply_rate"` - DivideRate float64 `json:"divide_rate"` - Client pkgdto.IdNameResponse `json:"client"` - Category pkgdto.IdNameResponse `json:"category"` - Uom pkgdto.IdNameResponse `json:"uom"` - DimUom pkgdto.IdNameResponse `json:"dim_uom"` - WeightUom pkgdto.IdNameResponse `json:"weight_uom"` - VolumeUom pkgdto.IdNameResponse `json:"volume_uom"` - MinStockUom pkgdto.IdNameResponse `json:"min_stock_uom"` - MaxStockUom pkgdto.IdNameResponse `json:"max_stock_uom"` - LeadTimeUom pkgdto.IdNameResponse `json:"lead_time_uom"` - UomToUom pkgdto.IdNameResponse `json:"uom_to_uom"` - CrossReferences []ProductVendorResponse `json:"cross_references"` - InvTransactions []ProductInventoryTransactionResponse `json:"inv_transactions"` + ID string `json:"id"` + Name string `json:"name"` + RefNumber string `json:"ref_number"` + SKU string `json:"sku"` + Description string `json:"description"` + Status string `json:"status"` + IsReturnable bool `json:"is_returnable"` + DimLength float64 `json:"dim_length"` + DimWidth float64 `json:"dim_width"` + DimHeight float64 `json:"dim_height"` + Weight float64 `json:"weight"` + Volume float64 `json:"volume"` + MaxStackHeight int `json:"max_stack_height"` + Temperature string `json:"temperature"` + IsHazardous bool `json:"is_hazardous"` + MinStock int `json:"min_stock"` + MaxStock int `json:"max_stock"` + ReplenishType string `json:"replenish_type"` + CycleCount string `json:"cycle_count"` + LotRules string `json:"lot_rules"` + LeadTime int `json:"lead_time"` + MultiplyRate string `json:"multiply_rate"` + DivideRate float64 `json:"divide_rate"` + Client pkgdto.IdNameResponse `json:"client"` + Category pkgdto.IdNameResponse `json:"category"` + Uom pkgdto.IdNameResponse `json:"uom"` + DimUom pkgdto.IdNameResponse `json:"dim_uom"` + WeightUom pkgdto.IdNameResponse `json:"weight_uom"` + VolumeUom pkgdto.IdNameResponse `json:"volume_uom"` + MinStockUom pkgdto.IdNameResponse `json:"min_stock_uom"` + MaxStockUom pkgdto.IdNameResponse `json:"max_stock_uom"` + LeadTimeUom pkgdto.IdNameResponse `json:"lead_time_uom"` + UomToUom pkgdto.IdNameResponse `json:"uom_to_uom"` + // CrossReferences []ProductVendorResponse `json:"cross_references"` + // InvTransactions []ProductInventoryTransactionResponse `json:"inv_transactions"` + // InvStorages []ProductInventoryStorageResponse `json:"inv_storages"` } CrossReferenceRequest struct { @@ -167,82 +174,21 @@ type ( InvIssueRef string `json:"inv_issue_ref,omitempty"` InvMoveRef string `json:"inv_move_ref,omitempty"` } + + ProductInventoryStorageResponse struct { + ID string `json:"id"` + Locater string `json:"locater"` + Lot string `json:"lot"` + OnHandQuantity float64 `json:"on_hand_quantity"` + AvailableQuantity float64 `json:"available_quantity"` + AisleID pkgdto.IdNameResponse `json:"aisle_id"` + Uom pkgdto.IdNameResponse `json:"uom"` + InvReceipt pkgdto.IdNameResponse `json:"inv_receipt"` + InvRequest pkgdto.IdNameResponse `json:"inv_request"` + } ) func MapProductToResponse(product entities.MProductEntity) ProductResponse { - crossRefs := make([]ProductVendorResponse, 0, len(product.CrossReferences)) - for _, v := range product.CrossReferences { - crossRefs = append(crossRefs, ProductVendorResponse{ - ID: v.Vendor.ID.String(), - Name: v.Vendor.Name, - Address: v.Vendor.Address, - ContactPerson: v.Vendor.ContactPerson, - SearchKey: v.Vendor.SearchKey, - }) - } - - invTransactions := make([]ProductInventoryTransactionResponse, 0, len(product.InventoryTransactions)) - for _, it := range product.InventoryTransactions { - var transactionQuantity float64 - var lot, locater, invReceiptRef, invIssueRef, invMoveRef string - var transactionDate string - valid := false - - // Receipt - if it.InvReceipt.ID != uuid.Nil && it.InvReceipt.Status == constants.COMPLETED { - invReceiptRef = it.InvReceipt.ReferenceNumber - transactionDate = utils.DateTimeToString(it.TransactionDate) - for _, line := range it.InvReceipt.ReceiptLines { - if line.ProductID == it.ProductID { - transactionQuantity = line.Quantity - lot = line.BatchNumber - valid = true - break - } - } - } - - // Issue - if it.InvIssue.ID != uuid.Nil && it.InvIssue.Status == constants.COMPLETED { - invIssueRef = it.InvIssue.DocumentNumber - transactionDate = utils.DateTimeToString(it.TransactionDate) - for _, line := range it.InvIssue.IssueLines { - if line.ProductID == it.ProductID { - transactionQuantity = line.IssuedQuantity - valid = true - break - } - } - } - - // Move - if it.InvMove.ID != uuid.Nil && it.InvMove.Status == constants.COMPLETED { - invMoveRef = it.InvMove.MovementNumber - transactionDate = utils.DateTimeToString(it.TransactionDate) - for _, line := range it.InvMove.MovementLines { - if line.ProductID == it.ProductID { - transactionQuantity = line.MovedQuantity - valid = true - break - } - } - } - - if valid { - invTransactions = append(invTransactions, ProductInventoryTransactionResponse{ - ID: it.ID.String(), - TransactionDate: transactionDate, - TransactionType: it.TransactionType, - TransactionQuantity: transactionQuantity, - Lot: lot, - Locater: locater, - InvReceiptRef: invReceiptRef, - InvIssueRef: invIssueRef, - InvMoveRef: invMoveRef, - }) - } - } - return ProductResponse{ ID: product.ID.String(), Name: product.Name, @@ -307,7 +253,111 @@ func MapProductToResponse(product entities.MProductEntity) ProductResponse { ID: product.UomToUom.ID.String(), Name: product.UomToUom.Name, }, - CrossReferences: crossRefs, - InvTransactions: invTransactions, } } + +func MapInventoryTransactionsToResponses(transactions []entities.InventoryTransactionEntity) []ProductInventoryTransactionResponse { + responses := make([]ProductInventoryTransactionResponse, 0, len(transactions)) + for _, t := range transactions { + resp := MapInventoryTransactionToProductInventoryTransactionResponse(t) + if resp.ID != "" { // hanya tambahkan jika mapping valid + responses = append(responses, resp) + } + } + return responses +} + +func MapInventoryTransactionToProductInventoryTransactionResponse(transaction entities.InventoryTransactionEntity) ProductInventoryTransactionResponse { + var transactionQuantity float64 + var lot, locater, invReceiptRef, invIssueRef, invMoveRef string + var transactionDate string + valid := false + + if transaction.InvReceipt.ID != uuid.Nil && transaction.InvReceipt.Status == constants.COMPLETED { + invReceiptRef = transaction.InvReceipt.ReferenceNumber + transactionDate = utils.DateTimeToString(transaction.TransactionDate) + for _, line := range transaction.InvReceipt.ReceiptLines { + if line.ProductID == transaction.ProductID { + transactionQuantity = line.Quantity + lot = line.BatchNumber + valid = true + break + } + } + } + + if transaction.InvIssue.ID != uuid.Nil && transaction.InvIssue.Status == constants.COMPLETED { + invIssueRef = transaction.InvIssue.DocumentNumber + transactionDate = utils.DateTimeToString(transaction.TransactionDate) + for _, line := range transaction.InvIssue.IssueLines { + if line.ProductID == transaction.ProductID { + transactionQuantity = line.IssuedQuantity + valid = true + break + } + } + } + + if transaction.InvMove.ID != uuid.Nil && transaction.InvMove.Status == constants.COMPLETED { + invMoveRef = transaction.InvMove.MovementNumber + transactionDate = utils.DateTimeToString(transaction.TransactionDate) + for _, line := range transaction.InvMove.MovementLines { + if line.ProductID == transaction.ProductID { + transactionQuantity = line.MovedQuantity + valid = true + break + } + } + } + + if !valid { + return ProductInventoryTransactionResponse{} + } + + return ProductInventoryTransactionResponse{ + ID: transaction.ID.String(), + TransactionDate: transactionDate, + TransactionType: transaction.TransactionType, + TransactionQuantity: transactionQuantity, + Lot: lot, + Locater: locater, + InvReceiptRef: invReceiptRef, + InvIssueRef: invIssueRef, + InvMoveRef: invMoveRef, + } +} + +func MapInventoryStorageToProductInventoryStorageResponse(storage entities.InventoryStorageEntity) ProductInventoryStorageResponse { + return ProductInventoryStorageResponse{ + ID: storage.ID.String(), + // Locater: storage.Locater, + // Lot: storage.BatchNumber, + OnHandQuantity: storage.OnHandQuantity, + AvailableQuantity: storage.AvailableQuantity, + AisleID: pkgdto.IdNameResponse{ + ID: storage.Aisle.ID.String(), + Name: storage.Aisle.Name, + }, + Uom: pkgdto.IdNameResponse{ + ID: storage.Uom.ID.String(), + Name: storage.Uom.Name, + }, + InvReceipt: pkgdto.IdNameResponse{ + ID: storage.InvReceipt.ID.String(), + Name: storage.InvReceipt.ReferenceNumber, + }, + InvRequest: pkgdto.IdNameResponse{ + ID: storage.InvRequest.ID.String(), + Name: storage.InvRequest.ReferenceNumber, + }, + } +} + +func MapCrossReferenceToProductVendorResponse(crossRef entities.MCrossReferenceEntity) ProductVendorResponse { + return ProductVendorResponse{ + ID: crossRef.Vendor.ID.String(), + Name: crossRef.Vendor.Name, + ContactPerson: crossRef.Vendor.ContactPerson, + SearchKey: crossRef.Vendor.SearchKey, + Address: crossRef.Vendor.Address} +} diff --git a/modules/product/repository/product_repository.go b/modules/product/repository/product_repository.go index 15c75b9..dd43ba9 100644 --- a/modules/product/repository/product_repository.go +++ b/modules/product/repository/product_repository.go @@ -19,12 +19,27 @@ type ProductRepository interface { Delete(ctx context.Context, tx *gorm.DB, productId string) error AssignCrossReference(ctx context.Context, tx *gorm.DB, productId string, vendorIds []string) error RemoveCrossReference(ctx context.Context, tx *gorm.DB, productId string, vendorIds []string) error + GetCrossReferencesByProduct(ctx context.Context, tx *gorm.DB, productId string) ([]entities.MCrossReferenceEntity, error) } type productRepository struct { db *gorm.DB } +// GetCrossReferencesByProduct implements ProductRepository. +func (r *productRepository) GetCrossReferencesByProduct(ctx context.Context, tx *gorm.DB, productId string) ([]entities.MCrossReferenceEntity, error) { + if tx == nil { + tx = r.db + } + var crossReferences []entities.MCrossReferenceEntity + if err := tx.WithContext(ctx). + Where("product_id = ?", productId). + Find(&crossReferences).Error; err != nil { + return nil, err + } + return crossReferences, nil +} + // GetByCode implements ProductRepository. func (r *productRepository) GetByCode(ctx context.Context, tx *gorm.DB, productCode string, clientId string) (entities.MProductEntity, error) { if tx == nil { @@ -156,6 +171,12 @@ func (r *productRepository) GetById(ctx context.Context, tx *gorm.DB, productId Preload("CrossReferences"). Preload("CrossReferences.Vendor"). Preload("InventoryStorages"). + Preload("InventoryStorages.Client"). + Preload("InventoryStorages.Product"). + Preload("InventoryStorages.Aisle"). + Preload("InventoryStorages.Uom"). + Preload("InventoryStorages.InvReceipt"). + Preload("InventoryStorages.InvRequest"). Preload("InventoryTransactions"). Preload("InventoryTransactions.Client"). Preload("InventoryTransactions.Aisle"). diff --git a/modules/product/routes.go b/modules/product/routes.go index ae6c0bd..5ec8add 100644 --- a/modules/product/routes.go +++ b/modules/product/routes.go @@ -22,5 +22,8 @@ func RegisterRoutes(server *gin.Engine, injector *do.Injector) { productRoutes.GET("", middlewares.Authenticate(jwtService), productController.GetAll) productRoutes.POST("/:id/assign-cross-reference", middlewares.Authenticate(jwtService), productController.AssignCrossReference) productRoutes.POST("/:id/remove-cross-reference", middlewares.Authenticate(jwtService), productController.RemoveCrossReference) + productRoutes.GET("/:id/inv-storages", middlewares.Authenticate(jwtService), productController.GetInvStoragesByProductAndClient) + productRoutes.GET("/:id/inv-transactions", middlewares.Authenticate(jwtService), productController.GetInvTransactionsByProductAndClient) + productRoutes.GET("/:id/cross-references", middlewares.Authenticate(jwtService), productController.GetCrossReferencesByProductAndClient) } } diff --git a/modules/product/service/product_service.go b/modules/product/service/product_service.go index a41cde4..509be15 100644 --- a/modules/product/service/product_service.go +++ b/modules/product/service/product_service.go @@ -4,6 +4,8 @@ import ( "context" "github.com/Caknoooo/go-gin-clean-starter/database/entities" + invstoragerepo "github.com/Caknoooo/go-gin-clean-starter/modules/inventory_storage/repository" + invtransactionrepo "github.com/Caknoooo/go-gin-clean-starter/modules/inventory_transaction/repository" "github.com/Caknoooo/go-gin-clean-starter/modules/product/dto" "github.com/Caknoooo/go-gin-clean-starter/modules/product/query" "github.com/Caknoooo/go-gin-clean-starter/modules/product/repository" @@ -19,11 +21,55 @@ type ProductService interface { Delete(ctx context.Context, productId string) error AssignCrossReference(ctx context.Context, productId string, vendorIds []string) error RemoveCrossReference(ctx context.Context, productId string, vendorIds []string) error + GetInvTransactionsByProductAndClient(ctx context.Context, productId string, clientId string) ([]dto.ProductInventoryTransactionResponse, error) + GetInvStoragesByProductAndClient(ctx context.Context, productId string, clientId string) ([]dto.ProductInventoryStorageResponse, error) + GetCrossReferencesByProduct(ctx context.Context, productId string) ([]dto.ProductVendorResponse, error) } type productService struct { - db *gorm.DB - productRepo repository.ProductRepository + db *gorm.DB + productRepo repository.ProductRepository + inventoryTransactionRepo invtransactionrepo.InventoryTransactionRepository + inventoryStorageRepo invstoragerepo.InventoryStorageRepository +} + +// GetCrossReferencesByProductAndClient implements ProductService. +func (s *productService) GetCrossReferencesByProduct(ctx context.Context, productId string) ([]dto.ProductVendorResponse, error) { + crossReferences, err := s.productRepo.GetCrossReferencesByProduct(ctx, nil, productId) + if err != nil { + return nil, err + } + var responses []dto.ProductVendorResponse + for _, cr := range crossReferences { + responses = append(responses, dto.MapCrossReferenceToProductVendorResponse(cr)) + } + if len(responses) == 0 { + responses = []dto.ProductVendorResponse{} // pastikan slice kosong, bukan nil + } + return responses, nil +} + +// GetInvStoragesByProductAndClient implements ProductService. +func (s *productService) GetInvStoragesByProductAndClient(ctx context.Context, productId string, clientId string) ([]dto.ProductInventoryStorageResponse, error) { + invStorages, err := s.inventoryStorageRepo.GetStoragesByProductAndClient(ctx, nil, productId, clientId) + if err != nil { + return nil, err + } + var responses []dto.ProductInventoryStorageResponse + for _, invStorage := range invStorages { + responses = append(responses, dto.MapInventoryStorageToProductInventoryStorageResponse(invStorage)) + } + return responses, nil +} + +// GetInvTransactionsByProductAndClient implements ProductService. +func (s *productService) GetInvTransactionsByProductAndClient(ctx context.Context, productId string, clientId string) ([]dto.ProductInventoryTransactionResponse, error) { + invTransactions, err := s.inventoryTransactionRepo.GetByProductAndClient(ctx, nil, productId, clientId) + if err != nil { + return nil, err + } + responses := dto.MapInventoryTransactionsToResponses(invTransactions) + return responses, nil } // AssignCrossReference implements ProductService. @@ -66,10 +112,12 @@ func (s *productService) RemoveCrossReference(ctx context.Context, productId str return nil } -func NewProductService(productRepo repository.ProductRepository, db *gorm.DB) ProductService { +func NewProductService(productRepo repository.ProductRepository, db *gorm.DB, inventoryTransactionRepo invtransactionrepo.InventoryTransactionRepository, inventoryStorageRepo invstoragerepo.InventoryStorageRepository) ProductService { return &productService{ - productRepo: productRepo, - db: db, + productRepo: productRepo, + db: db, + inventoryTransactionRepo: inventoryTransactionRepo, + inventoryStorageRepo: inventoryStorageRepo, } } diff --git a/providers/core.go b/providers/core.go index 03dbbde..c231b5a 100644 --- a/providers/core.go +++ b/providers/core.go @@ -162,7 +162,7 @@ func RegisterDependencies(injector *do.Injector) { // Service userServ := userService.NewUserService(userRepository, roleRepository, warehouseRepository, clientRepository, refreshTokenRepository, jwtService, db) - productService := productService.NewProductService(productRepository, db) + productService := productService.NewProductService(productRepository, db, inventoryTransactionRepository, inventoryStorageRepository) roleServ := roleService.NewRoleService(roleRepository, refreshTokenRepository, jwtService, userServ, db) menuSvc := menuService.NewMenuService(menuRepository, jwtService, db) maintenanceGroupServ := maintGroupService.NewMaintenanceGroupService(maintenanceGroupRepository, maintenanceGroupRoleRepository, maintenanceGroupRoleUserRepository, db) @@ -174,7 +174,7 @@ func RegisterDependencies(injector *do.Injector) { warehouseServ := warehouseService.NewWarehouseService(warehouseRepository, db) zonaServ := zonaService.NewZonaService(zonaRepository, db) aisleServ := aisleService.NewAisleService(aisleRepository, db) - inventoryReceiptServ := inventoryReceiptService.NewInventoryReceiptService(db, inventoryReceiptRepository, inventoryReceiptLineRepository, productRepository, uomRepository) + inventoryReceiptServ := inventoryReceiptService.NewInventoryReceiptService(db, inventoryReceiptRepository, inventoryReceiptLineRepository, productRepository, uomRepository, inventoryStorageRepository) assignmentServ := assignmentService.NewAssignmentService(db, assignmentRepository, assignmentUserRepository) inventoryRequestServ := inventoryRequestService.NewInventoryRequestService(db, inventoryRequestRepository, inventoryRequestLineRepository) inventoryIssueServ := inventoryIssueService.NewInventoryIssueService(db, inventoryIssueRepository, inventoryIssueLineRepository)