Skip to main content

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    Initialized {
15        values: AccountValues,
16    },
17    Updated {
18        values: AccountValues,
19        fields: Vec<String>,
20    },
21}
22
23#[derive(EsEntity, Builder)]
24#[builder(pattern = "owned", build_fn(error = "EntityHydrationError"))]
25pub struct Account {
26    pub id: AccountId,
27    values: AccountValues,
28    events: EntityEvents<AccountEvent>,
29}
30
31impl Account {
32    pub fn id(&self) -> AccountId {
33        self.values.id
34    }
35
36    pub(super) fn is_account_set(&self) -> bool {
37        self.values.config.is_account_set
38    }
39
40    pub fn values(&self) -> &AccountValues {
41        &self.values
42    }
43
44    pub fn into_values(self) -> AccountValues {
45        self.values
46    }
47
48    pub(super) fn context_values(&self) -> VelocityContextAccountValues {
49        VelocityContextAccountValues::from(self.values())
50    }
51
52    pub fn update_status(&mut self, status: Status) -> es_entity::Idempotent<()> {
53        let mut update = AccountUpdate::default();
54        update.status(status);
55        self.update(update)
56    }
57
58    pub fn update(&mut self, builder: impl Into<AccountUpdate>) -> es_entity::Idempotent<()> {
59        let AccountUpdateValues {
60            external_id,
61            code,
62            name,
63            normal_balance_type,
64            description,
65            status,
66            metadata,
67        } = builder
68            .into()
69            .build()
70            .expect("AccountUpdateValues always exist");
71
72        let mut updated_fields = Vec::new();
73
74        if let Some(code) = code {
75            if code != self.values().code {
76                self.values.code.clone_from(&code);
77                updated_fields.push("code".to_string());
78            }
79        }
80        if let Some(name) = name {
81            if name != self.values().name {
82                self.values.name.clone_from(&name);
83                updated_fields.push("name".to_string());
84            }
85        }
86        if let Some(normal_balance_type) = normal_balance_type {
87            if normal_balance_type != self.values().normal_balance_type {
88                self.values
89                    .normal_balance_type
90                    .clone_from(&normal_balance_type);
91                updated_fields.push("normal_balance_type".to_string());
92            }
93        }
94        if let Some(status) = status {
95            if status != self.values().status {
96                self.values.status.clone_from(&status);
97                updated_fields.push("status".to_string());
98            }
99        }
100        if external_id.is_some() && external_id != self.values().external_id {
101            self.values.external_id.clone_from(&external_id);
102            updated_fields.push("external_id".to_string());
103        }
104        if description.is_some() && description != self.values().description {
105            self.values.description.clone_from(&description);
106            updated_fields.push("description".to_string());
107        }
108        if let Some(metadata) = metadata {
109            if metadata != serde_json::Value::Null
110                && Some(&metadata) != self.values().metadata.as_ref()
111            {
112                self.values.metadata = Some(metadata);
113                updated_fields.push("metadata".to_string());
114            }
115        }
116
117        if updated_fields.is_empty() {
118            return es_entity::Idempotent::AlreadyApplied;
119        }
120
121        self.events.push(AccountEvent::Updated {
122            values: self.values.clone(),
123            fields: updated_fields,
124        });
125
126        es_entity::Idempotent::Executed(())
127    }
128
129    pub fn created_at(&self) -> chrono::DateTime<chrono::Utc> {
130        self.events
131            .entity_first_persisted_at()
132            .expect("Entity not persisted")
133    }
134
135    pub fn modified_at(&self) -> chrono::DateTime<chrono::Utc> {
136        self.events
137            .entity_last_modified_at()
138            .expect("Entity not persisted")
139    }
140
141    pub fn metadata<T: serde::de::DeserializeOwned>(&self) -> Result<Option<T>, serde_json::Error> {
142        match &self.values.metadata {
143            Some(metadata) => Ok(Some(serde_json::from_value(metadata.clone())?)),
144            None => Ok(None),
145        }
146    }
147}
148
149impl TryFromEvents<AccountEvent> for Account {
150    fn try_from_events(events: EntityEvents<AccountEvent>) -> Result<Self, EntityHydrationError> {
151        let mut builder = AccountBuilder::default();
152        for event in events.iter_all() {
153            match event {
154                AccountEvent::Initialized { values } => {
155                    builder = builder.id(values.id).values(values.clone());
156                }
157                AccountEvent::Updated { values, .. } => {
158                    builder = builder.values(values.clone());
159                }
160            }
161        }
162        builder.events(events).build()
163    }
164}
165
166#[derive(Debug, Builder, Default)]
167#[builder(name = "AccountUpdate", default)]
168pub struct AccountUpdateValues {
169    #[builder(setter(strip_option, into))]
170    pub external_id: Option<String>,
171    #[builder(setter(strip_option, into))]
172    pub code: Option<String>,
173    #[builder(setter(strip_option, into))]
174    pub name: Option<String>,
175    #[builder(setter(strip_option, into))]
176    pub normal_balance_type: Option<DebitOrCredit>,
177    #[builder(setter(strip_option, into))]
178    pub description: Option<String>,
179    #[builder(setter(strip_option, into))]
180    pub status: Option<Status>,
181    #[builder(setter(custom))]
182    pub metadata: Option<serde_json::Value>,
183}
184
185impl AccountUpdate {
186    pub fn metadata<T: serde::Serialize>(
187        &mut self,
188        metadata: T,
189    ) -> Result<&mut Self, serde_json::Error> {
190        self.metadata = Some(Some(serde_json::to_value(metadata)?));
191        Ok(self)
192    }
193}
194
195/// Representation of a ***new*** ledger account entity with required/optional properties and a builder.
196#[derive(Builder, Debug, Clone)]
197pub struct NewAccount {
198    #[builder(setter(into))]
199    pub id: AccountId,
200    #[builder(setter(into))]
201    pub(super) code: String,
202    #[builder(setter(into))]
203    pub(super) name: String,
204    #[builder(setter(strip_option, into), default)]
205    pub(super) external_id: Option<String>,
206    #[builder(default)]
207    pub(super) normal_balance_type: DebitOrCredit,
208    #[builder(default)]
209    pub(super) status: Status,
210    #[builder(setter(custom), default)]
211    pub(super) eventually_consistent: bool,
212    #[builder(setter(custom), default)]
213    pub(super) is_account_set: bool,
214    #[builder(setter(custom), default)]
215    velocity_context_values: Option<VelocityContextAccountValues>,
216    #[builder(setter(strip_option, into), default)]
217    description: Option<String>,
218    #[builder(setter(custom), default)]
219    metadata: Option<serde_json::Value>,
220}
221
222impl NewAccount {
223    pub fn builder() -> NewAccountBuilder {
224        NewAccountBuilder::default()
225    }
226
227    pub(super) fn context_values(&self) -> VelocityContextAccountValues {
228        self.velocity_context_values
229            .clone()
230            .unwrap_or_else(|| VelocityContextAccountValues::from(self.clone().into_values()))
231    }
232
233    pub(super) fn into_values(self) -> AccountValues {
234        AccountValues {
235            id: self.id,
236            version: 1,
237            code: self.code,
238            name: self.name,
239            external_id: self.external_id,
240            normal_balance_type: self.normal_balance_type,
241            status: self.status,
242            description: self.description,
243            metadata: self.metadata,
244            config: AccountConfig {
245                is_account_set: self.is_account_set,
246                eventually_consistent: self.eventually_consistent,
247            },
248        }
249    }
250}
251
252impl IntoEvents<AccountEvent> for NewAccount {
253    fn into_events(self) -> EntityEvents<AccountEvent> {
254        let values = self.into_values();
255        EntityEvents::init(values.id, [AccountEvent::Initialized { values }])
256    }
257}
258
259impl NewAccountBuilder {
260    pub fn metadata<T: serde::Serialize>(
261        &mut self,
262        metadata: T,
263    ) -> Result<&mut Self, serde_json::Error> {
264        self.metadata = Some(Some(serde_json::to_value(metadata)?));
265        Ok(self)
266    }
267
268    pub(crate) fn velocity_context_values(
269        &mut self,
270        values: impl Into<VelocityContextAccountValues>,
271    ) -> &mut Self {
272        self.velocity_context_values = Some(Some(values.into()));
273        self
274    }
275
276    pub(crate) fn is_account_set(&mut self, is_account_set: bool) -> &mut Self {
277        self.is_account_set = Some(is_account_set);
278        self
279    }
280
281    pub(crate) fn eventually_consistent(&mut self, eventually_consistent: bool) -> &mut Self {
282        self.eventually_consistent = Some(eventually_consistent);
283        self
284    }
285}
286
287#[cfg(test)]
288mod tests {
289    use super::*;
290
291    #[test]
292    fn it_builds() {
293        let new_account = NewAccount::builder()
294            .id(uuid::Uuid::now_v7())
295            .code("code")
296            .name("name")
297            .build()
298            .unwrap();
299        assert_eq!(new_account.code, "code");
300        assert_eq!(new_account.name, "name");
301        assert_eq!(new_account.normal_balance_type, DebitOrCredit::Credit);
302        assert_eq!(new_account.description, None);
303        assert_eq!(new_account.status, Status::Active);
304        assert_eq!(new_account.metadata, None);
305    }
306
307    #[test]
308    fn fails_when_mandatory_fields_are_missing() {
309        let new_account = NewAccount::builder().build();
310        assert!(new_account.is_err());
311    }
312
313    #[test]
314    fn accepts_metadata() {
315        use serde_json::json;
316        let new_account = NewAccount::builder()
317            .id(uuid::Uuid::now_v7())
318            .code("code")
319            .name("name")
320            .metadata(json!({"foo": "bar"}))
321            .unwrap()
322            .build()
323            .unwrap();
324        assert_eq!(new_account.metadata, Some(json!({"foo": "bar"})));
325    }
326}