345 lines
11 KiB
Go
345 lines
11 KiB
Go
package processor
|
|
|
|
import (
|
|
"apskel-pos-be/internal/appcontext"
|
|
"apskel-pos-be/internal/entities"
|
|
"apskel-pos-be/internal/models"
|
|
"apskel-pos-be/internal/transformer"
|
|
"context"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
type IngredientProcessorImpl struct {
|
|
ingredientRepo IngredientRepository
|
|
unitRepo UnitRepository
|
|
compositionRepo IngredientCompositionRepository
|
|
}
|
|
|
|
func NewIngredientProcessor(ingredientRepo IngredientRepository, unitRepo UnitRepository, compositionRepo IngredientCompositionRepository) *IngredientProcessorImpl {
|
|
return &IngredientProcessorImpl{
|
|
ingredientRepo: ingredientRepo,
|
|
unitRepo: unitRepo,
|
|
compositionRepo: compositionRepo,
|
|
}
|
|
}
|
|
|
|
func (p *IngredientProcessorImpl) CreateIngredient(ctx context.Context, req *models.CreateIngredientRequest) (*models.IngredientResponse, error) {
|
|
if _, err := p.unitRepo.GetByID(ctx, req.UnitID, req.OrganizationID); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
ingredient := &entities.Ingredient{
|
|
ID: uuid.New(),
|
|
OrganizationID: req.OrganizationID,
|
|
OutletID: req.OutletID,
|
|
Name: req.Name,
|
|
UnitID: req.UnitID,
|
|
Cost: req.Cost, // akan di-override oleh recalculateCost kalau semi-finished
|
|
Stock: req.Stock,
|
|
IsSemiFinished: req.IsSemiFinished,
|
|
IsActive: req.IsActive,
|
|
Metadata: req.Metadata,
|
|
CreatedAt: time.Now(),
|
|
UpdatedAt: time.Now(),
|
|
}
|
|
|
|
if req.IsSemiFinished {
|
|
ingredient.Cost = 0 // dihitung dari compositions
|
|
}
|
|
|
|
if err := p.ingredientRepo.Create(ctx, ingredient); 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
|
|
}
|
|
}
|
|
|
|
return p.buildIngredientResponse(ctx, ingredient.ID, req.OrganizationID)
|
|
}
|
|
|
|
func (p *IngredientProcessorImpl) GetIngredientByID(ctx context.Context, id uuid.UUID) (*models.IngredientResponse, error) {
|
|
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) {
|
|
if page < 1 {
|
|
page = 1
|
|
}
|
|
if limit < 1 {
|
|
limit = 10
|
|
}
|
|
if limit > 100 {
|
|
limit = 100
|
|
}
|
|
|
|
ingredients, total, err := p.ingredientRepo.GetAll(ctx, organizationID, outletID, page, limit, search, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
ingredientResponses := transformer.IngredientsToResponses(ingredients)
|
|
|
|
return &models.PaginatedResponse[models.IngredientResponse]{
|
|
Data: ingredientResponses,
|
|
Pagination: models.Pagination{
|
|
Page: page,
|
|
Limit: limit,
|
|
Total: int64(total),
|
|
TotalPages: (total + limit - 1) / limit,
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
func (p *IngredientProcessorImpl) UpdateIngredient(ctx context.Context, id uuid.UUID, req *models.UpdateIngredientRequest) (*models.IngredientResponse, error) {
|
|
ctxInfo := appcontext.FromGinContext(ctx)
|
|
organizationID := ctxInfo.OrganizationID
|
|
|
|
existing, err := p.ingredientRepo.GetByID(ctx, id, organizationID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if req.UnitID != existing.UnitID {
|
|
if _, err := p.unitRepo.GetByID(ctx, req.UnitID, organizationID); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
if err := p.ingredientRepo.Update(ctx, existing); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return p.buildIngredientResponse(ctx, id, organizationID)
|
|
}
|
|
|
|
func (p *IngredientProcessorImpl) DeleteIngredient(ctx context.Context, id uuid.UUID) error {
|
|
ctxInfo := appcontext.FromGinContext(ctx)
|
|
return p.ingredientRepo.Delete(ctx, id, ctxInfo.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
|
|
}
|
|
|
|
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
|
|
}
|