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_by_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 { first, after };
196
197                let result = match ctx.data_opt::<DbOp>() {
198                    Some(op) => {
199                        let mut op = op.try_lock().expect("Lock held concurrently");
200                        app.ledger()
201                            .account_sets()
202                            .find_where_member_in_op(&mut op, account_set_id, query_args)
203                            .await?
204                    }
205                    None => {
206                        app.ledger()
207                            .account_sets()
208                            .find_where_member(account_set_id, query_args)
209                            .await?
210                    }
211                };
212
213                let mut connection = Connection::new(false, result.has_next_page);
214                connection
215                    .edges
216                    .extend(result.entities.into_iter().map(|entity| {
217                        let cursor = AccountSetsByNameCursor::from(&entity);
218                        Edge::new(cursor, AccountSet::from(entity))
219                    }));
220
221                Ok::<_, async_graphql::Error>(connection)
222            },
223        )
224        .await
225    }
226}
227
228#[derive(InputObject)]
229pub(super) struct AccountSetCreateInput {
230    pub account_set_id: UUID,
231    pub journal_id: UUID,
232    pub name: String,
233    #[graphql(default)]
234    pub normal_balance_type: DebitOrCredit,
235    pub description: Option<String>,
236    pub metadata: Option<JSON>,
237}
238
239#[derive(SimpleObject)]
240pub(super) struct AccountSetCreatePayload {
241    pub account_set: AccountSet,
242}
243
244#[derive(Enum, Copy, Clone, Eq, PartialEq)]
245pub enum AccountSetMemberType {
246    Account,
247    AccountSet,
248}
249
250#[derive(InputObject)]
251pub(super) struct AddToAccountSetInput {
252    pub account_set_id: UUID,
253    pub member_id: UUID,
254    pub member_type: AccountSetMemberType,
255}
256
257impl From<AddToAccountSetInput> for AccountSetMemberId {
258    fn from(input: AddToAccountSetInput) -> Self {
259        match input.member_type {
260            AccountSetMemberType::Account => {
261                AccountSetMemberId::Account(AccountId::from(input.member_id))
262            }
263            AccountSetMemberType::AccountSet => {
264                AccountSetMemberId::AccountSet(AccountSetId::from(input.member_id))
265            }
266        }
267    }
268}
269
270#[derive(SimpleObject)]
271pub(super) struct AddToAccountSetPayload {
272    pub account_set: AccountSet,
273}
274
275#[derive(InputObject)]
276pub(super) struct RemoveFromAccountSetInput {
277    pub account_set_id: UUID,
278    pub member_id: UUID,
279    pub member_type: AccountSetMemberType,
280}
281
282impl From<RemoveFromAccountSetInput> for AccountSetMemberId {
283    fn from(input: RemoveFromAccountSetInput) -> Self {
284        match input.member_type {
285            AccountSetMemberType::Account => {
286                AccountSetMemberId::Account(AccountId::from(input.member_id))
287            }
288            AccountSetMemberType::AccountSet => {
289                AccountSetMemberId::AccountSet(AccountSetId::from(input.member_id))
290            }
291        }
292    }
293}
294
295#[derive(SimpleObject)]
296pub(super) struct RemoveFromAccountSetPayload {
297    pub account_set: AccountSet,
298}
299
300impl ToGlobalId for cala_ledger::AccountSetId {
301    fn to_global_id(&self) -> async_graphql::types::ID {
302        async_graphql::types::ID::from(format!("account_set:{}", self))
303    }
304}
305
306impl From<cala_ledger::account_set::AccountSet> for AccountSet {
307    fn from(account_set: cala_ledger::account_set::AccountSet) -> Self {
308        let created_at = account_set.created_at();
309        let modified_at = account_set.modified_at();
310        let values = account_set.into_values();
311        Self {
312            id: values.id.to_global_id(),
313            account_set_id: UUID::from(values.id),
314            version: values.version,
315            journal_id: UUID::from(values.journal_id),
316            name: values.name,
317            normal_balance_type: values.normal_balance_type,
318            description: values.description,
319            metadata: values.metadata.map(JSON::from),
320            created_at: created_at.into(),
321            modified_at: modified_at.into(),
322        }
323    }
324}
325
326impl From<cala_ledger::account_set::AccountSet> for AccountSetCreatePayload {
327    fn from(value: cala_ledger::account_set::AccountSet) -> Self {
328        Self {
329            account_set: AccountSet::from(value),
330        }
331    }
332}
333
334impl From<cala_ledger::account_set::AccountSet> for AddToAccountSetPayload {
335    fn from(value: cala_ledger::account_set::AccountSet) -> Self {
336        Self {
337            account_set: AccountSet::from(value),
338        }
339    }
340}
341
342impl From<cala_ledger::account_set::AccountSet> for RemoveFromAccountSetPayload {
343    fn from(value: cala_ledger::account_set::AccountSet) -> Self {
344        Self {
345            account_set: AccountSet::from(value),
346        }
347    }
348}
349
350#[derive(InputObject)]
351pub(super) struct AccountSetUpdateInput {
352    pub name: Option<String>,
353    pub normal_balance_type: Option<DebitOrCredit>,
354    pub description: Option<String>,
355    pub metadata: Option<JSON>,
356}
357
358#[derive(SimpleObject)]
359pub(super) struct AccountSetUpdatePayload {
360    pub account_set: AccountSet,
361}
362
363impl From<cala_ledger::account_set::AccountSet> for AccountSetUpdatePayload {
364    fn from(value: cala_ledger::account_set::AccountSet) -> Self {
365        Self {
366            account_set: AccountSet::from(value),
367        }
368    }
369}