1use async_graphql::{dataloader::*, types::connection::*, *};
2
3use cala_ledger::{
4 account_set::AccountSetMemberId,
5 balance::*,
6 entry::EntriesByCreatedAtCursor,
7 primitives::{AccountId, AccountSetId, Currency, JournalId},
8};
9
10pub use cala_ledger::account_set::{AccountSetMembersByCreatedAtCursor, AccountSetsByNameCursor};
11
12use super::{
13 balance::*, convert::ToGlobalId, entry::Entry, loader::LedgerDataLoader, primitives::*,
14};
15use crate::app::CalaApp;
16
17#[derive(Union)]
18enum AccountSetMember {
19 Account(super::account::Account),
20 AccountSet(AccountSet),
21}
22
23#[derive(Clone, SimpleObject)]
24#[graphql(complex)]
25pub struct AccountSet {
26 id: ID,
27 account_set_id: UUID,
28 version: u32,
29 journal_id: UUID,
30 name: String,
31 normal_balance_type: DebitOrCredit,
32 description: Option<String>,
33 metadata: Option<JSON>,
34 created_at: Timestamp,
35 modified_at: Timestamp,
36}
37
38#[ComplexObject]
39impl AccountSet {
40 async fn balance(
41 &self,
42 ctx: &Context<'_>,
43 currency: CurrencyCode,
44 ) -> async_graphql::Result<Option<Balance>> {
45 let journal_id = JournalId::from(self.journal_id);
46 let account_id = AccountId::from(self.account_set_id);
47 let currency = Currency::from(currency);
48
49 let balance: Option<AccountBalance> = match ctx.data_opt::<DbOp>() {
50 Some(op) => {
51 let app = ctx.data_unchecked::<CalaApp>();
52 let mut op = op.try_lock().expect("Lock held concurrently");
53 Some(
54 app.ledger()
55 .balances()
56 .find_in_op(&mut op, journal_id, account_id, currency)
57 .await?,
58 )
59 }
60 None => {
61 let loader = ctx.data_unchecked::<DataLoader<LedgerDataLoader>>();
62 loader.load_one((journal_id, account_id, currency)).await?
63 }
64 };
65 Ok(balance.map(Balance::from))
66 }
67
68 async fn members(
69 &self,
70 ctx: &Context<'_>,
71 first: i32,
72 after: Option<String>,
73 ) -> Result<
74 Connection<AccountSetMembersByCreatedAtCursor, AccountSetMember, EmptyFields, EmptyFields>,
75 > {
76 let app = ctx.data_unchecked::<CalaApp>();
77 let account_set_id = AccountSetId::from(self.account_set_id);
78
79 query(
80 after.clone(),
81 None,
82 Some(first),
83 None,
84 |after, _, first, _| async move {
85 let first = first.expect("First always exists");
86 let query_args = cala_ledger::es_entity::PaginatedQueryArgs { first, after };
87
88 let (members, mut accounts, mut sets) = match ctx.data_opt::<DbOp>() {
89 Some(op) => {
90 let mut op = op.try_lock().expect("Lock held concurrently");
91 let account_sets = app.ledger().account_sets();
92 let accounts = app.ledger().accounts();
93 let members = account_sets
94 .list_members_by_created_at_in_op(&mut op, account_set_id, query_args)
95 .await?;
96 let mut account_ids = Vec::new();
97 let mut set_ids = Vec::new();
98 for member in members.entities.iter() {
99 match member.id {
100 AccountSetMemberId::Account(id) => account_ids.push(id),
101 AccountSetMemberId::AccountSet(id) => set_ids.push(id),
102 }
103 }
104 (
105 members,
106 accounts.find_all_in_op(&mut op, &account_ids).await?,
107 account_sets.find_all_in_op(&mut op, &set_ids).await?,
108 )
109 }
110 None => {
111 let members = app
112 .ledger()
113 .account_sets()
114 .list_members_by_created_at(account_set_id, query_args)
115 .await?;
116 let mut account_ids = Vec::new();
117 let mut set_ids = Vec::new();
118 for member in members.entities.iter() {
119 match member.id {
120 AccountSetMemberId::Account(id) => account_ids.push(id),
121 AccountSetMemberId::AccountSet(id) => set_ids.push(id),
122 }
123 }
124 let loader = ctx.data_unchecked::<DataLoader<LedgerDataLoader>>();
125 (
126 members,
127 loader.load_many(account_ids).await?,
128 loader.load_many(set_ids).await?,
129 )
130 }
131 };
132 let mut connection = Connection::new(false, members.has_next_page);
133 connection.edges.extend(members.entities.into_iter().map(
134 |member| match member.id {
135 AccountSetMemberId::Account(id) => {
136 let entity = accounts.remove(&id).expect("Account exists");
137 let cursor = AccountSetMembersByCreatedAtCursor::from(&member);
138 Edge::new(cursor, AccountSetMember::Account(entity))
139 }
140 AccountSetMemberId::AccountSet(id) => {
141 let entity = sets.remove(&id).expect("Account exists");
142 let cursor = AccountSetMembersByCreatedAtCursor::from(&member);
143 Edge::new(cursor, AccountSetMember::AccountSet(entity))
144 }
145 },
146 ));
147 Ok::<_, async_graphql::Error>(connection)
148 },
149 )
150 .await
151 }
152
153 async fn sets(
154 &self,
155 ctx: &Context<'_>,
156 first: i32,
157 after: Option<String>,
158 ) -> Result<Connection<AccountSetsByNameCursor, AccountSet, EmptyFields, EmptyFields>> {
159 let app = ctx.data_unchecked::<CalaApp>();
160 let account_set_id = AccountSetId::from(self.account_set_id);
161
162 query(
163 after.clone(),
164 None,
165 Some(first),
166 None,
167 |after, _, first, _| async move {
168 let first = first.expect("First always exists");
169 let query_args = cala_ledger::es_entity::PaginatedQueryArgs { first, after };
170
171 let result = match ctx.data_opt::<DbOp>() {
172 Some(op) => {
173 let mut op = op.try_lock().expect("Lock held concurrently");
174 app.ledger()
175 .account_sets()
176 .find_where_member_in_op(&mut op, account_set_id, query_args)
177 .await?
178 }
179 None => {
180 app.ledger()
181 .account_sets()
182 .find_where_member(account_set_id, query_args)
183 .await?
184 }
185 };
186
187 let mut connection = Connection::new(false, result.has_next_page);
188 connection
189 .edges
190 .extend(result.entities.into_iter().map(|entity| {
191 let cursor = AccountSetsByNameCursor::from(&entity);
192 Edge::new(cursor, AccountSet::from(entity))
193 }));
194
195 Ok::<_, async_graphql::Error>(connection)
196 },
197 )
198 .await
199 }
200
201 async fn entries(
202 &self,
203 ctx: &Context<'_>,
204 first: i32,
205 after: Option<String>,
206 ) -> Result<Connection<EntriesByCreatedAtCursor, Entry, EmptyFields, EmptyFields>> {
207 let app = ctx.data_unchecked::<CalaApp>();
208 let account_set_id = AccountSetId::from(self.account_set_id);
209 query(
210 after,
211 None,
212 Some(first),
213 None,
214 |after, _, first, _| async move {
215 let first = first.expect("First always exists");
216 let result = app
217 .ledger()
218 .entries()
219 .list_for_account_set_id(
220 account_set_id,
221 cala_ledger::es_entity::PaginatedQueryArgs { first, after },
222 cala_ledger::es_entity::ListDirection::Descending,
223 )
224 .await?;
225 let mut connection = Connection::new(false, result.has_next_page);
226 connection
227 .edges
228 .extend(result.entities.into_iter().map(|entity| {
229 let cursor = EntriesByCreatedAtCursor::from(&entity);
230 Edge::new(cursor, Entry::from(entity))
231 }));
232 Ok::<_, async_graphql::Error>(connection)
233 },
234 )
235 .await
236 }
237}
238
239#[derive(InputObject)]
240pub(super) struct AccountSetCreateInput {
241 pub account_set_id: UUID,
242 pub journal_id: UUID,
243 pub name: String,
244 #[graphql(default)]
245 pub normal_balance_type: DebitOrCredit,
246 pub description: Option<String>,
247 pub metadata: Option<JSON>,
248}
249
250#[derive(SimpleObject)]
251pub(super) struct AccountSetCreatePayload {
252 pub account_set: AccountSet,
253}
254
255#[derive(Enum, Copy, Clone, Eq, PartialEq)]
256pub enum AccountSetMemberType {
257 Account,
258 AccountSet,
259}
260
261#[derive(InputObject)]
262pub(super) struct AddToAccountSetInput {
263 pub account_set_id: UUID,
264 pub member_id: UUID,
265 pub member_type: AccountSetMemberType,
266}
267
268impl From<AddToAccountSetInput> for AccountSetMemberId {
269 fn from(input: AddToAccountSetInput) -> Self {
270 match input.member_type {
271 AccountSetMemberType::Account => {
272 AccountSetMemberId::Account(AccountId::from(input.member_id))
273 }
274 AccountSetMemberType::AccountSet => {
275 AccountSetMemberId::AccountSet(AccountSetId::from(input.member_id))
276 }
277 }
278 }
279}
280
281#[derive(SimpleObject)]
282pub(super) struct AddToAccountSetPayload {
283 pub account_set: AccountSet,
284}
285
286#[derive(InputObject)]
287pub(super) struct RemoveFromAccountSetInput {
288 pub account_set_id: UUID,
289 pub member_id: UUID,
290 pub member_type: AccountSetMemberType,
291}
292
293impl From<RemoveFromAccountSetInput> for AccountSetMemberId {
294 fn from(input: RemoveFromAccountSetInput) -> Self {
295 match input.member_type {
296 AccountSetMemberType::Account => {
297 AccountSetMemberId::Account(AccountId::from(input.member_id))
298 }
299 AccountSetMemberType::AccountSet => {
300 AccountSetMemberId::AccountSet(AccountSetId::from(input.member_id))
301 }
302 }
303 }
304}
305
306#[derive(SimpleObject)]
307pub(super) struct RemoveFromAccountSetPayload {
308 pub account_set: AccountSet,
309}
310
311impl ToGlobalId for cala_ledger::AccountSetId {
312 fn to_global_id(&self) -> async_graphql::types::ID {
313 async_graphql::types::ID::from(format!("account_set:{self}"))
314 }
315}
316
317impl From<cala_ledger::account_set::AccountSet> for AccountSet {
318 fn from(account_set: cala_ledger::account_set::AccountSet) -> Self {
319 let created_at = account_set.created_at();
320 let modified_at = account_set.modified_at();
321 let values = account_set.into_values();
322 Self {
323 id: values.id.to_global_id(),
324 account_set_id: UUID::from(values.id),
325 version: values.version,
326 journal_id: UUID::from(values.journal_id),
327 name: values.name,
328 normal_balance_type: values.normal_balance_type,
329 description: values.description,
330 metadata: values.metadata.map(JSON::from),
331 created_at: created_at.into(),
332 modified_at: modified_at.into(),
333 }
334 }
335}
336
337impl From<cala_ledger::account_set::AccountSet> for AccountSetCreatePayload {
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 AddToAccountSetPayload {
346 fn from(value: cala_ledger::account_set::AccountSet) -> Self {
347 Self {
348 account_set: AccountSet::from(value),
349 }
350 }
351}
352
353impl From<cala_ledger::account_set::AccountSet> for RemoveFromAccountSetPayload {
354 fn from(value: cala_ledger::account_set::AccountSet) -> Self {
355 Self {
356 account_set: AccountSet::from(value),
357 }
358 }
359}
360
361#[derive(InputObject)]
362pub(super) struct AccountSetUpdateInput {
363 pub name: Option<String>,
364 pub normal_balance_type: Option<DebitOrCredit>,
365 pub description: Option<String>,
366 pub metadata: Option<JSON>,
367}
368
369#[derive(SimpleObject)]
370pub(super) struct AccountSetUpdatePayload {
371 pub account_set: AccountSet,
372}
373
374impl From<cala_ledger::account_set::AccountSet> for AccountSetUpdatePayload {
375 fn from(value: cala_ledger::account_set::AccountSet) -> Self {
376 Self {
377 account_set: AccountSet::from(value),
378 }
379 }
380}