Skip to main content

tuitbot_core/storage/approval_queue/
mod.rs

1//! Storage operations for the approval queue.
2//!
3//! Provides CRUD operations for queuing posts for human review
4//! when `approval_mode` is enabled.
5
6mod edit_history;
7mod queries;
8#[cfg(test)]
9mod tests;
10
11pub use edit_history::{get_edit_history, record_edit, EditHistoryEntry};
12pub use queries::*;
13
14/// Row type for approval queue queries (expanded with review and QA metadata).
15#[derive(Debug, Clone, sqlx::FromRow)]
16struct ApprovalRow {
17    id: i64,
18    action_type: String,
19    target_tweet_id: String,
20    target_author: String,
21    generated_content: String,
22    topic: String,
23    archetype: String,
24    score: f64,
25    status: String,
26    created_at: String,
27    media_paths: String,
28    reviewed_by: Option<String>,
29    review_notes: Option<String>,
30    reason: Option<String>,
31    detected_risks: String,
32    qa_report: String,
33    qa_hard_flags: String,
34    qa_soft_flags: String,
35    qa_recommendations: String,
36    qa_score: f64,
37    qa_requires_override: i64,
38    qa_override_by: Option<String>,
39    qa_override_note: Option<String>,
40    qa_override_at: Option<String>,
41}
42
43/// A pending item in the approval queue.
44#[derive(Debug, Clone, serde::Serialize)]
45pub struct ApprovalItem {
46    pub id: i64,
47    pub action_type: String,
48    pub target_tweet_id: String,
49    pub target_author: String,
50    pub generated_content: String,
51    pub topic: String,
52    pub archetype: String,
53    pub score: f64,
54    pub status: String,
55    pub created_at: String,
56    /// JSON-encoded list of local media file paths.
57    #[serde(serialize_with = "serialize_json_string")]
58    pub media_paths: String,
59    pub reviewed_by: Option<String>,
60    pub review_notes: Option<String>,
61    pub reason: Option<String>,
62    /// JSON-encoded list of detected risks.
63    #[serde(serialize_with = "serialize_json_string")]
64    pub detected_risks: String,
65    /// Full QA report payload as JSON.
66    #[serde(serialize_with = "serialize_json_string")]
67    pub qa_report: String,
68    /// JSON-encoded hard QA flags.
69    #[serde(serialize_with = "serialize_json_string")]
70    pub qa_hard_flags: String,
71    /// JSON-encoded soft QA flags.
72    #[serde(serialize_with = "serialize_json_string")]
73    pub qa_soft_flags: String,
74    /// JSON-encoded QA recommendations.
75    #[serde(serialize_with = "serialize_json_string")]
76    pub qa_recommendations: String,
77    /// QA score summary (0-100).
78    pub qa_score: f64,
79    /// Whether approval requires explicit hard-flag override.
80    pub qa_requires_override: bool,
81    /// Actor who performed override.
82    pub qa_override_by: Option<String>,
83    /// Required override note.
84    pub qa_override_note: Option<String>,
85    /// Timestamp of override action.
86    pub qa_override_at: Option<String>,
87}
88
89/// Serialize a JSON-encoded string as a raw JSON value.
90///
91/// The database stores `media_paths` and `detected_risks` as JSON strings.
92/// This serializer emits them as actual JSON arrays in the API response.
93fn serialize_json_string<S: serde::Serializer>(
94    value: &str,
95    serializer: S,
96) -> Result<S::Ok, S::Error> {
97    use serde::Serialize;
98    let parsed: serde_json::Value =
99        serde_json::from_str(value).unwrap_or(serde_json::Value::Array(vec![]));
100    parsed.serialize(serializer)
101}
102
103impl From<ApprovalRow> for ApprovalItem {
104    fn from(r: ApprovalRow) -> Self {
105        Self {
106            id: r.id,
107            action_type: r.action_type,
108            target_tweet_id: r.target_tweet_id,
109            target_author: r.target_author,
110            generated_content: r.generated_content,
111            topic: r.topic,
112            archetype: r.archetype,
113            score: r.score,
114            status: r.status,
115            created_at: r.created_at,
116            media_paths: r.media_paths,
117            reviewed_by: r.reviewed_by,
118            review_notes: r.review_notes,
119            reason: r.reason,
120            detected_risks: r.detected_risks,
121            qa_report: r.qa_report,
122            qa_hard_flags: r.qa_hard_flags,
123            qa_soft_flags: r.qa_soft_flags,
124            qa_recommendations: r.qa_recommendations,
125            qa_score: r.qa_score,
126            qa_requires_override: r.qa_requires_override != 0,
127            qa_override_by: r.qa_override_by,
128            qa_override_note: r.qa_override_note,
129            qa_override_at: r.qa_override_at,
130        }
131    }
132}
133
134/// Counts of approval items grouped by status.
135#[derive(Debug, Clone, serde::Serialize)]
136pub struct ApprovalStats {
137    pub pending: i64,
138    pub approved: i64,
139    pub rejected: i64,
140}
141
142/// Optional review metadata for approve/reject actions.
143#[derive(Debug, Clone, Default, serde::Deserialize)]
144pub struct ReviewAction {
145    pub actor: Option<String>,
146    pub notes: Option<String>,
147}