systemprompt_content/models/
link.rs1use 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}