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::*,
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}