feat(purchasae): added team with category parent and central
This commit is contained in:
@@ -24,6 +24,7 @@ type CategoryRepository interface {
|
||||
GetByID(ctx context.Context, id uuid.UUID) (*entities.Category, error)
|
||||
GetWithProducts(ctx context.Context, id uuid.UUID) (*entities.Category, error)
|
||||
GetByOrganization(ctx context.Context, organizationID uuid.UUID) ([]*entities.Category, error)
|
||||
ListParentCategories(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID) ([]*entities.Category, error)
|
||||
GetByBusinessType(ctx context.Context, businessType string) ([]*entities.Category, error)
|
||||
Update(ctx context.Context, category *entities.Category) error
|
||||
Delete(ctx context.Context, id uuid.UUID) error
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
package processor
|
||||
|
||||
import (
|
||||
"apskel-pos-be/internal/constants"
|
||||
"apskel-pos-be/internal/entities"
|
||||
"apskel-pos-be/internal/mappers"
|
||||
"apskel-pos-be/internal/models"
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
@@ -19,6 +21,7 @@ type PurchaseOrderProcessor interface {
|
||||
GetPurchaseOrdersByStatus(ctx context.Context, organizationID uuid.UUID, status string) ([]*models.PurchaseOrderResponse, error)
|
||||
GetOverduePurchaseOrders(ctx context.Context, organizationID uuid.UUID) ([]*models.PurchaseOrderResponse, error)
|
||||
UpdatePurchaseOrderStatus(ctx context.Context, id, organizationID, userID, outletID uuid.UUID, status string) (*models.PurchaseOrderResponse, error)
|
||||
ListPurchaseTeams(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID) (*models.ListPurchaseTeamsResponse, error)
|
||||
}
|
||||
|
||||
type PurchaseOrderProcessorImpl struct {
|
||||
@@ -26,6 +29,7 @@ type PurchaseOrderProcessorImpl struct {
|
||||
vendorRepo VendorRepository
|
||||
ingredientRepo IngredientRepository
|
||||
purchaseCategoryRepo PurchaseCategoryRepository
|
||||
categoryRepo CategoryRepository
|
||||
unitRepo UnitRepository
|
||||
fileRepo FileRepository
|
||||
inventoryMovementService InventoryMovementService
|
||||
@@ -37,6 +41,7 @@ func NewPurchaseOrderProcessorImpl(
|
||||
vendorRepo VendorRepository,
|
||||
ingredientRepo IngredientRepository,
|
||||
purchaseCategoryRepo PurchaseCategoryRepository,
|
||||
categoryRepo CategoryRepository,
|
||||
unitRepo UnitRepository,
|
||||
fileRepo FileRepository,
|
||||
inventoryMovementService InventoryMovementService,
|
||||
@@ -47,6 +52,7 @@ func NewPurchaseOrderProcessorImpl(
|
||||
vendorRepo: vendorRepo,
|
||||
ingredientRepo: ingredientRepo,
|
||||
purchaseCategoryRepo: purchaseCategoryRepo,
|
||||
categoryRepo: categoryRepo,
|
||||
unitRepo: unitRepo,
|
||||
fileRepo: fileRepo,
|
||||
inventoryMovementService: inventoryMovementService,
|
||||
@@ -63,6 +69,11 @@ func (p *PurchaseOrderProcessorImpl) CreatePurchaseOrder(ctx context.Context, or
|
||||
}
|
||||
}
|
||||
|
||||
teamScope, teamCategoryID, err := p.resolvePurchaseTeam(ctx, organizationID, outletID, req.TeamScope, req.TeamCategoryID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Check if PO number already exists in organization
|
||||
existingPO, err := p.purchaseOrderRepo.GetByPONumber(ctx, req.PONumber, organizationID)
|
||||
if err == nil && existingPO != nil {
|
||||
@@ -124,6 +135,8 @@ func (p *PurchaseOrderProcessorImpl) CreatePurchaseOrder(ctx context.Context, or
|
||||
Status: "draft", // Default status
|
||||
Message: req.Message,
|
||||
TotalAmount: totalAmount,
|
||||
TeamScope: teamScope,
|
||||
TeamCategoryID: teamCategoryID,
|
||||
}
|
||||
|
||||
if req.Status != nil {
|
||||
@@ -221,6 +234,16 @@ func (p *PurchaseOrderProcessorImpl) UpdatePurchaseOrder(ctx context.Context, id
|
||||
poEntity.Message = req.Message
|
||||
}
|
||||
|
||||
// An omitted team_scope leaves the team as it is; an empty one clears it.
|
||||
if req.TeamScope != nil {
|
||||
teamScope, teamCategoryID, err := p.resolvePurchaseTeam(ctx, organizationID, poEntity.OutletID, req.TeamScope, req.TeamCategoryID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
poEntity.TeamScope = teamScope
|
||||
poEntity.TeamCategoryID = teamCategoryID
|
||||
}
|
||||
|
||||
// Update items if provided
|
||||
if req.Items != nil {
|
||||
totalAmount := 0.0
|
||||
@@ -501,6 +524,79 @@ func (p *PurchaseOrderProcessorImpl) UpdatePurchaseOrderStatus(ctx context.Conte
|
||||
return mappers.PurchaseOrderEntityToResponse(updatedPO), nil
|
||||
}
|
||||
|
||||
// ListPurchaseTeams returns the teams a purchase can be charged to: the parent
|
||||
// categories of the outlet in scope, followed by Pusat. Pusat has no category row,
|
||||
// so it is appended here rather than read from the database.
|
||||
func (p *PurchaseOrderProcessorImpl) ListPurchaseTeams(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID) (*models.ListPurchaseTeamsResponse, error) {
|
||||
categories, err := p.categoryRepo.ListParentCategories(ctx, organizationID, outletID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list parent categories: %w", err)
|
||||
}
|
||||
|
||||
teams := make([]models.PurchaseTeam, 0, len(categories)+1)
|
||||
for _, category := range categories {
|
||||
categoryID := category.ID
|
||||
teams = append(teams, models.PurchaseTeam{
|
||||
Scope: constants.PurchaseTeamScopeCategory,
|
||||
CategoryID: &categoryID,
|
||||
Name: category.Name,
|
||||
})
|
||||
}
|
||||
|
||||
teams = append(teams, models.PurchaseTeam{
|
||||
Scope: constants.PurchaseTeamScopeCentral,
|
||||
Name: constants.PurchaseTeamCentralName,
|
||||
})
|
||||
|
||||
return &models.ListPurchaseTeamsResponse{Teams: teams}, nil
|
||||
}
|
||||
|
||||
// resolvePurchaseTeam turns a requested team into the scope/category pair stored on
|
||||
// the purchase order, mirroring the database check constraint. A nil or empty scope
|
||||
// leaves the purchase without a team, which is deliberately different from Pusat.
|
||||
// Which outlet's Pusat a purchase belongs to comes from the purchase order's outlet,
|
||||
// so 'central' needs nothing stored beyond the scope itself.
|
||||
func (p *PurchaseOrderProcessorImpl) resolvePurchaseTeam(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID, scope *string, categoryID *uuid.UUID) (*string, *uuid.UUID, error) {
|
||||
if scope == nil {
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
switch strings.TrimSpace(*scope) {
|
||||
case "":
|
||||
return nil, nil, nil
|
||||
|
||||
case constants.PurchaseTeamScopeCentral:
|
||||
resolved := constants.PurchaseTeamScopeCentral
|
||||
return &resolved, nil, nil
|
||||
|
||||
case constants.PurchaseTeamScopeCategory:
|
||||
if categoryID == nil {
|
||||
return nil, nil, fmt.Errorf("team_category_id is required when team_scope is category")
|
||||
}
|
||||
|
||||
category, err := p.categoryRepo.GetByID(ctx, *categoryID)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("team category not found: %w", err)
|
||||
}
|
||||
if category.OrganizationID != organizationID {
|
||||
return nil, nil, fmt.Errorf("team category does not belong to this organization")
|
||||
}
|
||||
if category.ParentID != nil {
|
||||
return nil, nil, fmt.Errorf("team must be a parent category")
|
||||
}
|
||||
// Categories without an outlet are shared, so only an outlet-specific
|
||||
// category has to match the outlet the purchase is booked against.
|
||||
if category.OutletID != nil && outletID != nil && *category.OutletID != *outletID {
|
||||
return nil, nil, fmt.Errorf("team category belongs to a different outlet")
|
||||
}
|
||||
|
||||
resolved := constants.PurchaseTeamScopeCategory
|
||||
return &resolved, &category.ID, nil
|
||||
}
|
||||
|
||||
return nil, nil, fmt.Errorf("team_scope must be one of: category, central")
|
||||
}
|
||||
|
||||
func (p *PurchaseOrderProcessorImpl) validatePurchaseCategory(ctx context.Context, categoryID, organizationID uuid.UUID, itemIndex int) (*entities.PurchaseCategory, error) {
|
||||
category, err := p.purchaseCategoryRepo.GetByIDAndOrganizationID(ctx, categoryID, organizationID)
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user