package service import ( "context" "time" "git.sundynix.cn/Blizzard/Veyra/internal/apperror" "git.sundynix.cn/Blizzard/Veyra/internal/model" "github.com/google/uuid" ) type CandidateRepository interface { List(context.Context, bool, string, int) ([]model.CandidateRevision, error) Detail(context.Context, string, bool) (model.CandidateDetail, error) Create(context.Context, model.CandidateDraftInput, string, string, string, int64) (model.CandidateRevision, error) Transition(context.Context, string, string, model.CandidateActionInput, string, string) (model.CandidateRevision, error) } type CandidateService struct { repo CandidateRepository development bool } func NewCandidateService(repo CandidateRepository, development bool) *CandidateService { return &CandidateService{repo: repo, development: development} } func (s *CandidateService) authorize(actor model.User, admin bool) error { if actor.Status != "ACTIVE" || (admin && actor.Role != "ADMIN") || (!admin && actor.Role != "USER") { return apperror.ErrForbidden } if s == nil || s.repo == nil { return apperror.ErrUnavailable } // Manual simulation publishing is a development capability, not production authorization. if !s.development { return apperror.ErrUnavailable } return nil } func (s *CandidateService) List(ctx context.Context, actor model.User, admin bool, status string, limit int) ([]model.CandidateRevision, error) { if err := s.authorize(actor, admin); err != nil { return nil, err } if limit < 1 || limit > 100 { return nil, apperror.ErrInvalidInput } switch status { case "", "DRAFT", "PENDING_REVIEW", "PUBLISHED", "RETURNED", "WITHDRAWN": default: return nil, apperror.ErrInvalidInput } if !admin && status != "" { return nil, apperror.ErrInvalidInput } return s.repo.List(ctx, admin, status, limit) } func (s *CandidateService) Detail(ctx context.Context, actor model.User, id string, admin bool) (model.CandidateDetail, error) { if err := s.authorize(actor, admin); err != nil { return model.CandidateDetail{}, err } if _, err := uuid.Parse(id); err != nil { return model.CandidateDetail{}, apperror.ErrInvalidInput } return s.repo.Detail(ctx, id, admin) } func (s *CandidateService) Create(ctx context.Context, actor model.User, source, requestID string, input model.CandidateDraftInput, expected int64) (model.CandidateRevision, error) { if err := s.authorize(actor, true); err != nil { return model.CandidateRevision{}, err } if source != "" { if _, err := uuid.Parse(source); err != nil { return model.CandidateRevision{}, apperror.ErrInvalidInput } if expected < 1 { return model.CandidateRevision{}, apperror.ErrInvalidInput } } if _, err := uuid.Parse(requestID); err != nil { return model.CandidateRevision{}, apperror.ErrInvalidInput } if err := model.ValidateDraft(input, time.Now().UTC()); err != nil { return model.CandidateRevision{}, err } return s.repo.Create(ctx, input, actor.ID, requestID, source, expected) } func (s *CandidateService) Transition(ctx context.Context, actor model.User, id, action, requestID string, input model.CandidateActionInput) (model.CandidateRevision, error) { if err := s.authorize(actor, true); err != nil { return model.CandidateRevision{}, err } if _, err := uuid.Parse(id); err != nil { return model.CandidateRevision{}, apperror.ErrInvalidInput } if _, err := uuid.Parse(requestID); err != nil { return model.CandidateRevision{}, apperror.ErrInvalidInput } if input.ExpectedVersion < 1 { return model.CandidateRevision{}, apperror.ErrInvalidInput } if err := model.ValidateReason(input.Reason); err != nil { return model.CandidateRevision{}, err } return s.repo.Transition(ctx, id, action, input, actor.ID, requestID) }