Skip to main content

stateset_core/models/
customer.rs

1//! Customer domain models
2
3use 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/// Customer entity
12#[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/// Customer address
31#[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/// Customer status enumeration
52#[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/// Address type enumeration
67#[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/// Input for creating a customer
81#[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    /// Validate a customer create request.
94    ///
95    /// Requires a non-empty, well-formed email and non-empty first/last names.
96    /// The phone number, when supplied, must be a plausible phone number.
97    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/// Input for updating a customer
109#[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/// Input for creating a customer address
122#[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    /// Validate a customer-address create request.
141    ///
142    /// Requires a non-nil customer reference and non-empty name / address line /
143    /// city / postal code / country fields.
144    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/// Customer filter for querying
158#[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    /// Keyset cursor: return records after this `(sort_key, id)` pair.
167    /// Sort key is `created_at` (DESC ordering).
168    pub after_cursor: Option<(String, String)>,
169}
170
171impl Customer {
172    /// Get full name
173    #[must_use]
174    pub fn full_name(&self) -> String {
175        format!("{} {}", self.first_name, self.last_name)
176    }
177
178    /// Check if customer can receive marketing
179    #[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    // ============================================================================
190    // Test Helpers
191    // ============================================================================
192
193    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    // ============================================================================
240    // Customer Tests
241    // ============================================================================
242
243    #[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    // ============================================================================
294    // CustomerStatus Tests
295    // ============================================================================
296
297    #[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    // ============================================================================
329    // AddressType Tests
330    // ============================================================================
331
332    #[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    // ============================================================================
363    // CustomerAddress Tests
364    // ============================================================================
365
366    #[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    // ============================================================================
375    // CreateCustomer Tests
376    // ============================================================================
377
378    #[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    // ============================================================================
406    // UpdateCustomer Tests
407    // ============================================================================
408
409    #[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    // ============================================================================
431    // CustomerFilter Tests
432    // ============================================================================
433
434    #[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    // ============================================================================
460    // Customer Serialization Tests
461    // ============================================================================
462
463    #[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    // ============================================================================
485    // Validation Tests
486    // ============================================================================
487
488    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}