systemprompt_analytics/repository/funnel/
mutations.rs1use crate::Result;
11use chrono::Utc;
12use systemprompt_identifiers::{FunnelId, FunnelProgressId, SessionId};
13
14use super::FunnelRepository;
15use crate::models::{CreateFunnelInput, Funnel, FunnelProgress, FunnelStep, FunnelWithSteps};
16
17impl FunnelRepository {
18 pub async fn create_funnel(&self, input: &CreateFunnelInput) -> Result<FunnelWithSteps> {
19 let funnel_id = FunnelId::generate();
20 let now = Utc::now();
21
22 sqlx::query!(
23 r#"
24 INSERT INTO funnels (id, name, description, is_active, created_at, updated_at)
25 VALUES ($1, $2, $3, TRUE, $4, $4)
26 "#,
27 funnel_id.as_str(),
28 input.name,
29 input.description,
30 now
31 )
32 .execute(&*self.write_pool)
33 .await?;
34
35 let mut funnel_ids_arr = Vec::with_capacity(input.steps.len());
36 let mut step_orders = Vec::with_capacity(input.steps.len());
37 let mut names = Vec::with_capacity(input.steps.len());
38 let mut patterns = Vec::with_capacity(input.steps.len());
39 let mut match_types = Vec::with_capacity(input.steps.len());
40 let mut steps = Vec::with_capacity(input.steps.len());
41
42 for (idx, step_input) in input.steps.iter().enumerate() {
43 let step_order = i32::try_from(idx).unwrap_or(0);
44 funnel_ids_arr.push(funnel_id.as_str().to_owned());
45 step_orders.push(step_order);
46 names.push(step_input.name.clone());
47 patterns.push(step_input.match_pattern.clone());
48 match_types.push(step_input.match_type.as_str().to_owned());
49
50 steps.push(FunnelStep {
51 funnel_id: funnel_id.clone(),
52 step_order,
53 name: step_input.name.clone(),
54 match_pattern: step_input.match_pattern.clone(),
55 match_type: step_input.match_type,
56 });
57 }
58
59 if !steps.is_empty() {
60 sqlx::query!(
61 r#"
62 INSERT INTO funnel_steps (funnel_id, step_order, name, match_pattern, match_type)
63 SELECT * FROM UNNEST($1::text[], $2::int4[], $3::text[], $4::text[], $5::text[])
64 "#,
65 &funnel_ids_arr,
66 &step_orders,
67 &names,
68 &patterns,
69 &match_types
70 )
71 .execute(&*self.write_pool)
72 .await?;
73 }
74
75 let funnel = Funnel {
76 id: funnel_id,
77 name: input.name.clone(),
78 description: input.description.clone(),
79 is_active: true,
80 created_at: now,
81 updated_at: now,
82 };
83
84 Ok(FunnelWithSteps { funnel, steps })
85 }
86
87 pub async fn deactivate(&self, id: &FunnelId) -> Result<bool> {
88 let result = sqlx::query!(
89 r#"
90 UPDATE funnels SET is_active = FALSE, updated_at = $2 WHERE id = $1
91 "#,
92 id.as_str(),
93 Utc::now()
94 )
95 .execute(&*self.write_pool)
96 .await?;
97
98 Ok(result.rows_affected() > 0)
99 }
100
101 pub async fn delete(&self, id: &FunnelId) -> Result<bool> {
102 let result = sqlx::query!(r#"DELETE FROM funnels WHERE id = $1"#, id.as_str())
103 .execute(&*self.write_pool)
104 .await?;
105
106 Ok(result.rows_affected() > 0)
107 }
108
109 pub async fn record_progress(
110 &self,
111 funnel_id: &FunnelId,
112 session_id: &SessionId,
113 step: i32,
114 ) -> Result<FunnelProgress> {
115 let now = Utc::now();
116 let step_timestamp = serde_json::json!({
117 "step": step,
118 "timestamp": now.to_rfc3339()
119 });
120
121 if let Some(mut progress) = self.find_progress(funnel_id, session_id).await? {
122 if step > progress.current_step {
123 let mut timestamps = progress
124 .step_timestamps
125 .as_array()
126 .cloned()
127 .unwrap_or_else(Vec::new);
128 timestamps.push(step_timestamp);
129
130 sqlx::query!(
131 r#"
132 UPDATE funnel_progress
133 SET current_step = $3, step_timestamps = $4, updated_at = $5
134 WHERE funnel_id = $1 AND session_id = $2
135 "#,
136 funnel_id.as_str(),
137 session_id.as_str(),
138 step,
139 serde_json::Value::Array(timestamps.clone()),
140 now
141 )
142 .execute(&*self.write_pool)
143 .await?;
144
145 progress.current_step = step;
146 progress.step_timestamps = serde_json::Value::Array(timestamps);
147 progress.updated_at = now;
148 }
149 return Ok(progress);
150 }
151
152 let id = FunnelProgressId::generate();
153 let timestamps = serde_json::json!([step_timestamp]);
154
155 sqlx::query!(
156 r#"
157 INSERT INTO funnel_progress (
158 id, funnel_id, session_id, current_step, step_timestamps, created_at, updated_at
159 )
160 VALUES ($1, $2, $3, $4, $5, $6, $6)
161 "#,
162 id.as_str(),
163 funnel_id.as_str(),
164 session_id.as_str(),
165 step,
166 timestamps,
167 now
168 )
169 .execute(&*self.write_pool)
170 .await?;
171
172 Ok(FunnelProgress {
173 id,
174 funnel_id: funnel_id.clone(),
175 session_id: session_id.clone(),
176 current_step: step,
177 completed_at: None,
178 dropped_at_step: None,
179 step_timestamps: timestamps,
180 created_at: now,
181 updated_at: now,
182 })
183 }
184
185 pub async fn mark_completed(
186 &self,
187 funnel_id: &FunnelId,
188 session_id: &SessionId,
189 ) -> Result<bool> {
190 let now = Utc::now();
191 let result = sqlx::query!(
192 r#"
193 UPDATE funnel_progress
194 SET completed_at = $3, updated_at = $3
195 WHERE funnel_id = $1 AND session_id = $2
196 "#,
197 funnel_id.as_str(),
198 session_id.as_str(),
199 now
200 )
201 .execute(&*self.write_pool)
202 .await?;
203
204 Ok(result.rows_affected() > 0)
205 }
206}