Skip to main content

cala_ledger/tx_template/
entity.rs

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/// Contains the transaction-level details needed to create a `Transaction`.
216#[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}