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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user