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_by_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 { first, after };
196
197 let result = match ctx.data_opt::<DbOp>() {
198 Some(op) => {
199 let mut op = op.try_lock().expect("Lock held concurrently");
200 app.ledger()
201 .account_sets()
202 .find_where_member_in_op(&mut op, account_set_id, query_args)
203 .await?
204 }
205 None => {
206 app.ledger()
207 .account_sets()
208 .find_where_member(account_set_id, query_args)
209 .await?
210 }
211 };
212
213 let mut connection = Connection::new(false, result.has_next_page);
214 connection
215 .edges
216 .extend(result.entities.into_iter().map(|entity| {
217 let cursor = AccountSetsByNameCursor::from(&entity);
218 Edge::new(cursor, AccountSet::from(entity))
219 }));
220
221 Ok::<_, async_graphql::Error>(connection)
222 },
223 )
224 .await
225 }
226}
227
228#[derive(InputObject)]
229pub(super) struct AccountSetCreateInput {
230 pub account_set_id: UUID,
231 pub journal_id: UUID,
232 pub name: String,
233 #[graphql(default)]
234 pub normal_balance_type: DebitOrCredit,
235 pub description: Option<String>,
236 pub metadata: Option<JSON>,
237}
238
239#[derive(SimpleObject)]
240pub(super) struct AccountSetCreatePayload {
241 pub account_set: AccountSet,
242}
243
244#[derive(Enum, Copy, Clone, Eq, PartialEq)]
245pub enum AccountSetMemberType {
246 Account,
247 AccountSet,
248}
249
250#[derive(InputObject)]
251pub(super) struct AddToAccountSetInput {
252 pub account_set_id: UUID,
253 pub member_id: UUID,
254 pub member_type: AccountSetMemberType,
255}
256
257impl From<AddToAccountSetInput> for AccountSetMemberId {
258 fn from(input: AddToAccountSetInput) -> Self {
259 match input.member_type {
260 AccountSetMemberType::Account => {
261 AccountSetMemberId::Account(AccountId::from(input.member_id))
262 }
263 AccountSetMemberType::AccountSet => {
264 AccountSetMemberId::AccountSet(AccountSetId::from(input.member_id))
265 }
266 }
267 }
268}
269
270#[derive(SimpleObject)]
271pub(super) struct AddToAccountSetPayload {
272 pub account_set: AccountSet,
273}
274
275#[derive(InputObject)]
276pub(super) struct RemoveFromAccountSetInput {
277 pub account_set_id: UUID,
278 pub member_id: UUID,
279 pub member_type: AccountSetMemberType,
280}
281
282impl From<RemoveFromAccountSetInput> for AccountSetMemberId {
283 fn from(input: RemoveFromAccountSetInput) -> Self {
284 match input.member_type {
285 AccountSetMemberType::Account => {
286 AccountSetMemberId::Account(AccountId::from(input.member_id))
287 }
288 AccountSetMemberType::AccountSet => {
289 AccountSetMemberId::AccountSet(AccountSetId::from(input.member_id))
290 }
291 }
292 }
293}
294
295#[derive(SimpleObject)]
296pub(super) struct RemoveFromAccountSetPayload {
297 pub account_set: AccountSet,
298}
299
300impl ToGlobalId for cala_ledger::AccountSetId {
301 fn to_global_id(&self) -> async_graphql::types::ID {
302 async_graphql::types::ID::from(format!("account_set:{}", self))
303 }
304}
305
306impl From<cala_ledger::account_set::AccountSet> for AccountSet {
307 fn from(account_set: cala_ledger::account_set::AccountSet) -> Self {
308 let created_at = account_set.created_at();
309 let modified_at = account_set.modified_at();
310 let values = account_set.into_values();
311 Self {
312 id: values.id.to_global_id(),
313 account_set_id: UUID::from(values.id),
314 version: values.version,
315 journal_id: UUID::from(values.journal_id),
316 name: values.name,
317 normal_balance_type: values.normal_balance_type,
318 description: values.description,
319 metadata: values.metadata.map(JSON::from),
320 created_at: created_at.into(),
321 modified_at: modified_at.into(),
322 }
323 }
324}
325
326impl From<cala_ledger::account_set::AccountSet> for AccountSetCreatePayload {
327 fn from(value: cala_ledger::account_set::AccountSet) -> Self {
328 Self {
329 account_set: AccountSet::from(value),
330 }
331 }
332}
333
334impl From<cala_ledger::account_set::AccountSet> for AddToAccountSetPayload {
335 fn from(value: cala_ledger::account_set::AccountSet) -> Self {
336 Self {
337 account_set: AccountSet::from(value),
338 }
339 }
340}
341
342impl From<cala_ledger::account_set::AccountSet> for RemoveFromAccountSetPayload {
343 fn from(value: cala_ledger::account_set::AccountSet) -> Self {
344 Self {
345 account_set: AccountSet::from(value),
346 }
347 }
348}
349
350#[derive(InputObject)]
351pub(super) struct AccountSetUpdateInput {
352 pub name: Option<String>,
353 pub normal_balance_type: Option<DebitOrCredit>,
354 pub description: Option<String>,
355 pub metadata: Option<JSON>,
356}
357
358#[derive(SimpleObject)]
359pub(super) struct AccountSetUpdatePayload {
360 pub account_set: AccountSet,
361}
362
363impl From<cala_ledger::account_set::AccountSet> for AccountSetUpdatePayload {
364 fn from(value: cala_ledger::account_set::AccountSet) -> Self {
365 Self {
366 account_set: AccountSet::from(value),
367 }
368 }
369}