308 lines
11 KiB
Go
308 lines
11 KiB
Go
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
|
|
}
|