datasynth_generators/treasury/
treasury_accounting.rs1use chrono::NaiveDate;
9use datasynth_core::accounts::{expense_accounts, treasury_accounts};
10use datasynth_core::models::{
11 BusinessProcess, CashPoolSweep, DebtInstrument, HedgeRelationship, HedgeType,
12 HedgingInstrument, JournalEntry, JournalEntryLine, TransactionSource,
13};
14use rust_decimal::Decimal;
15
16pub struct TreasuryAccounting;
22
23impl TreasuryAccounting {
24 pub fn generate_debt_jes(
37 instruments: &[DebtInstrument],
38 period_end: NaiveDate,
39 ) -> Vec<JournalEntry> {
40 let mut jes = Vec::new();
41
42 for debt in instruments {
43 if period_end > debt.maturity_date {
45 continue;
46 }
47
48 let quarterly_interest =
49 (debt.principal * debt.interest_rate / Decimal::from(4)).round_dp(2);
50
51 if quarterly_interest == Decimal::ZERO {
52 continue;
53 }
54
55 let mut je = JournalEntry::new_simple(
56 format!("JE-TREAS-INT-{}", debt.id),
57 debt.entity_id.clone(),
58 period_end,
59 format!("Interest accrual on {} from {}", debt.id, debt.lender),
60 );
61 je.header.currency = debt.currency.clone();
62 je.header.business_process = Some(BusinessProcess::Treasury);
63 je.header.source = TransactionSource::Automated;
64 let doc_id = je.header.document_id;
65
66 je.add_line(JournalEntryLine {
67 document_id: doc_id,
68 line_number: 1,
69 gl_account: expense_accounts::INTEREST_EXPENSE.to_string(),
70 account_code: expense_accounts::INTEREST_EXPENSE.to_string(),
71 debit_amount: quarterly_interest,
72 local_amount: quarterly_interest,
73 reference: Some(debt.id.clone()),
74 text: Some(format!("Interest expense — {}", debt.lender)),
75 ..Default::default()
76 });
77 je.add_line(JournalEntryLine {
78 document_id: doc_id,
79 line_number: 2,
80 gl_account: treasury_accounts::INTEREST_PAYABLE.to_string(),
81 account_code: treasury_accounts::INTEREST_PAYABLE.to_string(),
82 credit_amount: quarterly_interest,
83 local_amount: -quarterly_interest,
84 reference: Some(debt.id.clone()),
85 text: Some(format!("Interest payable — {}", debt.lender)),
86 ..Default::default()
87 });
88
89 jes.push(je);
90 }
91
92 jes
93 }
94
95 pub fn generate_hedge_jes(
108 instruments: &[HedgingInstrument],
109 relationships: &[HedgeRelationship],
110 period_end: NaiveDate,
111 entity_id: &str,
112 ) -> Vec<JournalEntry> {
113 let mut jes = Vec::new();
114
115 for instrument in instruments {
116 if !instrument.is_active() {
117 continue;
118 }
119 if instrument.fair_value == Decimal::ZERO {
120 continue;
121 }
122
123 let relationship = relationships
125 .iter()
126 .find(|r| r.hedging_instrument_id == instrument.id);
127
128 let hedge_type = relationship
129 .map(|r| r.hedge_type)
130 .unwrap_or(HedgeType::FairValueHedge);
131
132 let abs_fv = instrument.fair_value.abs();
133 let is_asset = instrument.fair_value > Decimal::ZERO;
134
135 let pnl_oci_account = match hedge_type {
137 HedgeType::CashFlowHedge | HedgeType::NetInvestmentHedge => {
138 treasury_accounts::OCI_CASH_FLOW_HEDGE
139 }
140 HedgeType::FairValueHedge => expense_accounts::FX_GAIN_LOSS,
141 };
142
143 let mut je = JournalEntry::new_simple(
144 format!("JE-TREAS-HEDGE-{}", instrument.id),
145 entity_id.to_string(),
146 period_end,
147 format!(
148 "Hedge accounting — {} ({})",
149 instrument.id,
150 if is_asset { "asset" } else { "liability" }
151 ),
152 );
153 je.header.currency = instrument.currency.clone();
154 je.header.business_process = Some(BusinessProcess::Treasury);
155 je.header.source = TransactionSource::Automated;
156 let doc_id = je.header.document_id;
157
158 if is_asset {
159 je.add_line(JournalEntryLine {
161 document_id: doc_id,
162 line_number: 1,
163 gl_account: treasury_accounts::DERIVATIVE_ASSET.to_string(),
164 account_code: treasury_accounts::DERIVATIVE_ASSET.to_string(),
165 debit_amount: abs_fv,
166 local_amount: abs_fv,
167 reference: Some(instrument.id.clone()),
168 text: Some("Derivative asset — positive MTM".to_string()),
169 ..Default::default()
170 });
171 je.add_line(JournalEntryLine {
172 document_id: doc_id,
173 line_number: 2,
174 gl_account: pnl_oci_account.to_string(),
175 account_code: pnl_oci_account.to_string(),
176 credit_amount: abs_fv,
177 local_amount: -abs_fv,
178 reference: Some(instrument.id.clone()),
179 text: Some(format!(
180 "{} — hedge gain",
181 if hedge_type == HedgeType::CashFlowHedge
182 || hedge_type == HedgeType::NetInvestmentHedge
183 {
184 "OCI"
185 } else {
186 "FX Gain/Loss"
187 }
188 )),
189 ..Default::default()
190 });
191 } else {
192 je.add_line(JournalEntryLine {
194 document_id: doc_id,
195 line_number: 1,
196 gl_account: pnl_oci_account.to_string(),
197 account_code: pnl_oci_account.to_string(),
198 debit_amount: abs_fv,
199 local_amount: abs_fv,
200 reference: Some(instrument.id.clone()),
201 text: Some(format!(
202 "{} — hedge loss",
203 if hedge_type == HedgeType::CashFlowHedge
204 || hedge_type == HedgeType::NetInvestmentHedge
205 {
206 "OCI"
207 } else {
208 "FX Gain/Loss"
209 }
210 )),
211 ..Default::default()
212 });
213 je.add_line(JournalEntryLine {
214 document_id: doc_id,
215 line_number: 2,
216 gl_account: treasury_accounts::DERIVATIVE_LIABILITY.to_string(),
217 account_code: treasury_accounts::DERIVATIVE_LIABILITY.to_string(),
218 credit_amount: abs_fv,
219 local_amount: -abs_fv,
220 reference: Some(instrument.id.clone()),
221 text: Some("Derivative liability — negative MTM".to_string()),
222 ..Default::default()
223 });
224 }
225
226 jes.push(je);
227
228 if let Some(rel) = relationship {
230 if !rel.is_effective && rel.ineffectiveness_amount > Decimal::ZERO {
231 let ineff = rel.ineffectiveness_amount.round_dp(2);
232
233 let mut je_ineff = JournalEntry::new_simple(
234 format!("JE-TREAS-INEFF-{}", instrument.id),
235 entity_id.to_string(),
236 period_end,
237 format!("Hedge ineffectiveness — {}", instrument.id),
238 );
239 je_ineff.header.currency = instrument.currency.clone();
240 je_ineff.header.business_process = Some(BusinessProcess::Treasury);
241 je_ineff.header.source = TransactionSource::Automated;
242 let doc_id_ineff = je_ineff.header.document_id;
243
244 je_ineff.add_line(JournalEntryLine {
245 document_id: doc_id_ineff,
246 line_number: 1,
247 gl_account: treasury_accounts::HEDGE_INEFFECTIVENESS.to_string(),
248 account_code: treasury_accounts::HEDGE_INEFFECTIVENESS.to_string(),
249 debit_amount: ineff,
250 local_amount: ineff,
251 reference: Some(instrument.id.clone()),
252 text: Some("Hedge ineffectiveness expense".to_string()),
253 ..Default::default()
254 });
255 je_ineff.add_line(JournalEntryLine {
256 document_id: doc_id_ineff,
257 line_number: 2,
258 gl_account: treasury_accounts::OCI_CASH_FLOW_HEDGE.to_string(),
259 account_code: treasury_accounts::OCI_CASH_FLOW_HEDGE.to_string(),
260 credit_amount: ineff,
261 local_amount: -ineff,
262 reference: Some(instrument.id.clone()),
263 text: Some("OCI reclassification — ineffectiveness".to_string()),
264 ..Default::default()
265 });
266
267 jes.push(je_ineff);
268 }
269 }
270 }
271
272 jes
273 }
274
275 pub fn generate_cash_pool_sweep_jes(
285 sweeps: &[CashPoolSweep],
286 header_entity: &str,
287 ) -> Vec<JournalEntry> {
288 let mut jes = Vec::new();
289
290 for sweep in sweeps {
291 if sweep.amount == Decimal::ZERO {
292 continue;
293 }
294
295 let abs_amount = sweep.amount.abs();
296
297 let mut je = JournalEntry::new_simple(
298 format!("JE-TREAS-SWEEP-{}", sweep.id),
299 header_entity.to_string(),
300 sweep.date,
301 format!(
302 "Cash pool sweep {} → {} (pool {})",
303 sweep.from_account_id, sweep.to_account_id, sweep.pool_id
304 ),
305 );
306 je.header.currency = sweep.currency.clone();
307 je.header.business_process = Some(BusinessProcess::Treasury);
308 je.header.source = TransactionSource::Automated;
309 let doc_id = je.header.document_id;
310
311 je.add_line(JournalEntryLine {
312 document_id: doc_id,
313 line_number: 1,
314 gl_account: treasury_accounts::CASH_POOL_IC_RECEIVABLE.to_string(),
315 account_code: treasury_accounts::CASH_POOL_IC_RECEIVABLE.to_string(),
316 debit_amount: abs_amount,
317 local_amount: abs_amount,
318 reference: Some(sweep.id.clone()),
319 text: Some(format!(
320 "IC receivable — sweep from {}",
321 sweep.from_account_id
322 )),
323 ..Default::default()
324 });
325 je.add_line(JournalEntryLine {
326 document_id: doc_id,
327 line_number: 2,
328 gl_account: treasury_accounts::CASH_POOL_IC_PAYABLE.to_string(),
329 account_code: treasury_accounts::CASH_POOL_IC_PAYABLE.to_string(),
330 credit_amount: abs_amount,
331 local_amount: -abs_amount,
332 reference: Some(sweep.id.clone()),
333 text: Some(format!("IC payable — sweep to {}", sweep.to_account_id)),
334 ..Default::default()
335 });
336
337 jes.push(je);
338 }
339
340 jes
341 }
342}