systemprompt_content/models/
link.rs1use chrono::{DateTime, Utc};
12use serde::{Deserialize, Serialize};
13use sqlx::FromRow;
14use systemprompt_identifiers::{
15 CampaignId, ContentId, ContextId, LinkClickId, LinkId, SessionId, TaskId, UserId,
16};
17
18#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
19pub struct CampaignLink {
20 pub id: LinkId,
21 pub short_code: String,
22 pub target_url: String,
23 pub link_type: String,
24 pub campaign_id: Option<CampaignId>,
25 pub campaign_name: Option<String>,
26 pub source_content_id: Option<ContentId>,
27 pub source_page: Option<String>,
28 pub utm_params: Option<String>,
29 pub link_text: Option<String>,
30 pub link_position: Option<String>,
31 pub destination_type: Option<String>,
32 pub click_count: Option<i32>,
33 pub unique_click_count: Option<i32>,
34 pub conversion_count: Option<i32>,
35 pub is_active: Option<bool>,
36 pub expires_at: Option<DateTime<Utc>>,
37 pub created_at: Option<DateTime<Utc>>,
38 pub updated_at: Option<DateTime<Utc>>,
39}
40
41#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
42pub struct LinkClick {
43 pub id: LinkClickId,
44 pub link_id: LinkId,
45 pub session_id: SessionId,
46 pub user_id: Option<UserId>,
47 pub context_id: Option<ContextId>,
48 pub task_id: Option<TaskId>,
49 pub referrer_page: Option<String>,
50 pub referrer_url: Option<String>,
51 pub clicked_at: Option<DateTime<Utc>>,
52 pub user_agent: Option<String>,
53 pub ip_address: Option<String>,
54 pub device_type: Option<String>,
55 pub country: Option<String>,
56 pub is_first_click: Option<bool>,
57 pub is_conversion: Option<bool>,
58 pub conversion_at: Option<DateTime<Utc>>,
59 pub time_on_page_seconds: Option<i32>,
60 pub scroll_depth_percent: Option<i32>,
61}
62
63#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
64pub enum LinkType {
65 Redirect,
66 Utm,
67 Both,
68}
69
70impl LinkType {
71 pub const fn as_str(&self) -> &'static str {
72 match self {
73 Self::Redirect => "redirect",
74 Self::Utm => "utm",
75 Self::Both => "both",
76 }
77 }
78}
79
80impl std::fmt::Display for LinkType {
81 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
82 write!(f, "{}", self.as_str())
83 }
84}
85
86#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
87pub enum DestinationType {
88 Internal,
89 External,
90}
91
92impl DestinationType {
93 pub const fn as_str(&self) -> &'static str {
94 match self {
95 Self::Internal => "internal",
96 Self::External => "external",
97 }
98 }
99}
100
101impl std::fmt::Display for DestinationType {
102 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
103 write!(f, "{}", self.as_str())
104 }
105}
106
107#[derive(Debug, Clone, Serialize, Deserialize)]
108pub struct UtmParams {
109 pub source: Option<String>,
110 pub medium: Option<String>,
111 pub campaign: Option<String>,
112 pub term: Option<String>,
113 pub content: Option<String>,
114}
115
116impl UtmParams {
117 pub fn to_query_string(&self) -> String {
118 let mut parts = Vec::new();
119 if let Some(ref source) = self.source {
120 parts.push(format!("utm_source={source}"));
121 }
122 if let Some(ref medium) = self.medium {
123 parts.push(format!("utm_medium={medium}"));
124 }
125 if let Some(ref campaign) = self.campaign {
126 parts.push(format!("utm_campaign={campaign}"));
127 }
128 if let Some(ref term) = self.term {
129 parts.push(format!("utm_term={term}"));
130 }
131 if let Some(ref content) = self.content {
132 parts.push(format!("utm_content={content}"));
133 }
134 parts.join("&")
135 }
136
137 pub fn to_json(&self) -> Result<String, serde_json::Error> {
138 serde_json::to_string(self)
139 }
140}
141
142#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
143pub struct LinkPerformance {
144 pub link_id: LinkId,
145 pub click_count: i64,
146 pub unique_click_count: i64,
147 pub conversion_count: i64,
148 pub conversion_rate: Option<f64>,
149}
150
151#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
152pub struct CampaignPerformance {
153 pub campaign_id: CampaignId,
154 pub total_clicks: i64,
155 pub link_count: i64,
156 pub unique_visitors: Option<i64>,
157 pub conversion_count: Option<i64>,
158}
159
160#[derive(Debug, Clone, Serialize, Deserialize)]
161pub struct ContentJourneyNode {
162 pub source_content_id: ContentId,
163 pub target_url: String,
164 pub click_count: i32,
165}
166
167impl CampaignLink {
168 pub fn get_full_url(&self) -> String {
169 if let Some(ref params_json) = self.utm_params
170 && let Ok(params) = serde_json::from_str::<UtmParams>(params_json)
171 {
172 let query = params.to_query_string();
173 if !query.is_empty() {
174 let separator = if self.target_url.contains('?') {
175 "&"
176 } else {
177 "?"
178 };
179 return format!("{}{}{}", self.target_url, separator, query);
180 }
181 }
182 self.target_url.clone()
183 }
184}