Skip to main content

allsource_core/domain/entities/
creator.rs

1use crate::{
2    domain::value_objects::{CreatorId, TenantId, WalletAddress},
3    error::Result,
4};
5use chrono::{DateTime, Utc};
6use serde::{Deserialize, Serialize};
7
8/// Creator status in the paywall system
9#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
10pub enum CreatorStatus {
11    /// Creator has signed up but not verified email
12    #[default]
13    Pending,
14    /// Creator is active and can receive payments
15    Active,
16    /// Creator has been temporarily suspended
17    Suspended,
18    /// Creator has been permanently deactivated
19    Deactivated,
20}
21
22/// Creator tier determining fees and features
23#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
24pub enum CreatorTier {
25    /// Free tier: 10% platform fee
26    #[default]
27    Free,
28    /// Creator tier ($29/month): 7% platform fee
29    Creator,
30    /// Pro tier ($99/month): 5% platform fee
31    Pro,
32    /// Enterprise tier (custom pricing): Custom fee
33    Enterprise,
34}
35
36impl CreatorTier {
37    /// Get the platform fee percentage for this tier
38    pub fn fee_percentage(&self) -> u64 {
39        match self {
40            CreatorTier::Free => 10,
41            CreatorTier::Creator => 7,
42            CreatorTier::Pro => 5,
43            CreatorTier::Enterprise => 3,
44        }
45    }
46}
47
48/// Creator settings for customization
49#[derive(Debug, Clone, Serialize, Deserialize)]
50pub struct CreatorSettings {
51    /// Default price for new articles (in cents)
52    pub default_price_cents: u64,
53    /// Whether to show reading time estimates
54    pub show_reading_time: bool,
55    /// Custom branding color (hex)
56    pub brand_color: Option<String>,
57    /// Custom CTA text
58    pub unlock_button_text: Option<String>,
59}
60
61impl Default for CreatorSettings {
62    fn default() -> Self {
63        Self {
64            default_price_cents: 50, // $0.50 default
65            show_reading_time: true,
66            brand_color: None,
67            unlock_button_text: None,
68        }
69    }
70}
71
72/// Domain Entity: Creator
73///
74/// Represents a content creator in the paywall system.
75/// Creators sign up, configure their paywall settings, and receive payments.
76///
77/// Domain Rules:
78/// - Creator must have a valid email
79/// - Creator must have a valid wallet address for receiving payments
80/// - Only active creators can receive payments
81/// - Creator tier determines platform fee
82/// - API key must be kept secure
83#[derive(Debug, Clone, Serialize, Deserialize)]
84pub struct Creator {
85    id: CreatorId,
86    tenant_id: TenantId,
87    email: String,
88    name: Option<String>,
89    wallet_address: WalletAddress,
90    blog_url: Option<String>,
91    status: CreatorStatus,
92    tier: CreatorTier,
93    settings: CreatorSettings,
94    api_key_hash: Option<String>,
95    email_verified: bool,
96    total_revenue_cents: u64,
97    total_articles: u32,
98    created_at: DateTime<Utc>,
99    updated_at: DateTime<Utc>,
100    metadata: serde_json::Value,
101}
102
103impl Creator {
104    /// Create a new creator with validation
105    pub fn new(
106        tenant_id: TenantId,
107        email: String,
108        wallet_address: WalletAddress,
109        blog_url: Option<String>,
110    ) -> Result<Self> {
111        Self::validate_email(&email)?;
112
113        if let Some(ref url) = blog_url {
114            Self::validate_url(url)?;
115        }
116
117        let now = Utc::now();
118        Ok(Self {
119            id: CreatorId::new(),
120            tenant_id,
121            email,
122            name: None,
123            wallet_address,
124            blog_url,
125            status: CreatorStatus::Pending,
126            tier: CreatorTier::Free,
127            settings: CreatorSettings::default(),
128            api_key_hash: None,
129            email_verified: false,
130            total_revenue_cents: 0,
131            total_articles: 0,
132            created_at: now,
133            updated_at: now,
134            metadata: serde_json::json!({}),
135        })
136    }
137
138    /// Reconstruct creator from storage (bypasses validation)
139    #[allow(clippy::too_many_arguments)]
140    pub fn reconstruct(
141        id: CreatorId,
142        tenant_id: TenantId,
143        email: String,
144        name: Option<String>,
145        wallet_address: WalletAddress,
146        blog_url: Option<String>,
147        status: CreatorStatus,
148        tier: CreatorTier,
149        settings: CreatorSettings,
150        api_key_hash: Option<String>,
151        email_verified: bool,
152        total_revenue_cents: u64,
153        total_articles: u32,
154        created_at: DateTime<Utc>,
155        updated_at: DateTime<Utc>,
156        metadata: serde_json::Value,
157    ) -> Self {
158        Self {
159            id,
160            tenant_id,
161            email,
162            name,
163            wallet_address,
164            blog_url,
165            status,
166            tier,
167            settings,
168            api_key_hash,
169            email_verified,
170            total_revenue_cents,
171            total_articles,
172            created_at,
173            updated_at,
174            metadata,
175        }
176    }
177
178    // Getters
179
180    pub fn id(&self) -> &CreatorId {
181        &self.id
182    }
183
184    pub fn tenant_id(&self) -> &TenantId {
185        &self.tenant_id
186    }
187
188    pub fn email(&self) -> &str {
189        &self.email
190    }
191
192    pub fn name(&self) -> Option<&str> {
193        self.name.as_deref()
194    }
195
196    pub fn wallet_address(&self) -> &WalletAddress {
197        &self.wallet_address
198    }
199
200    pub fn blog_url(&self) -> Option<&str> {
201        self.blog_url.as_deref()
202    }
203
204    pub fn status(&self) -> CreatorStatus {
205        self.status
206    }
207
208    pub fn tier(&self) -> CreatorTier {
209        self.tier
210    }
211
212    pub fn settings(&self) -> &CreatorSettings {
213        &self.settings
214    }
215
216    pub fn api_key_hash(&self) -> Option<&str> {
217        self.api_key_hash.as_deref()
218    }
219
220    pub fn is_email_verified(&self) -> bool {
221        self.email_verified
222    }
223
224    pub fn total_revenue_cents(&self) -> u64 {
225        self.total_revenue_cents
226    }
227
228    pub fn total_articles(&self) -> u32 {
229        self.total_articles
230    }
231
232    pub fn created_at(&self) -> DateTime<Utc> {
233        self.created_at
234    }
235
236    pub fn updated_at(&self) -> DateTime<Utc> {
237        self.updated_at
238    }
239
240    pub fn metadata(&self) -> &serde_json::Value {
241        &self.metadata
242    }
243
244    // Domain behavior methods
245
246    /// Check if creator is active and can receive payments
247    pub fn is_active(&self) -> bool {
248        self.status == CreatorStatus::Active && self.email_verified
249    }
250
251    /// Check if creator can receive payments
252    pub fn can_receive_payments(&self) -> Result<()> {
253        if !self.email_verified {
254            return Err(crate::error::AllSourceError::ValidationError(
255                "Email not verified".to_string(),
256            ));
257        }
258
259        if self.status != CreatorStatus::Active {
260            return Err(crate::error::AllSourceError::ValidationError(format!(
261                "Creator is not active (status: {:?})",
262                self.status
263            )));
264        }
265
266        Ok(())
267    }
268
269    /// Get the platform fee percentage for this creator
270    pub fn fee_percentage(&self) -> u64 {
271        self.tier.fee_percentage()
272    }
273
274    /// Verify the email address
275    pub fn verify_email(&mut self) {
276        self.email_verified = true;
277        if self.status == CreatorStatus::Pending {
278            self.status = CreatorStatus::Active;
279        }
280        self.updated_at = Utc::now();
281    }
282
283    /// Update the name
284    pub fn update_name(&mut self, name: Option<String>) {
285        self.name = name;
286        self.updated_at = Utc::now();
287    }
288
289    /// Update the wallet address
290    pub fn update_wallet_address(&mut self, wallet_address: WalletAddress) {
291        self.wallet_address = wallet_address;
292        self.updated_at = Utc::now();
293    }
294
295    /// Update the blog URL
296    pub fn update_blog_url(&mut self, blog_url: Option<String>) -> Result<()> {
297        if let Some(ref url) = blog_url {
298            Self::validate_url(url)?;
299        }
300        self.blog_url = blog_url;
301        self.updated_at = Utc::now();
302        Ok(())
303    }
304
305    /// Update settings
306    pub fn update_settings(&mut self, settings: CreatorSettings) {
307        self.settings = settings;
308        self.updated_at = Utc::now();
309    }
310
311    /// Set the API key hash
312    pub fn set_api_key_hash(&mut self, hash: String) {
313        self.api_key_hash = Some(hash);
314        self.updated_at = Utc::now();
315    }
316
317    /// Upgrade tier
318    pub fn upgrade_tier(&mut self, tier: CreatorTier) {
319        self.tier = tier;
320        self.updated_at = Utc::now();
321    }
322
323    /// Suspend the creator
324    pub fn suspend(&mut self) {
325        self.status = CreatorStatus::Suspended;
326        self.updated_at = Utc::now();
327    }
328
329    /// Reactivate a suspended creator
330    pub fn reactivate(&mut self) -> Result<()> {
331        if self.status == CreatorStatus::Deactivated {
332            return Err(crate::error::AllSourceError::ValidationError(
333                "Cannot reactivate a deactivated creator".to_string(),
334            ));
335        }
336
337        if self.email_verified {
338            self.status = CreatorStatus::Active;
339        } else {
340            self.status = CreatorStatus::Pending;
341        }
342        self.updated_at = Utc::now();
343        Ok(())
344    }
345
346    /// Permanently deactivate the creator
347    pub fn deactivate(&mut self) {
348        self.status = CreatorStatus::Deactivated;
349        self.updated_at = Utc::now();
350    }
351
352    /// Record revenue from a transaction
353    pub fn record_revenue(&mut self, amount_cents: u64) {
354        self.total_revenue_cents += amount_cents;
355        self.updated_at = Utc::now();
356    }
357
358    /// Increment article count
359    pub fn increment_articles(&mut self) {
360        self.total_articles += 1;
361        self.updated_at = Utc::now();
362    }
363
364    /// Decrement article count
365    pub fn decrement_articles(&mut self) {
366        self.total_articles = self.total_articles.saturating_sub(1);
367        self.updated_at = Utc::now();
368    }
369
370    /// Update metadata
371    pub fn update_metadata(&mut self, metadata: serde_json::Value) {
372        self.metadata = metadata;
373        self.updated_at = Utc::now();
374    }
375
376    // Validation
377
378    fn validate_email(email: &str) -> Result<()> {
379        if email.is_empty() {
380            return Err(crate::error::AllSourceError::InvalidInput(
381                "Email cannot be empty".to_string(),
382            ));
383        }
384
385        if !email.contains('@') || !email.contains('.') {
386            return Err(crate::error::AllSourceError::InvalidInput(
387                "Invalid email format".to_string(),
388            ));
389        }
390
391        if email.len() > 254 {
392            return Err(crate::error::AllSourceError::InvalidInput(
393                "Email cannot exceed 254 characters".to_string(),
394            ));
395        }
396
397        Ok(())
398    }
399
400    fn validate_url(url: &str) -> Result<()> {
401        if url.is_empty() {
402            return Err(crate::error::AllSourceError::InvalidInput(
403                "URL cannot be empty".to_string(),
404            ));
405        }
406
407        if !url.starts_with("http://") && !url.starts_with("https://") {
408            return Err(crate::error::AllSourceError::InvalidInput(
409                "URL must start with http:// or https://".to_string(),
410            ));
411        }
412
413        if url.len() > 2048 {
414            return Err(crate::error::AllSourceError::InvalidInput(
415                "URL cannot exceed 2048 characters".to_string(),
416            ));
417        }
418
419        Ok(())
420    }
421}
422
423#[cfg(test)]
424mod tests {
425    use super::*;
426
427    const VALID_WALLET: &str = "9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM";
428
429    fn test_tenant_id() -> TenantId {
430        TenantId::new("test-tenant".to_string()).unwrap()
431    }
432
433    fn test_wallet() -> WalletAddress {
434        WalletAddress::new(VALID_WALLET.to_string()).unwrap()
435    }
436
437    #[test]
438    fn test_create_creator() {
439        let creator = Creator::new(
440            test_tenant_id(),
441            "test@example.com".to_string(),
442            test_wallet(),
443            Some("https://blog.example.com".to_string()),
444        );
445
446        assert!(creator.is_ok());
447        let creator = creator.unwrap();
448        assert_eq!(creator.email(), "test@example.com");
449        assert_eq!(creator.status(), CreatorStatus::Pending);
450        assert!(!creator.is_email_verified());
451    }
452
453    #[test]
454    fn test_reject_invalid_email() {
455        let result = Creator::new(
456            test_tenant_id(),
457            "invalid-email".to_string(),
458            test_wallet(),
459            None,
460        );
461
462        assert!(result.is_err());
463    }
464
465    #[test]
466    fn test_reject_empty_email() {
467        let result = Creator::new(test_tenant_id(), String::new(), test_wallet(), None);
468
469        assert!(result.is_err());
470    }
471
472    #[test]
473    fn test_reject_invalid_url() {
474        let result = Creator::new(
475            test_tenant_id(),
476            "test@example.com".to_string(),
477            test_wallet(),
478            Some("not-a-url".to_string()),
479        );
480
481        assert!(result.is_err());
482    }
483
484    #[test]
485    fn test_verify_email_activates_creator() {
486        let mut creator = Creator::new(
487            test_tenant_id(),
488            "test@example.com".to_string(),
489            test_wallet(),
490            None,
491        )
492        .unwrap();
493
494        assert_eq!(creator.status(), CreatorStatus::Pending);
495        assert!(!creator.is_active());
496
497        creator.verify_email();
498
499        assert_eq!(creator.status(), CreatorStatus::Active);
500        assert!(creator.is_active());
501    }
502
503    #[test]
504    fn test_can_receive_payments() {
505        let mut creator = Creator::new(
506            test_tenant_id(),
507            "test@example.com".to_string(),
508            test_wallet(),
509            None,
510        )
511        .unwrap();
512
513        // Cannot receive payments when not verified
514        assert!(creator.can_receive_payments().is_err());
515
516        // Verify and check again
517        creator.verify_email();
518        assert!(creator.can_receive_payments().is_ok());
519
520        // Suspend and check again
521        creator.suspend();
522        assert!(creator.can_receive_payments().is_err());
523    }
524
525    #[test]
526    fn test_fee_percentage() {
527        let mut creator = Creator::new(
528            test_tenant_id(),
529            "test@example.com".to_string(),
530            test_wallet(),
531            None,
532        )
533        .unwrap();
534
535        // Free tier: 10%
536        assert_eq!(creator.fee_percentage(), 10);
537
538        // Upgrade to Creator tier: 7%
539        creator.upgrade_tier(CreatorTier::Creator);
540        assert_eq!(creator.fee_percentage(), 7);
541
542        // Upgrade to Pro tier: 5%
543        creator.upgrade_tier(CreatorTier::Pro);
544        assert_eq!(creator.fee_percentage(), 5);
545    }
546
547    #[test]
548    fn test_suspend_and_reactivate() {
549        let mut creator = Creator::new(
550            test_tenant_id(),
551            "test@example.com".to_string(),
552            test_wallet(),
553            None,
554        )
555        .unwrap();
556
557        creator.verify_email();
558        assert!(creator.is_active());
559
560        creator.suspend();
561        assert_eq!(creator.status(), CreatorStatus::Suspended);
562        assert!(!creator.is_active());
563
564        creator.reactivate().unwrap();
565        assert_eq!(creator.status(), CreatorStatus::Active);
566        assert!(creator.is_active());
567    }
568
569    #[test]
570    fn test_cannot_reactivate_deactivated() {
571        let mut creator = Creator::new(
572            test_tenant_id(),
573            "test@example.com".to_string(),
574            test_wallet(),
575            None,
576        )
577        .unwrap();
578
579        creator.deactivate();
580        assert!(creator.reactivate().is_err());
581    }
582
583    #[test]
584    fn test_record_revenue() {
585        let mut creator = Creator::new(
586            test_tenant_id(),
587            "test@example.com".to_string(),
588            test_wallet(),
589            None,
590        )
591        .unwrap();
592
593        assert_eq!(creator.total_revenue_cents(), 0);
594
595        creator.record_revenue(1000);
596        assert_eq!(creator.total_revenue_cents(), 1000);
597
598        creator.record_revenue(500);
599        assert_eq!(creator.total_revenue_cents(), 1500);
600    }
601
602    #[test]
603    fn test_article_count() {
604        let mut creator = Creator::new(
605            test_tenant_id(),
606            "test@example.com".to_string(),
607            test_wallet(),
608            None,
609        )
610        .unwrap();
611
612        assert_eq!(creator.total_articles(), 0);
613
614        creator.increment_articles();
615        assert_eq!(creator.total_articles(), 1);
616
617        creator.increment_articles();
618        assert_eq!(creator.total_articles(), 2);
619
620        creator.decrement_articles();
621        assert_eq!(creator.total_articles(), 1);
622    }
623
624    #[test]
625    fn test_update_settings() {
626        let mut creator = Creator::new(
627            test_tenant_id(),
628            "test@example.com".to_string(),
629            test_wallet(),
630            None,
631        )
632        .unwrap();
633
634        let new_settings = CreatorSettings {
635            default_price_cents: 100,
636            show_reading_time: false,
637            brand_color: Some("#FF0000".to_string()),
638            unlock_button_text: Some("Read Now".to_string()),
639        };
640
641        creator.update_settings(new_settings);
642
643        assert_eq!(creator.settings().default_price_cents, 100);
644        assert!(!creator.settings().show_reading_time);
645        assert_eq!(creator.settings().brand_color, Some("#FF0000".to_string()));
646    }
647
648    #[test]
649    fn test_serde_serialization() {
650        let creator = Creator::new(
651            test_tenant_id(),
652            "test@example.com".to_string(),
653            test_wallet(),
654            None,
655        )
656        .unwrap();
657
658        let json = serde_json::to_string(&creator);
659        assert!(json.is_ok());
660
661        let deserialized: Creator = serde_json::from_str(&json.unwrap()).unwrap();
662        assert_eq!(deserialized.email(), "test@example.com");
663    }
664}