cala_ledger/account/
entity.rs

1use derive_builder::Builder;
2use es_entity::*;
3use serde::{Deserialize, Serialize};
4
5pub use cala_types::{account::*, primitives::AccountId, velocity::VelocityContextAccountValues};
6
7use crate::primitives::*;
8
9#[derive(EsEvent, Debug, Serialize, Deserialize)]
10#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
11#[serde(tag = "type", rename_all = "snake_case")]
12#[es_event(id = "AccountId", event_context = false)]
13pub enum AccountEvent {
14    #[cfg(feature = "import")]
15    Imported {
16        source: DataSource,
17        values: AccountValues,
18    },
19    Initialized {
20        values: AccountValues,
21    },
22    Updated {
23        values: AccountValues,
24        fields: Vec<String>,
25    },
26}
27
28#[derive(EsEntity, Builder)]
29#[builder(pattern = "owned", build_fn(error = "EsEntityError"))]
30pub struct Account {
31    pub id: AccountId,
32    values: AccountValues,
33    events: EntityEvents<AccountEvent>,
34}
35
36impl Account {
37    #[cfg(feature = "import")]
38    pub(super) fn import(source: DataSourceId, values: AccountValues) -> Self {
39        let events = EntityEvents::init(
40            values.id,
41            [AccountEvent::Imported {
42                source: DataSource::Remote { id: source },
43                values,
44            }],
45        );
46        Self::try_from_events(events).expect("Failed to build account from events")
47    }
48
49    pub fn id(&self) -> AccountId {
50        self.values.id
51    }
52
53    pub(super) fn is_account_set(&self) -> bool {
54        self.values.config.is_account_set
55    }
56
57    pub fn values(&self) -> &AccountValues {
58        &self.values
59    }
60
61    pub fn into_values(self) -> AccountValues {
62        self.values
63    }
64
65    pub(super) fn context_values(&self) -> VelocityContextAccountValues {
66        VelocityContextAccountValues::from(self.values())
67    }
68
69    pub fn update_status(&mut self, status: Status) -> es_entity::Idempotent<()> {
70        let mut update = AccountUpdate::default();
71        update.status(status);
72        self.update(update)
73    }
74
75    pub fn update(&mut self, builder: impl Into<AccountUpdate>) -> es_entity::Idempotent<()> {
76        let AccountUpdateValues {
77            external_id,
78            code,
79            name,
80            normal_balance_type,
81            description,
82            status,
83            metadata,
84        } = builder
85            .into()
86            .build()
87            .expect("AccountUpdateValues always exist");
88
89        let mut updated_fields = Vec::new();
90
91        if let Some(code) = code {
92            if code != self.values().code {
93                self.values.code.clone_from(&code);
94                updated_fields.push("code".to_string());
95            }
96        }
97        if let Some(name) = name {
98            if name != self.values().name {
99                self.values.name.clone_from(&name);
100                updated_fields.push("name".to_string());
101            }
102        }
103        if let Some(normal_balance_type) = normal_balance_type {
104            if normal_balance_type != self.values().normal_balance_type {
105                self.values
106                    .normal_balance_type
107                    .clone_from(&normal_balance_type);
108                updated_fields.push("normal_balance_type".to_string());
109            }
110        }
111        if let Some(status) = status {
112            if status != self.values().status {
113                self.values.status.clone_from(&status);
114                updated_fields.push("status".to_string());
115            }
116        }
117        if external_id.is_some() && external_id != self.values().external_id {
118            self.values.external_id.clone_from(&external_id);
119            updated_fields.push("external_id".to_string());
120        }
121        if description.is_some() && description != self.values().description {
122            self.values.description.clone_from(&description);
123            updated_fields.push("description".to_string());
124        }
125        if let Some(metadata) = metadata {
126            if metadata != serde_json::Value::Null
127                && Some(&metadata) != self.values().metadata.as_ref()
128            {
129                self.values.metadata = Some(metadata);
130                updated_fields.push("metadata".to_string());
131            }
132        }
133
134        if updated_fields.is_empty() {
135            return es_entity::Idempotent::AlreadyApplied;
136        }
137
138        self.events.push(AccountEvent::Updated {
139            values: self.values.clone(),
140            fields: updated_fields,
141        });
142
143        es_entity::Idempotent::Executed(())
144    }
145
146    pub fn created_at(&self) -> chrono::DateTime<chrono::Utc> {
147        self.events
148            .entity_first_persisted_at()
149            .expect("Entity not persisted")
150    }
151
152    pub fn modified_at(&self) -> chrono::DateTime<chrono::Utc> {
153        self.events
154            .entity_last_modified_at()
155            .expect("Entity not persisted")
156    }
157
158    pub fn metadata<T: serde::de::DeserializeOwned>(&self) -> Result<Option<T>, serde_json::Error> {
159        match &self.values.metadata {
160            Some(metadata) => Ok(Some(serde_json::from_value(metadata.clone())?)),
161            None => Ok(None),
162        }
163    }
164}
165
166impl TryFromEvents<AccountEvent> for Account {
167    fn try_from_events(events: EntityEvents<AccountEvent>) -> Result<Self, EsEntityError> {
168        let mut builder = AccountBuilder::default();
169        for event in events.iter_all() {
170            match event {
171                #[cfg(feature = "import")]
172                AccountEvent::Imported { source: _, values } => {
173                    builder = builder.id(values.id).values(values.clone());
174                }
175                AccountEvent::Initialized { values } => {
176                    builder = builder.id(values.id).values(values.clone());
177                }
178                AccountEvent::Updated { values, .. } => {
179                    builder = builder.values(values.clone());
180                }
181            }
182        }
183        builder.events(events).build()
184    }
185}
186
187#[derive(Debug, Builder, Default)]
188#[builder(name = "AccountUpdate", default)]
189pub struct AccountUpdateValues {
190    #[builder(setter(strip_option, into))]
191    pub external_id: Option<String>,
192    #[builder(setter(strip_option, into))]
193    pub code: Option<String>,
194    #[builder(setter(strip_option, into))]
195    pub name: Option<String>,
196    #[builder(setter(strip_option, into))]
197    pub normal_balance_type: Option<DebitOrCredit>,
198    #[builder(setter(strip_option, into))]
199    pub description: Option<String>,
200    #[builder(setter(strip_option, into))]
201    pub status: Option<Status>,
202    #[builder(setter(custom))]
203    pub metadata: Option<serde_json::Value>,
204}
205
206impl AccountUpdate {
207    pub fn metadata<T: serde::Serialize>(
208        &mut self,
209        metadata: T,
210    ) -> Result<&mut Self, serde_json::Error> {
211        self.metadata = Some(Some(serde_json::to_value(metadata)?));
212        Ok(self)
213    }
214}
215
216#[cfg(feature = "import")]
217impl From<(AccountValues, Vec<String>)> for AccountUpdate {
218    fn from((values, fields): (AccountValues, Vec<String>)) -> Self {
219        let mut builder = AccountUpdate::default();
220        for field in fields {
221            match field.as_str() {
222                "external_id" => {
223                    if let Some(ref ext_id) = values.external_id {
224                        builder.external_id(ext_id);
225                    }
226                }
227                "code" => {
228                    builder.code(values.code.clone());
229                }
230                "name" => {
231                    builder.name(values.name.clone());
232                }
233                "normal_balance_type" => {
234                    builder.normal_balance_type(values.normal_balance_type);
235                }
236                "description" => {
237                    if let Some(ref desc) = values.description {
238                        builder.description(desc);
239                    }
240                }
241                "status" => {
242                    builder.status(values.status);
243                }
244                "metadata" => {
245                    if let Some(metadata) = values.metadata.clone() {
246                        builder
247                            .metadata(metadata)
248                            .expect("Failed to serialize metadata");
249                    }
250                }
251                _ => unreachable!("Unknown field: {}", field),
252            }
253        }
254        builder
255    }
256}
257
258/// Representation of a ***new*** ledger account entity with required/optional properties and a builder.
259#[derive(Builder, Debug, Clone)]
260pub struct NewAccount {
261    #[builder(setter(into))]
262    pub id: AccountId,
263    #[builder(setter(into))]
264    pub(super) code: String,
265    #[builder(setter(into))]
266    pub(super) name: String,
267    #[builder(setter(strip_option, into), default)]
268    pub(super) external_id: Option<String>,
269    #[builder(default)]
270    pub(super) normal_balance_type: DebitOrCredit,
271    #[builder(default)]
272    pub(super) status: Status,
273    #[builder(setter(custom), default)]
274    pub(super) eventually_consistent: bool,
275    #[builder(setter(custom), default)]
276    pub(super) is_account_set: bool,
277    #[builder(setter(custom), default)]
278    velocity_context_values: Option<VelocityContextAccountValues>,
279    #[builder(setter(strip_option, into), default)]
280    description: Option<String>,
281    #[builder(setter(custom), default)]
282    metadata: Option<serde_json::Value>,
283}
284
285impl NewAccount {
286    pub fn builder() -> NewAccountBuilder {
287        NewAccountBuilder::default()
288    }
289
290    pub(super) fn data_source(&self) -> DataSource {
291        DataSource::Local
292    }
293
294    pub(super) fn context_values(&self) -> VelocityContextAccountValues {
295        self.velocity_context_values
296            .clone()
297            .unwrap_or_else(|| VelocityContextAccountValues::from(self.clone().into_values()))
298    }
299
300    pub(super) fn into_values(self) -> AccountValues {
301        AccountValues {
302            id: self.id,
303            version: 1,
304            code: self.code,
305            name: self.name,
306            external_id: self.external_id,
307            normal_balance_type: self.normal_balance_type,
308            status: self.status,
309            description: self.description,
310            metadata: self.metadata,
311            config: AccountConfig {
312                is_account_set: self.is_account_set,
313                eventually_consistent: false,
314            },
315        }
316    }
317}
318
319impl IntoEvents<AccountEvent> for NewAccount {
320    fn into_events(self) -> EntityEvents<AccountEvent> {
321        let values = self.into_values();
322        EntityEvents::init(values.id, [AccountEvent::Initialized { values }])
323    }
324}
325
326impl NewAccountBuilder {
327    pub fn metadata<T: serde::Serialize>(
328        &mut self,
329        metadata: T,
330    ) -> Result<&mut Self, serde_json::Error> {
331        self.metadata = Some(Some(serde_json::to_value(metadata)?));
332        Ok(self)
333    }
334
335    pub(crate) fn velocity_context_values(
336        &mut self,
337        values: impl Into<VelocityContextAccountValues>,
338    ) -> &mut Self {
339        self.velocity_context_values = Some(Some(values.into()));
340        self
341    }
342
343    pub(crate) fn is_account_set(&mut self, is_account_set: bool) -> &mut Self {
344        self.is_account_set = Some(is_account_set);
345        self.eventually_consistent(is_account_set)
346    }
347
348    // dummy fn since its not fully supported yet
349    fn eventually_consistent(&mut self, eventually_consistent: bool) -> &mut Self {
350        self.is_account_set = Some(eventually_consistent);
351        self
352    }
353}
354
355#[cfg(test)]
356mod tests {
357    use super::*;
358
359    #[test]
360    fn it_builds() {
361        let new_account = NewAccount::builder()
362            .id(uuid::Uuid::now_v7())
363            .code("code")
364            .name("name")
365            .build()
366            .unwrap();
367        assert_eq!(new_account.code, "code");
368        assert_eq!(new_account.name, "name");
369        assert_eq!(new_account.normal_balance_type, DebitOrCredit::Credit);
370        assert_eq!(new_account.description, None);
371        assert_eq!(new_account.status, Status::Active);
372        assert_eq!(new_account.metadata, None);
373    }
374
375    #[test]
376    fn fails_when_mandatory_fields_are_missing() {
377        let new_account = NewAccount::builder().build();
378        assert!(new_account.is_err());
379    }
380
381    #[test]
382    fn accepts_metadata() {
383        use serde_json::json;
384        let new_account = NewAccount::builder()
385            .id(uuid::Uuid::now_v7())
386            .code("code")
387            .name("name")
388            .metadata(json!({"foo": "bar"}))
389            .unwrap()
390            .build()
391            .unwrap();
392        assert_eq!(new_account.metadata, Some(json!({"foo": "bar"})));
393    }
394}