Skip to main content

max / makenotwork

3.1 KB · 121 lines History Blame Raw
1 //! Creator platform import system.
2 //!
3 //! Converts data from external platforms (Patreon, Ko-fi, Gumroad, Bandcamp,
4 //! Substack, Ghost, Lemon Squeezy) into a common intermediate format, then
5 //! feeds it through a generic pipeline that creates MNW entities.
6
7 pub mod csv_converter;
8 pub mod pipeline;
9
10 use chrono::{DateTime, Utc};
11 use serde::Deserialize;
12
13 // Re-export enums from db layer (where impl_str_enum macro lives).
14 pub use crate::db::{ImportJobStatus, ImportSource};
15
16 // ── Common Intermediate Format ──
17
18 #[derive(Debug, Clone, Default)]
19 pub struct ImportPayload {
20 pub subscribers: Vec<ImportSubscriber>,
21 pub items: Vec<ImportItem>,
22 pub tiers: Vec<ImportTier>,
23 pub transactions: Vec<ImportTransaction>,
24 pub tags: Vec<String>,
25 }
26
27 impl ImportPayload {
28 /// Total number of entities across all categories.
29 pub fn total_rows(&self) -> usize {
30 self.subscribers.len()
31 + self.items.len()
32 + self.tiers.len()
33 + self.transactions.len()
34 }
35 }
36
37 #[derive(Debug, Clone)]
38 pub struct ImportSubscriber {
39 pub email: String,
40 pub name: Option<String>,
41 pub tier_name: Option<String>,
42 pub status: Option<String>,
43 pub joined_at: Option<DateTime<Utc>>,
44 pub lifetime_amount_cents: Option<i64>,
45 pub stripe_customer_id: Option<String>,
46 }
47
48 #[derive(Debug, Clone)]
49 pub struct ImportItem {
50 pub title: String,
51 pub description: Option<String>,
52 pub price_cents: Option<i64>,
53 pub body_html: Option<String>,
54 pub tags: Vec<String>,
55 pub published_at: Option<DateTime<Utc>>,
56 pub is_public: bool,
57 }
58
59 #[derive(Debug, Clone)]
60 pub struct ImportTier {
61 pub name: String,
62 pub description: Option<String>,
63 pub price_cents: i64,
64 }
65
66 #[derive(Debug, Clone)]
67 pub struct ImportTransaction {
68 pub buyer_email: String,
69 pub buyer_name: Option<String>,
70 pub item_title: Option<String>,
71 pub amount_cents: i64,
72 pub currency: String,
73 pub date: DateTime<Utc>,
74 pub status: Option<String>,
75 }
76
77 // ── Column Mapping (CSV) ──
78
79 /// Maps CSV column indices to semantic fields.
80 #[derive(Debug, Clone, Default, Deserialize)]
81 pub struct ColumnMapping {
82 pub email: Option<usize>,
83 pub name: Option<usize>,
84 pub amount: Option<usize>,
85 pub date: Option<usize>,
86 pub item_title: Option<usize>,
87 pub tier: Option<usize>,
88 pub status: Option<usize>,
89 }
90
91 #[cfg(test)]
92 mod tests {
93 use super::*;
94
95 #[test]
96 fn payload_total_rows() {
97 let mut p = ImportPayload::default();
98 assert_eq!(p.total_rows(), 0);
99
100 p.subscribers.push(ImportSubscriber {
101 email: "a@b.com".into(),
102 name: None,
103 tier_name: None,
104 status: None,
105 joined_at: None,
106 lifetime_amount_cents: None,
107 stripe_customer_id: None,
108 });
109 p.transactions.push(ImportTransaction {
110 buyer_email: "a@b.com".into(),
111 buyer_name: None,
112 item_title: None,
113 amount_cents: 100,
114 currency: "USD".into(),
115 date: Utc::now(),
116 status: None,
117 });
118 assert_eq!(p.total_rows(), 2);
119 }
120 }
121