ingredient composition
This commit is contained in:
@@ -3,29 +3,31 @@ package processor
|
||||
import (
|
||||
"apskel-pos-be/internal/appcontext"
|
||||
"apskel-pos-be/internal/entities"
|
||||
"apskel-pos-be/internal/mappers"
|
||||
"apskel-pos-be/internal/models"
|
||||
"apskel-pos-be/internal/transformer"
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type IngredientProcessorImpl struct {
|
||||
ingredientRepo IngredientRepository
|
||||
unitRepo UnitRepository
|
||||
ingredientRepo IngredientRepository
|
||||
unitRepo UnitRepository
|
||||
compositionRepo IngredientCompositionRepository
|
||||
}
|
||||
|
||||
func NewIngredientProcessor(ingredientRepo IngredientRepository, unitRepo UnitRepository) *IngredientProcessorImpl {
|
||||
func NewIngredientProcessor(ingredientRepo IngredientRepository, unitRepo UnitRepository, compositionRepo IngredientCompositionRepository) *IngredientProcessorImpl {
|
||||
return &IngredientProcessorImpl{
|
||||
ingredientRepo: ingredientRepo,
|
||||
unitRepo: unitRepo,
|
||||
ingredientRepo: ingredientRepo,
|
||||
unitRepo: unitRepo,
|
||||
compositionRepo: compositionRepo,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *IngredientProcessorImpl) CreateIngredient(ctx context.Context, req *models.CreateIngredientRequest) (*models.IngredientResponse, error) {
|
||||
_, err := p.unitRepo.GetByID(ctx, req.UnitID, req.OrganizationID)
|
||||
if err != nil {
|
||||
if _, err := p.unitRepo.GetByID(ctx, req.UnitID, req.OrganizationID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -35,7 +37,7 @@ func (p *IngredientProcessorImpl) CreateIngredient(ctx context.Context, req *mod
|
||||
OutletID: req.OutletID,
|
||||
Name: req.Name,
|
||||
UnitID: req.UnitID,
|
||||
Cost: req.Cost,
|
||||
Cost: req.Cost, // akan di-override oleh recalculateCost kalau semi-finished
|
||||
Stock: req.Stock,
|
||||
IsSemiFinished: req.IsSemiFinished,
|
||||
IsActive: req.IsActive,
|
||||
@@ -44,70 +46,30 @@ func (p *IngredientProcessorImpl) CreateIngredient(ctx context.Context, req *mod
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
err = p.ingredientRepo.Create(ctx, ingredient)
|
||||
if err != nil {
|
||||
if req.IsSemiFinished {
|
||||
ingredient.Cost = 0 // dihitung dari compositions
|
||||
}
|
||||
|
||||
if err := p.ingredientRepo.Create(ctx, ingredient); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ingredientWithUnit, err := p.ingredientRepo.GetByID(ctx, ingredient.ID, req.OrganizationID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
// Save compositions if provided (only valid for semi-finished ingredients)
|
||||
if req.IsSemiFinished && len(req.Compositions) > 0 {
|
||||
if err := p.saveCompositions(ctx, ingredient.ID, req.OrganizationID, req.Compositions); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
ingredientModel := mappers.MapIngredientEntityToModel(ingredientWithUnit)
|
||||
response := &models.IngredientResponse{
|
||||
ID: ingredientModel.ID,
|
||||
OrganizationID: ingredientModel.OrganizationID,
|
||||
OutletID: ingredientModel.OutletID,
|
||||
Name: ingredientModel.Name,
|
||||
UnitID: ingredientModel.UnitID,
|
||||
Cost: ingredientModel.Cost,
|
||||
Stock: ingredientModel.Stock,
|
||||
IsSemiFinished: ingredientModel.IsSemiFinished,
|
||||
IsActive: ingredientModel.IsActive,
|
||||
Metadata: ingredientModel.Metadata,
|
||||
CreatedAt: ingredientModel.CreatedAt,
|
||||
UpdatedAt: ingredientModel.UpdatedAt,
|
||||
Unit: ingredientModel.Unit,
|
||||
}
|
||||
|
||||
return response, nil
|
||||
return p.buildIngredientResponse(ctx, ingredient.ID, req.OrganizationID)
|
||||
}
|
||||
|
||||
func (p *IngredientProcessorImpl) GetIngredientByID(ctx context.Context, id uuid.UUID) (*models.IngredientResponse, error) {
|
||||
contextInfo := appcontext.FromGinContext(ctx)
|
||||
|
||||
// For now, we'll need to get organizationID from context or request
|
||||
// This is a limitation of the current interface design
|
||||
organizationID := contextInfo.OrganizationID // This should come from context
|
||||
|
||||
ingredient, err := p.ingredientRepo.GetByID(ctx, id, organizationID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ingredientModel := mappers.MapIngredientEntityToModel(ingredient)
|
||||
response := &models.IngredientResponse{
|
||||
ID: ingredientModel.ID,
|
||||
OrganizationID: ingredientModel.OrganizationID,
|
||||
OutletID: ingredientModel.OutletID,
|
||||
Name: ingredientModel.Name,
|
||||
UnitID: ingredientModel.UnitID,
|
||||
Cost: ingredientModel.Cost,
|
||||
Stock: ingredientModel.Stock,
|
||||
IsSemiFinished: ingredientModel.IsSemiFinished,
|
||||
IsActive: ingredientModel.IsActive,
|
||||
Metadata: ingredientModel.Metadata,
|
||||
CreatedAt: ingredientModel.CreatedAt,
|
||||
UpdatedAt: ingredientModel.UpdatedAt,
|
||||
Unit: ingredientModel.Unit,
|
||||
}
|
||||
|
||||
return response, nil
|
||||
ctxInfo := appcontext.FromGinContext(ctx)
|
||||
return p.buildIngredientResponse(ctx, id, ctxInfo.OrganizationID)
|
||||
}
|
||||
|
||||
func (p *IngredientProcessorImpl) ListIngredients(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID, page, limit int, search string) (*models.PaginatedResponse[models.IngredientResponse], error) {
|
||||
// Set default values
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
@@ -123,30 +85,9 @@ func (p *IngredientProcessorImpl) ListIngredients(ctx context.Context, organizat
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Map to response models
|
||||
ingredientModels := mappers.MapIngredientEntitiesToModels(ingredients)
|
||||
ingredientResponses := make([]models.IngredientResponse, len(ingredientModels))
|
||||
ingredientResponses := transformer.IngredientsToResponses(ingredients)
|
||||
|
||||
for i, ingredientModel := range ingredientModels {
|
||||
ingredientResponses[i] = models.IngredientResponse{
|
||||
ID: ingredientModel.ID,
|
||||
OrganizationID: ingredientModel.OrganizationID,
|
||||
OutletID: ingredientModel.OutletID,
|
||||
Name: ingredientModel.Name,
|
||||
UnitID: ingredientModel.UnitID,
|
||||
Cost: ingredientModel.Cost,
|
||||
Stock: ingredientModel.Stock,
|
||||
IsSemiFinished: ingredientModel.IsSemiFinished,
|
||||
IsActive: ingredientModel.IsActive,
|
||||
Metadata: ingredientModel.Metadata,
|
||||
CreatedAt: ingredientModel.CreatedAt,
|
||||
UpdatedAt: ingredientModel.UpdatedAt,
|
||||
Unit: ingredientModel.Unit,
|
||||
}
|
||||
}
|
||||
|
||||
// Create paginated response
|
||||
paginatedResponse := &models.PaginatedResponse[models.IngredientResponse]{
|
||||
return &models.PaginatedResponse[models.IngredientResponse]{
|
||||
Data: ingredientResponses,
|
||||
Pagination: models.Pagination{
|
||||
Page: page,
|
||||
@@ -154,86 +95,250 @@ func (p *IngredientProcessorImpl) ListIngredients(ctx context.Context, organizat
|
||||
Total: int64(total),
|
||||
TotalPages: (total + limit - 1) / limit,
|
||||
},
|
||||
}
|
||||
|
||||
return paginatedResponse, nil
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (p *IngredientProcessorImpl) UpdateIngredient(ctx context.Context, id uuid.UUID, req *models.UpdateIngredientRequest) (*models.IngredientResponse, error) {
|
||||
contextInfo := appcontext.FromGinContext(ctx)
|
||||
// For now, we'll need to get organizationID from context or request
|
||||
// This is a limitation of the current interface design
|
||||
organizationID := contextInfo.OrganizationID // This should come from context
|
||||
ctxInfo := appcontext.FromGinContext(ctx)
|
||||
organizationID := ctxInfo.OrganizationID
|
||||
|
||||
// Get existing ingredient
|
||||
existingIngredient, err := p.ingredientRepo.GetByID(ctx, id, organizationID)
|
||||
existing, err := p.ingredientRepo.GetByID(ctx, id, organizationID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Validate unit exists if changed
|
||||
if req.UnitID != existingIngredient.UnitID {
|
||||
_, err := p.unitRepo.GetByID(ctx, req.UnitID, organizationID)
|
||||
if err != nil {
|
||||
if req.UnitID != existing.UnitID {
|
||||
if _, err := p.unitRepo.GetByID(ctx, req.UnitID, organizationID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Update fields
|
||||
if req.OutletID != nil {
|
||||
existingIngredient.OutletID = req.OutletID
|
||||
existing.OutletID = req.OutletID
|
||||
existing.Name = req.Name
|
||||
existing.UnitID = req.UnitID
|
||||
existing.Stock = req.Stock
|
||||
existing.IsSemiFinished = req.IsSemiFinished
|
||||
existing.IsActive = req.IsActive
|
||||
existing.Metadata = req.Metadata
|
||||
existing.UpdatedAt = time.Now()
|
||||
|
||||
// Cost hanya dipakai kalau bukan semi-finished
|
||||
// Kalau semi-finished, cost dihitung dari compositions
|
||||
if !req.IsSemiFinished {
|
||||
existing.Cost = req.Cost
|
||||
}
|
||||
|
||||
existingIngredient.Name = req.Name
|
||||
existingIngredient.UnitID = req.UnitID
|
||||
existingIngredient.Cost = req.Cost
|
||||
existingIngredient.Stock = req.Stock
|
||||
existingIngredient.IsSemiFinished = req.IsSemiFinished
|
||||
existingIngredient.IsActive = req.IsActive
|
||||
existingIngredient.Metadata = req.Metadata
|
||||
existingIngredient.UpdatedAt = time.Now()
|
||||
|
||||
// Save to database
|
||||
err = p.ingredientRepo.Update(ctx, existingIngredient)
|
||||
if err != nil {
|
||||
if err := p.ingredientRepo.Update(ctx, existing); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Get with relations
|
||||
ingredientWithUnit, err := p.ingredientRepo.GetByID(ctx, id, organizationID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Map to response
|
||||
ingredientModel := mappers.MapIngredientEntityToModel(ingredientWithUnit)
|
||||
response := &models.IngredientResponse{
|
||||
ID: ingredientModel.ID,
|
||||
OrganizationID: ingredientModel.OrganizationID,
|
||||
OutletID: ingredientModel.OutletID,
|
||||
Name: ingredientModel.Name,
|
||||
UnitID: ingredientModel.UnitID,
|
||||
Cost: ingredientModel.Cost,
|
||||
Stock: ingredientModel.Stock,
|
||||
IsSemiFinished: ingredientModel.IsSemiFinished,
|
||||
IsActive: ingredientModel.IsActive,
|
||||
Metadata: ingredientModel.Metadata,
|
||||
CreatedAt: ingredientModel.CreatedAt,
|
||||
UpdatedAt: ingredientModel.UpdatedAt,
|
||||
Unit: ingredientModel.Unit,
|
||||
}
|
||||
|
||||
return response, nil
|
||||
return p.buildIngredientResponse(ctx, id, organizationID)
|
||||
}
|
||||
|
||||
func (p *IngredientProcessorImpl) DeleteIngredient(ctx context.Context, id uuid.UUID) error {
|
||||
contextInfo := appcontext.FromGinContext(ctx)
|
||||
organizationID := contextInfo.OrganizationID
|
||||
ctxInfo := appcontext.FromGinContext(ctx)
|
||||
return p.ingredientRepo.Delete(ctx, id, ctxInfo.OrganizationID)
|
||||
}
|
||||
|
||||
err := p.ingredientRepo.Delete(ctx, id, organizationID)
|
||||
func (p *IngredientProcessorImpl) AddCompositions(ctx context.Context, parentID uuid.UUID, req *models.AddIngredientCompositionsRequest) (*models.AddIngredientCompositionsResponse, error) {
|
||||
ctxInfo := appcontext.FromGinContext(ctx)
|
||||
organizationID := ctxInfo.OrganizationID
|
||||
|
||||
// Parent must exist and be semi-finished
|
||||
parent, err := p.ingredientRepo.GetByID(ctx, parentID, organizationID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parent ingredient not found: %w", err)
|
||||
}
|
||||
if !parent.IsSemiFinished {
|
||||
return nil, fmt.Errorf("parent ingredient must be marked as semi-finished")
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
createdIDs := make([]uuid.UUID, 0, len(req.Compositions))
|
||||
|
||||
// Validate and create all compositions
|
||||
for _, item := range req.Compositions {
|
||||
// Child must exist
|
||||
if _, err := p.ingredientRepo.GetByID(ctx, item.ChildIngredientID, organizationID); err != nil {
|
||||
return nil, fmt.Errorf("child ingredient %s not found: %w", item.ChildIngredientID, err)
|
||||
}
|
||||
|
||||
if item.ChildIngredientID == parentID {
|
||||
return nil, fmt.Errorf("child ingredient cannot be the same as parent")
|
||||
}
|
||||
|
||||
// Resolve outlet
|
||||
outletID := item.OutletID
|
||||
if outletID == nil && ctxInfo.OutletID != uuid.Nil {
|
||||
outletID = &ctxInfo.OutletID
|
||||
}
|
||||
|
||||
compositionID := uuid.New()
|
||||
composition := &entities.IngredientComposition{
|
||||
ID: compositionID,
|
||||
OrganizationID: organizationID,
|
||||
OutletID: outletID,
|
||||
ParentIngredientID: parentID,
|
||||
ChildIngredientID: item.ChildIngredientID,
|
||||
Quantity: item.Quantity,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
|
||||
if err := p.compositionRepo.Create(ctx, composition); err != nil {
|
||||
return nil, fmt.Errorf("failed to add composition: %w", err)
|
||||
}
|
||||
|
||||
createdIDs = append(createdIDs, compositionID)
|
||||
}
|
||||
|
||||
// Recalculate parent cost
|
||||
if err := p.recalculateCost(ctx, parentID, organizationID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Get updated parent ingredient with all compositions
|
||||
updatedParent, err := p.buildIngredientResponse(ctx, parentID, organizationID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Get created compositions with full details
|
||||
createdCompositions := make([]*models.IngredientCompositionResponse, 0, len(createdIDs))
|
||||
for _, id := range createdIDs {
|
||||
comp, err := p.compositionRepo.GetByID(ctx, id, organizationID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
createdCompositions = append(createdCompositions, transformer.CompositionEntityToResponse(comp))
|
||||
}
|
||||
|
||||
return &models.AddIngredientCompositionsResponse{
|
||||
ParentIngredient: updatedParent,
|
||||
Compositions: createdCompositions,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (p *IngredientProcessorImpl) UpdateComposition(ctx context.Context, id uuid.UUID, req *models.UpdateIngredientCompositionRequest) (*models.IngredientCompositionResponse, error) {
|
||||
ctxInfo := appcontext.FromGinContext(ctx)
|
||||
|
||||
existing, err := p.compositionRepo.GetByID(ctx, id, ctxInfo.OrganizationID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("composition not found: %w", err)
|
||||
}
|
||||
|
||||
existing.Quantity = req.Quantity
|
||||
existing.OutletID = req.OutletID
|
||||
existing.UpdatedAt = time.Now()
|
||||
|
||||
if err := p.compositionRepo.Update(ctx, existing); err != nil {
|
||||
return nil, fmt.Errorf("failed to update composition: %w", err)
|
||||
}
|
||||
|
||||
// Recalculate parent cost since quantity changed
|
||||
if err := p.recalculateCost(ctx, existing.ParentIngredientID, ctxInfo.OrganizationID); err != nil {
|
||||
return nil, fmt.Errorf("failed to recalculate cost: %w", err)
|
||||
}
|
||||
|
||||
updated, err := p.compositionRepo.GetByID(ctx, id, ctxInfo.OrganizationID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return transformer.CompositionEntityToResponse(updated), nil
|
||||
}
|
||||
|
||||
func (p *IngredientProcessorImpl) DeleteComposition(ctx context.Context, id uuid.UUID) (*models.IngredientResponse, error) {
|
||||
ctxInfo := appcontext.FromGinContext(ctx)
|
||||
|
||||
existing, err := p.compositionRepo.GetByID(ctx, id, ctxInfo.OrganizationID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("composition not found: %w", err)
|
||||
}
|
||||
|
||||
parentID := existing.ParentIngredientID
|
||||
|
||||
if err := p.compositionRepo.Delete(ctx, id, ctxInfo.OrganizationID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Recalculate parent cost after removal
|
||||
if err := p.recalculateCost(ctx, parentID, ctxInfo.OrganizationID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Return updated parent ingredient
|
||||
return p.buildIngredientResponse(ctx, parentID, ctxInfo.OrganizationID)
|
||||
}
|
||||
|
||||
// --- private helpers ---
|
||||
|
||||
func (p *IngredientProcessorImpl) saveCompositions(ctx context.Context, parentID, organizationID uuid.UUID, items []models.CompositionItemRequest) error {
|
||||
ctxInfo := appcontext.FromGinContext(ctx)
|
||||
now := time.Now()
|
||||
|
||||
for _, item := range items {
|
||||
if item.ChildIngredientID == parentID {
|
||||
return fmt.Errorf("child ingredient cannot be the same as parent")
|
||||
}
|
||||
if _, err := p.ingredientRepo.GetByID(ctx, item.ChildIngredientID, organizationID); err != nil {
|
||||
return fmt.Errorf("child ingredient %s not found: %w", item.ChildIngredientID, err)
|
||||
}
|
||||
|
||||
// Resolve outlet: use item's outlet if provided, otherwise fall back to context outlet
|
||||
outletID := item.OutletID
|
||||
if outletID == nil && ctxInfo.OutletID != uuid.Nil {
|
||||
outletID = &ctxInfo.OutletID
|
||||
}
|
||||
|
||||
composition := &entities.IngredientComposition{
|
||||
ID: uuid.New(),
|
||||
OrganizationID: organizationID,
|
||||
OutletID: outletID,
|
||||
ParentIngredientID: parentID,
|
||||
ChildIngredientID: item.ChildIngredientID,
|
||||
Quantity: item.Quantity,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
if err := p.compositionRepo.Create(ctx, composition); err != nil {
|
||||
return fmt.Errorf("failed to save composition: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Recalculate cost of the semi-finished parent from child costs
|
||||
return p.recalculateCost(ctx, parentID, organizationID)
|
||||
}
|
||||
|
||||
// recalculateCost sums up (child.cost * composition.quantity) and updates the parent ingredient cost.
|
||||
func (p *IngredientProcessorImpl) recalculateCost(ctx context.Context, parentID, organizationID uuid.UUID) error {
|
||||
compositions, err := p.compositionRepo.GetByParentIngredientID(ctx, parentID, organizationID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
var totalCost float64
|
||||
for _, c := range compositions {
|
||||
if c.ChildIngredient != nil {
|
||||
totalCost += c.ChildIngredient.Cost * c.Quantity
|
||||
}
|
||||
}
|
||||
|
||||
parent, err := p.ingredientRepo.GetByID(ctx, parentID, organizationID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
parent.Cost = totalCost
|
||||
parent.UpdatedAt = time.Now()
|
||||
return p.ingredientRepo.Update(ctx, parent)
|
||||
}
|
||||
|
||||
func (p *IngredientProcessorImpl) buildIngredientResponse(ctx context.Context, id, organizationID uuid.UUID) (*models.IngredientResponse, error) {
|
||||
ingredient, err := p.ingredientRepo.GetByID(ctx, id, organizationID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return transformer.IngredientEntityToResponse(ingredient), nil
|
||||
}
|
||||
|
||||
@@ -1,180 +0,0 @@
|
||||
package processor
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"apskel-pos-be/internal/models"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
// Mock repositories for testing
|
||||
type MockProductRepository struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
type MockCategoryRepository struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
type MockProductVariantRepository struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
type MockInventoryRepository struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
type MockOutletRepository struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
// Test helper functions
|
||||
func TestCreateProductWithInventory(t *testing.T) {
|
||||
// This is a basic test structure - in a real implementation,
|
||||
// you would use a proper testing framework with database mocks
|
||||
|
||||
t.Run("should create product with inventory when create_inventory is true", func(t *testing.T) {
|
||||
// Arrange
|
||||
productRepo := &MockProductRepository{}
|
||||
categoryRepo := &MockCategoryRepository{}
|
||||
productVariantRepo := &MockProductVariantRepository{}
|
||||
inventoryRepo := &MockInventoryRepository{}
|
||||
outletRepo := &MockOutletRepository{}
|
||||
|
||||
processor := NewProductProcessorImpl(
|
||||
productRepo,
|
||||
categoryRepo,
|
||||
productVariantRepo,
|
||||
inventoryRepo,
|
||||
outletRepo,
|
||||
)
|
||||
|
||||
req := &models.CreateProductRequest{
|
||||
OrganizationID: uuid.New(),
|
||||
CategoryID: uuid.New(),
|
||||
Name: "Test Product",
|
||||
Price: 10.0,
|
||||
Cost: 5.0,
|
||||
InitialStock: &[]int{100}[0],
|
||||
ReorderLevel: &[]int{20}[0],
|
||||
CreateInventory: true,
|
||||
}
|
||||
|
||||
// Mock expectations
|
||||
categoryRepo.On("GetByID", mock.Anything, req.CategoryID).Return(&models.Category{}, nil)
|
||||
productRepo.On("ExistsBySKU", mock.Anything, req.OrganizationID, mock.Anything, mock.Anything).Return(false, nil)
|
||||
productRepo.On("ExistsByName", mock.Anything, req.OrganizationID, req.Name, mock.Anything).Return(false, nil)
|
||||
productRepo.On("Create", mock.Anything, mock.Anything).Return(nil)
|
||||
productRepo.On("GetWithCategory", mock.Anything, mock.Anything).Return(&models.Product{}, nil)
|
||||
|
||||
// Mock outlets
|
||||
outlets := []*models.Outlet{
|
||||
{ID: uuid.New()},
|
||||
{ID: uuid.New()},
|
||||
}
|
||||
outletRepo.On("GetByOrganizationID", mock.Anything, req.OrganizationID).Return(outlets, nil)
|
||||
|
||||
// Mock inventory creation
|
||||
inventoryRepo.On("BulkCreate", mock.Anything, mock.Anything).Return(nil)
|
||||
|
||||
// Act
|
||||
result, err := processor.CreateProduct(context.Background(), req)
|
||||
|
||||
// Assert
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, result)
|
||||
|
||||
// Verify that inventory was created
|
||||
inventoryRepo.AssertCalled(t, "BulkCreate", mock.Anything, mock.Anything)
|
||||
outletRepo.AssertCalled(t, "GetByOrganizationID", mock.Anything, req.OrganizationID)
|
||||
})
|
||||
|
||||
t.Run("should not create inventory when create_inventory is false", func(t *testing.T) {
|
||||
// Arrange
|
||||
productRepo := &MockProductRepository{}
|
||||
categoryRepo := &MockCategoryRepository{}
|
||||
productVariantRepo := &MockProductVariantRepository{}
|
||||
inventoryRepo := &MockInventoryRepository{}
|
||||
outletRepo := &MockOutletRepository{}
|
||||
|
||||
processor := NewProductProcessorImpl(
|
||||
productRepo,
|
||||
categoryRepo,
|
||||
productVariantRepo,
|
||||
inventoryRepo,
|
||||
outletRepo,
|
||||
)
|
||||
|
||||
req := &models.CreateProductRequest{
|
||||
OrganizationID: uuid.New(),
|
||||
CategoryID: uuid.New(),
|
||||
Name: "Test Product",
|
||||
Price: 10.0,
|
||||
Cost: 5.0,
|
||||
CreateInventory: false,
|
||||
}
|
||||
|
||||
// Mock expectations
|
||||
categoryRepo.On("GetByID", mock.Anything, req.CategoryID).Return(&models.Category{}, nil)
|
||||
productRepo.On("ExistsBySKU", mock.Anything, req.OrganizationID, mock.Anything, mock.Anything).Return(false, nil)
|
||||
productRepo.On("ExistsByName", mock.Anything, req.OrganizationID, req.Name, mock.Anything).Return(false, nil)
|
||||
productRepo.On("Create", mock.Anything, mock.Anything).Return(nil)
|
||||
productRepo.On("GetWithCategory", mock.Anything, mock.Anything).Return(&models.Product{}, nil)
|
||||
|
||||
// Act
|
||||
result, err := processor.CreateProduct(context.Background(), req)
|
||||
|
||||
// Assert
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, result)
|
||||
|
||||
// Verify that inventory was NOT created
|
||||
inventoryRepo.AssertNotCalled(t, "BulkCreate", mock.Anything, mock.Anything)
|
||||
outletRepo.AssertNotCalled(t, "GetByOrganizationID", mock.Anything, mock.Anything)
|
||||
})
|
||||
}
|
||||
|
||||
// Mock implementations (simplified for testing)
|
||||
func (m *MockProductRepository) Create(ctx context.Context, product *models.Product) error {
|
||||
args := m.Called(ctx, product)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
func (m *MockProductRepository) GetByID(ctx context.Context, id uuid.UUID) (*models.Product, error) {
|
||||
args := m.Called(ctx, id)
|
||||
return args.Get(0).(*models.Product), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockProductRepository) GetWithCategory(ctx context.Context, id uuid.UUID) (*models.Product, error) {
|
||||
args := m.Called(ctx, id)
|
||||
return args.Get(0).(*models.Product), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockProductRepository) ExistsBySKU(ctx context.Context, organizationID uuid.UUID, sku string, excludeID *uuid.UUID) (bool, error) {
|
||||
args := m.Called(ctx, organizationID, sku, excludeID)
|
||||
return args.Bool(0), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockProductRepository) ExistsByName(ctx context.Context, organizationID uuid.UUID, name string, excludeID *uuid.UUID) (bool, error) {
|
||||
args := m.Called(ctx, organizationID, name, excludeID)
|
||||
return args.Bool(0), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockCategoryRepository) GetByID(ctx context.Context, id uuid.UUID) (*models.Category, error) {
|
||||
args := m.Called(ctx, id)
|
||||
return args.Get(0).(*models.Category), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockInventoryRepository) BulkCreate(ctx context.Context, inventoryItems []*models.Inventory) error {
|
||||
args := m.Called(ctx, inventoryItems)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
func (m *MockOutletRepository) GetByOrganizationID(ctx context.Context, organizationID uuid.UUID) ([]*models.Outlet, error) {
|
||||
args := m.Called(ctx, organizationID)
|
||||
return args.Get(0).([]*models.Outlet), args.Error(1)
|
||||
}
|
||||
@@ -82,3 +82,12 @@ type UnitRepository interface {
|
||||
Update(ctx context.Context, unit *entities.Unit) error
|
||||
Delete(ctx context.Context, id, organizationID uuid.UUID) error
|
||||
}
|
||||
|
||||
type IngredientCompositionRepository interface {
|
||||
Create(ctx context.Context, composition *entities.IngredientComposition) error
|
||||
GetByID(ctx context.Context, id, organizationID uuid.UUID) (*entities.IngredientComposition, error)
|
||||
GetByParentIngredientID(ctx context.Context, parentIngredientID, organizationID uuid.UUID) ([]*entities.IngredientComposition, error)
|
||||
Update(ctx context.Context, composition *entities.IngredientComposition) error
|
||||
Delete(ctx context.Context, id, organizationID uuid.UUID) error
|
||||
DeleteByParentIngredientID(ctx context.Context, parentIngredientID, organizationID uuid.UUID) error
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user