cala_ledger/transaction/
entity.rs

1use derive_builder::Builder;
2use serde::{Deserialize, Serialize};
3
4use crate::primitives::*;
5pub use cala_types::{primitives::TransactionId, transaction::*};
6use es_entity::*;
7
8#[derive(EsEvent, Debug, Serialize, Deserialize)]
9#[serde(tag = "type", rename_all = "snake_case")]
10#[es_event(id = "TransactionId")]
11pub enum TransactionEvent {
12    #[cfg(feature = "import")]
13    Imported {
14        source: DataSource,
15        values: TransactionValues,
16    },
17    Initialized {
18        values: TransactionValues,
19    },
20}
21
22#[derive(EsEntity, Builder)]
23#[builder(pattern = "owned", build_fn(error = "EsEntityError"))]
24pub struct Transaction {
25    pub id: TransactionId,
26    values: TransactionValues,
27    pub(super) events: EntityEvents<TransactionEvent>,
28}
29
30impl Transaction {
31    #[cfg(feature = "import")]
32    pub(super) fn import(source: DataSourceId, values: TransactionValues) -> Self {
33        let events = EntityEvents::init(
34            values.id,
35            [TransactionEvent::Imported {
36                source: DataSource::Remote { id: source },
37                values,
38            }],
39        );
40        Self::try_from_events(events).expect("Failed to build transaction from events")
41    }
42
43    pub fn id(&self) -> TransactionId {
44        self.values.id
45    }
46
47    pub fn journal_id(&self) -> JournalId {
48        self.values.journal_id
49    }
50
51    pub fn values(&self) -> &TransactionValues {
52        &self.values
53    }
54
55    pub fn into_values(self) -> TransactionValues {
56        self.values
57    }
58
59    pub fn created_at(&self) -> chrono::DateTime<chrono::Utc> {
60        self.events
61            .entity_first_persisted_at()
62            .expect("No persisted events")
63    }
64
65    pub fn modified_at(&self) -> chrono::DateTime<chrono::Utc> {
66        self.events
67            .entity_last_modified_at()
68            .expect("Entity not persisted")
69    }
70}
71
72impl TryFromEvents<TransactionEvent> for Transaction {
73    fn try_from_events(events: EntityEvents<TransactionEvent>) -> Result<Self, EsEntityError> {
74        let mut builder = TransactionBuilder::default();
75        for event in events.iter_all() {
76            match event {
77                #[cfg(feature = "import")]
78                TransactionEvent::Imported { source: _, values } => {
79                    builder = builder.id(values.id).values(values.clone());
80                }
81                TransactionEvent::Initialized { values } => {
82                    builder = builder.id(values.id).values(values.clone());
83                }
84            }
85        }
86        builder.events(events).build()
87    }
88}
89
90#[derive(Builder, Debug)]
91#[allow(dead_code)]
92pub struct NewTransaction {
93    #[builder(setter(custom))]
94    pub(super) id: TransactionId,
95    pub(super) created_at: chrono::DateTime<chrono::Utc>,
96    #[builder(setter(into))]
97    pub(super) journal_id: JournalId,
98    #[builder(setter(into))]
99    pub(super) tx_template_id: TxTemplateId,
100    pub(super) effective: chrono::NaiveDate,
101    #[builder(setter(into), default)]
102    pub(super) correlation_id: String,
103    #[builder(setter(strip_option, into), default)]
104    pub(super) external_id: Option<String>,
105    #[builder(setter(strip_option, into), default)]
106    pub(super) description: Option<String>,
107    #[builder(setter(into), default)]
108    pub(super) metadata: Option<serde_json::Value>,
109    pub(super) entry_ids: Vec<EntryId>,
110}
111
112impl NewTransaction {
113    pub fn builder() -> NewTransactionBuilder {
114        NewTransactionBuilder::default()
115    }
116
117    pub(super) fn data_source(&self) -> DataSource {
118        DataSource::Local
119    }
120}
121
122impl IntoEvents<TransactionEvent> for NewTransaction {
123    fn into_events(self) -> EntityEvents<TransactionEvent> {
124        EntityEvents::init(
125            self.id,
126            [TransactionEvent::Initialized {
127                values: TransactionValues {
128                    id: self.id,
129                    version: 1,
130                    created_at: self.created_at,
131                    modified_at: self.created_at,
132                    journal_id: self.journal_id,
133                    tx_template_id: self.tx_template_id,
134                    effective: self.effective,
135                    correlation_id: self.correlation_id,
136                    external_id: self.external_id,
137                    description: self.description,
138                    metadata: self.metadata,
139                    entry_ids: self.entry_ids,
140                },
141            }],
142        )
143    }
144}
145
146impl NewTransactionBuilder {
147    pub fn id(&mut self, id: impl Into<TransactionId>) -> &mut Self {
148        self.id = Some(id.into());
149        if self.correlation_id.is_none() {
150            self.correlation_id = Some(self.id.unwrap().to_string());
151        }
152        self
153    }
154}
155
156#[cfg(test)]
157mod tests {
158    use super::*;
159
160    #[test]
161    fn it_builds() {
162        let id = uuid::Uuid::new_v4();
163        let new_transaction = NewTransaction::builder()
164            .id(id)
165            .created_at(chrono::Utc::now())
166            .journal_id(uuid::Uuid::new_v4())
167            .tx_template_id(uuid::Uuid::new_v4())
168            .entry_ids(vec![EntryId::new()])
169            .effective(chrono::NaiveDate::default())
170            .build()
171            .unwrap();
172        assert_eq!(id.to_string(), new_transaction.correlation_id);
173        assert!(new_transaction.external_id.is_none());
174    }
175
176    #[test]
177    fn fails_when_mandatory_fields_are_missing() {
178        let new_transaction = NewTransaction::builder().build();
179        assert!(new_transaction.is_err());
180    }
181
182    #[test]
183    fn accepts_metadata() {
184        use serde_json::json;
185        let new_transaction = NewTransaction::builder()
186            .id(uuid::Uuid::new_v4())
187            .created_at(chrono::Utc::now())
188            .journal_id(uuid::Uuid::new_v4())
189            .tx_template_id(uuid::Uuid::new_v4())
190            .effective(chrono::NaiveDate::default())
191            .metadata(json!({"foo": "bar"}))
192            .entry_ids(vec![EntryId::new()])
193            .build()
194            .unwrap();
195        assert_eq!(new_transaction.metadata, Some(json!({"foo": "bar"})));
196    }
197}