apskel-pos-backend/internal/processor/product_processor.go

308 lines
11 KiB
Go
Raw Normal View History

2025-07-18 20:10:29 +07:00
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
2025-07-30 23:18:20 +07:00
inventoryRepo repository.InventoryRepository
2025-07-18 20:10:29 +07:00
outletRepo OutletRepository
}
2025-07-30 23:18:20 +07:00
func NewProductProcessorImpl(productRepo ProductRepository, categoryRepo CategoryRepository, productVariantRepo repository.ProductVariantRepository, inventoryRepo repository.InventoryRepository, outletRepo OutletRepository) *ProductProcessorImpl {
2025-07-18 20:10:29 +07:00
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
}