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