// Package service provides business logic services. package service import ( "context" "fmt" "github.com/orchard9/rdev/internal/domain" "github.com/orchard9/rdev/internal/logging" "github.com/orchard9/rdev/internal/port" ) // VerifyService orchestrates verify task submission and tracking. // It coordinates between the work queue (execution) for visual captures. type VerifyService struct { queue port.WorkQueue } // NewVerifyService creates a new verify service. func NewVerifyService(queue port.WorkQueue) *VerifyService { return &VerifyService{ queue: queue, } } // SubmitCapture validates spec and enqueues a verify task. // Returns the task ID for status tracking. func (s *VerifyService) SubmitCapture(ctx context.Context, projectID string, spec domain.VerifySpec) (string, error) { if err := spec.Validate(); err != nil { return "", fmt.Errorf("invalid verify spec: %w", err) } if projectID == "" { return "", fmt.Errorf("project_id is required") } // Apply defaults specWithDefaults := spec.WithDefaults() // Build work task spec from verify spec taskSpec := map[string]any{ "url": specWithDefaults.URL, "viewports": specWithDefaults.Viewports, "wait_for": specWithDefaults.WaitFor, "wait_timeout": specWithDefaults.WaitTimeout, "full_page": specWithDefaults.FullPage, "video": specWithDefaults.Video, } if specWithDefaults.Evaluate { taskSpec["evaluate"] = specWithDefaults.Evaluate } if specWithDefaults.Prompt != "" { taskSpec["prompt"] = specWithDefaults.Prompt } if specWithDefaults.CallbackURL != "" { taskSpec["callback_url"] = specWithDefaults.CallbackURL } // Create work task task := &domain.WorkTask{ ProjectID: projectID, Type: domain.WorkTaskTypeVerify, Spec: taskSpec, CallbackURL: specWithDefaults.CallbackURL, MaxRetries: 1, // Verify tasks shouldn't retry by default } // Enqueue to work queue taskID, err := s.queue.Enqueue(ctx, task) if err != nil { return "", fmt.Errorf("enqueue verify task: %w", err) } log := logging.FromContext(ctx).WithService("verify") log.Info("verify task enqueued", "task_id", taskID, logging.FieldProjectID, projectID, "url", specWithDefaults.URL, "viewports", specWithDefaults.Viewports, ) return taskID, nil } // GetCapture retrieves a verify task by ID. func (s *VerifyService) GetCapture(ctx context.Context, taskID string) (*domain.WorkTask, error) { task, err := s.queue.GetTask(ctx, taskID) if err != nil { return nil, err } // Verify it's actually a verify task if task.Type != domain.WorkTaskTypeVerify { return nil, domain.ErrWorkTaskNotFound } return task, nil } // ListCaptures returns verify tasks for a project. func (s *VerifyService) ListCaptures(ctx context.Context, projectID string, opts domain.WorkListOptions) (*domain.WorkListResult, error) { opts.Normalize() // Get all tasks for the project result, err := s.queue.ListByProject(ctx, projectID, nil, opts) if err != nil { return nil, err } // Filter to only verify tasks var verifyTasks []*domain.WorkTask for _, task := range result.Tasks { if task.Type == domain.WorkTaskTypeVerify { verifyTasks = append(verifyTasks, task) } } return &domain.WorkListResult{ Tasks: verifyTasks, Total: int64(len(verifyTasks)), // Note: this is filtered total, not DB total Limit: result.Limit, Offset: result.Offset, }, nil } // CancelCapture cancels a pending verify task. func (s *VerifyService) CancelCapture(ctx context.Context, taskID string) error { // Verify it's a verify task first task, err := s.queue.GetTask(ctx, taskID) if err != nil { return err } if task.Type != domain.WorkTaskTypeVerify { return domain.ErrWorkTaskNotFound } return s.queue.Cancel(ctx, taskID) }