cala_ledger/journal/
entity.rs1use 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#[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}