add total out
This commit is contained in:
parent
07b3eda263
commit
13d8c75be7
@ -210,7 +210,7 @@ func (a *App) initProcessors(cfg *config.Config, repos *repositories) *processor
|
||||
categoryProcessor: processor.NewCategoryProcessorImpl(repos.categoryRepo),
|
||||
productProcessor: processor.NewProductProcessorImpl(repos.productRepo, repos.categoryRepo, repos.productVariantRepo, repos.inventoryRepo, repos.outletRepo),
|
||||
productVariantProcessor: processor.NewProductVariantProcessorImpl(repos.productVariantRepo, repos.productRepo),
|
||||
inventoryProcessor: processor.NewInventoryProcessorImpl(repos.inventoryRepo, repos.productRepo, repos.outletRepo),
|
||||
inventoryProcessor: processor.NewInventoryProcessorImpl(repos.inventoryRepo, repos.productRepo, repos.outletRepo, repos.ingredientRepo, repos.inventoryMovementRepo),
|
||||
orderProcessor: processor.NewOrderProcessorImpl(repos.orderRepo, repos.orderItemRepo, repos.paymentRepo, repos.paymentOrderItemRepo, repos.productRepo, repos.paymentMethodRepo, repos.inventoryRepo, repos.inventoryMovementRepo, repos.productVariantRepo, repos.outletRepo, repos.customerRepo, repos.txManager, repos.productRecipeRepo, repos.ingredientRepo, inventoryMovementService),
|
||||
paymentMethodProcessor: processor.NewPaymentMethodProcessorImpl(repos.paymentMethodRepo),
|
||||
fileProcessor: processor.NewFileProcessorImpl(repos.fileRepo, fileClient),
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
package contract
|
||||
|
||||
import (
|
||||
"github.com/google/uuid"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type CreateInventoryRequest struct {
|
||||
@ -24,6 +25,18 @@ type AdjustInventoryRequest struct {
|
||||
Reason string `json:"reason" validate:"required,min=1,max=255"`
|
||||
}
|
||||
|
||||
type RestockInventoryRequest struct {
|
||||
OutletID uuid.UUID `json:"outlet_id" validate:"required"`
|
||||
Items []RestockItem `json:"items" validate:"required,min=1,dive"`
|
||||
Reason string `json:"reason" validate:"required,min=1,max=255"`
|
||||
}
|
||||
|
||||
type RestockItem struct {
|
||||
ItemID uuid.UUID `json:"item_id" validate:"required"`
|
||||
ItemType string `json:"item_type" validate:"required,oneof=PRODUCT INGREDIENT"`
|
||||
Quantity int `json:"quantity" validate:"required,min=1"`
|
||||
}
|
||||
|
||||
type ListInventoryRequest struct {
|
||||
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
|
||||
ProductID *uuid.UUID `json:"product_id,omitempty"`
|
||||
@ -68,6 +81,24 @@ type InventoryAdjustmentResponse struct {
|
||||
AdjustedAt time.Time `json:"adjusted_at"`
|
||||
}
|
||||
|
||||
type RestockInventoryResponse struct {
|
||||
OutletID uuid.UUID `json:"outlet_id"`
|
||||
Items []RestockItemResult `json:"items"`
|
||||
Reason string `json:"reason"`
|
||||
RestockedAt time.Time `json:"restocked_at"`
|
||||
}
|
||||
|
||||
type RestockItemResult struct {
|
||||
ItemID uuid.UUID `json:"item_id"`
|
||||
ItemType string `json:"item_type"`
|
||||
ItemName string `json:"item_name"`
|
||||
PreviousQty int `json:"previous_quantity"`
|
||||
NewQty int `json:"new_quantity"`
|
||||
AddedQty int `json:"added_quantity"`
|
||||
Success bool `json:"success"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// Inventory Report Contracts
|
||||
type InventoryReportSummaryResponse struct {
|
||||
TotalProducts int `json:"total_products"`
|
||||
|
||||
@ -238,6 +238,34 @@ func (h *InventoryHandler) AdjustInventory(c *gin.Context) {
|
||||
util.HandleResponse(c.Writer, c.Request, inventoryResponse, "InventoryHandler::AdjustInventory")
|
||||
}
|
||||
|
||||
func (h *InventoryHandler) RestockInventory(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
|
||||
var req contract.RestockInventoryRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
logger.FromContext(c.Request.Context()).WithError(err).Error("InventoryHandler::RestockInventory -> request binding failed")
|
||||
validationResponseError := contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, err.Error())
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "InventoryHandler::RestockInventory")
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: Add validation for restock request
|
||||
// validationError, validationErrorCode := h.inventoryValidator.ValidateRestockInventoryRequest(&req)
|
||||
// if validationError != nil {
|
||||
// validationResponseError := contract.NewResponseError(validationErrorCode, constants.RequestEntity, validationError.Error())
|
||||
// util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "InventoryHandler::RestockInventory")
|
||||
// return
|
||||
// }
|
||||
|
||||
inventoryResponse := h.inventoryService.RestockInventory(ctx, &req)
|
||||
if inventoryResponse.HasErrors() {
|
||||
errorResp := inventoryResponse.GetErrors()[0]
|
||||
logger.FromContext(ctx).WithError(errorResp).Error("InventoryHandler::RestockInventory -> Failed to restock inventory from service")
|
||||
}
|
||||
|
||||
util.HandleResponse(c.Writer, c.Request, inventoryResponse, "InventoryHandler::RestockInventory")
|
||||
}
|
||||
|
||||
func (h *InventoryHandler) GetLowStockItems(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
|
||||
|
||||
@ -1,10 +1,13 @@
|
||||
package processor
|
||||
|
||||
import (
|
||||
"apskel-pos-be/internal/appcontext"
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"apskel-pos-be/internal/contract"
|
||||
"apskel-pos-be/internal/entities"
|
||||
"apskel-pos-be/internal/mappers"
|
||||
"apskel-pos-be/internal/models"
|
||||
"apskel-pos-be/internal/repository"
|
||||
@ -26,6 +29,7 @@ type InventoryProcessor interface {
|
||||
AdjustQuantity(ctx context.Context, productID, outletID, organizationID uuid.UUID, delta int) (*models.InventoryResponse, error)
|
||||
SetQuantity(ctx context.Context, productID, outletID, organizationID uuid.UUID, quantity int) (*models.InventoryResponse, error)
|
||||
UpdateReorderLevel(ctx context.Context, id uuid.UUID, reorderLevel int, organizationID uuid.UUID) error
|
||||
RestockInventory(ctx context.Context, outletID uuid.UUID, items []contract.RestockItem, reason string) (*contract.RestockInventoryResponse, error)
|
||||
GetInventoryReportSummary(ctx context.Context, outletID, organizationID uuid.UUID, dateFrom, dateTo *time.Time) (*models.InventoryReportSummary, error)
|
||||
GetInventoryReportDetails(ctx context.Context, filter *models.InventoryReportFilter, organizationID uuid.UUID) (*models.InventoryReportDetail, error)
|
||||
}
|
||||
@ -34,17 +38,23 @@ type InventoryProcessorImpl struct {
|
||||
inventoryRepo repository.InventoryRepository
|
||||
productRepo ProductRepository
|
||||
outletRepo OutletRepository
|
||||
ingredientRepo IngredientRepository
|
||||
inventoryMovementRepo repository.InventoryMovementRepository
|
||||
}
|
||||
|
||||
func NewInventoryProcessorImpl(
|
||||
inventoryRepo repository.InventoryRepository,
|
||||
productRepo ProductRepository,
|
||||
outletRepo OutletRepository,
|
||||
ingredientRepo IngredientRepository,
|
||||
inventoryMovementRepo repository.InventoryMovementRepository,
|
||||
) *InventoryProcessorImpl {
|
||||
return &InventoryProcessorImpl{
|
||||
inventoryRepo: inventoryRepo,
|
||||
productRepo: productRepo,
|
||||
outletRepo: outletRepo,
|
||||
ingredientRepo: ingredientRepo,
|
||||
inventoryMovementRepo: inventoryMovementRepo,
|
||||
}
|
||||
}
|
||||
|
||||
@ -401,3 +411,165 @@ func (p *InventoryProcessorImpl) GetZeroStockItems(ctx context.Context, outletID
|
||||
|
||||
return responses, nil
|
||||
}
|
||||
|
||||
func (p *InventoryProcessorImpl) RestockInventory(ctx context.Context, outletID uuid.UUID, items []contract.RestockItem, reason string) (*contract.RestockInventoryResponse, error) {
|
||||
outlet, err := p.outletRepo.GetByID(ctx, outletID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid outlet: %w", err)
|
||||
}
|
||||
|
||||
if len(items) == 0 {
|
||||
return nil, fmt.Errorf("no items provided for restocking")
|
||||
}
|
||||
|
||||
restockResults := make([]contract.RestockItemResult, 0, len(items))
|
||||
restockedAt := time.Now()
|
||||
|
||||
for _, item := range items {
|
||||
result := contract.RestockItemResult{
|
||||
ItemID: item.ItemID,
|
||||
ItemType: item.ItemType,
|
||||
AddedQty: item.Quantity,
|
||||
Success: false,
|
||||
}
|
||||
|
||||
switch item.ItemType {
|
||||
case "PRODUCT":
|
||||
if err := p.restockProduct(ctx, outletID, item.ItemID, item.Quantity, reason, outlet.OrganizationID); err != nil {
|
||||
result.Error = err.Error()
|
||||
}
|
||||
case "INGREDIENT":
|
||||
if err := p.restockIngredient(ctx, outletID, item.ItemID, item.Quantity, reason, outlet.OrganizationID); err != nil {
|
||||
result.Error = err.Error()
|
||||
}
|
||||
default:
|
||||
result.Error = fmt.Sprintf("unsupported item type: %s", item.ItemType)
|
||||
}
|
||||
|
||||
restockResults = append(restockResults, result)
|
||||
}
|
||||
|
||||
return &contract.RestockInventoryResponse{
|
||||
OutletID: outletID,
|
||||
Items: restockResults,
|
||||
Reason: reason,
|
||||
RestockedAt: restockedAt,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (p *InventoryProcessorImpl) restockProduct(ctx context.Context, outletID, productID uuid.UUID, quantity int, reason string, organizationID uuid.UUID) error {
|
||||
contextInfo := appcontext.FromGinContext(ctx)
|
||||
|
||||
var previousQuantity int
|
||||
var newQuantity int
|
||||
|
||||
inventory, err := p.inventoryRepo.GetByProductAndOutlet(ctx, productID, outletID)
|
||||
if err != nil {
|
||||
createReq := &models.CreateInventoryRequest{
|
||||
OutletID: outletID,
|
||||
ProductID: productID,
|
||||
Quantity: quantity,
|
||||
ReorderLevel: 0,
|
||||
}
|
||||
inventoryEntity := mappers.CreateInventoryRequestToEntity(createReq)
|
||||
if err := p.inventoryRepo.Create(ctx, inventoryEntity); err != nil {
|
||||
return fmt.Errorf("failed to create inventory for product %s: %w", productID, err)
|
||||
}
|
||||
previousQuantity = 0
|
||||
newQuantity = quantity
|
||||
} else {
|
||||
previousQuantity = inventory.Quantity
|
||||
newQuantity = inventory.Quantity + quantity
|
||||
updateReq := &models.UpdateInventoryRequest{
|
||||
Quantity: &newQuantity,
|
||||
}
|
||||
mappers.UpdateInventoryEntityFromRequest(inventory, updateReq)
|
||||
|
||||
if err := p.inventoryRepo.Update(ctx, inventory); err != nil {
|
||||
return fmt.Errorf("failed to update inventory for product %s: %w", productID, err)
|
||||
}
|
||||
}
|
||||
|
||||
referenceType := entities.InventoryMovementReferenceTypeManual
|
||||
movement := &entities.InventoryMovement{
|
||||
OrganizationID: organizationID,
|
||||
OutletID: outletID,
|
||||
ItemID: productID,
|
||||
ItemType: "PRODUCT",
|
||||
MovementType: entities.InventoryMovementTypePurchase,
|
||||
Quantity: float64(quantity),
|
||||
PreviousQuantity: float64(previousQuantity),
|
||||
NewQuantity: float64(newQuantity),
|
||||
ReferenceType: &referenceType,
|
||||
Reason: &reason,
|
||||
UserID: contextInfo.UserID,
|
||||
}
|
||||
|
||||
if err := p.inventoryMovementRepo.Create(ctx, movement); err != nil {
|
||||
return fmt.Errorf("failed to create inventory movement record: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *InventoryProcessorImpl) restockIngredient(ctx context.Context, outletID, ingredientID uuid.UUID, quantity int, reason string, organizationID uuid.UUID) error {
|
||||
contextInfo := appcontext.FromGinContext(ctx)
|
||||
ingredient, err := p.getIngredientByID(ctx, ingredientID, organizationID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get ingredient %s: %w", ingredientID, err)
|
||||
}
|
||||
|
||||
previousStock := ingredient.Stock
|
||||
newStock := ingredient.Stock + float64(quantity)
|
||||
|
||||
if err := p.updateIngredientStock(ctx, ingredientID, float64(quantity), organizationID); err != nil {
|
||||
return fmt.Errorf("failed to update ingredient stock: %w", err)
|
||||
}
|
||||
|
||||
referenceType := entities.InventoryMovementReferenceTypeManual
|
||||
movement := &entities.InventoryMovement{
|
||||
OrganizationID: organizationID,
|
||||
OutletID: outletID,
|
||||
ItemID: ingredientID,
|
||||
ItemType: "INGREDIENT",
|
||||
MovementType: entities.InventoryMovementTypePurchase,
|
||||
Quantity: float64(quantity),
|
||||
PreviousQuantity: previousStock,
|
||||
NewQuantity: newStock,
|
||||
ReferenceType: &referenceType,
|
||||
Reason: &reason,
|
||||
UserID: contextInfo.UserID,
|
||||
}
|
||||
|
||||
if err := p.inventoryMovementRepo.Create(ctx, movement); err != nil {
|
||||
return fmt.Errorf("failed to create inventory movement record: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *InventoryProcessorImpl) getIngredientByID(ctx context.Context, ingredientID, organizationID uuid.UUID) (*models.Ingredient, error) {
|
||||
ingredient, err := p.ingredientRepo.GetByID(ctx, ingredientID, organizationID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get ingredient %s: %w", ingredientID, err)
|
||||
}
|
||||
|
||||
return &models.Ingredient{
|
||||
ID: ingredient.ID,
|
||||
Name: ingredient.Name,
|
||||
Stock: ingredient.Stock,
|
||||
UnitID: ingredient.UnitID,
|
||||
OrganizationID: ingredient.OrganizationID,
|
||||
OutletID: ingredient.OutletID,
|
||||
IsActive: ingredient.IsActive,
|
||||
CreatedAt: ingredient.CreatedAt,
|
||||
UpdatedAt: ingredient.UpdatedAt,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (p *InventoryProcessorImpl) updateIngredientStock(ctx context.Context, ingredientID uuid.UUID, quantityToAdd float64, organizationID uuid.UUID) error {
|
||||
if err := p.ingredientRepo.UpdateStock(ctx, ingredientID, quantityToAdd, organizationID); err != nil {
|
||||
return fmt.Errorf("failed to update ingredient stock: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -203,6 +203,7 @@ func (r *Router) addAppRoutes(rg *gin.Engine) {
|
||||
inventory.PUT("/:id", r.inventoryHandler.UpdateInventory)
|
||||
inventory.DELETE("/:id", r.inventoryHandler.DeleteInventory)
|
||||
inventory.POST("/adjust", r.inventoryHandler.AdjustInventory)
|
||||
inventory.POST("/restock", r.inventoryHandler.RestockInventory)
|
||||
inventory.GET("/low-stock/:outlet_id", r.inventoryHandler.GetLowStockItems)
|
||||
inventory.GET("/zero-stock/:outlet_id", r.inventoryHandler.GetZeroStockItems)
|
||||
inventory.GET("/report/summary/:outlet_id", r.inventoryHandler.GetInventoryReportSummary)
|
||||
|
||||
@ -21,6 +21,7 @@ type InventoryService interface {
|
||||
GetInventoryByID(ctx context.Context, id uuid.UUID) *contract.Response
|
||||
ListInventory(ctx context.Context, req *contract.ListInventoryRequest) *contract.Response
|
||||
AdjustInventory(ctx context.Context, req *contract.AdjustInventoryRequest) *contract.Response
|
||||
RestockInventory(ctx context.Context, req *contract.RestockInventoryRequest) *contract.Response
|
||||
GetLowStockItems(ctx context.Context, outletID uuid.UUID) *contract.Response
|
||||
GetZeroStockItems(ctx context.Context, outletID uuid.UUID) *contract.Response
|
||||
GetInventoryReportSummary(ctx context.Context, outletID, organizationID uuid.UUID, dateFrom, dateTo *time.Time) (*contract.InventoryReportSummaryResponse, error)
|
||||
@ -153,6 +154,16 @@ func (s *InventoryServiceImpl) AdjustInventory(ctx context.Context, req *contrac
|
||||
return contract.BuildSuccessResponse(contractResponse)
|
||||
}
|
||||
|
||||
func (s *InventoryServiceImpl) RestockInventory(ctx context.Context, req *contract.RestockInventoryRequest) *contract.Response {
|
||||
restockResponse, err := s.inventoryProcessor.RestockInventory(ctx, req.OutletID, req.Items, req.Reason)
|
||||
if err != nil {
|
||||
errorResp := contract.NewResponseError(constants.InternalServerErrorCode, constants.InventoryServiceEntity, err.Error())
|
||||
return contract.BuildErrorResponse([]*contract.ResponseError{errorResp})
|
||||
}
|
||||
|
||||
return contract.BuildSuccessResponse(restockResponse)
|
||||
}
|
||||
|
||||
func (s *InventoryServiceImpl) GetLowStockItems(ctx context.Context, outletID uuid.UUID) *contract.Response {
|
||||
inventory, err := s.inventoryProcessor.GetLowStock(ctx, outletID, uuid.Nil) // TODO: Get organizationID from context
|
||||
if err != nil {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user