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