1use derive_builder::Builder;
2use serde::{Deserialize, Serialize};
3
4use crate::primitives::*;
5pub use cala_types::{primitives::TransactionId, transaction::*};
6use es_entity::*;
7
8use super::TransactionError;
9
10#[derive(EsEvent, Debug, Serialize, Deserialize)]
11#[serde(tag = "type", rename_all = "snake_case")]
12#[es_event(id = "TransactionId", event_context = false)]
13pub enum TransactionEvent {
14 #[cfg(feature = "import")]
15 Imported {
16 source: DataSource,
17 values: TransactionValues,
18 },
19 Initialized {
20 values: TransactionValues,
21 },
22 Updated {
23 values: TransactionValues,
24 fields: Vec<String>,
25 },
26}
27
28#[derive(EsEntity, Builder)]
29#[builder(pattern = "owned", build_fn(error = "EsEntityError"))]
30pub struct Transaction {
31 pub id: TransactionId,
32 values: TransactionValues,
33 events: EntityEvents<TransactionEvent>,
34}
35
36impl Transaction {
37 #[cfg(feature = "import")]
38 pub(super) fn import(source: DataSourceId, values: TransactionValues) -> Self {
39 let events = EntityEvents::init(
40 values.id,
41 [TransactionEvent::Imported {
42 source: DataSource::Remote { id: source },
43 values,
44 }],
45 );
46 Self::try_from_events(events).expect("Failed to build transaction from events")
47 }
48
49 pub fn id(&self) -> TransactionId {
50 self.values.id
51 }
52
53 pub fn journal_id(&self) -> JournalId {
54 self.values.journal_id
55 }
56
57 pub fn values(&self) -> &TransactionValues {
58 &self.values
59 }
60
61 pub fn into_values(self) -> TransactionValues {
62 self.values
63 }
64
65 pub fn created_at(&self) -> chrono::DateTime<chrono::Utc> {
66 self.events
67 .entity_first_persisted_at()
68 .expect("No persisted events")
69 }
70
71 pub fn effective(&self) -> chrono::NaiveDate {
72 self.values.effective
73 }
74
75 pub fn modified_at(&self) -> chrono::DateTime<chrono::Utc> {
76 self.events
77 .entity_last_modified_at()
78 .expect("Entity not persisted")
79 }
80
81 pub fn metadata<T: serde::de::DeserializeOwned>(&self) -> Result<Option<T>, serde_json::Error> {
82 match &self.values.metadata {
83 Some(metadata) => Ok(Some(serde_json::from_value(metadata.clone())?)),
84 None => Ok(None),
85 }
86 }
87
88 fn is_voided(&self) -> bool {
89 self.events.iter_all()
90 .any(|event| matches!(event, TransactionEvent::Updated { values, .. } if values.voided_by.is_some()))
91 }
92
93 pub(super) fn void(
94 &mut self,
95 new_tx_id: TransactionId,
96 entry_ids: Vec<EntryId>,
97 created_at: chrono::DateTime<chrono::Utc>,
98 ) -> Result<NewTransaction, TransactionError> {
99 if self.is_voided() {
100 return Err(TransactionError::AlreadyVoided(self.id));
101 }
102
103 self.values.voided_by = Some(new_tx_id);
104 let fields = vec!["voided_by".to_string()];
105
106 self.events.push(TransactionEvent::Updated {
107 values: self.values.clone(),
108 fields,
109 });
110
111 let values = self.values();
112
113 let mut builder = NewTransaction::builder();
114 builder
115 .id(new_tx_id)
116 .tx_template_id(values.tx_template_id)
117 .void_of(values.id)
118 .entry_ids(entry_ids.into_iter().collect())
119 .effective(chrono::Utc::now().date_naive())
120 .journal_id(values.journal_id)
121 .correlation_id(values.correlation_id.clone())
122 .created_at(created_at);
123
124 if let Some(ref external_id) = values.external_id {
125 builder.external_id(external_id);
126 }
127 if let Some(ref description) = values.description {
128 builder.description(description);
129 }
130 if let Some(ref metadata) = values.metadata {
131 builder.metadata(metadata.clone());
132 }
133 let new_transaction = builder.build().expect("Couldn't build voided transaction");
134 Ok(new_transaction)
135 }
136}
137
138impl TryFromEvents<TransactionEvent> for Transaction {
139 fn try_from_events(events: EntityEvents<TransactionEvent>) -> Result<Self, EsEntityError> {
140 let mut builder = TransactionBuilder::default();
141 for event in events.iter_all() {
142 match event {
143 #[cfg(feature = "import")]
144 TransactionEvent::Imported { source: _, values } => {
145 builder = builder.id(values.id).values(values.clone());
146 }
147 TransactionEvent::Initialized { values } => {
148 builder = builder.id(values.id).values(values.clone());
149 }
150 TransactionEvent::Updated { values, .. } => {
151 builder = builder.id(values.id).values(values.clone());
152 }
153 }
154 }
155 builder.events(events).build()
156 }
157}
158
159#[derive(Builder, Debug)]
160#[allow(dead_code)]
161pub struct NewTransaction {
162 #[builder(setter(custom))]
163 pub(super) id: TransactionId,
164 pub(super) created_at: chrono::DateTime<chrono::Utc>,
165 #[builder(setter(into))]
166 pub(super) journal_id: JournalId,
167 #[builder(setter(into))]
168 pub(super) tx_template_id: TxTemplateId,
169 pub(super) effective: chrono::NaiveDate,
170 #[builder(setter(into), default)]
171 pub(super) correlation_id: String,
172 #[builder(setter(strip_option, into), default)]
173 pub(super) void_of: Option<TransactionId>,
174 #[builder(setter(strip_option, into), default)]
175 pub(super) external_id: Option<String>,
176 #[builder(setter(strip_option, into), default)]
177 pub(super) description: Option<String>,
178 #[builder(setter(into), default)]
179 pub(super) metadata: Option<serde_json::Value>,
180 pub(super) entry_ids: Vec<EntryId>,
181}
182
183impl NewTransaction {
184 pub fn builder() -> NewTransactionBuilder {
185 NewTransactionBuilder::default()
186 }
187
188 pub(super) fn data_source(&self) -> DataSource {
189 DataSource::Local
190 }
191}
192
193impl IntoEvents<TransactionEvent> for NewTransaction {
194 fn into_events(self) -> EntityEvents<TransactionEvent> {
195 EntityEvents::init(
196 self.id,
197 [TransactionEvent::Initialized {
198 values: TransactionValues {
199 id: self.id,
200 version: 1,
201 created_at: self.created_at,
202 modified_at: self.created_at,
203 journal_id: self.journal_id,
204 tx_template_id: self.tx_template_id,
205 effective: self.effective,
206 correlation_id: self.correlation_id,
207 external_id: self.external_id,
208 description: self.description,
209 metadata: self.metadata,
210 entry_ids: self.entry_ids,
211 void_of: self.void_of,
212 voided_by: None,
213 },
214 }],
215 )
216 }
217}
218
219impl NewTransactionBuilder {
220 pub fn id(&mut self, id: impl Into<TransactionId>) -> &mut Self {
221 self.id = Some(id.into());
222 if self.correlation_id.is_none() {
223 self.correlation_id = Some(self.id.unwrap().to_string());
224 }
225 self
226 }
227}
228
229#[cfg(test)]
230mod tests {
231 use super::*;
232
233 fn transaction() -> Transaction {
234 let id = TransactionId::new();
235 let values = TransactionValues {
236 id,
237 version: 1,
238 created_at: chrono::Utc::now(),
239 modified_at: chrono::Utc::now(),
240 journal_id: JournalId::new(),
241 tx_template_id: TxTemplateId::new(),
242 entry_ids: vec![],
243 effective: chrono::Utc::now().date_naive(),
244 correlation_id: "correlation_id".to_string(),
245 external_id: Some("external_id".to_string()),
246 description: None,
247 voided_by: None,
248 void_of: None,
249 metadata: Some(serde_json::json!({
250 "tx": "metadata",
251 "test": true,
252 })),
253 };
254
255 let events = es_entity::EntityEvents::init(id, [TransactionEvent::Initialized { values }]);
256 Transaction::try_from_events(events).unwrap()
257 }
258
259 #[test]
260 fn it_builds() {
261 let id = uuid::Uuid::now_v7();
262 let new_transaction = NewTransaction::builder()
263 .id(id)
264 .created_at(chrono::Utc::now())
265 .journal_id(uuid::Uuid::now_v7())
266 .tx_template_id(uuid::Uuid::now_v7())
267 .entry_ids(vec![EntryId::new()])
268 .effective(chrono::NaiveDate::default())
269 .build()
270 .unwrap();
271 assert_eq!(id.to_string(), new_transaction.correlation_id);
272 assert!(new_transaction.external_id.is_none());
273 }
274
275 #[test]
276 fn fails_when_mandatory_fields_are_missing() {
277 let new_transaction = NewTransaction::builder().build();
278 assert!(new_transaction.is_err());
279 }
280
281 #[test]
282 fn accepts_metadata() {
283 use serde_json::json;
284 let new_transaction = NewTransaction::builder()
285 .id(uuid::Uuid::now_v7())
286 .created_at(chrono::Utc::now())
287 .journal_id(uuid::Uuid::now_v7())
288 .tx_template_id(uuid::Uuid::now_v7())
289 .effective(chrono::NaiveDate::default())
290 .metadata(json!({"foo": "bar"}))
291 .entry_ids(vec![EntryId::new()])
292 .build()
293 .unwrap();
294 assert_eq!(new_transaction.metadata, Some(json!({"foo": "bar"})));
295 }
296
297 #[test]
298 fn void_transaction() {
299 let mut transaction = transaction();
300 let new_tx_id = TransactionId::new();
301 let entry_ids = vec![EntryId::new()];
302 let created_at = chrono::Utc::now();
303
304 let new_tx = transaction.void(new_tx_id, entry_ids.clone(), created_at);
305 assert!(new_tx.is_ok());
306 assert!(transaction.is_voided());
307
308 let new_tx = new_tx.unwrap();
309 assert_eq!(new_tx.void_of, Some(transaction.id));
310
311 let new_tx = transaction.void(new_tx_id, entry_ids, created_at);
312 assert!(matches!(new_tx, Err(TransactionError::AlreadyVoided(_))));
313 }
314}