apskel-pos-backend/internal/processor/inventory_processor.go
2025-08-14 00:38:26 +07:00

404 lines
15 KiB
Go

package processor
import (
"context"
"fmt"
"time"
"apskel-pos-be/internal/mappers"
"apskel-pos-be/internal/models"
"apskel-pos-be/internal/repository"
"github.com/google/uuid"
)
type InventoryProcessor interface {
Create(ctx context.Context, req *models.CreateInventoryRequest, organizationID uuid.UUID) (*models.InventoryResponse, error)
GetByID(ctx context.Context, id, organizationID uuid.UUID) (*models.InventoryResponse, error)
GetByProductAndOutlet(ctx context.Context, productID, outletID, organizationID uuid.UUID) (*models.InventoryResponse, error)
GetByOutlet(ctx context.Context, outletID, organizationID uuid.UUID) ([]*models.InventoryResponse, error)
GetByProduct(ctx context.Context, productID, organizationID uuid.UUID) ([]*models.InventoryResponse, error)
GetLowStock(ctx context.Context, outletID, organizationID uuid.UUID) ([]*models.InventoryResponse, error)
GetZeroStock(ctx context.Context, outletID, organizationID uuid.UUID) ([]*models.InventoryResponse, error)
Update(ctx context.Context, id uuid.UUID, req *models.UpdateInventoryRequest, organizationID uuid.UUID) (*models.InventoryResponse, error)
Delete(ctx context.Context, id, organizationID uuid.UUID) error
List(ctx context.Context, filters map[string]interface{}, limit, offset int, organizationID uuid.UUID) ([]*models.InventoryResponse, int64, error)
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
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)
}
type InventoryProcessorImpl struct {
inventoryRepo repository.InventoryRepository
productRepo ProductRepository
outletRepo OutletRepository
}
func NewInventoryProcessorImpl(
inventoryRepo repository.InventoryRepository,
productRepo ProductRepository,
outletRepo OutletRepository,
) *InventoryProcessorImpl {
return &InventoryProcessorImpl{
inventoryRepo: inventoryRepo,
productRepo: productRepo,
outletRepo: outletRepo,
}
}
// Create creates a new inventory record
func (p *InventoryProcessorImpl) Create(ctx context.Context, req *models.CreateInventoryRequest, organizationID uuid.UUID) (*models.InventoryResponse, error) {
_, err := p.productRepo.GetByID(ctx, req.ProductID)
if err != nil {
return nil, fmt.Errorf("invalid product: %w", err)
}
// Validate outlet exists
_, err = p.outletRepo.GetByID(ctx, req.OutletID)
if err != nil {
return nil, fmt.Errorf("invalid outlet: %w", err)
}
// Check if inventory already exists for this product-outlet combination
existingInventory, err := p.inventoryRepo.GetByProductAndOutlet(ctx, req.ProductID, req.OutletID)
if err == nil && existingInventory != nil {
return nil, fmt.Errorf("inventory already exists for product %s in outlet %s", req.ProductID, req.OutletID)
}
// Map request to entity
inventoryEntity := mappers.CreateInventoryRequestToEntity(req)
// Create inventory
if err := p.inventoryRepo.Create(ctx, inventoryEntity); err != nil {
return nil, fmt.Errorf("failed to create inventory: %w", err)
}
// Get inventory with relations for response
inventoryWithRelations, err := p.inventoryRepo.GetWithRelations(ctx, inventoryEntity.ID)
if err != nil {
return nil, fmt.Errorf("failed to retrieve created inventory: %w", err)
}
// Map entity to response model
response := mappers.InventoryEntityToResponse(inventoryWithRelations)
return response, nil
}
// Update updates an existing inventory record
func (p *InventoryProcessorImpl) Update(ctx context.Context, id uuid.UUID, req *models.UpdateInventoryRequest, organizationID uuid.UUID) (*models.InventoryResponse, error) {
// Get existing inventory
existingInventory, err := p.inventoryRepo.GetByID(ctx, id)
if err != nil {
return nil, fmt.Errorf("inventory not found: %w", err)
}
// Apply updates to entity
mappers.UpdateInventoryEntityFromRequest(existingInventory, req)
// Update inventory
if err := p.inventoryRepo.Update(ctx, existingInventory); err != nil {
return nil, fmt.Errorf("failed to update inventory: %w", err)
}
// Get updated inventory with relations for response
inventoryWithRelations, err := p.inventoryRepo.GetWithRelations(ctx, id)
if err != nil {
return nil, fmt.Errorf("failed to retrieve updated inventory: %w", err)
}
// Map entity to response model
response := mappers.InventoryEntityToResponse(inventoryWithRelations)
return response, nil
}
// Delete deletes an inventory record
func (p *InventoryProcessorImpl) Delete(ctx context.Context, id uuid.UUID, organizationID uuid.UUID) error {
// Check if inventory exists
_, err := p.inventoryRepo.GetByID(ctx, id)
if err != nil {
return fmt.Errorf("inventory not found: %w", err)
}
// Delete inventory
if err := p.inventoryRepo.Delete(ctx, id); err != nil {
return fmt.Errorf("failed to delete inventory: %w", err)
}
return nil
}
// GetByID retrieves an inventory record by ID
func (p *InventoryProcessorImpl) GetByID(ctx context.Context, id, organizationID uuid.UUID) (*models.InventoryResponse, error) {
inventory, err := p.inventoryRepo.GetWithRelations(ctx, id)
if err != nil {
return nil, fmt.Errorf("inventory not found: %w", err)
}
response := mappers.InventoryEntityToResponse(inventory)
return response, nil
}
// GetByProductAndOutlet retrieves inventory by product and outlet
func (p *InventoryProcessorImpl) GetByProductAndOutlet(ctx context.Context, productID, outletID, organizationID uuid.UUID) (*models.InventoryResponse, error) {
inventory, err := p.inventoryRepo.GetByProductAndOutlet(ctx, productID, outletID)
if err != nil {
return nil, fmt.Errorf("inventory not found: %w", err)
}
response := mappers.InventoryEntityToResponse(inventory)
return response, nil
}
// GetByOutlet retrieves all inventory records for a specific outlet
func (p *InventoryProcessorImpl) GetByOutlet(ctx context.Context, outletID, organizationID uuid.UUID) ([]*models.InventoryResponse, error) {
inventories, err := p.inventoryRepo.GetByOutlet(ctx, outletID)
if err != nil {
return nil, fmt.Errorf("failed to get inventory by outlet: %w", err)
}
var responses []*models.InventoryResponse
for _, inventory := range inventories {
response := mappers.InventoryEntityToResponse(inventory)
responses = append(responses, response)
}
return responses, nil
}
// GetByProduct retrieves all inventory records for a specific product
func (p *InventoryProcessorImpl) GetByProduct(ctx context.Context, productID, organizationID uuid.UUID) ([]*models.InventoryResponse, error) {
inventories, err := p.inventoryRepo.GetByProduct(ctx, productID)
if err != nil {
return nil, fmt.Errorf("failed to get inventory by product: %w", err)
}
var responses []*models.InventoryResponse
for _, inventory := range inventories {
response := mappers.InventoryEntityToResponse(inventory)
responses = append(responses, response)
}
return responses, nil
}
// List retrieves inventory records with filtering and pagination
func (p *InventoryProcessorImpl) List(ctx context.Context, filters map[string]interface{}, limit, offset int, organizationID uuid.UUID) ([]*models.InventoryResponse, int64, error) {
inventories, totalCount, err := p.inventoryRepo.List(ctx, filters, limit, offset)
if err != nil {
return nil, 0, fmt.Errorf("failed to list inventory: %w", err)
}
var responses []*models.InventoryResponse
for _, inventory := range inventories {
response := mappers.InventoryEntityToResponse(inventory)
responses = append(responses, response)
}
return responses, totalCount, nil
}
// AdjustQuantity adjusts the quantity of an inventory item
func (p *InventoryProcessorImpl) AdjustQuantity(ctx context.Context, productID, outletID, organizationID uuid.UUID, delta int) (*models.InventoryResponse, error) {
inventory, err := p.inventoryRepo.AdjustQuantity(ctx, productID, outletID, delta)
if err != nil {
return nil, fmt.Errorf("failed to adjust inventory quantity: %w", err)
}
response := mappers.InventoryEntityToResponse(inventory)
return response, nil
}
// SetQuantity sets the quantity of an inventory item
func (p *InventoryProcessorImpl) SetQuantity(ctx context.Context, productID, outletID, organizationID uuid.UUID, quantity int) (*models.InventoryResponse, error) {
inventory, err := p.inventoryRepo.SetQuantity(ctx, productID, outletID, quantity)
if err != nil {
return nil, fmt.Errorf("failed to set inventory quantity: %w", err)
}
response := mappers.InventoryEntityToResponse(inventory)
return response, nil
}
// UpdateReorderLevel updates the reorder level of an inventory item
func (p *InventoryProcessorImpl) UpdateReorderLevel(ctx context.Context, id uuid.UUID, reorderLevel int, organizationID uuid.UUID) error {
return p.inventoryRepo.UpdateReorderLevel(ctx, id, reorderLevel)
}
// GetLowStock retrieves low stock inventory items for a specific outlet
func (p *InventoryProcessorImpl) GetLowStock(ctx context.Context, outletID, organizationID uuid.UUID) ([]*models.InventoryResponse, error) {
inventories, err := p.inventoryRepo.GetLowStock(ctx, outletID)
if err != nil {
return nil, fmt.Errorf("failed to get low stock inventory: %w", err)
}
var responses []*models.InventoryResponse
for _, inventory := range inventories {
response := mappers.InventoryEntityToResponse(inventory)
responses = append(responses, response)
}
return responses, nil
}
// GetZeroStock retrieves zero stock inventory items for a specific outlet
func (p *InventoryProcessorImpl) GetZeroStock(ctx context.Context, outletID, organizationID uuid.UUID) ([]*models.InventoryResponse, error) {
inventories, err := p.inventoryRepo.GetZeroStock(ctx, outletID)
if err != nil {
return nil, fmt.Errorf("failed to get zero stock inventory: %w", err)
}
var responses []*models.InventoryResponse
for _, inventory := range inventories {
response := mappers.InventoryEntityToResponse(inventory)
responses = append(responses, response)
}
return responses, nil
}
func (p *InventoryProcessorImpl) GetInventoryReportSummary(ctx context.Context, outletID, organizationID uuid.UUID, dateFrom, dateTo *time.Time) (*models.InventoryReportSummary, error) {
outlet, err := p.outletRepo.GetByID(ctx, outletID)
if err != nil {
return nil, fmt.Errorf("outlet not found: %w", err)
}
if outlet.OrganizationID != organizationID {
return nil, fmt.Errorf("outlet does not belong to the organization")
}
summary, err := p.inventoryRepo.GetInventoryReportSummary(ctx, outletID, dateFrom, dateTo)
if err != nil {
return nil, fmt.Errorf("failed to get inventory report summary: %w", err)
}
return summary, nil
}
// GetInventoryReportDetails returns detailed inventory report with products and ingredients
func (p *InventoryProcessorImpl) GetInventoryReportDetails(ctx context.Context, filter *models.InventoryReportFilter, organizationID uuid.UUID) (*models.InventoryReportDetail, error) {
if filter.OutletID == nil {
return nil, fmt.Errorf("outlet_id is required for inventory report")
}
outlet, err := p.outletRepo.GetByID(ctx, *filter.OutletID)
if err != nil {
return nil, fmt.Errorf("outlet not found: %w", err)
}
if outlet.OrganizationID != organizationID {
return nil, fmt.Errorf("outlet does not belong to the organization")
}
report, err := p.inventoryRepo.GetInventoryReportDetails(ctx, filter)
if err != nil {
return nil, fmt.Errorf("failed to get inventory report details: %w", err)
}
return report, nil
}
func (p *InventoryProcessorImpl) GetInventoryByID(ctx context.Context, id uuid.UUID) (*models.InventoryResponse, error) {
inventoryEntity, err := p.inventoryRepo.GetWithRelations(ctx, id)
if err != nil {
return nil, fmt.Errorf("inventory not found: %w", err)
}
response := mappers.InventoryEntityToResponse(inventoryEntity)
return response, nil
}
func (p *InventoryProcessorImpl) ListInventory(ctx context.Context, filters map[string]interface{}, page, limit int) ([]models.InventoryResponse, int, error) {
offset := (page - 1) * limit
inventoryEntities, total, err := p.inventoryRepo.List(ctx, filters, limit, offset)
if err != nil {
return nil, 0, fmt.Errorf("failed to list inventory: %w", err)
}
responses := make([]models.InventoryResponse, len(inventoryEntities))
for i, entity := range inventoryEntities {
response := mappers.InventoryEntityToResponse(entity)
if response != nil {
responses[i] = *response
}
}
return responses, int(total), nil
}
func (p *InventoryProcessorImpl) AdjustInventory(ctx context.Context, productID, outletID uuid.UUID, req *models.InventoryAdjustmentRequest) (*models.InventoryResponse, error) {
// Validate product exists
_, err := p.productRepo.GetByID(ctx, productID)
if err != nil {
return nil, fmt.Errorf("invalid product: %w", err)
}
// Validate outlet exists
_, err = p.outletRepo.GetByID(ctx, outletID)
if err != nil {
return nil, fmt.Errorf("invalid outlet: %w", err)
}
// Perform quantity adjustment
adjustedInventory, err := p.inventoryRepo.AdjustQuantity(ctx, productID, outletID, req.Delta)
if err != nil {
return nil, fmt.Errorf("failed to adjust inventory quantity: %w", err)
}
// Get inventory with relations for response
inventoryWithRelations, err := p.inventoryRepo.GetWithRelations(ctx, adjustedInventory.ID)
if err != nil {
return nil, fmt.Errorf("failed to retrieve adjusted inventory: %w", err)
}
// Map entity to response model
response := mappers.InventoryEntityToResponse(inventoryWithRelations)
return response, nil
}
func (p *InventoryProcessorImpl) GetLowStockItems(ctx context.Context, outletID uuid.UUID) ([]models.InventoryResponse, error) {
// Validate outlet exists
_, err := p.outletRepo.GetByID(ctx, outletID)
if err != nil {
return nil, fmt.Errorf("invalid outlet: %w", err)
}
inventoryEntities, err := p.inventoryRepo.GetLowStock(ctx, outletID)
if err != nil {
return nil, fmt.Errorf("failed to get low stock items: %w", err)
}
responses := make([]models.InventoryResponse, len(inventoryEntities))
for i, entity := range inventoryEntities {
response := mappers.InventoryEntityToResponse(entity)
if response != nil {
responses[i] = *response
}
}
return responses, nil
}
func (p *InventoryProcessorImpl) GetZeroStockItems(ctx context.Context, outletID uuid.UUID) ([]models.InventoryResponse, error) {
// Validate outlet exists
_, err := p.outletRepo.GetByID(ctx, outletID)
if err != nil {
return nil, fmt.Errorf("invalid outlet: %w", err)
}
inventoryEntities, err := p.inventoryRepo.GetZeroStock(ctx, outletID)
if err != nil {
return nil, fmt.Errorf("failed to get zero stock items: %w", err)
}
responses := make([]models.InventoryResponse, len(inventoryEntities))
for i, entity := range inventoryEntities {
response := mappers.InventoryEntityToResponse(entity)
if response != nil {
responses[i] = *response
}
}
return responses, nil
}