allsource_core/application/dto/
article_dto.rs1use crate::domain::entities::{ArticleStats, ArticleStatus, PaywallArticle};
2use chrono::{DateTime, Utc};
3use serde::{Deserialize, Serialize};
4
5#[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 pub is_draft: Option<bool>,
19}
20
21#[derive(Debug, Serialize)]
23pub struct CreateArticleResponse {
24 pub article: ArticleDto,
25}
26
27#[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#[derive(Debug, Serialize)]
40pub struct UpdateArticleResponse {
41 pub article: ArticleDto,
42}
43
44#[derive(Debug, Serialize)]
46pub struct ListArticlesResponse {
47 pub articles: Vec<ArticleDto>,
48 pub count: usize,
49}
50
51#[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#[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#[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}