1use crate::errors::Result;
4use crate::validation::{Validate, ValidationBuilder};
5use chrono::{DateTime, Utc};
6use serde::{Deserialize, Serialize};
7use stateset_primitives::CustomerId;
8use strum::{Display, EnumString};
9use uuid::Uuid;
10
11#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
13pub struct Customer {
14 pub id: CustomerId,
15 pub email: String,
16 pub first_name: String,
17 pub last_name: String,
18 pub phone: Option<String>,
19 pub status: CustomerStatus,
20 pub accepts_marketing: bool,
21 pub email_verified: bool,
22 pub tags: Vec<String>,
23 pub metadata: Option<serde_json::Value>,
24 pub default_shipping_address_id: Option<Uuid>,
25 pub default_billing_address_id: Option<Uuid>,
26 pub created_at: DateTime<Utc>,
27 pub updated_at: DateTime<Utc>,
28}
29
30#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
32pub struct CustomerAddress {
33 pub id: Uuid,
34 pub customer_id: CustomerId,
35 pub address_type: AddressType,
36 pub first_name: String,
37 pub last_name: String,
38 pub company: Option<String>,
39 pub line1: String,
40 pub line2: Option<String>,
41 pub city: String,
42 pub state: Option<String>,
43 pub postal_code: String,
44 pub country: String,
45 pub phone: Option<String>,
46 pub is_default: bool,
47 pub created_at: DateTime<Utc>,
48 pub updated_at: DateTime<Utc>,
49}
50
51#[derive(
53 Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize, Display, EnumString,
54)]
55#[serde(rename_all = "snake_case")]
56#[strum(serialize_all = "snake_case", ascii_case_insensitive)]
57#[non_exhaustive]
58pub enum CustomerStatus {
59 #[default]
60 Active,
61 Inactive,
62 Suspended,
63 Deleted,
64}
65
66#[derive(
68 Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize, Display, EnumString,
69)]
70#[serde(rename_all = "snake_case")]
71#[strum(serialize_all = "snake_case", ascii_case_insensitive)]
72#[non_exhaustive]
73pub enum AddressType {
74 Shipping,
75 Billing,
76 #[default]
77 Both,
78}
79
80#[derive(Debug, Clone, Default, Serialize, Deserialize)]
82pub struct CreateCustomer {
83 pub email: String,
84 pub first_name: String,
85 pub last_name: String,
86 pub phone: Option<String>,
87 pub accepts_marketing: Option<bool>,
88 pub tags: Option<Vec<String>>,
89 pub metadata: Option<serde_json::Value>,
90}
91
92impl Validate for CreateCustomer {
93 fn validate(&self) -> Result<()> {
98 ValidationBuilder::new()
99 .required("email", &self.email)
100 .email("email", &self.email)
101 .required("first_name", &self.first_name)
102 .required("last_name", &self.last_name)
103 .required_if_present("phone", self.phone.as_deref())
104 .build()
105 }
106}
107
108#[derive(Debug, Clone, Default, Serialize, Deserialize)]
110pub struct UpdateCustomer {
111 pub email: Option<String>,
112 pub first_name: Option<String>,
113 pub last_name: Option<String>,
114 pub phone: Option<String>,
115 pub status: Option<CustomerStatus>,
116 pub accepts_marketing: Option<bool>,
117 pub tags: Option<Vec<String>>,
118 pub metadata: Option<serde_json::Value>,
119}
120
121#[derive(Debug, Clone, Serialize, Deserialize)]
123pub struct CreateCustomerAddress {
124 pub customer_id: CustomerId,
125 pub address_type: Option<AddressType>,
126 pub first_name: String,
127 pub last_name: String,
128 pub company: Option<String>,
129 pub line1: String,
130 pub line2: Option<String>,
131 pub city: String,
132 pub state: Option<String>,
133 pub postal_code: String,
134 pub country: String,
135 pub phone: Option<String>,
136 pub is_default: Option<bool>,
137}
138
139impl Validate for CreateCustomerAddress {
140 fn validate(&self) -> Result<()> {
145 ValidationBuilder::new()
146 .uuid_not_nil("customer_id", self.customer_id.into_uuid())
147 .required("first_name", &self.first_name)
148 .required("last_name", &self.last_name)
149 .required("line1", &self.line1)
150 .required("city", &self.city)
151 .required("postal_code", &self.postal_code)
152 .required("country", &self.country)
153 .build()
154 }
155}
156
157#[derive(Debug, Clone, Default, Serialize, Deserialize)]
159pub struct CustomerFilter {
160 pub email: Option<String>,
161 pub status: Option<CustomerStatus>,
162 pub tag: Option<String>,
163 pub accepts_marketing: Option<bool>,
164 pub limit: Option<u32>,
165 pub offset: Option<u32>,
166 pub after_cursor: Option<(String, String)>,
169}
170
171impl Customer {
172 #[must_use]
174 pub fn full_name(&self) -> String {
175 format!("{} {}", self.first_name, self.last_name)
176 }
177
178 #[must_use]
180 pub fn can_receive_marketing(&self) -> bool {
181 self.accepts_marketing && self.email_verified && self.status == CustomerStatus::Active
182 }
183}
184
185#[cfg(test)]
186mod tests {
187 use super::*;
188
189 fn create_test_customer(
194 status: CustomerStatus,
195 accepts_marketing: bool,
196 email_verified: bool,
197 ) -> Customer {
198 let now = Utc::now();
199 Customer {
200 id: CustomerId::new(),
201 email: "test@example.com".to_string(),
202 first_name: "John".to_string(),
203 last_name: "Doe".to_string(),
204 phone: Some("+1-555-123-4567".to_string()),
205 status,
206 accepts_marketing,
207 email_verified,
208 tags: vec!["vip".to_string(), "wholesale".to_string()],
209 metadata: None,
210 default_shipping_address_id: None,
211 default_billing_address_id: None,
212 created_at: now,
213 updated_at: now,
214 }
215 }
216
217 fn create_test_customer_address() -> CustomerAddress {
218 let now = Utc::now();
219 CustomerAddress {
220 id: Uuid::new_v4(),
221 customer_id: CustomerId::new(),
222 address_type: AddressType::Both,
223 first_name: "John".to_string(),
224 last_name: "Doe".to_string(),
225 company: Some("Acme Inc".to_string()),
226 line1: "123 Main St".to_string(),
227 line2: Some("Suite 100".to_string()),
228 city: "San Francisco".to_string(),
229 state: Some("CA".to_string()),
230 postal_code: "94102".to_string(),
231 country: "US".to_string(),
232 phone: Some("+1-555-123-4567".to_string()),
233 is_default: true,
234 created_at: now,
235 updated_at: now,
236 }
237 }
238
239 #[test]
244 fn test_customer_full_name() {
245 let customer = create_test_customer(CustomerStatus::Active, true, true);
246 assert_eq!(customer.full_name(), "John Doe");
247 }
248
249 #[test]
250 fn test_customer_full_name_with_spaces() {
251 let mut customer = create_test_customer(CustomerStatus::Active, true, true);
252 customer.first_name = "Mary Jane".to_string();
253 customer.last_name = "Watson Parker".to_string();
254 assert_eq!(customer.full_name(), "Mary Jane Watson Parker");
255 }
256
257 #[test]
258 fn test_customer_can_receive_marketing_all_conditions_met() {
259 let customer = create_test_customer(CustomerStatus::Active, true, true);
260 assert!(customer.can_receive_marketing());
261 }
262
263 #[test]
264 fn test_customer_cannot_receive_marketing_not_opted_in() {
265 let customer = create_test_customer(CustomerStatus::Active, false, true);
266 assert!(!customer.can_receive_marketing());
267 }
268
269 #[test]
270 fn test_customer_cannot_receive_marketing_email_not_verified() {
271 let customer = create_test_customer(CustomerStatus::Active, true, false);
272 assert!(!customer.can_receive_marketing());
273 }
274
275 #[test]
276 fn test_customer_cannot_receive_marketing_inactive() {
277 let customer = create_test_customer(CustomerStatus::Inactive, true, true);
278 assert!(!customer.can_receive_marketing());
279 }
280
281 #[test]
282 fn test_customer_cannot_receive_marketing_suspended() {
283 let customer = create_test_customer(CustomerStatus::Suspended, true, true);
284 assert!(!customer.can_receive_marketing());
285 }
286
287 #[test]
288 fn test_customer_cannot_receive_marketing_deleted() {
289 let customer = create_test_customer(CustomerStatus::Deleted, true, true);
290 assert!(!customer.can_receive_marketing());
291 }
292
293 #[test]
298 fn test_customer_status_default() {
299 assert_eq!(CustomerStatus::default(), CustomerStatus::Active);
300 }
301
302 #[test]
303 fn test_customer_status_display() {
304 assert_eq!(format!("{}", CustomerStatus::Active), "active");
305 assert_eq!(format!("{}", CustomerStatus::Inactive), "inactive");
306 assert_eq!(format!("{}", CustomerStatus::Suspended), "suspended");
307 assert_eq!(format!("{}", CustomerStatus::Deleted), "deleted");
308 }
309
310 #[test]
311 fn test_customer_status_from_str() {
312 use std::str::FromStr;
313
314 assert_eq!(CustomerStatus::from_str("active").unwrap(), CustomerStatus::Active);
315 assert_eq!(CustomerStatus::from_str("suspended").unwrap(), CustomerStatus::Suspended);
316 }
317
318 #[test]
319 fn test_customer_status_serialization() {
320 let status = CustomerStatus::Suspended;
321 let json = serde_json::to_string(&status).unwrap();
322 assert_eq!(json, "\"suspended\"");
323
324 let deserialized: CustomerStatus = serde_json::from_str(&json).unwrap();
325 assert_eq!(deserialized, status);
326 }
327
328 #[test]
333 fn test_address_type_default() {
334 assert_eq!(AddressType::default(), AddressType::Both);
335 }
336
337 #[test]
338 fn test_address_type_display() {
339 assert_eq!(format!("{}", AddressType::Shipping), "shipping");
340 assert_eq!(format!("{}", AddressType::Billing), "billing");
341 assert_eq!(format!("{}", AddressType::Both), "both");
342 }
343
344 #[test]
345 fn test_address_type_from_str() {
346 use std::str::FromStr;
347
348 assert_eq!(AddressType::from_str("shipping").unwrap(), AddressType::Shipping);
349 assert_eq!(AddressType::from_str("both").unwrap(), AddressType::Both);
350 }
351
352 #[test]
353 fn test_address_type_serialization() {
354 let addr_type = AddressType::Shipping;
355 let json = serde_json::to_string(&addr_type).unwrap();
356 assert_eq!(json, "\"shipping\"");
357
358 let deserialized: AddressType = serde_json::from_str(&json).unwrap();
359 assert_eq!(deserialized, addr_type);
360 }
361
362 #[test]
367 fn test_customer_address_serialization_roundtrip() {
368 let address = create_test_customer_address();
369 let json = serde_json::to_string(&address).unwrap();
370 let deserialized: CustomerAddress = serde_json::from_str(&json).unwrap();
371 assert_eq!(address, deserialized);
372 }
373
374 #[test]
379 fn test_create_customer_default() {
380 let create = CreateCustomer::default();
381 assert!(create.email.is_empty());
382 assert!(create.first_name.is_empty());
383 assert!(create.last_name.is_empty());
384 assert!(create.phone.is_none());
385 assert!(create.accepts_marketing.is_none());
386 }
387
388 #[test]
389 fn test_create_customer_with_values() {
390 let create = CreateCustomer {
391 email: "new@example.com".to_string(),
392 first_name: "Jane".to_string(),
393 last_name: "Smith".to_string(),
394 phone: Some("+1-555-987-6543".to_string()),
395 accepts_marketing: Some(true),
396 tags: Some(vec!["new".to_string()]),
397 metadata: None,
398 };
399
400 assert_eq!(create.email, "new@example.com");
401 assert_eq!(create.first_name, "Jane");
402 assert_eq!(create.accepts_marketing, Some(true));
403 }
404
405 #[test]
410 fn test_update_customer_default() {
411 let update = UpdateCustomer::default();
412 assert!(update.email.is_none());
413 assert!(update.first_name.is_none());
414 assert!(update.status.is_none());
415 }
416
417 #[test]
418 fn test_update_customer_partial() {
419 let update = UpdateCustomer {
420 status: Some(CustomerStatus::Inactive),
421 accepts_marketing: Some(false),
422 ..Default::default()
423 };
424
425 assert_eq!(update.status, Some(CustomerStatus::Inactive));
426 assert_eq!(update.accepts_marketing, Some(false));
427 assert!(update.email.is_none());
428 }
429
430 #[test]
435 fn test_customer_filter_default() {
436 let filter = CustomerFilter::default();
437 assert!(filter.email.is_none());
438 assert!(filter.status.is_none());
439 assert!(filter.tag.is_none());
440 assert!(filter.limit.is_none());
441 }
442
443 #[test]
444 fn test_customer_filter_with_values() {
445 let filter = CustomerFilter {
446 email: Some("test@example.com".to_string()),
447 status: Some(CustomerStatus::Active),
448 accepts_marketing: Some(true),
449 limit: Some(50),
450 offset: Some(0),
451 ..Default::default()
452 };
453
454 assert_eq!(filter.email, Some("test@example.com".to_string()));
455 assert_eq!(filter.status, Some(CustomerStatus::Active));
456 assert_eq!(filter.limit, Some(50));
457 }
458
459 #[test]
464 fn test_customer_serialization_roundtrip() {
465 let customer = create_test_customer(CustomerStatus::Active, true, true);
466 let json = serde_json::to_string(&customer).unwrap();
467 let deserialized: Customer = serde_json::from_str(&json).unwrap();
468 assert_eq!(customer, deserialized);
469 }
470
471 #[test]
472 fn test_customer_with_metadata() {
473 let mut customer = create_test_customer(CustomerStatus::Active, true, true);
474 customer.metadata = Some(serde_json::json!({
475 "loyalty_tier": "gold",
476 "total_orders": 42
477 }));
478
479 let json = serde_json::to_string(&customer).unwrap();
480 let deserialized: Customer = serde_json::from_str(&json).unwrap();
481 assert_eq!(customer, deserialized);
482 }
483
484 fn valid_create_customer() -> CreateCustomer {
489 CreateCustomer {
490 email: "alice@example.com".to_string(),
491 first_name: "Alice".to_string(),
492 last_name: "Smith".to_string(),
493 ..Default::default()
494 }
495 }
496
497 #[test]
498 fn create_customer_rejects_empty_email() {
499 let input = CreateCustomer { email: String::new(), ..valid_create_customer() };
500 let err = input.validate().expect_err("empty email must be rejected");
501 assert!(
502 matches!(err, crate::CommerceError::InvalidInput { ref field, .. } if field == "email")
503 );
504 }
505
506 #[test]
507 fn create_customer_rejects_malformed_email() {
508 let input = CreateCustomer { email: "not-an-email".to_string(), ..valid_create_customer() };
509 let err = input.validate().expect_err("malformed email must be rejected");
510 assert!(
511 matches!(err, crate::CommerceError::InvalidInput { ref field, .. } if field == "email")
512 );
513 }
514
515 #[test]
516 fn create_customer_rejects_empty_names() {
517 assert!(
518 CreateCustomer { first_name: String::new(), ..valid_create_customer() }
519 .validate()
520 .is_err()
521 );
522 assert!(
523 CreateCustomer { last_name: " ".to_string(), ..valid_create_customer() }
524 .validate()
525 .is_err()
526 );
527 }
528
529 #[test]
530 fn create_customer_accepts_valid_input() {
531 assert!(valid_create_customer().validate().is_ok());
532 let with_phone = CreateCustomer {
533 phone: Some("+1-555-123-4567".to_string()),
534 ..valid_create_customer()
535 };
536 assert!(with_phone.validate().is_ok());
537 }
538
539 #[test]
540 fn create_customer_address_rejects_empty_required_fields() {
541 let base = CreateCustomerAddress {
542 customer_id: CustomerId::new(),
543 address_type: None,
544 first_name: "Alice".to_string(),
545 last_name: "Smith".to_string(),
546 company: None,
547 line1: "123 Main St".to_string(),
548 line2: None,
549 city: "San Francisco".to_string(),
550 state: None,
551 postal_code: "94102".to_string(),
552 country: "US".to_string(),
553 phone: None,
554 is_default: None,
555 };
556 assert!(base.validate().is_ok());
557 assert!(CreateCustomerAddress { line1: String::new(), ..base.clone() }.validate().is_err());
558 assert!(CreateCustomerAddress { city: String::new(), ..base.clone() }.validate().is_err());
559 assert!(CreateCustomerAddress { country: String::new(), ..base }.validate().is_err());
560 }
561}