Skip to main content

systemprompt_content/models/
link.rs

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