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