use super::{ DateTime, NewProblem, Problem, ProblemId, ProblemStatus, ProjectId, Result, TaskId, UserId, Utc, async_trait, }; /// Filters for listing problems. All fields are ANDed; `None` means unfiltered. #[derive(Debug, Clone, Default)] pub struct ProblemFilter { /// Restrict to one adapter, e.g. `wam`. pub source: Option, /// Restrict to one triage state. The inbox passes `Open`. pub status: Option, /// Restrict to problems attributed to one project. pub project_id: Option, } /// Repository for the Problems inbox. /// /// Problems are a mirror of external sources, so the write surface splits in /// two: [`ProblemRepository::ingest`] is for adapters and upserts on /// `(source, source_ref)`, while the triage methods are for the human and are /// never touched by a re-pull. /// /// All operations are scoped to a specific user for multi-tenancy support. #[async_trait] pub trait ProblemRepository: Send + Sync { /// Lists problems matching a filter. /// /// Ordered by painhours descending, ties broken by newest. That score moves /// with the clock, so it is computed and sorted after the fetch rather than /// in SQL. async fn list(&self, user_id: UserId, filter: &ProblemFilter) -> Result>; /// Retrieves a problem by ID, returning `None` if not found. async fn get_by_id(&self, id: ProblemId, user_id: UserId) -> Result>; /// Retrieves a problem by its provenance key. async fn find_by_source_ref( &self, user_id: UserId, source: &str, source_ref: &str, ) -> Result>; /// Ingests a problem from a source, inserting it or updating the existing /// row with the same `(source, source_ref)`. /// /// Upserts refresh what the source owns (title, body, pain, scale, /// timestamps, `last_seen_at`) and leave what GoingsOn owns alone, so /// re-pulling never undoes a triage decision. The one exception is a problem /// the source reports as fixed, which settles an untriaged row as /// `Resolved`; a problem already promoted or dismissed keeps that state. async fn ingest(&self, user_id: UserId, problem: NewProblem) -> Result; /// Records that a task was created from this problem: sets `Promoted`, the /// backlink, and the settle instant that freezes its score. /// /// Returns `None` if the problem does not exist. async fn promote( &self, id: ProblemId, user_id: UserId, task_id: TaskId, ) -> Result>; /// Sets triage state directly, for dismissing and for reopening something /// settled by mistake. Clears the backlink and the settle instant when /// moving back to `Open`. async fn set_status( &self, id: ProblemId, user_id: UserId, status: ProblemStatus, ) -> Result>; /// Attributes a problem to a project, or clears the attribution. async fn set_project( &self, id: ProblemId, user_id: UserId, project_id: Option, ) -> Result>; /// Deletes a problem. Triage history is worth keeping, so prefer dismissing; /// this exists for genuinely bad ingests. async fn delete(&self, id: ProblemId, user_id: UserId) -> Result; /// The most recent `last_seen_at` across a source's problems, i.e. when that /// adapter last completed a pull. Callers compare a problem's `last_seen_at` /// against this to decide staleness; `None` means the source has never been /// pulled. async fn last_pulled_at(&self, user_id: UserId, source: &str) -> Result>>; }