sqlx_ledger/tx_template/
entity.rs

1use derive_builder::Builder;
2use serde::Serialize;
3
4use cel_interpreter::CelExpression;
5
6use super::param_definition::*;
7use crate::primitives::*;
8
9/// Representation of a new TxTemplate created via a builder.
10///
11/// TxTemplate is an entity that takes a set of params including
12/// a `TxInput` entity and a set of `EntryInput` entities. It can
13/// later be used to create a `Transaction`.
14#[derive(Builder)]
15pub struct NewTxTemplate {
16    #[builder(setter(into))]
17    pub(super) id: TxTemplateId,
18    #[builder(setter(into))]
19    pub(super) code: String,
20    #[builder(setter(strip_option, into), default)]
21    pub(super) description: Option<String>,
22    #[builder(setter(strip_option), default)]
23    pub(super) params: Option<Vec<ParamDefinition>>,
24    pub(super) tx_input: TxInput,
25    pub(super) entries: Vec<EntryInput>,
26    #[builder(setter(custom), default)]
27    pub(super) metadata: Option<serde_json::Value>,
28}
29
30impl NewTxTemplate {
31    pub fn builder() -> NewTxTemplateBuilder {
32        NewTxTemplateBuilder::default()
33    }
34}
35
36impl NewTxTemplateBuilder {
37    pub fn metadata<T: serde::Serialize>(
38        &mut self,
39        metadata: T,
40    ) -> Result<&mut Self, serde_json::Error> {
41        self.metadata = Some(Some(serde_json::to_value(metadata)?));
42        Ok(self)
43    }
44}
45
46/// Contains the transaction-level details needed to create a `Transaction`.
47#[derive(Clone, Serialize, Builder)]
48#[builder(build_fn(validate = "Self::validate"))]
49pub struct TxInput {
50    #[builder(setter(into))]
51    effective: String,
52    #[builder(setter(into))]
53    journal_id: String,
54    #[builder(setter(strip_option, into), default)]
55    correlation_id: Option<String>,
56    #[builder(setter(strip_option, into), default)]
57    external_id: Option<String>,
58    #[builder(setter(strip_option, into), default)]
59    description: Option<String>,
60    #[builder(setter(strip_option, into), default)]
61    metadata: Option<String>,
62}
63
64impl TxInput {
65    pub fn builder() -> TxInputBuilder {
66        TxInputBuilder::default()
67    }
68}
69
70impl TxInputBuilder {
71    fn validate(&self) -> Result<(), String> {
72        validate_expression(
73            self.effective
74                .as_ref()
75                .expect("Mandatory field 'effective' not set"),
76        )?;
77        validate_expression(
78            self.journal_id
79                .as_ref()
80                .expect("Mandatory field 'journal_id' not set"),
81        )?;
82        validate_optional_expression(&self.correlation_id)?;
83        validate_optional_expression(&self.external_id)?;
84        validate_optional_expression(&self.description)?;
85        validate_optional_expression(&self.metadata)
86    }
87}
88
89/// Contains the details for each accounting entry in a `Transaction`.
90#[derive(Clone, Serialize, Builder)]
91#[builder(build_fn(validate = "Self::validate"))]
92pub struct EntryInput {
93    #[builder(setter(into))]
94    entry_type: String,
95    #[builder(setter(into))]
96    account_id: String,
97    #[builder(setter(into))]
98    layer: String,
99    #[builder(setter(into))]
100    direction: String,
101    #[builder(setter(into))]
102    units: String,
103    #[builder(setter(into))]
104    currency: String,
105    #[builder(setter(strip_option), default)]
106    description: Option<String>,
107}
108
109impl EntryInput {
110    pub fn builder() -> EntryInputBuilder {
111        EntryInputBuilder::default()
112    }
113}
114impl EntryInputBuilder {
115    fn validate(&self) -> Result<(), String> {
116        validate_expression(
117            self.entry_type
118                .as_ref()
119                .expect("Mandatory field 'entry_type' not set"),
120        )?;
121        validate_expression(
122            self.account_id
123                .as_ref()
124                .expect("Mandatory field 'account_id' not set"),
125        )?;
126        validate_expression(
127            self.layer
128                .as_ref()
129                .expect("Mandatory field 'layer' not set"),
130        )?;
131        validate_expression(
132            self.direction
133                .as_ref()
134                .expect("Mandatory field 'direction' not set"),
135        )?;
136        validate_expression(
137            self.units
138                .as_ref()
139                .expect("Mandatory field 'units' not set"),
140        )?;
141        validate_expression(
142            self.currency
143                .as_ref()
144                .expect("Mandatory field 'currency' not set"),
145        )?;
146        validate_optional_expression(&self.description)
147    }
148}
149
150fn validate_expression(expr: &str) -> Result<(), String> {
151    CelExpression::try_from(expr).map_err(|e| e.to_string())?;
152    Ok(())
153}
154fn validate_optional_expression(expr: &Option<Option<String>>) -> Result<(), String> {
155    if let Some(Some(expr)) = expr.as_ref() {
156        CelExpression::try_from(expr.as_str()).map_err(|e| e.to_string())?;
157    }
158    Ok(())
159}
160
161#[cfg(test)]
162mod tests {
163    use super::*;
164    use uuid::Uuid;
165
166    #[test]
167    fn it_builds() {
168        let journal_id = Uuid::new_v4();
169        let entries = vec![EntryInput::builder()
170            .entry_type("'TEST_DR'")
171            .account_id("param.recipient")
172            .layer("'Settled'")
173            .direction("'Settled'")
174            .units("1290")
175            .currency("'BTC'")
176            .build()
177            .unwrap()];
178        let new_journal = NewTxTemplate::builder()
179            .id(Uuid::new_v4())
180            .code("CODE")
181            .tx_input(
182                TxInput::builder()
183                    .effective("date('2022-11-01')")
184                    .journal_id(format!("'{journal_id}'"))
185                    .build()
186                    .unwrap(),
187            )
188            .entries(entries)
189            .build()
190            .unwrap();
191        assert_eq!(new_journal.description, None);
192    }
193
194    #[test]
195    fn fails_when_mandatory_fields_are_missing() {
196        let new_account = NewTxTemplate::builder().build();
197        assert!(new_account.is_err());
198    }
199}