package processor import ( "context" "fmt" "apskel-pos-be/internal/entities" "apskel-pos-be/internal/mappers" "apskel-pos-be/internal/models" "apskel-pos-be/internal/repository" "github.com/google/uuid" ) type ProductProcessor interface { CreateProduct(ctx context.Context, req *models.CreateProductRequest) (*models.ProductResponse, error) UpdateProduct(ctx context.Context, id uuid.UUID, req *models.UpdateProductRequest) (*models.ProductResponse, error) DeleteProduct(ctx context.Context, id uuid.UUID) error GetProductByID(ctx context.Context, id uuid.UUID) (*models.ProductResponse, error) ListProducts(ctx context.Context, filters map[string]interface{}, page, limit int) ([]models.ProductResponse, int, error) } type ProductRepository interface { Create(ctx context.Context, product *entities.Product) error GetByID(ctx context.Context, id uuid.UUID) (*entities.Product, error) GetWithCategory(ctx context.Context, id uuid.UUID) (*entities.Product, error) GetWithRelations(ctx context.Context, id uuid.UUID) (*entities.Product, error) GetByOrganization(ctx context.Context, organizationID uuid.UUID) ([]*entities.Product, error) GetByCategory(ctx context.Context, categoryID uuid.UUID) ([]*entities.Product, error) GetByBusinessType(ctx context.Context, businessType string) ([]*entities.Product, error) GetActiveByCategoryID(ctx context.Context, categoryID uuid.UUID) ([]*entities.Product, error) Update(ctx context.Context, product *entities.Product) error Delete(ctx context.Context, id uuid.UUID) error List(ctx context.Context, filters map[string]interface{}, limit, offset int) ([]*entities.Product, int64, error) Count(ctx context.Context, filters map[string]interface{}) (int64, error) GetBySKU(ctx context.Context, organizationID uuid.UUID, sku string) (*entities.Product, error) ExistsBySKU(ctx context.Context, organizationID uuid.UUID, sku string, excludeID *uuid.UUID) (bool, error) GetByName(ctx context.Context, organizationID uuid.UUID, name string) (*entities.Product, error) ExistsByName(ctx context.Context, organizationID uuid.UUID, name string, excludeID *uuid.UUID) (bool, error) UpdateActiveStatus(ctx context.Context, id uuid.UUID, isActive bool) error GetLowCostProducts(ctx context.Context, organizationID uuid.UUID, maxCost float64) ([]*entities.Product, error) } type ProductProcessorImpl struct { productRepo ProductRepository categoryRepo CategoryRepository productVariantRepo repository.ProductVariantRepository inventoryRepo InventoryRepository outletRepo OutletRepository } func NewProductProcessorImpl(productRepo ProductRepository, categoryRepo CategoryRepository, productVariantRepo repository.ProductVariantRepository, inventoryRepo InventoryRepository, outletRepo OutletRepository) *ProductProcessorImpl { return &ProductProcessorImpl{ productRepo: productRepo, categoryRepo: categoryRepo, productVariantRepo: productVariantRepo, inventoryRepo: inventoryRepo, outletRepo: outletRepo, } } func (p *ProductProcessorImpl) CreateProduct(ctx context.Context, req *models.CreateProductRequest) (*models.ProductResponse, error) { _, err := p.categoryRepo.GetByID(ctx, req.CategoryID) if err != nil { return nil, fmt.Errorf("invalid category: %w", err) } if req.SKU != nil && *req.SKU != "" { exists, err := p.productRepo.ExistsBySKU(ctx, req.OrganizationID, *req.SKU, nil) if err != nil { return nil, fmt.Errorf("failed to check SKU uniqueness: %w", err) } if exists { return nil, fmt.Errorf("product with SKU '%s' already exists for this organization", *req.SKU) } } exists, err := p.productRepo.ExistsByName(ctx, req.OrganizationID, req.Name, nil) if err != nil { return nil, fmt.Errorf("failed to check product name uniqueness: %w", err) } if exists { return nil, fmt.Errorf("product with name '%s' already exists for this organization", req.Name) } productEntity := mappers.CreateProductRequestToEntity(req) if err := p.productRepo.Create(ctx, productEntity); err != nil { return nil, fmt.Errorf("failed to create product: %w", err) } // Create variants if provided if req.Variants != nil && len(req.Variants) > 0 { for _, variantReq := range req.Variants { // Set the product ID for the variant variantReq.ProductID = productEntity.ID // Check variant name uniqueness within the same product exists, err := p.productVariantRepo.ExistsByName(ctx, productEntity.ID, variantReq.Name, nil) if err != nil { return nil, fmt.Errorf("failed to check variant name uniqueness: %w", err) } if exists { return nil, fmt.Errorf("variant with name '%s' already exists for this product", variantReq.Name) } variantEntity := mappers.CreateProductVariantRequestToEntity(&variantReq) if err := p.productVariantRepo.Create(ctx, variantEntity); err != nil { return nil, fmt.Errorf("failed to create product variant: %w", err) } } } // Create inventory records for all outlets if requested if req.CreateInventory { if err := p.createInventoryForAllOutlets(ctx, productEntity.ID, req.OrganizationID, req.InitialStock, req.ReorderLevel); err != nil { return nil, fmt.Errorf("failed to create inventory records: %w", err) } } productWithCategory, err := p.productRepo.GetWithCategory(ctx, productEntity.ID) if err != nil { return nil, fmt.Errorf("failed to retrieve created product: %w", err) } response := mappers.ProductEntityToResponse(productWithCategory) return response, nil } func (p *ProductProcessorImpl) UpdateProduct(ctx context.Context, id uuid.UUID, req *models.UpdateProductRequest) (*models.ProductResponse, error) { existingProduct, err := p.productRepo.GetByID(ctx, id) if err != nil { return nil, fmt.Errorf("product not found: %w", err) } if req.CategoryID != nil { _, err := p.categoryRepo.GetByID(ctx, *req.CategoryID) if err != nil { return nil, fmt.Errorf("invalid category: %w", err) } } if req.SKU != nil && *req.SKU != "" { currentSKU := "" if existingProduct.SKU != nil { currentSKU = *existingProduct.SKU } if *req.SKU != currentSKU { exists, err := p.productRepo.ExistsBySKU(ctx, existingProduct.OrganizationID, *req.SKU, &id) if err != nil { return nil, fmt.Errorf("failed to check SKU uniqueness: %w", err) } if exists { return nil, fmt.Errorf("product with SKU '%s' already exists for this organization", *req.SKU) } } } if req.Name != nil && *req.Name != existingProduct.Name { exists, err := p.productRepo.ExistsByName(ctx, existingProduct.OrganizationID, *req.Name, &id) if err != nil { return nil, fmt.Errorf("failed to check product name uniqueness: %w", err) } if exists { return nil, fmt.Errorf("product with name '%s' already exists for this organization", *req.Name) } } mappers.UpdateProductEntityFromRequest(existingProduct, req) if err := p.productRepo.Update(ctx, existingProduct); err != nil { return nil, fmt.Errorf("failed to update product: %w", err) } // Update reorder level for all existing inventory records if provided if req.ReorderLevel != nil { if err := p.updateReorderLevelForAllOutlets(ctx, id, *req.ReorderLevel); err != nil { return nil, fmt.Errorf("failed to update reorder levels: %w", err) } } productWithCategory, err := p.productRepo.GetWithCategory(ctx, id) if err != nil { return nil, fmt.Errorf("failed to retrieve updated product: %w", err) } response := mappers.ProductEntityToResponse(productWithCategory) return response, nil } func (p *ProductProcessorImpl) DeleteProduct(ctx context.Context, id uuid.UUID) error { _, err := p.productRepo.GetByID(ctx, id) if err != nil { return fmt.Errorf("product not found: %w", err) } productWithRelations, err := p.productRepo.GetWithRelations(ctx, id) if err != nil { return fmt.Errorf("failed to check product relations: %w", err) } if len(productWithRelations.Inventory) > 0 { return fmt.Errorf("cannot delete product: it has inventory records associated with it") } if len(productWithRelations.OrderItems) > 0 { return fmt.Errorf("cannot delete product: it has order items associated with it") } if err := p.productRepo.Delete(ctx, id); err != nil { return fmt.Errorf("failed to delete product: %w", err) } return nil } func (p *ProductProcessorImpl) GetProductByID(ctx context.Context, id uuid.UUID) (*models.ProductResponse, error) { productEntity, err := p.productRepo.GetWithCategory(ctx, id) if err != nil { return nil, fmt.Errorf("product not found: %w", err) } response := mappers.ProductEntityToResponse(productEntity) return response, nil } func (p *ProductProcessorImpl) ListProducts(ctx context.Context, filters map[string]interface{}, page, limit int) ([]models.ProductResponse, int, error) { offset := (page - 1) * limit productEntities, total, err := p.productRepo.List(ctx, filters, limit, offset) if err != nil { return nil, 0, fmt.Errorf("failed to list products: %w", err) } responses := make([]models.ProductResponse, len(productEntities)) for i, entity := range productEntities { response := mappers.ProductEntityToResponse(entity) if response != nil { responses[i] = *response } } return responses, int(total), nil } // Helper methods for inventory management // createInventoryForAllOutlets creates inventory records for all outlets of an organization func (p *ProductProcessorImpl) createInventoryForAllOutlets(ctx context.Context, productID, organizationID uuid.UUID, initialStock, reorderLevel *int) error { // Get all outlets for the organization outlets, err := p.outletRepo.GetByOrganizationID(ctx, organizationID) if err != nil { return fmt.Errorf("failed to get outlets for organization: %w", err) } if len(outlets) == 0 { return fmt.Errorf("no outlets found for organization") } // Prepare inventory items for bulk creation var inventoryItems []*entities.Inventory for _, outlet := range outlets { quantity := 0 if initialStock != nil { quantity = *initialStock } reorderLevelValue := 0 if reorderLevel != nil { reorderLevelValue = *reorderLevel } inventoryItem := &entities.Inventory{ OutletID: outlet.ID, ProductID: productID, Quantity: quantity, ReorderLevel: reorderLevelValue, } inventoryItems = append(inventoryItems, inventoryItem) } // Bulk create inventory records if err := p.inventoryRepo.BulkCreate(ctx, inventoryItems); err != nil { return fmt.Errorf("failed to bulk create inventory records: %w", err) } return nil } // updateReorderLevelForAllOutlets updates the reorder level for all inventory records of a product func (p *ProductProcessorImpl) updateReorderLevelForAllOutlets(ctx context.Context, productID uuid.UUID, reorderLevel int) error { // Get all inventory records for the product inventoryRecords, err := p.inventoryRepo.GetByProduct(ctx, productID) if err != nil { return fmt.Errorf("failed to get inventory records for product: %w", err) } // Update reorder level for each inventory record for _, inventory := range inventoryRecords { inventory.ReorderLevel = reorderLevel if err := p.inventoryRepo.Update(ctx, inventory); err != nil { return fmt.Errorf("failed to update inventory reorder level: %w", err) } } return nil }