cala_server/graphql/
account_set.rs

1use async_graphql::{dataloader::*, types::connection::*, *};
2
3use cala_ledger::{
4    account_set::AccountSetMemberId,
5    balance::*,
6    primitives::{AccountId, AccountSetId, Currency, JournalId},
7};
8
9pub use cala_ledger::account_set::{AccountSetMembersByCreatedAtCursor, AccountSetsByNameCursor};
10
11use super::{
12    balance::*, convert::ToGlobalId, loader::LedgerDataLoader, primitives::*, schema::DbOp,
13};
14use crate::app::CalaApp;
15
16#[derive(Union)]
17enum AccountSetMember {
18    Account(super::account::Account),
19    AccountSet(AccountSet),
20}
21
22#[derive(Clone, SimpleObject)]
23#[graphql(complex)]
24pub struct AccountSet {
25    id: ID,
26    account_set_id: UUID,
27    version: u32,
28    journal_id: UUID,
29    name: String,
30    normal_balance_type: DebitOrCredit,
31    description: Option<String>,
32    metadata: Option<JSON>,
33    created_at: Timestamp,
34    modified_at: Timestamp,
35}
36
37#[ComplexObject]
38impl AccountSet {
39    async fn balance(
40        &self,
41        ctx: &Context<'_>,
42        currency: CurrencyCode,
43    ) -> async_graphql::Result<Option<Balance>> {
44        let journal_id = JournalId::from(self.journal_id);
45        let account_id = AccountId::from(self.account_set_id);
46        let currency = Currency::from(currency);
47
48        let balance: Option<AccountBalance> = match ctx.data_opt::<DbOp>() {
49            Some(op) => {
50                let app = ctx.data_unchecked::<CalaApp>();
51                let mut op = op.try_lock().expect("Lock held concurrently");
52                Some(
53                    app.ledger()
54                        .balances()
55                        .find_in_op(&mut op, journal_id, account_id, currency)
56                        .await?,
57                )
58            }
59            None => {
60                let loader = ctx.data_unchecked::<DataLoader<LedgerDataLoader>>();
61                loader.load_one((journal_id, account_id, currency)).await?
62            }
63        };
64        Ok(balance.map(Balance::from))
65    }
66
67    async fn balance_in_range(
68        &self,
69        ctx: &Context<'_>,
70        journal_id: UUID,
71        currency: CurrencyCode,
72        from: Timestamp,
73        until: Option<Timestamp>,
74    ) -> async_graphql::Result<Option<RangedBalance>> {
75        let app = ctx.data_unchecked::<CalaApp>();
76        match app
77            .ledger()
78            .balances()
79            .find_in_range(
80                JournalId::from(journal_id),
81                AccountId::from(self.account_set_id),
82                Currency::from(currency),
83                from.into_inner(),
84                until.map(|ts| ts.into_inner()),
85            )
86            .await
87        {
88            Ok(balance) => Ok(Some(balance.into())),
89            Err(cala_ledger::balance::error::BalanceError::NotFound(_, _, _)) => Ok(None),
90            Err(err) => Err(err.into()),
91        }
92    }
93
94    async fn members(
95        &self,
96        ctx: &Context<'_>,
97        first: i32,
98        after: Option<String>,
99    ) -> Result<
100        Connection<AccountSetMembersByCreatedAtCursor, AccountSetMember, EmptyFields, EmptyFields>,
101    > {
102        let app = ctx.data_unchecked::<CalaApp>();
103        let account_set_id = AccountSetId::from(self.account_set_id);
104
105        query(
106            after.clone(),
107            None,
108            Some(first),
109            None,
110            |after, _, first, _| async move {
111                let first = first.expect("First always exists");
112                let query_args = cala_ledger::es_entity::PaginatedQueryArgs { first, after };
113
114                let (members, mut accounts, mut sets) = match ctx.data_opt::<DbOp>() {
115                    Some(op) => {
116                        let mut op = op.try_lock().expect("Lock held concurrently");
117                        let account_sets = app.ledger().account_sets();
118                        let accounts = app.ledger().accounts();
119                        let members = account_sets
120                            .list_members_created_at_in_op(&mut op, account_set_id, query_args)
121                            .await?;
122                        let mut account_ids = Vec::new();
123                        let mut set_ids = Vec::new();
124                        for member in members.entities.iter() {
125                            match member.id {
126                                AccountSetMemberId::Account(id) => account_ids.push(id),
127                                AccountSetMemberId::AccountSet(id) => set_ids.push(id),
128                            }
129                        }
130                        (
131                            members,
132                            accounts.find_all_in_op(&mut op, &account_ids).await?,
133                            account_sets.find_all_in_op(&mut op, &set_ids).await?,
134                        )
135                    }
136                    None => {
137                        let members = app
138                            .ledger()
139                            .account_sets()
140                            .list_members_by_created_at(account_set_id, query_args)
141                            .await?;
142                        let mut account_ids = Vec::new();
143                        let mut set_ids = Vec::new();
144                        for member in members.entities.iter() {
145                            match member.id {
146                                AccountSetMemberId::Account(id) => account_ids.push(id),
147                                AccountSetMemberId::AccountSet(id) => set_ids.push(id),
148                            }
149                        }
150                        let loader = ctx.data_unchecked::<DataLoader<LedgerDataLoader>>();
151                        (
152                            members,
153                            loader.load_many(account_ids).await?,
154                            loader.load_many(set_ids).await?,
155                        )
156                    }
157                };
158                let mut connection = Connection::new(false, members.has_next_page);
159                connection.edges.extend(members.entities.into_iter().map(
160                    |member| match member.id {
161                        AccountSetMemberId::Account(id) => {
162                            let entity = accounts.remove(&id).expect("Account exists");
163                            let cursor = AccountSetMembersByCreatedAtCursor::from(&member);
164                            Edge::new(cursor, AccountSetMember::Account(entity))
165                        }
166                        AccountSetMemberId::AccountSet(id) => {
167                            let entity = sets.remove(&id).expect("Account exists");
168                            let cursor = AccountSetMembersByCreatedAtCursor::from(&member);
169                            Edge::new(cursor, AccountSetMember::AccountSet(entity))
170                        }
171                    },
172                ));
173                Ok::<_, async_graphql::Error>(connection)
174            },
175        )
176        .await
177    }
178
179    async fn sets(
180        &self,
181        ctx: &Context<'_>,
182        first: i32,
183        after: Option<String>,
184    ) -> Result<Connection<AccountSetsByNameCursor, AccountSet, EmptyFields, EmptyFields>> {
185        let app = ctx.data_unchecked::<CalaApp>();
186        let account_set_id = AccountSetId::from(self.account_set_id);
187
188        query(
189            after.clone(),
190            None,
191            Some(first),
192            None,
193            |after, _, first, _| async move {
194                let first = first.expect("First always exists");
195                let query_args = cala_ledger::es_entity::PaginatedQueryArgs {
196                    first,
197                    after: after.map(cala_ledger::account_set::AccountSetsByNameCursor::from),
198                };
199
200                let result = match ctx.data_opt::<DbOp>() {
201                    Some(op) => {
202                        let mut op = op.try_lock().expect("Lock held concurrently");
203                        app.ledger()
204                            .account_sets()
205                            .find_where_member_in_op(&mut op, account_set_id, query_args)
206                            .await?
207                    }
208                    None => {
209                        app.ledger()
210                            .account_sets()
211                            .find_where_member(account_set_id, query_args)
212                            .await?
213                    }
214                };
215
216                let mut connection = Connection::new(false, result.has_next_page);
217                connection
218                    .edges
219                    .extend(result.entities.into_iter().map(|entity| {
220                        let cursor = AccountSetsByNameCursor::from(&entity);
221                        Edge::new(cursor, AccountSet::from(entity))
222                    }));
223
224                Ok::<_, async_graphql::Error>(connection)
225            },
226        )
227        .await
228    }
229}
230
231#[derive(InputObject)]
232pub(super) struct AccountSetCreateInput {
233    pub account_set_id: UUID,
234    pub journal_id: UUID,
235    pub name: String,
236    #[graphql(default)]
237    pub normal_balance_type: DebitOrCredit,
238    pub description: Option<String>,
239    pub metadata: Option<JSON>,
240}
241
242#[derive(SimpleObject)]
243pub(super) struct AccountSetCreatePayload {
244    pub account_set: AccountSet,
245}
246
247#[derive(Enum, Copy, Clone, Eq, PartialEq)]
248pub enum AccountSetMemberType {
249    Account,
250    AccountSet,
251}
252
253#[derive(InputObject)]
254pub(super) struct AddToAccountSetInput {
255    pub account_set_id: UUID,
256    pub member_id: UUID,
257    pub member_type: AccountSetMemberType,
258}
259
260impl From<AddToAccountSetInput> for AccountSetMemberId {
261    fn from(input: AddToAccountSetInput) -> Self {
262        match input.member_type {
263            AccountSetMemberType::Account => {
264                AccountSetMemberId::Account(AccountId::from(input.member_id))
265            }
266            AccountSetMemberType::AccountSet => {
267                AccountSetMemberId::AccountSet(AccountSetId::from(input.member_id))
268            }
269        }
270    }
271}
272
273#[derive(SimpleObject)]
274pub(super) struct AddToAccountSetPayload {
275    pub account_set: AccountSet,
276}
277
278#[derive(InputObject)]
279pub(super) struct RemoveFromAccountSetInput {
280    pub account_set_id: UUID,
281    pub member_id: UUID,
282    pub member_type: AccountSetMemberType,
283}
284
285impl From<RemoveFromAccountSetInput> for AccountSetMemberId {
286    fn from(input: RemoveFromAccountSetInput) -> Self {
287        match input.member_type {
288            AccountSetMemberType::Account => {
289                AccountSetMemberId::Account(AccountId::from(input.member_id))
290            }
291            AccountSetMemberType::AccountSet => {
292                AccountSetMemberId::AccountSet(AccountSetId::from(input.member_id))
293            }
294        }
295    }
296}
297
298#[derive(SimpleObject)]
299pub(super) struct RemoveFromAccountSetPayload {
300    pub account_set: AccountSet,
301}
302
303impl ToGlobalId for cala_ledger::AccountSetId {
304    fn to_global_id(&self) -> async_graphql::types::ID {
305        async_graphql::types::ID::from(format!("account_set:{}", self))
306    }
307}
308
309impl From<cala_ledger::account_set::AccountSet> for AccountSet {
310    fn from(account_set: cala_ledger::account_set::AccountSet) -> Self {
311        let created_at = account_set.created_at();
312        let modified_at = account_set.modified_at();
313        let values = account_set.into_values();
314        Self {
315            id: values.id.to_global_id(),
316            account_set_id: UUID::from(values.id),
317            version: values.version,
318            journal_id: UUID::from(values.journal_id),
319            name: values.name,
320            normal_balance_type: values.normal_balance_type,
321            description: values.description,
322            metadata: values.metadata.map(JSON::from),
323            created_at: created_at.into(),
324            modified_at: modified_at.into(),
325        }
326    }
327}
328
329impl From<cala_ledger::account_set::AccountSet> for AccountSetCreatePayload {
330    fn from(value: cala_ledger::account_set::AccountSet) -> Self {
331        Self {
332            account_set: AccountSet::from(value),
333        }
334    }
335}
336
337impl From<cala_ledger::account_set::AccountSet> for AddToAccountSetPayload {
338    fn from(value: cala_ledger::account_set::AccountSet) -> Self {
339        Self {
340            account_set: AccountSet::from(value),
341        }
342    }
343}
344
345impl From<cala_ledger::account_set::AccountSet> for RemoveFromAccountSetPayload {
346    fn from(value: cala_ledger::account_set::AccountSet) -> Self {
347        Self {
348            account_set: AccountSet::from(value),
349        }
350    }
351}
352
353#[derive(InputObject)]
354pub(super) struct AccountSetUpdateInput {
355    pub name: Option<String>,
356    pub normal_balance_type: Option<DebitOrCredit>,
357    pub description: Option<String>,
358    pub metadata: Option<JSON>,
359}
360
361#[derive(SimpleObject)]
362pub(super) struct AccountSetUpdatePayload {
363    pub account_set: AccountSet,
364}
365
366impl From<cala_ledger::account_set::AccountSet> for AccountSetUpdatePayload {
367    fn from(value: cala_ledger::account_set::AccountSet) -> Self {
368        Self {
369            account_set: AccountSet::from(value),
370        }
371    }
372}