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