cala_server/graphql/
account.rs

1use async_graphql::{dataloader::*, types::connection::*, *};
2
3use cala_ledger::{
4    balance::*,
5    entry::EntriesByCreatedAtCursor,
6    primitives::{AccountId, Currency, JournalId},
7};
8
9pub use cala_ledger::account::AccountsByNameCursor;
10
11use crate::app::CalaApp;
12
13use super::{
14    account_set::*, balance::Balance, convert::ToGlobalId, entry::Entry, loader::LedgerDataLoader,
15    primitives::*, schema::DbOp,
16};
17
18#[derive(Clone, SimpleObject)]
19#[graphql(complex)]
20pub struct Account {
21    id: ID,
22    account_id: UUID,
23    version: u32,
24    code: String,
25    name: String,
26    normal_balance_type: DebitOrCredit,
27    status: Status,
28    external_id: Option<String>,
29    description: Option<String>,
30    metadata: Option<JSON>,
31    pub(super) created_at: Timestamp,
32    modified_at: Timestamp,
33}
34
35#[ComplexObject]
36impl Account {
37    async fn balance(
38        &self,
39        ctx: &Context<'_>,
40        journal_id: UUID,
41        currency: CurrencyCode,
42    ) -> async_graphql::Result<Option<Balance>> {
43        let journal_id = JournalId::from(journal_id);
44        let account_id = AccountId::from(self.account_id);
45        let currency = Currency::from(currency);
46
47        let balance: Option<AccountBalance> = match ctx.data_opt::<DbOp>() {
48            Some(op) => {
49                let app = ctx.data_unchecked::<CalaApp>();
50                let mut op = op.try_lock().expect("Lock held concurrently");
51                Some(
52                    app.ledger()
53                        .balances()
54                        .find_in_op(&mut op, journal_id, account_id, currency)
55                        .await?,
56                )
57            }
58            None => {
59                let loader = ctx.data_unchecked::<DataLoader<LedgerDataLoader>>();
60                loader.load_one((journal_id, account_id, currency)).await?
61            }
62        };
63        Ok(balance.map(Balance::from))
64    }
65
66    async fn sets(
67        &self,
68        ctx: &Context<'_>,
69        first: i32,
70        after: Option<String>,
71    ) -> Result<Connection<AccountSetsByNameCursor, AccountSet, EmptyFields, EmptyFields>> {
72        let app = ctx.data_unchecked::<CalaApp>();
73        let account_id = AccountId::from(self.account_id);
74        query(
75            after,
76            None,
77            Some(first),
78            None,
79            |after, _, first, _| async move {
80                let first = first.expect("First always exists");
81                let query_args = cala_ledger::es_entity::PaginatedQueryArgs { first, after };
82
83                let result = match ctx.data_opt::<DbOp>() {
84                    Some(op) => {
85                        let mut op = op.try_lock().expect("Lock held concurrently");
86                        app.ledger()
87                            .account_sets()
88                            .find_where_member_in_op(&mut op, account_id, query_args)
89                            .await?
90                    }
91                    None => {
92                        app.ledger()
93                            .account_sets()
94                            .find_where_member(account_id, query_args)
95                            .await?
96                    }
97                };
98                let mut connection = Connection::new(false, result.has_next_page);
99                connection
100                    .edges
101                    .extend(result.entities.into_iter().map(|entity| {
102                        let cursor = AccountSetsByNameCursor::from(&entity);
103                        Edge::new(cursor, AccountSet::from(entity))
104                    }));
105                Ok::<_, async_graphql::Error>(connection)
106            },
107        )
108        .await
109    }
110
111    async fn entries(
112        &self,
113        ctx: &Context<'_>,
114        first: i32,
115        after: Option<String>,
116    ) -> Result<Connection<EntriesByCreatedAtCursor, Entry, EmptyFields, EmptyFields>> {
117        let app = ctx.data_unchecked::<CalaApp>();
118        let account_id = AccountId::from(self.account_id);
119        query(
120            after,
121            None,
122            Some(first),
123            None,
124            |after, _, first, _| async move {
125                let first = first.expect("First always exists");
126                let result = app
127                    .ledger()
128                    .entries()
129                    .list_for_account_id(
130                        account_id,
131                        cala_ledger::es_entity::PaginatedQueryArgs { first, after },
132                        cala_ledger::es_entity::ListDirection::Descending,
133                    )
134                    .await?;
135                let mut connection = Connection::new(false, result.has_next_page);
136                connection
137                    .edges
138                    .extend(result.entities.into_iter().map(|entity| {
139                        let cursor = EntriesByCreatedAtCursor::from(&entity);
140                        Edge::new(cursor, Entry::from(entity))
141                    }));
142                Ok::<_, async_graphql::Error>(connection)
143            },
144        )
145        .await
146    }
147}
148
149#[derive(InputObject)]
150pub(super) struct AccountCreateInput {
151    pub account_id: UUID,
152    pub external_id: Option<String>,
153    pub code: String,
154    pub name: String,
155    #[graphql(default)]
156    pub normal_balance_type: DebitOrCredit,
157    pub description: Option<String>,
158    #[graphql(default)]
159    pub status: Status,
160    pub metadata: Option<JSON>,
161    pub account_set_ids: Option<Vec<UUID>>,
162}
163
164#[derive(SimpleObject)]
165pub(super) struct AccountCreatePayload {
166    pub account: Account,
167}
168
169#[derive(InputObject)]
170pub(super) struct AccountUpdateInput {
171    pub external_id: Option<String>,
172    pub code: Option<String>,
173    pub name: Option<String>,
174    pub normal_balance_type: Option<DebitOrCredit>,
175    pub description: Option<String>,
176    pub status: Option<Status>,
177    pub metadata: Option<JSON>,
178}
179
180#[derive(SimpleObject)]
181pub(super) struct AccountUpdatePayload {
182    pub account: Account,
183}
184
185impl ToGlobalId for cala_ledger::AccountId {
186    fn to_global_id(&self) -> async_graphql::types::ID {
187        async_graphql::types::ID::from(format!("account:{}", self))
188    }
189}
190
191impl From<cala_ledger::account::Account> for Account {
192    fn from(account: cala_ledger::account::Account) -> Self {
193        let created_at = account.created_at();
194        let modified_at = account.modified_at();
195        let values = account.into_values();
196        Self {
197            id: values.id.to_global_id(),
198            account_id: UUID::from(values.id),
199            version: values.version,
200            code: values.code,
201            name: values.name,
202            normal_balance_type: values.normal_balance_type,
203            status: values.status,
204            external_id: values.external_id,
205            description: values.description,
206            metadata: values.metadata.map(JSON::from),
207            created_at: created_at.into(),
208            modified_at: modified_at.into(),
209        }
210    }
211}
212
213impl From<cala_ledger::account::Account> for AccountCreatePayload {
214    fn from(value: cala_ledger::account::Account) -> Self {
215        Self {
216            account: Account::from(value),
217        }
218    }
219}
220
221impl From<cala_ledger::account::Account> for AccountUpdatePayload {
222    fn from(value: cala_ledger::account::Account) -> Self {
223        Self {
224            account: Account::from(value),
225        }
226    }
227}