cala_server/graphql/
account_set.rs

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