package processor import ( "context" "fmt" "strings" "apskel-pos-be/internal/constants" "apskel-pos-be/internal/models" "github.com/google/uuid" ) // Teams are the parent product categories, plus Pusat for spending that belongs to // no single team. Both purchase orders and cash advances are charged to one, so the rules // for picking and storing a team live here rather than in either processor. // listTeams returns the teams money 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 listTeams(ctx context.Context, categoryRepo CategoryRepository, organizationID uuid.UUID, outletID *uuid.UUID) (*models.ListPurchaseTeamsResponse, error) { categories, err := 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 } // resolveTeamSelection turns a requested team into the scope/category pair that gets // stored, mirroring the database check constraint. A nil or empty scope means no // team, which is deliberately different from Pusat — callers that require a team // reject that case before getting here. Which outlet's Pusat it is comes from the // record's own outlet, so 'central' needs nothing stored beyond the scope itself. func resolveTeamSelection(ctx context.Context, categoryRepo CategoryRepository, 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 := 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 record 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") }