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