1use async_graphql::{dataloader::*, types::connection::*, *};
2
3use cala_ledger::{
4 balance::*,
5 primitives::{AccountId, Currency, JournalId},
6};
7
8pub use cala_ledger::account::AccountsByNameCursor;
9
10use crate::app::CalaApp;
11
12use super::{
13 account_set::*,
14 balance::{Balance, RangedBalance},
15 convert::ToGlobalId,
16 loader::LedgerDataLoader,
17 primitives::*,
18 schema::DbOp,
19};
20
21#[derive(Clone, SimpleObject)]
22#[graphql(complex)]
23pub struct Account {
24 id: ID,
25 account_id: UUID,
26 version: u32,
27 code: String,
28 name: String,
29 normal_balance_type: DebitOrCredit,
30 status: Status,
31 external_id: Option<String>,
32 description: Option<String>,
33 metadata: Option<JSON>,
34 pub(super) created_at: Timestamp,
35 modified_at: Timestamp,
36}
37
38#[ComplexObject]
39impl Account {
40 async fn balance(
41 &self,
42 ctx: &Context<'_>,
43 journal_id: UUID,
44 currency: CurrencyCode,
45 ) -> async_graphql::Result<Option<Balance>> {
46 let journal_id = JournalId::from(journal_id);
47 let account_id = AccountId::from(self.account_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 balance_in_range(
70 &self,
71 ctx: &Context<'_>,
72 journal_id: UUID,
73 currency: CurrencyCode,
74 from: Timestamp,
75 until: Option<Timestamp>,
76 ) -> async_graphql::Result<Option<RangedBalance>> {
77 let app = ctx.data_unchecked::<CalaApp>();
78 match app
79 .ledger()
80 .balances()
81 .find_in_range(
82 JournalId::from(journal_id),
83 AccountId::from(self.account_id),
84 Currency::from(currency),
85 from.into_inner(),
86 until.map(|ts| ts.into_inner()),
87 )
88 .await
89 {
90 Ok(balance) => Ok(Some(balance.into())),
91 Err(cala_ledger::balance::error::BalanceError::NotFound(_, _, _)) => Ok(None),
92 Err(err) => Err(err.into()),
93 }
94 }
95
96 async fn sets(
97 &self,
98 ctx: &Context<'_>,
99 first: i32,
100 after: Option<String>,
101 ) -> Result<Connection<AccountSetsByNameCursor, AccountSet, EmptyFields, EmptyFields>> {
102 let app = ctx.data_unchecked::<CalaApp>();
103 let account_id = AccountId::from(self.account_id);
104 query(
105 after,
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 result = match ctx.data_opt::<DbOp>() {
114 Some(op) => {
115 let mut op = op.try_lock().expect("Lock held concurrently");
116 app.ledger()
117 .account_sets()
118 .find_where_member_in_op(&mut op, account_id, query_args)
119 .await?
120 }
121 None => {
122 app.ledger()
123 .account_sets()
124 .find_where_member(account_id, query_args)
125 .await?
126 }
127 };
128 let mut connection = Connection::new(false, result.has_next_page);
129 connection
130 .edges
131 .extend(result.entities.into_iter().map(|entity| {
132 let cursor = AccountSetsByNameCursor::from(&entity);
133 Edge::new(cursor, AccountSet::from(entity))
134 }));
135 Ok::<_, async_graphql::Error>(connection)
136 },
137 )
138 .await
139 }
140}
141
142#[derive(InputObject)]
143pub(super) struct AccountCreateInput {
144 pub account_id: UUID,
145 pub external_id: Option<String>,
146 pub code: String,
147 pub name: String,
148 #[graphql(default)]
149 pub normal_balance_type: DebitOrCredit,
150 pub description: Option<String>,
151 #[graphql(default)]
152 pub status: Status,
153 pub metadata: Option<JSON>,
154 pub account_set_ids: Option<Vec<UUID>>,
155}
156
157#[derive(SimpleObject)]
158pub(super) struct AccountCreatePayload {
159 pub account: Account,
160}
161
162#[derive(InputObject)]
163pub(super) struct AccountUpdateInput {
164 pub external_id: Option<String>,
165 pub code: Option<String>,
166 pub name: Option<String>,
167 pub normal_balance_type: Option<DebitOrCredit>,
168 pub description: Option<String>,
169 pub status: Option<Status>,
170 pub metadata: Option<JSON>,
171}
172
173#[derive(SimpleObject)]
174pub(super) struct AccountUpdatePayload {
175 pub account: Account,
176}
177
178impl ToGlobalId for cala_ledger::AccountId {
179 fn to_global_id(&self) -> async_graphql::types::ID {
180 async_graphql::types::ID::from(format!("account:{}", self))
181 }
182}
183
184impl From<cala_ledger::account::Account> for Account {
185 fn from(account: cala_ledger::account::Account) -> Self {
186 let created_at = account.created_at();
187 let modified_at = account.modified_at();
188 let values = account.into_values();
189 Self {
190 id: values.id.to_global_id(),
191 account_id: UUID::from(values.id),
192 version: values.version,
193 code: values.code,
194 name: values.name,
195 normal_balance_type: values.normal_balance_type,
196 status: values.status,
197 external_id: values.external_id,
198 description: values.description,
199 metadata: values.metadata.map(JSON::from),
200 created_at: created_at.into(),
201 modified_at: modified_at.into(),
202 }
203 }
204}
205
206impl From<cala_ledger::account::Account> for AccountCreatePayload {
207 fn from(value: cala_ledger::account::Account) -> Self {
208 Self {
209 account: Account::from(value),
210 }
211 }
212}
213
214impl From<cala_ledger::account::Account> for AccountUpdatePayload {
215 fn from(value: cala_ledger::account::Account) -> Self {
216 Self {
217 account: Account::from(value),
218 }
219 }
220}