cala_ledger/transaction/
entity.rs1use 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}