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}