1use derive_builder::Builder;
2use serde::{Deserialize, Serialize};
3use tracing::instrument;
4
5pub use crate::param::definition::*;
6pub use cala_types::{primitives::TxTemplateId, tx_template::*};
7use cel_interpreter::CelExpression;
8use es_entity::*;
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 = "TxTemplateId", event_context = false)]
14pub enum TxTemplateEvent {
15 Initialized { values: TxTemplateValues },
16}
17
18impl TxTemplateEvent {
19 pub fn into_values(self) -> TxTemplateValues {
20 match self {
21 TxTemplateEvent::Initialized { values } => values,
22 }
23 }
24}
25
26#[derive(EsEntity, Builder)]
27#[builder(pattern = "owned", build_fn(error = "EntityHydrationError"))]
28pub struct TxTemplate {
29 pub id: TxTemplateId,
30 values: TxTemplateValues,
31 events: EntityEvents<TxTemplateEvent>,
32}
33
34impl TxTemplate {
35 pub fn id(&self) -> TxTemplateId {
36 self.values.id
37 }
38
39 pub fn values(&self) -> &TxTemplateValues {
40 &self.values
41 }
42
43 pub fn into_values(self) -> TxTemplateValues {
44 self.values
45 }
46
47 pub fn created_at(&self) -> chrono::DateTime<chrono::Utc> {
48 self.events
49 .entity_first_persisted_at()
50 .expect("No persisted events")
51 }
52
53 pub fn modified_at(&self) -> chrono::DateTime<chrono::Utc> {
54 self.events
55 .entity_last_modified_at()
56 .expect("No events for account")
57 }
58}
59
60impl TryFromEvents<TxTemplateEvent> for TxTemplate {
61 fn try_from_events(
62 events: EntityEvents<TxTemplateEvent>,
63 ) -> Result<Self, EntityHydrationError> {
64 let mut builder = TxTemplateBuilder::default();
65 for event in events.iter_all() {
66 match event {
67 TxTemplateEvent::Initialized { values } => {
68 builder = builder.id(values.id).values(values.clone());
69 }
70 }
71 }
72 builder.events(events).build()
73 }
74}
75
76#[derive(Builder, Debug)]
77pub struct NewTxTemplate {
78 #[builder(setter(into))]
79 pub(super) id: TxTemplateId,
80 #[builder(setter(into))]
81 pub(super) code: String,
82 #[builder(setter(strip_option, into), default)]
83 pub(super) description: Option<String>,
84 #[builder(setter(strip_option), default)]
85 pub(super) params: Option<Vec<NewParamDefinition>>,
86 pub(super) transaction: NewTxTemplateTransaction,
87 pub(super) entries: Vec<NewTxTemplateEntry>,
88 #[builder(setter(custom), default)]
89 pub(super) metadata: Option<serde_json::Value>,
90}
91
92impl NewTxTemplate {
93 pub fn builder() -> NewTxTemplateBuilder {
94 NewTxTemplateBuilder::default()
95 }
96}
97
98impl IntoEvents<TxTemplateEvent> for NewTxTemplate {
99 fn into_events(self) -> EntityEvents<TxTemplateEvent> {
100 EntityEvents::init(
101 self.id,
102 [TxTemplateEvent::Initialized {
103 values: TxTemplateValues {
104 id: self.id,
105 version: 1,
106 code: self.code,
107 description: self.description,
108 params: self
109 .params
110 .map(|p| p.into_iter().map(|p| p.into()).collect()),
111 transaction: self.transaction.into(),
112 entries: self.entries.into_iter().map(|e| e.into()).collect(),
113 metadata: self.metadata,
114 },
115 }],
116 )
117 }
118}
119
120impl NewTxTemplateBuilder {
121 pub fn metadata<T: serde::Serialize>(
122 &mut self,
123 metadata: T,
124 ) -> Result<&mut Self, serde_json::Error> {
125 self.metadata = Some(Some(serde_json::to_value(metadata)?));
126 Ok(self)
127 }
128}
129
130#[derive(Clone, Debug, Builder)]
131#[builder(build_fn(validate = "Self::validate"))]
132pub struct NewTxTemplateEntry {
133 #[builder(setter(into))]
134 entry_type: String,
135 #[builder(setter(into))]
136 account_id: String,
137 #[builder(setter(into))]
138 layer: String,
139 #[builder(setter(into))]
140 direction: String,
141 #[builder(setter(into))]
142 units: String,
143 #[builder(setter(into))]
144 currency: String,
145 #[builder(setter(strip_option, into), default)]
146 description: Option<String>,
147 #[builder(setter(strip_option, into), default)]
148 metadata: Option<String>,
149}
150
151impl NewTxTemplateEntry {
152 pub fn builder() -> NewTxTemplateEntryBuilder {
153 NewTxTemplateEntryBuilder::default()
154 }
155}
156impl NewTxTemplateEntryBuilder {
157 #[instrument(name = "tx_template_entry.validate", skip(self), err)]
158 fn validate(&self) -> Result<(), String> {
159 validate_expression(
160 self.entry_type
161 .as_ref()
162 .expect("Mandatory field 'entry_type' not set"),
163 )?;
164 validate_expression(
165 self.account_id
166 .as_ref()
167 .expect("Mandatory field 'account_id' not set"),
168 )?;
169 validate_expression(
170 self.layer
171 .as_ref()
172 .expect("Mandatory field 'layer' not set"),
173 )?;
174 validate_expression(
175 self.direction
176 .as_ref()
177 .expect("Mandatory field 'direction' not set"),
178 )?;
179 validate_expression(
180 self.units
181 .as_ref()
182 .expect("Mandatory field 'units' not set"),
183 )?;
184 validate_expression(
185 self.currency
186 .as_ref()
187 .expect("Mandatory field 'currency' not set"),
188 )?;
189 validate_optional_expression(&self.description)?;
190 validate_optional_expression(&self.metadata)
191 }
192}
193
194impl From<NewTxTemplateEntry> for cala_types::tx_template::TxTemplateEntry {
195 fn from(input: NewTxTemplateEntry) -> Self {
196 cala_types::tx_template::TxTemplateEntry {
197 entry_type: CelExpression::try_from(input.entry_type)
198 .expect("always a valid entry type"),
199 account_id: CelExpression::try_from(input.account_id)
200 .expect("always a valid account id"),
201 layer: CelExpression::try_from(input.layer).expect("always a valid layer"),
202 direction: CelExpression::try_from(input.direction).expect("always a valid direction"),
203 units: CelExpression::try_from(input.units).expect("always a valid units"),
204 currency: CelExpression::try_from(input.currency).expect("always a valid currency"),
205 description: input
206 .description
207 .map(|d| CelExpression::try_from(d).expect("always a valid description")),
208 metadata: input
209 .metadata
210 .map(|m| CelExpression::try_from(m).expect("always a valid metadata")),
211 }
212 }
213}
214
215#[derive(Clone, Debug, Serialize, Builder, Deserialize)]
217#[builder(build_fn(validate = "Self::validate"))]
218pub struct NewTxTemplateTransaction {
219 #[builder(setter(into))]
220 effective: String,
221 #[builder(setter(into))]
222 journal_id: String,
223 #[builder(setter(strip_option, into), default)]
224 correlation_id: Option<String>,
225 #[builder(setter(strip_option, into), default)]
226 external_id: Option<String>,
227 #[builder(setter(strip_option, into), default)]
228 description: Option<String>,
229 #[builder(setter(strip_option, into), default)]
230 metadata: Option<String>,
231}
232
233impl NewTxTemplateTransaction {
234 pub fn builder() -> NewTxTemplateTransactionBuilder {
235 NewTxTemplateTransactionBuilder::default()
236 }
237}
238
239impl NewTxTemplateTransactionBuilder {
240 #[instrument(name = "tx_template_transaction.validate", skip(self), err)]
241 fn validate(&self) -> Result<(), String> {
242 validate_expression(
243 self.effective
244 .as_ref()
245 .expect("Mandatory field 'effective' not set"),
246 )?;
247 validate_expression(
248 self.journal_id
249 .as_ref()
250 .expect("Mandatory field 'journal_id' not set"),
251 )?;
252 validate_optional_expression(&self.correlation_id)?;
253 validate_optional_expression(&self.external_id)?;
254 validate_optional_expression(&self.description)?;
255 validate_optional_expression(&self.metadata)
256 }
257}
258
259impl From<NewTxTemplateTransaction> for cala_types::tx_template::TxTemplateTransaction {
260 fn from(
261 NewTxTemplateTransaction {
262 effective,
263 journal_id,
264 correlation_id,
265 external_id,
266 description,
267 metadata,
268 }: NewTxTemplateTransaction,
269 ) -> Self {
270 cala_types::tx_template::TxTemplateTransaction {
271 effective: CelExpression::try_from(effective).expect("always a valid effective date"),
272 journal_id: CelExpression::try_from(journal_id).expect("always a valid journal id"),
273 correlation_id: correlation_id
274 .map(|c| CelExpression::try_from(c).expect("always a valid correlation id")),
275 external_id: external_id
276 .map(|id| CelExpression::try_from(id).expect("always a valid external id")),
277 description: description
278 .map(|d| CelExpression::try_from(d).expect("always a valid description")),
279 metadata: metadata
280 .map(|m| CelExpression::try_from(m).expect("always a valid metadata")),
281 }
282 }
283}
284
285#[instrument(name = "tx_template.validate_expression", skip(expr), fields(expression = %expr), err)]
286fn validate_expression(expr: &str) -> Result<(), String> {
287 CelExpression::try_from(expr).map_err(|e| e.to_string())?;
288 Ok(())
289}
290
291#[instrument(name = "tx_template.validate_optional_expression", skip(expr), err)]
292fn validate_optional_expression(expr: &Option<Option<String>>) -> Result<(), String> {
293 if let Some(Some(expr)) = expr.as_ref() {
294 CelExpression::try_from(expr.as_str()).map_err(|e| e.to_string())?;
295 }
296 Ok(())
297}
298
299#[cfg(test)]
300mod tests {
301 use super::*;
302 use uuid::Uuid;
303
304 #[test]
305 fn it_builds() {
306 let journal_id = Uuid::now_v7();
307 let entries = vec![NewTxTemplateEntry::builder()
308 .entry_type("'TEST_DR'")
309 .account_id("param.recipient")
310 .layer("'Settled'")
311 .direction("'Settled'")
312 .units("1290")
313 .currency("'BTC'")
314 .metadata(r#"{"sender": param.sender}"#)
315 .build()
316 .unwrap()];
317 let new_tx_template = NewTxTemplate::builder()
318 .id(TxTemplateId::new())
319 .code("CODE")
320 .transaction(
321 NewTxTemplateTransaction::builder()
322 .effective("date('2022-11-01')")
323 .journal_id(format!("uuid('{journal_id}')"))
324 .build()
325 .unwrap(),
326 )
327 .entries(entries)
328 .build()
329 .unwrap();
330 assert_eq!(new_tx_template.description, None);
331 }
332
333 #[test]
334 fn fails_when_mandatory_fields_are_missing() {
335 let new_tx_template = NewTxTemplate::builder().build();
336 assert!(new_tx_template.is_err());
337 }
338}