package tasks import ( "context" "inbox/internal/base/timeutil" "inbox/internal/domain/task" ) type Repository interface { CreateTask(ctx context.Context, value task.Record, dependencies []task.Dependency) (task.Record, error) GetTask(ctx context.Context, taskID string) (task.Record, error) ListTasksByTopic(ctx context.Context, topicID string) ([]task.Record, error) ListTasksByLane(ctx context.Context, laneID string) ([]task.Record, error) UpdateTaskWithDependencies(ctx context.Context, value task.Record, dependencies *[]task.Dependency) (task.Record, error) ListTaskDependencies(ctx context.Context, taskID string) ([]task.Dependency, error) AppendTaskEvent(ctx context.Context, value task.Event) (task.Event, error) ListTaskEvents(ctx context.Context, taskID string) ([]task.Event, error) } type Service struct { repo Repository clock timeutil.Clock } type CreateInput struct { Task task.Record Dependencies []task.Dependency } type Patch struct { Title *string BodyMarkdown *string AcceptanceMarkdown *string Kind *task.Kind Deliverables *[]string BatchKey *string Status *task.Status Priority *int TaskOrder *int BlockingReasonMarkdown *string ResultSummaryMarkdown *string AssignedRunID *string StartedAt *string CompletedAt *string Dependencies *[]task.Dependency } func NewService(repo Repository, clock timeutil.Clock) *Service { if clock == nil { clock = timeutil.SystemClock{} } return &Service{repo: repo, clock: clock} } func (s *Service) Create(ctx context.Context, input CreateInput) (task.Record, error) { input.Task = task.NormalizeRecord(input.Task) if input.Task.Status == "" { input.Task.Status = task.StatusDraft } item, err := s.repo.CreateTask(ctx, input.Task, input.Dependencies) if err != nil { return task.Record{}, err } _, _ = s.repo.AppendTaskEvent(ctx, task.Event{ TaskID: item.ID, EventType: "created", BodyMarkdown: item.BodyMarkdown, CreatedByRoleName: item.CreatedByRoleName, CreatedAt: item.CreatedAt, }) return item, nil } func (s *Service) Get(ctx context.Context, taskID string) (task.Record, error) { return s.repo.GetTask(ctx, taskID) } func (s *Service) ListByTopic(ctx context.Context, topicID string) ([]task.Record, error) { return s.repo.ListTasksByTopic(ctx, topicID) } func (s *Service) ListByLane(ctx context.Context, laneID string) ([]task.Record, error) { return s.repo.ListTasksByLane(ctx, laneID) } func (s *Service) Dependencies(ctx context.Context, taskID string) ([]task.Dependency, error) { return s.repo.ListTaskDependencies(ctx, taskID) } func (s *Service) Events(ctx context.Context, taskID string) ([]task.Event, error) { return s.repo.ListTaskEvents(ctx, taskID) } func (s *Service) Patch(ctx context.Context, taskID string, patch Patch, changedBy string) (task.Record, error) { current, err := s.repo.GetTask(ctx, taskID) if err != nil { return task.Record{}, err } current = task.NormalizeRecord(current) if patch.Title != nil { current.Title = *patch.Title } if patch.BodyMarkdown != nil { current.BodyMarkdown = *patch.BodyMarkdown } if patch.AcceptanceMarkdown != nil { current.AcceptanceMarkdown = *patch.AcceptanceMarkdown } if patch.Kind != nil { current.Kind = *patch.Kind } if patch.Deliverables != nil { current.Deliverables = append([]string(nil), (*patch.Deliverables)...) } if patch.BatchKey != nil { current.BatchKey = *patch.BatchKey } if patch.Status != nil { current.Status = *patch.Status if current.Status == task.StatusRunning && current.StartedAt == "" { current.StartedAt = timeutil.FormatRFC3339(s.clock.Now()) } if (current.Status == task.StatusSucceeded || current.Status == task.StatusFailed || current.Status == task.StatusCancelled) && current.CompletedAt == "" { current.CompletedAt = timeutil.FormatRFC3339(s.clock.Now()) } } if patch.Priority != nil { current.Priority = *patch.Priority } if patch.TaskOrder != nil { current.TaskOrder = *patch.TaskOrder } if patch.BlockingReasonMarkdown != nil { current.BlockingReasonMarkdown = *patch.BlockingReasonMarkdown } if patch.ResultSummaryMarkdown != nil { current.ResultSummaryMarkdown = *patch.ResultSummaryMarkdown } if patch.AssignedRunID != nil { current.AssignedRunID = *patch.AssignedRunID } if patch.StartedAt != nil { current.StartedAt = *patch.StartedAt } if patch.CompletedAt != nil { current.CompletedAt = *patch.CompletedAt } item, err := s.repo.UpdateTaskWithDependencies(ctx, current, patch.Dependencies) if err != nil { return task.Record{}, err } if changedBy != "" { _, _ = s.repo.AppendTaskEvent(ctx, task.Event{ TaskID: item.ID, EventType: "updated", BodyMarkdown: item.ResultSummaryMarkdown, CreatedByRoleName: changedBy, CreatedAt: timeutil.FormatRFC3339(s.clock.Now()), }) } return item, nil } func (s *Service) AppendEvent(ctx context.Context, value task.Event) (task.Event, error) { return s.repo.AppendTaskEvent(ctx, value) }