1use crate::{
2 domain::value_objects::{CreatorId, TenantId, WalletAddress},
3 error::Result,
4};
5use chrono::{DateTime, Utc};
6use serde::{Deserialize, Serialize};
7
8#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
10pub enum CreatorStatus {
11 #[default]
13 Pending,
14 Active,
16 Suspended,
18 Deactivated,
20}
21
22#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
24pub enum CreatorTier {
25 #[default]
27 Free,
28 Creator,
30 Pro,
32 Enterprise,
34}
35
36impl CreatorTier {
37 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#[derive(Debug, Clone, Serialize, Deserialize)]
50pub struct CreatorSettings {
51 pub default_price_cents: u64,
53 pub show_reading_time: bool,
55 pub brand_color: Option<String>,
57 pub unlock_button_text: Option<String>,
59}
60
61impl Default for CreatorSettings {
62 fn default() -> Self {
63 Self {
64 default_price_cents: 50, show_reading_time: true,
66 brand_color: None,
67 unlock_button_text: None,
68 }
69 }
70}
71
72#[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 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 #[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 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 pub fn is_active(&self) -> bool {
248 self.status == CreatorStatus::Active && self.email_verified
249 }
250
251 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 pub fn fee_percentage(&self) -> u64 {
271 self.tier.fee_percentage()
272 }
273
274 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 pub fn update_name(&mut self, name: Option<String>) {
285 self.name = name;
286 self.updated_at = Utc::now();
287 }
288
289 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 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 pub fn update_settings(&mut self, settings: CreatorSettings) {
307 self.settings = settings;
308 self.updated_at = Utc::now();
309 }
310
311 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 pub fn upgrade_tier(&mut self, tier: CreatorTier) {
319 self.tier = tier;
320 self.updated_at = Utc::now();
321 }
322
323 pub fn suspend(&mut self) {
325 self.status = CreatorStatus::Suspended;
326 self.updated_at = Utc::now();
327 }
328
329 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 pub fn deactivate(&mut self) {
348 self.status = CreatorStatus::Deactivated;
349 self.updated_at = Utc::now();
350 }
351
352 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 pub fn increment_articles(&mut self) {
360 self.total_articles += 1;
361 self.updated_at = Utc::now();
362 }
363
364 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 pub fn update_metadata(&mut self, metadata: serde_json::Value) {
372 self.metadata = metadata;
373 self.updated_at = Utc::now();
374 }
375
376 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 assert!(creator.can_receive_payments().is_err());
515
516 creator.verify_email();
518 assert!(creator.can_receive_payments().is_ok());
519
520 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 assert_eq!(creator.fee_percentage(), 10);
537
538 creator.upgrade_tier(CreatorTier::Creator);
540 assert_eq!(creator.fee_percentage(), 7);
541
542 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}