1use chrono::{DateTime, Utc};
7use serde::{Deserialize, Serialize};
8use stateset_primitives::{CustomerId, SegmentId};
9use strum::{Display, EnumString};
10
11#[derive(
13 Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default, Display, EnumString,
14)]
15#[serde(rename_all = "snake_case")]
16#[strum(serialize_all = "snake_case", ascii_case_insensitive)]
17#[non_exhaustive]
18pub enum SegmentType {
19 #[default]
21 Static,
22 Dynamic,
24}
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Display, EnumString)]
28#[serde(rename_all = "snake_case")]
29#[strum(serialize_all = "snake_case", ascii_case_insensitive)]
30#[non_exhaustive]
31pub enum SegmentOperator {
32 Eq,
34 Neq,
36 Gt,
38 Gte,
40 Lt,
42 Lte,
44 Contains,
46 In,
48 Between,
50 StartsWith,
52 EndsWith,
54}
55
56#[derive(Debug, Clone, Serialize, Deserialize)]
58pub struct SegmentRule {
59 pub field: String,
61 pub operator: SegmentOperator,
63 pub value: String,
65}
66
67#[derive(Debug, Clone, Serialize, Deserialize)]
69pub struct Segment {
70 pub id: SegmentId,
72 pub name: String,
74 pub description: Option<String>,
76 pub segment_type: SegmentType,
78 pub rules: Vec<SegmentRule>,
80 pub member_count: u64,
82 pub created_at: DateTime<Utc>,
84 pub updated_at: DateTime<Utc>,
86}
87
88#[derive(Debug, Clone, Serialize, Deserialize)]
90pub struct SegmentMembership {
91 pub segment_id: SegmentId,
93 pub customer_id: CustomerId,
95 pub joined_at: DateTime<Utc>,
97}
98
99#[derive(Debug, Clone, Serialize, Deserialize)]
101pub struct CreateSegment {
102 pub name: String,
104 pub description: Option<String>,
106 pub segment_type: SegmentType,
108 pub rules: Vec<SegmentRule>,
110}
111
112#[derive(Debug, Clone, Serialize, Deserialize, Default)]
114pub struct UpdateSegment {
115 pub name: Option<String>,
117 pub description: Option<Option<String>>,
119 pub rules: Option<Vec<SegmentRule>>,
121}
122
123#[derive(Debug, Clone, Serialize, Deserialize, Default)]
125pub struct SegmentFilter {
126 pub segment_type: Option<SegmentType>,
128 pub name: Option<String>,
130 pub limit: Option<u32>,
132 pub offset: Option<u32>,
134}
135
136impl Segment {
137 #[must_use]
139 pub fn is_dynamic(&self) -> bool {
140 self.segment_type == SegmentType::Dynamic
141 }
142
143 #[must_use]
145 pub fn has_rules(&self) -> bool {
146 !self.rules.is_empty()
147 }
148}
149
150#[cfg(test)]
151mod tests {
152 use super::*;
153 use chrono::Utc;
154 use stateset_primitives::SegmentId;
155
156 fn make_segment(segment_type: SegmentType, rules: Vec<SegmentRule>) -> Segment {
157 Segment {
158 id: SegmentId::new(),
159 name: "Test Segment".to_string(),
160 description: None,
161 segment_type,
162 rules,
163 member_count: 0,
164 created_at: Utc::now(),
165 updated_at: Utc::now(),
166 }
167 }
168
169 fn make_rule(operator: SegmentOperator) -> SegmentRule {
170 SegmentRule { field: "total_orders".to_string(), operator, value: "5".to_string() }
171 }
172
173 #[test]
176 fn is_dynamic_returns_true_for_dynamic_segment() {
177 let segment = make_segment(SegmentType::Dynamic, vec![make_rule(SegmentOperator::Gt)]);
178 assert!(segment.is_dynamic());
179 }
180
181 #[test]
182 fn is_dynamic_returns_false_for_static_segment() {
183 let segment = make_segment(SegmentType::Static, vec![]);
184 assert!(!segment.is_dynamic());
185 }
186
187 #[test]
190 fn has_rules_returns_true_when_rules_exist() {
191 let segment = make_segment(SegmentType::Dynamic, vec![make_rule(SegmentOperator::Eq)]);
192 assert!(segment.has_rules());
193 }
194
195 #[test]
196 fn has_rules_returns_false_when_no_rules() {
197 let segment = make_segment(SegmentType::Static, vec![]);
198 assert!(!segment.has_rules());
199 }
200
201 #[test]
204 fn segment_type_display_fromstr_roundtrip() {
205 for seg_type in [SegmentType::Static, SegmentType::Dynamic] {
206 let s = seg_type.to_string();
207 let parsed: SegmentType = s.parse().unwrap();
208 assert_eq!(parsed, seg_type, "round-trip failed for {s}");
209 }
210 }
211
212 #[test]
213 fn segment_operator_key_variants_display_fromstr_roundtrip() {
214 for op in [
215 SegmentOperator::Eq,
216 SegmentOperator::Gt,
217 SegmentOperator::Contains,
218 SegmentOperator::Between,
219 SegmentOperator::Neq,
220 SegmentOperator::Gte,
221 SegmentOperator::Lt,
222 SegmentOperator::Lte,
223 SegmentOperator::In,
224 SegmentOperator::StartsWith,
225 SegmentOperator::EndsWith,
226 ] {
227 let s = op.to_string();
228 let parsed: SegmentOperator = s.parse().unwrap();
229 assert_eq!(parsed, op, "round-trip failed for {s}");
230 }
231 }
232
233 #[test]
236 fn segment_type_default_is_static() {
237 assert_eq!(SegmentType::default(), SegmentType::Static);
238 }
239}