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