Skip to main content

allsource_core/application/dto/
article_dto.rs

1use crate::domain::entities::{ArticleStats, ArticleStatus, PaywallArticle};
2use chrono::{DateTime, Utc};
3use serde::{Deserialize, Serialize};
4
5/// DTO for creating a new article
6#[derive(Debug, Deserialize)]
7pub struct CreateArticleRequest {
8    pub article_id: String,
9    pub tenant_id: String,
10    pub creator_id: String,
11    pub title: String,
12    pub url: String,
13    pub price_cents: u64,
14    pub description: Option<String>,
15    pub estimated_reading_time_minutes: Option<u16>,
16    pub preview_content: Option<String>,
17    /// If true, create as draft (not immediately published)
18    pub is_draft: Option<bool>,
19}
20
21/// DTO for article creation response
22#[derive(Debug, Serialize)]
23pub struct CreateArticleResponse {
24    pub article: ArticleDto,
25}
26
27/// DTO for updating an article
28#[derive(Debug, Deserialize)]
29pub struct UpdateArticleRequest {
30    pub title: Option<String>,
31    pub url: Option<String>,
32    pub price_cents: Option<u64>,
33    pub description: Option<String>,
34    pub estimated_reading_time_minutes: Option<u16>,
35    pub preview_content: Option<String>,
36}
37
38/// DTO for article update response
39#[derive(Debug, Serialize)]
40pub struct UpdateArticleResponse {
41    pub article: ArticleDto,
42}
43
44/// DTO for listing articles response
45#[derive(Debug, Serialize)]
46pub struct ListArticlesResponse {
47    pub articles: Vec<ArticleDto>,
48    pub count: usize,
49}
50
51/// DTO for article status
52#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
53#[serde(rename_all = "snake_case")]
54pub enum ArticleStatusDto {
55    Draft,
56    Active,
57    Archived,
58    Deleted,
59}
60
61impl From<ArticleStatus> for ArticleStatusDto {
62    fn from(status: ArticleStatus) -> Self {
63        match status {
64            ArticleStatus::Draft => ArticleStatusDto::Draft,
65            ArticleStatus::Active => ArticleStatusDto::Active,
66            ArticleStatus::Archived => ArticleStatusDto::Archived,
67            ArticleStatus::Deleted => ArticleStatusDto::Deleted,
68        }
69    }
70}
71
72/// DTO for article statistics
73#[derive(Debug, Clone, Serialize)]
74pub struct ArticleStatsDto {
75    pub total_purchases: u64,
76    pub total_revenue_cents: u64,
77    pub unique_readers: u64,
78    pub avg_read_duration_seconds: u64,
79    pub avg_scroll_depth: u8,
80    pub conversion_rate: f32,
81}
82
83impl From<&ArticleStats> for ArticleStatsDto {
84    fn from(stats: &ArticleStats) -> Self {
85        Self {
86            total_purchases: stats.total_purchases,
87            total_revenue_cents: stats.total_revenue_cents,
88            unique_readers: stats.unique_readers,
89            avg_read_duration_seconds: stats.avg_read_duration_seconds,
90            avg_scroll_depth: stats.avg_scroll_depth,
91            conversion_rate: stats.conversion_rate,
92        }
93    }
94}
95
96/// DTO for a paywall article in responses
97#[derive(Debug, Clone, Serialize)]
98pub struct ArticleDto {
99    pub id: String,
100    pub tenant_id: String,
101    pub creator_id: String,
102    pub title: String,
103    pub url: String,
104    pub price_cents: u64,
105    pub description: Option<String>,
106    pub estimated_reading_time_minutes: Option<u16>,
107    pub preview_content: Option<String>,
108    pub status: ArticleStatusDto,
109    pub stats: ArticleStatsDto,
110    pub is_purchasable: bool,
111    pub created_at: DateTime<Utc>,
112    pub updated_at: DateTime<Utc>,
113    pub published_at: Option<DateTime<Utc>>,
114}
115
116impl From<&PaywallArticle> for ArticleDto {
117    fn from(article: &PaywallArticle) -> Self {
118        Self {
119            id: article.id().to_string(),
120            tenant_id: article.tenant_id().to_string(),
121            creator_id: article.creator_id().as_uuid().to_string(),
122            title: article.title().to_string(),
123            url: article.url().to_string(),
124            price_cents: article.price_cents(),
125            description: article.description().map(|s| s.to_string()),
126            estimated_reading_time_minutes: article.estimated_reading_time_minutes(),
127            preview_content: article.preview_content().map(|s| s.to_string()),
128            status: article.status().into(),
129            stats: article.stats().into(),
130            is_purchasable: article.is_purchasable(),
131            created_at: article.created_at(),
132            updated_at: article.updated_at(),
133            published_at: article.published_at(),
134        }
135    }
136}
137
138impl From<PaywallArticle> for ArticleDto {
139    fn from(article: PaywallArticle) -> Self {
140        ArticleDto::from(&article)
141    }
142}