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