Skip to main content

cala_ledger/journal/
entity.rs

1use derive_builder::Builder;
2use es_entity::*;
3use serde::{Deserialize, Serialize};
4
5use crate::primitives::*;
6pub use cala_types::{journal::*, primitives::JournalId};
7
8#[derive(EsEvent, Debug, Serialize, Deserialize)]
9#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
10#[serde(tag = "type", rename_all = "snake_case")]
11#[es_event(id = "JournalId", event_context = false)]
12pub enum JournalEvent {
13    Initialized {
14        values: JournalValues,
15    },
16    Updated {
17        values: JournalValues,
18        fields: Vec<String>,
19    },
20}
21
22#[derive(EsEntity, Builder)]
23#[builder(pattern = "owned", build_fn(error = "EntityHydrationError"))]
24pub struct Journal {
25    pub id: JournalId,
26    values: JournalValues,
27    events: EntityEvents<JournalEvent>,
28}
29
30impl Journal {
31    pub fn id(&self) -> JournalId {
32        self.values.id
33    }
34
35    pub fn values(&self) -> &JournalValues {
36        &self.values
37    }
38
39    pub fn is_locked(&self) -> bool {
40        matches!(self.values.status, Status::Locked)
41    }
42
43    pub(crate) fn insert_effective_balances(&self) -> bool {
44        self.values.config.enable_effective_balances
45    }
46
47    pub fn update(&mut self, builder: impl Into<JournalUpdate>) -> es_entity::Idempotent<()> {
48        let JournalUpdateValues {
49            name,
50            status,
51            description,
52        } = builder
53            .into()
54            .build()
55            .expect("JournalUpdateValues always exist");
56        let mut updated_fields = Vec::new();
57
58        if let Some(name) = name {
59            if name != self.values().name {
60                self.values.name.clone_from(&name);
61                updated_fields.push("name".to_string());
62            }
63        }
64        if let Some(status) = status {
65            if status != self.values().status {
66                self.values.status.clone_from(&status);
67                updated_fields.push("status".to_string());
68            }
69        }
70        if description.is_some() && description != self.values().description {
71            self.values.description.clone_from(&description);
72            updated_fields.push("description".to_string());
73        }
74
75        if updated_fields.is_empty() {
76            return es_entity::Idempotent::AlreadyApplied;
77        } else {
78            self.events.push(JournalEvent::Updated {
79                values: self.values.clone(),
80                fields: updated_fields,
81            });
82        }
83
84        es_entity::Idempotent::Executed(())
85    }
86
87    pub fn into_values(self) -> JournalValues {
88        self.values
89    }
90
91    pub fn created_at(&self) -> chrono::DateTime<chrono::Utc> {
92        self.events
93            .entity_first_persisted_at()
94            .expect("Entity not persisted")
95    }
96
97    pub fn modified_at(&self) -> chrono::DateTime<chrono::Utc> {
98        self.events
99            .entity_last_modified_at()
100            .expect("Entity not persisted")
101    }
102}
103
104#[derive(Builder, Debug, Default)]
105#[builder(name = "JournalUpdate", default)]
106pub struct JournalUpdateValues {
107    #[builder(setter(into, strip_option))]
108    pub name: Option<String>,
109    #[builder(setter(into, strip_option))]
110    pub status: Option<Status>,
111    #[builder(setter(into, strip_option))]
112    pub description: Option<String>,
113}
114
115impl From<(JournalValues, Vec<String>)> for JournalUpdate {
116    fn from((values, fields): (JournalValues, Vec<String>)) -> Self {
117        let mut builder = JournalUpdate::default();
118
119        for field in fields {
120            match field.as_str() {
121                "name" => {
122                    builder.name(values.name.clone());
123                }
124                "status" => {
125                    builder.status(values.status);
126                }
127                "description" => {
128                    if let Some(ref desc) = values.description {
129                        builder.description(desc);
130                    }
131                }
132                _ => unreachable!("Unknown field: {}", field),
133            }
134        }
135        builder
136    }
137}
138
139impl TryFromEvents<JournalEvent> for Journal {
140    fn try_from_events(events: EntityEvents<JournalEvent>) -> Result<Self, EntityHydrationError> {
141        let mut builder = JournalBuilder::default();
142        for event in events.iter_all() {
143            match event {
144                JournalEvent::Initialized { values } => {
145                    builder = builder.id(values.id).values(values.clone());
146                }
147                JournalEvent::Updated { values, .. } => {
148                    builder = builder.values(values.clone());
149                }
150            }
151        }
152        builder.events(events).build()
153    }
154}
155
156/// Representation of a new ledger journal entity
157/// with required/optional properties and a builder.
158#[derive(Debug, Builder)]
159pub struct NewJournal {
160    #[builder(setter(into))]
161    pub id: JournalId,
162    #[builder(setter(into))]
163    pub(super) name: String,
164    #[builder(setter(strip_option, into), default)]
165    pub(super) code: Option<String>,
166    #[builder(setter(into), default)]
167    status: Status,
168    #[builder(setter(strip_option, into), default)]
169    description: Option<String>,
170    #[builder(default)]
171    enable_effective_balance: bool,
172}
173
174impl NewJournal {
175    pub fn builder() -> NewJournalBuilder {
176        NewJournalBuilder::default()
177    }
178}
179
180impl IntoEvents<JournalEvent> for NewJournal {
181    fn into_events(self) -> EntityEvents<JournalEvent> {
182        EntityEvents::init(
183            self.id,
184            [JournalEvent::Initialized {
185                values: JournalValues {
186                    id: self.id,
187                    version: 1,
188                    name: self.name,
189                    code: self.code,
190                    status: self.status,
191                    description: self.description,
192                    config: JournalConfig {
193                        enable_effective_balances: self.enable_effective_balance,
194                    },
195                },
196            }],
197        )
198    }
199}
200
201#[cfg(test)]
202mod tests {
203    use super::*;
204
205    #[test]
206    fn it_builds() {
207        let new_journal = NewJournal::builder()
208            .id(JournalId::new())
209            .name("name")
210            .build()
211            .unwrap();
212        assert_eq!(new_journal.name, "name");
213        assert_eq!(new_journal.status, Status::Active);
214        assert_eq!(new_journal.description, None);
215    }
216
217    #[test]
218    fn fails_when_mandatory_fields_are_missing() {
219        let new_account = NewJournal::builder().build();
220        assert!(new_account.is_err());
221    }
222}