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