apskel-pos-backend/internal/service/inventory_service.go
2025-08-14 01:00:36 +07:00

280 lines
12 KiB
Go

package service
import (
"context"
"time"
"apskel-pos-be/internal/appcontext"
"apskel-pos-be/internal/constants"
"apskel-pos-be/internal/contract"
"apskel-pos-be/internal/models"
"apskel-pos-be/internal/processor"
"apskel-pos-be/internal/transformer"
"github.com/google/uuid"
)
type InventoryService interface {
CreateInventory(ctx context.Context, apctx *appcontext.ContextInfo, req *contract.CreateInventoryRequest) *contract.Response
UpdateInventory(ctx context.Context, id uuid.UUID, req *contract.UpdateInventoryRequest) *contract.Response
DeleteInventory(ctx context.Context, id uuid.UUID) *contract.Response
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
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)
GetInventoryReportDetails(ctx context.Context, filter *models.InventoryReportFilter, organizationID uuid.UUID) (*contract.InventoryReportDetailResponse, error)
}
type InventoryServiceImpl struct {
inventoryProcessor processor.InventoryProcessor
}
func NewInventoryService(inventoryProcessor processor.InventoryProcessor) *InventoryServiceImpl {
return &InventoryServiceImpl{
inventoryProcessor: inventoryProcessor,
}
}
func (s *InventoryServiceImpl) CreateInventory(ctx context.Context, apctx *appcontext.ContextInfo, req *contract.CreateInventoryRequest) *contract.Response {
modelReq := transformer.CreateInventoryRequestToModel(req)
inventoryResponse, err := s.inventoryProcessor.Create(ctx, modelReq, apctx.OrganizationID)
if err != nil {
errorResp := contract.NewResponseError(constants.InternalServerErrorCode, constants.InventoryServiceEntity, err.Error())
return contract.BuildErrorResponse([]*contract.ResponseError{errorResp})
}
contractResponse := transformer.InventoryModelResponseToResponse(inventoryResponse)
return contract.BuildSuccessResponse(contractResponse)
}
func (s *InventoryServiceImpl) UpdateInventory(ctx context.Context, id uuid.UUID, req *contract.UpdateInventoryRequest) *contract.Response {
modelReq := transformer.UpdateInventoryRequestToModel(req)
inventoryResponse, err := s.inventoryProcessor.Update(ctx, id, modelReq, uuid.Nil) // TODO: Get organizationID from context
if err != nil {
errorResp := contract.NewResponseError(constants.InternalServerErrorCode, constants.InventoryServiceEntity, err.Error())
return contract.BuildErrorResponse([]*contract.ResponseError{errorResp})
}
contractResponse := transformer.InventoryModelResponseToResponse(inventoryResponse)
return contract.BuildSuccessResponse(contractResponse)
}
func (s *InventoryServiceImpl) DeleteInventory(ctx context.Context, id uuid.UUID) *contract.Response {
err := s.inventoryProcessor.Delete(ctx, id, uuid.Nil) // TODO: Get organizationID from context
if err != nil {
errorResp := contract.NewResponseError(constants.InternalServerErrorCode, constants.InventoryServiceEntity, err.Error())
return contract.BuildErrorResponse([]*contract.ResponseError{errorResp})
}
return contract.BuildSuccessResponse(map[string]interface{}{
"message": "Inventory deleted successfully",
})
}
func (s *InventoryServiceImpl) GetInventoryByID(ctx context.Context, id uuid.UUID) *contract.Response {
inventoryResponse, err := s.inventoryProcessor.GetByID(ctx, id, uuid.Nil) // TODO: Get organizationID from context
if err != nil {
errorResp := contract.NewResponseError(constants.InternalServerErrorCode, constants.InventoryServiceEntity, err.Error())
return contract.BuildErrorResponse([]*contract.ResponseError{errorResp})
}
contractResponse := transformer.InventoryModelResponseToResponse(inventoryResponse)
return contract.BuildSuccessResponse(contractResponse)
}
func (s *InventoryServiceImpl) ListInventory(ctx context.Context, req *contract.ListInventoryRequest) *contract.Response {
// Build filters
filters := make(map[string]interface{})
if req.OutletID != nil {
filters["outlet_id"] = *req.OutletID
}
if req.ProductID != nil {
filters["product_id"] = *req.ProductID
}
if req.CategoryID != nil {
filters["category_id"] = *req.CategoryID
}
if req.LowStockOnly != nil && *req.LowStockOnly {
filters["low_stock"] = true
}
if req.ZeroStockOnly != nil && *req.ZeroStockOnly {
filters["zero_stock"] = true
}
if req.Search != "" {
filters["search"] = req.Search
}
inventory, totalCount, err := s.inventoryProcessor.List(ctx, filters, req.Limit, req.Page, uuid.Nil) // TODO: Get organizationID from context
if err != nil {
errorResp := contract.NewResponseError(constants.InternalServerErrorCode, constants.InventoryServiceEntity, err.Error())
return contract.BuildErrorResponse([]*contract.ResponseError{errorResp})
}
// Convert from []*models.InventoryResponse to []models.InventoryResponse
var inventoryResponses []models.InventoryResponse
for _, inv := range inventory {
inventoryResponses = append(inventoryResponses, *inv)
}
// Convert to contract responses
contractResponses := transformer.InventoryToResponses(inventoryResponses)
// Calculate total pages
totalPages := int(totalCount) / req.Limit
if int(totalCount)%req.Limit > 0 {
totalPages++
}
listResponse := &contract.ListInventoryResponse{
Inventory: contractResponses,
TotalCount: int(totalCount),
Page: req.Page,
Limit: req.Limit,
TotalPages: totalPages,
}
return contract.BuildSuccessResponse(listResponse)
}
func (s *InventoryServiceImpl) AdjustInventory(ctx context.Context, req *contract.AdjustInventoryRequest) *contract.Response {
modelReq := transformer.AdjustInventoryRequestToModel(req)
inventoryResponse, err := s.inventoryProcessor.AdjustQuantity(ctx, modelReq.ProductID, modelReq.OutletID, uuid.Nil, modelReq.Delta) // TODO: Get organizationID from context
if err != nil {
errorResp := contract.NewResponseError(constants.InternalServerErrorCode, constants.InventoryServiceEntity, err.Error())
return contract.BuildErrorResponse([]*contract.ResponseError{errorResp})
}
contractResponse := transformer.InventoryModelResponseToResponse(inventoryResponse)
return contract.BuildSuccessResponse(contractResponse)
}
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 {
errorResp := contract.NewResponseError(constants.InternalServerErrorCode, constants.InventoryServiceEntity, err.Error())
return contract.BuildErrorResponse([]*contract.ResponseError{errorResp})
}
// Convert from []*models.InventoryResponse to []models.InventoryResponse
var inventoryResponses []models.InventoryResponse
for _, inv := range inventory {
inventoryResponses = append(inventoryResponses, *inv)
}
contractResponses := transformer.InventoryToResponses(inventoryResponses)
return contract.BuildSuccessResponse(contractResponses)
}
func (s *InventoryServiceImpl) GetZeroStockItems(ctx context.Context, outletID uuid.UUID) *contract.Response {
inventory, err := s.inventoryProcessor.GetZeroStock(ctx, outletID, uuid.Nil) // TODO: Get organizationID from context
if err != nil {
errorResp := contract.NewResponseError(constants.InternalServerErrorCode, constants.InventoryServiceEntity, err.Error())
return contract.BuildErrorResponse([]*contract.ResponseError{errorResp})
}
// Convert from []*models.InventoryResponse to []models.InventoryResponse
var inventoryResponses []models.InventoryResponse
for _, inv := range inventory {
inventoryResponses = append(inventoryResponses, *inv)
}
contractResponses := transformer.InventoryToResponses(inventoryResponses)
return contract.BuildSuccessResponse(contractResponses)
}
// GetInventoryReportSummary returns summary statistics for inventory report
func (s *InventoryServiceImpl) GetInventoryReportSummary(ctx context.Context, outletID, organizationID uuid.UUID, dateFrom, dateTo *time.Time) (*contract.InventoryReportSummaryResponse, error) {
summary, err := s.inventoryProcessor.GetInventoryReportSummary(ctx, outletID, organizationID, dateFrom, dateTo)
if err != nil {
return nil, err
}
return &contract.InventoryReportSummaryResponse{
TotalProducts: summary.TotalProducts,
TotalIngredients: summary.TotalIngredients,
TotalValue: summary.TotalValue,
LowStockProducts: summary.LowStockProducts,
LowStockIngredients: summary.LowStockIngredients,
ZeroStockProducts: summary.ZeroStockProducts,
ZeroStockIngredients: summary.ZeroStockIngredients,
TotalSoldProducts: summary.TotalSoldProducts,
TotalSoldIngredients: summary.TotalSoldIngredients,
OutletID: summary.OutletID.String(),
OutletName: summary.OutletName,
GeneratedAt: summary.GeneratedAt.Format(time.RFC3339),
}, nil
}
// GetInventoryReportDetails returns detailed inventory report with products and ingredients
func (s *InventoryServiceImpl) GetInventoryReportDetails(ctx context.Context, filter *models.InventoryReportFilter, organizationID uuid.UUID) (*contract.InventoryReportDetailResponse, error) {
report, err := s.inventoryProcessor.GetInventoryReportDetails(ctx, filter, organizationID)
if err != nil {
return nil, err
}
response := &contract.InventoryReportDetailResponse{}
if report.Summary != nil {
response.Summary = &contract.InventoryReportSummaryResponse{
TotalProducts: report.Summary.TotalProducts,
TotalIngredients: report.Summary.TotalIngredients,
TotalValue: report.Summary.TotalValue,
LowStockProducts: report.Summary.LowStockProducts,
LowStockIngredients: report.Summary.LowStockIngredients,
ZeroStockProducts: report.Summary.ZeroStockProducts,
ZeroStockIngredients: report.Summary.ZeroStockIngredients,
TotalSoldProducts: report.Summary.TotalSoldProducts,
TotalSoldIngredients: report.Summary.TotalSoldIngredients,
OutletID: report.Summary.OutletID.String(),
OutletName: report.Summary.OutletName,
GeneratedAt: report.Summary.GeneratedAt.Format(time.RFC3339),
}
}
response.Products = make([]*contract.InventoryProductDetailResponse, len(report.Products))
for i, product := range report.Products {
response.Products[i] = &contract.InventoryProductDetailResponse{
ID: product.ID.String(),
ProductID: product.ProductID.String(),
ProductName: product.ProductName,
CategoryName: product.CategoryName,
Quantity: product.Quantity,
ReorderLevel: product.ReorderLevel,
UnitCost: product.UnitCost,
TotalValue: product.TotalValue,
TotalIn: product.TotalIn,
TotalOut: product.TotalOut,
IsLowStock: product.IsLowStock,
IsZeroStock: product.IsZeroStock,
UpdatedAt: product.UpdatedAt.Format(time.RFC3339),
}
}
// Transform ingredients
response.Ingredients = make([]*contract.InventoryIngredientDetailResponse, len(report.Ingredients))
for i, ingredient := range report.Ingredients {
response.Ingredients[i] = &contract.InventoryIngredientDetailResponse{
ID: ingredient.ID.String(),
IngredientID: ingredient.IngredientID.String(),
IngredientName: ingredient.IngredientName,
UnitName: ingredient.UnitName,
Quantity: ingredient.Quantity,
ReorderLevel: ingredient.ReorderLevel,
UnitCost: ingredient.UnitCost,
TotalValue: ingredient.TotalValue,
TotalIn: ingredient.TotalIn,
TotalOut: ingredient.TotalOut,
IsLowStock: ingredient.IsLowStock,
IsZeroStock: ingredient.IsZeroStock,
UpdatedAt: ingredient.UpdatedAt.Format(time.RFC3339),
}
}
return response, nil
}