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 members(
68        &self,
69        ctx: &Context<'_>,
70        first: i32,
71        after: Option<String>,
72    ) -> Result<
73        Connection<AccountSetMembersByCreatedAtCursor, AccountSetMember, EmptyFields, EmptyFields>,
74    > {
75        let app = ctx.data_unchecked::<CalaApp>();
76        let account_set_id = AccountSetId::from(self.account_set_id);
77
78        query(
79            after.clone(),
80            None,
81            Some(first),
82            None,
83            |after, _, first, _| async move {
84                let first = first.expect("First always exists");
85                let query_args = cala_ledger::es_entity::PaginatedQueryArgs { first, after };
86
87                let (members, mut accounts, mut sets) = match ctx.data_opt::<DbOp>() {
88                    Some(op) => {
89                        let mut op = op.try_lock().expect("Lock held concurrently");
90                        let account_sets = app.ledger().account_sets();
91                        let accounts = app.ledger().accounts();
92                        let members = account_sets
93                            .list_members_by_created_at_in_op(&mut op, account_set_id, query_args)
94                            .await?;
95                        let mut account_ids = Vec::new();
96                        let mut set_ids = Vec::new();
97                        for member in members.entities.iter() {
98                            match member.id {
99                                AccountSetMemberId::Account(id) => account_ids.push(id),
100                                AccountSetMemberId::AccountSet(id) => set_ids.push(id),
101                            }
102                        }
103                        (
104                            members,
105                            accounts.find_all_in_op(&mut op, &account_ids).await?,
106                            account_sets.find_all_in_op(&mut op, &set_ids).await?,
107                        )
108                    }
109                    None => {
110                        let members = app
111                            .ledger()
112                            .account_sets()
113                            .list_members_by_created_at(account_set_id, query_args)
114                            .await?;
115                        let mut account_ids = Vec::new();
116                        let mut set_ids = Vec::new();
117                        for member in members.entities.iter() {
118                            match member.id {
119                                AccountSetMemberId::Account(id) => account_ids.push(id),
120                                AccountSetMemberId::AccountSet(id) => set_ids.push(id),
121                            }
122                        }
123                        let loader = ctx.data_unchecked::<DataLoader<LedgerDataLoader>>();
124                        (
125                            members,
126                            loader.load_many(account_ids).await?,
127                            loader.load_many(set_ids).await?,
128                        )
129                    }
130                };
131                let mut connection = Connection::new(false, members.has_next_page);
132                connection.edges.extend(members.entities.into_iter().map(
133                    |member| match member.id {
134                        AccountSetMemberId::Account(id) => {
135                            let entity = accounts.remove(&id).expect("Account exists");
136                            let cursor = AccountSetMembersByCreatedAtCursor::from(&member);
137                            Edge::new(cursor, AccountSetMember::Account(entity))
138                        }
139                        AccountSetMemberId::AccountSet(id) => {
140                            let entity = sets.remove(&id).expect("Account exists");
141                            let cursor = AccountSetMembersByCreatedAtCursor::from(&member);
142                            Edge::new(cursor, AccountSetMember::AccountSet(entity))
143                        }
144                    },
145                ));
146                Ok::<_, async_graphql::Error>(connection)
147            },
148        )
149        .await
150    }
151
152    async fn sets(
153        &self,
154        ctx: &Context<'_>,
155        first: i32,
156        after: Option<String>,
157    ) -> Result<Connection<AccountSetsByNameCursor, AccountSet, EmptyFields, EmptyFields>> {
158        let app = ctx.data_unchecked::<CalaApp>();
159        let account_set_id = AccountSetId::from(self.account_set_id);
160
161        query(
162            after.clone(),
163            None,
164            Some(first),
165            None,
166            |after, _, first, _| async move {
167                let first = first.expect("First always exists");
168                let query_args = cala_ledger::es_entity::PaginatedQueryArgs { first, after };
169
170                let result = match ctx.data_opt::<DbOp>() {
171                    Some(op) => {
172                        let mut op = op.try_lock().expect("Lock held concurrently");
173                        app.ledger()
174                            .account_sets()
175                            .find_where_member_in_op(&mut op, account_set_id, query_args)
176                            .await?
177                    }
178                    None => {
179                        app.ledger()
180                            .account_sets()
181                            .find_where_member(account_set_id, query_args)
182                            .await?
183                    }
184                };
185
186                let mut connection = Connection::new(false, result.has_next_page);
187                connection
188                    .edges
189                    .extend(result.entities.into_iter().map(|entity| {
190                        let cursor = AccountSetsByNameCursor::from(&entity);
191                        Edge::new(cursor, AccountSet::from(entity))
192                    }));
193
194                Ok::<_, async_graphql::Error>(connection)
195            },
196        )
197        .await
198    }
199}
200
201#[derive(InputObject)]
202pub(super) struct AccountSetCreateInput {
203    pub account_set_id: UUID,
204    pub journal_id: UUID,
205    pub name: String,
206    #[graphql(default)]
207    pub normal_balance_type: DebitOrCredit,
208    pub description: Option<String>,
209    pub metadata: Option<JSON>,
210}
211
212#[derive(SimpleObject)]
213pub(super) struct AccountSetCreatePayload {
214    pub account_set: AccountSet,
215}
216
217#[derive(Enum, Copy, Clone, Eq, PartialEq)]
218pub enum AccountSetMemberType {
219    Account,
220    AccountSet,
221}
222
223#[derive(InputObject)]
224pub(super) struct AddToAccountSetInput {
225    pub account_set_id: UUID,
226    pub member_id: UUID,
227    pub member_type: AccountSetMemberType,
228}
229
230impl From<AddToAccountSetInput> for AccountSetMemberId {
231    fn from(input: AddToAccountSetInput) -> Self {
232        match input.member_type {
233            AccountSetMemberType::Account => {
234                AccountSetMemberId::Account(AccountId::from(input.member_id))
235            }
236            AccountSetMemberType::AccountSet => {
237                AccountSetMemberId::AccountSet(AccountSetId::from(input.member_id))
238            }
239        }
240    }
241}
242
243#[derive(SimpleObject)]
244pub(super) struct AddToAccountSetPayload {
245    pub account_set: AccountSet,
246}
247
248#[derive(InputObject)]
249pub(super) struct RemoveFromAccountSetInput {
250    pub account_set_id: UUID,
251    pub member_id: UUID,
252    pub member_type: AccountSetMemberType,
253}
254
255impl From<RemoveFromAccountSetInput> for AccountSetMemberId {
256    fn from(input: RemoveFromAccountSetInput) -> Self {
257        match input.member_type {
258            AccountSetMemberType::Account => {
259                AccountSetMemberId::Account(AccountId::from(input.member_id))
260            }
261            AccountSetMemberType::AccountSet => {
262                AccountSetMemberId::AccountSet(AccountSetId::from(input.member_id))
263            }
264        }
265    }
266}
267
268#[derive(SimpleObject)]
269pub(super) struct RemoveFromAccountSetPayload {
270    pub account_set: AccountSet,
271}
272
273impl ToGlobalId for cala_ledger::AccountSetId {
274    fn to_global_id(&self) -> async_graphql::types::ID {
275        async_graphql::types::ID::from(format!("account_set:{}", self))
276    }
277}
278
279impl From<cala_ledger::account_set::AccountSet> for AccountSet {
280    fn from(account_set: cala_ledger::account_set::AccountSet) -> Self {
281        let created_at = account_set.created_at();
282        let modified_at = account_set.modified_at();
283        let values = account_set.into_values();
284        Self {
285            id: values.id.to_global_id(),
286            account_set_id: UUID::from(values.id),
287            version: values.version,
288            journal_id: UUID::from(values.journal_id),
289            name: values.name,
290            normal_balance_type: values.normal_balance_type,
291            description: values.description,
292            metadata: values.metadata.map(JSON::from),
293            created_at: created_at.into(),
294            modified_at: modified_at.into(),
295        }
296    }
297}
298
299impl From<cala_ledger::account_set::AccountSet> for AccountSetCreatePayload {
300    fn from(value: cala_ledger::account_set::AccountSet) -> Self {
301        Self {
302            account_set: AccountSet::from(value),
303        }
304    }
305}
306
307impl From<cala_ledger::account_set::AccountSet> for AddToAccountSetPayload {
308    fn from(value: cala_ledger::account_set::AccountSet) -> Self {
309        Self {
310            account_set: AccountSet::from(value),
311        }
312    }
313}
314
315impl From<cala_ledger::account_set::AccountSet> for RemoveFromAccountSetPayload {
316    fn from(value: cala_ledger::account_set::AccountSet) -> Self {
317        Self {
318            account_set: AccountSet::from(value),
319        }
320    }
321}
322
323#[derive(InputObject)]
324pub(super) struct AccountSetUpdateInput {
325    pub name: Option<String>,
326    pub normal_balance_type: Option<DebitOrCredit>,
327    pub description: Option<String>,
328    pub metadata: Option<JSON>,
329}
330
331#[derive(SimpleObject)]
332pub(super) struct AccountSetUpdatePayload {
333    pub account_set: AccountSet,
334}
335
336impl From<cala_ledger::account_set::AccountSet> for AccountSetUpdatePayload {
337    fn from(value: cala_ledger::account_set::AccountSet) -> Self {
338        Self {
339            account_set: AccountSet::from(value),
340        }
341    }
342}