2025-07-18 20:10:29 +07:00
|
|
|
package processor
|
|
|
|
|
|
|
|
|
|
import (
|
2025-08-14 01:35:19 +07:00
|
|
|
"apskel-pos-be/internal/appcontext"
|
2025-07-18 20:10:29 +07:00
|
|
|
"context"
|
|
|
|
|
"fmt"
|
2025-08-14 00:38:26 +07:00
|
|
|
"time"
|
2025-07-18 20:10:29 +07:00
|
|
|
|
2025-08-14 01:35:19 +07:00
|
|
|
"apskel-pos-be/internal/contract"
|
|
|
|
|
"apskel-pos-be/internal/entities"
|
2025-07-18 20:10:29 +07:00
|
|
|
"apskel-pos-be/internal/mappers"
|
|
|
|
|
"apskel-pos-be/internal/models"
|
2025-07-30 23:18:20 +07:00
|
|
|
"apskel-pos-be/internal/repository"
|
2025-07-18 20:10:29 +07:00
|
|
|
|
|
|
|
|
"github.com/google/uuid"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
type InventoryProcessor interface {
|
2025-08-13 23:36:31 +07:00
|
|
|
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
|
2025-08-14 01:35:19 +07:00
|
|
|
RestockInventory(ctx context.Context, outletID uuid.UUID, items []contract.RestockItem, reason string) (*contract.RestockInventoryResponse, error)
|
2025-08-14 00:38:26 +07:00
|
|
|
GetInventoryReportSummary(ctx context.Context, outletID, organizationID uuid.UUID, dateFrom, dateTo *time.Time) (*models.InventoryReportSummary, error)
|
2025-08-13 23:36:31 +07:00
|
|
|
GetInventoryReportDetails(ctx context.Context, filter *models.InventoryReportFilter, organizationID uuid.UUID) (*models.InventoryReportDetail, error)
|
2025-07-18 20:10:29 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type InventoryProcessorImpl struct {
|
2025-08-14 01:35:19 +07:00
|
|
|
inventoryRepo repository.InventoryRepository
|
|
|
|
|
productRepo ProductRepository
|
|
|
|
|
outletRepo OutletRepository
|
|
|
|
|
ingredientRepo IngredientRepository
|
|
|
|
|
inventoryMovementRepo repository.InventoryMovementRepository
|
2025-07-18 20:10:29 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func NewInventoryProcessorImpl(
|
2025-07-30 23:18:20 +07:00
|
|
|
inventoryRepo repository.InventoryRepository,
|
2025-07-18 20:10:29 +07:00
|
|
|
productRepo ProductRepository,
|
|
|
|
|
outletRepo OutletRepository,
|
2025-08-14 01:35:19 +07:00
|
|
|
ingredientRepo IngredientRepository,
|
|
|
|
|
inventoryMovementRepo repository.InventoryMovementRepository,
|
2025-07-18 20:10:29 +07:00
|
|
|
) *InventoryProcessorImpl {
|
|
|
|
|
return &InventoryProcessorImpl{
|
2025-08-14 01:35:19 +07:00
|
|
|
inventoryRepo: inventoryRepo,
|
|
|
|
|
productRepo: productRepo,
|
|
|
|
|
outletRepo: outletRepo,
|
|
|
|
|
ingredientRepo: ingredientRepo,
|
|
|
|
|
inventoryMovementRepo: inventoryMovementRepo,
|
2025-07-18 20:10:29 +07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-13 23:36:31 +07:00
|
|
|
// Create creates a new inventory record
|
|
|
|
|
func (p *InventoryProcessorImpl) Create(ctx context.Context, req *models.CreateInventoryRequest, organizationID uuid.UUID) (*models.InventoryResponse, error) {
|
2025-07-18 20:10:29 +07:00
|
|
|
_, 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
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-13 23:36:31 +07:00
|
|
|
// 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) {
|
2025-07-18 20:10:29 +07:00
|
|
|
// 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
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-13 23:36:31 +07:00
|
|
|
// Delete deletes an inventory record
|
|
|
|
|
func (p *InventoryProcessorImpl) Delete(ctx context.Context, id uuid.UUID, organizationID uuid.UUID) error {
|
2025-07-18 20:10:29 +07:00
|
|
|
// 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
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-13 23:36:31 +07:00
|
|
|
// 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
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-14 00:38:26 +07:00
|
|
|
func (p *InventoryProcessorImpl) GetInventoryReportSummary(ctx context.Context, outletID, organizationID uuid.UUID, dateFrom, dateTo *time.Time) (*models.InventoryReportSummary, error) {
|
2025-08-13 23:36:31 +07:00
|
|
|
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")
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-14 00:38:26 +07:00
|
|
|
summary, err := p.inventoryRepo.GetInventoryReportSummary(ctx, outletID, dateFrom, dateTo)
|
2025-08-13 23:36:31 +07:00
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-18 20:10:29 +07:00
|
|
|
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
|
|
|
|
|
}
|
2025-08-14 01:35:19 +07:00
|
|
|
|
|
|
|
|
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
|
|
|
|
|
}
|