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#[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}