cala_server/graphql/
account.rs

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