Skip to main content

datasynth_output/
treasury_export.rs

1//! Export treasury and cash management data to CSV files.
2//!
3//! Exports cash positions, forecasts, pool sweeps, hedging instruments,
4//! hedge relationships, debt instruments, covenants, amortization schedules,
5//! bank guarantees, netting runs, netting positions, and anomaly labels.
6
7use std::fs::File;
8use std::io::{BufWriter, Write};
9use std::path::{Path, PathBuf};
10
11use datasynth_core::error::SynthResult;
12use datasynth_core::models::{
13    BankGuarantee, CashForecast, CashPoolSweep, CashPosition, DebtInstrument, HedgeRelationship,
14    HedgingInstrument, NettingRun,
15};
16
17// ---------------------------------------------------------------------------
18// Anomaly label row (string-based for export)
19// ---------------------------------------------------------------------------
20
21/// A pre-serialized treasury anomaly label row for CSV export.
22#[derive(Debug, Clone)]
23pub struct TreasuryAnomalyLabelRow {
24    pub id: String,
25    pub anomaly_type: String,
26    pub severity: String,
27    pub document_type: String,
28    pub document_id: String,
29    pub description: String,
30    pub original_value: String,
31    pub anomalous_value: String,
32}
33
34// ---------------------------------------------------------------------------
35// Export summary
36// ---------------------------------------------------------------------------
37
38/// Summary of exported treasury data.
39#[derive(Debug, Default)]
40pub struct TreasuryExportSummary {
41    pub cash_positions_count: usize,
42    pub cash_forecasts_count: usize,
43    pub cash_forecast_items_count: usize,
44    pub cash_pool_sweeps_count: usize,
45    pub hedging_instruments_count: usize,
46    pub hedge_relationships_count: usize,
47    pub debt_instruments_count: usize,
48    pub debt_covenants_count: usize,
49    pub amortization_schedules_count: usize,
50    pub bank_guarantees_count: usize,
51    pub netting_runs_count: usize,
52    pub netting_positions_count: usize,
53    pub anomaly_labels_count: usize,
54}
55
56impl TreasuryExportSummary {
57    /// Total number of rows exported across all files.
58    pub fn total(&self) -> usize {
59        self.cash_positions_count
60            + self.cash_forecasts_count
61            + self.cash_forecast_items_count
62            + self.cash_pool_sweeps_count
63            + self.hedging_instruments_count
64            + self.hedge_relationships_count
65            + self.debt_instruments_count
66            + self.debt_covenants_count
67            + self.amortization_schedules_count
68            + self.bank_guarantees_count
69            + self.netting_runs_count
70            + self.netting_positions_count
71            + self.anomaly_labels_count
72    }
73}
74
75// ---------------------------------------------------------------------------
76// Exporter
77// ---------------------------------------------------------------------------
78
79/// Exporter for treasury and cash management data.
80pub struct TreasuryExporter {
81    output_dir: PathBuf,
82}
83
84impl TreasuryExporter {
85    /// Create a new treasury exporter writing to the given directory.
86    pub fn new(output_dir: impl AsRef<Path>) -> Self {
87        Self {
88            output_dir: output_dir.as_ref().to_path_buf(),
89        }
90    }
91
92    /// Export cash positions to `cash_positions.csv`.
93    pub fn export_cash_positions(&self, data: &[CashPosition]) -> SynthResult<usize> {
94        let path = self.output_dir.join("cash_positions.csv");
95        let file = File::create(&path)?;
96        let mut w = BufWriter::with_capacity(256 * 1024, file);
97
98        writeln!(
99            w,
100            "id,entity_id,bank_account_id,currency,date,opening_balance,inflows,outflows,closing_balance,available_balance,value_date_balance"
101        )?;
102
103        for p in data {
104            writeln!(
105                w,
106                "{},{},{},{},{},{},{},{},{},{},{}",
107                esc(&p.id),
108                esc(&p.entity_id),
109                esc(&p.bank_account_id),
110                esc(&p.currency),
111                p.date,
112                p.opening_balance,
113                p.inflows,
114                p.outflows,
115                p.closing_balance,
116                p.available_balance,
117                p.value_date_balance,
118            )?;
119        }
120
121        w.flush()?;
122        Ok(data.len())
123    }
124
125    /// Export cash forecasts to `cash_forecasts.csv`.
126    pub fn export_cash_forecasts(&self, data: &[CashForecast]) -> SynthResult<usize> {
127        let path = self.output_dir.join("cash_forecasts.csv");
128        let file = File::create(&path)?;
129        let mut w = BufWriter::with_capacity(256 * 1024, file);
130
131        writeln!(
132            w,
133            "id,entity_id,currency,forecast_date,horizon_days,net_position,confidence_level,item_count"
134        )?;
135
136        for f in data {
137            writeln!(
138                w,
139                "{},{},{},{},{},{},{},{}",
140                esc(&f.id),
141                esc(&f.entity_id),
142                esc(&f.currency),
143                f.forecast_date,
144                f.horizon_days,
145                f.net_position,
146                f.confidence_level,
147                f.items.len(),
148            )?;
149        }
150
151        w.flush()?;
152        Ok(data.len())
153    }
154
155    /// Export cash forecast items to `cash_forecast_items.csv`.
156    pub fn export_cash_forecast_items(&self, data: &[CashForecast]) -> SynthResult<usize> {
157        let path = self.output_dir.join("cash_forecast_items.csv");
158        let file = File::create(&path)?;
159        let mut w = BufWriter::with_capacity(256 * 1024, file);
160
161        writeln!(
162            w,
163            "id,forecast_id,date,category,amount,probability,source_document_type,source_document_id"
164        )?;
165
166        let mut count = 0;
167        for forecast in data {
168            for item in &forecast.items {
169                writeln!(
170                    w,
171                    "{},{},{},{:?},{},{},{},{}",
172                    esc(&item.id),
173                    esc(&forecast.id),
174                    item.date,
175                    item.category,
176                    item.amount,
177                    item.probability,
178                    item.source_document_type.as_deref().unwrap_or(""),
179                    item.source_document_id.as_deref().unwrap_or(""),
180                )?;
181                count += 1;
182            }
183        }
184
185        w.flush()?;
186        Ok(count)
187    }
188
189    /// Export cash pool sweeps to `cash_pool_sweeps.csv`.
190    pub fn export_cash_pool_sweeps(&self, data: &[CashPoolSweep]) -> SynthResult<usize> {
191        let path = self.output_dir.join("cash_pool_sweeps.csv");
192        let file = File::create(&path)?;
193        let mut w = BufWriter::with_capacity(256 * 1024, file);
194
195        writeln!(
196            w,
197            "id,pool_id,date,from_account_id,to_account_id,amount,currency"
198        )?;
199
200        for s in data {
201            writeln!(
202                w,
203                "{},{},{},{},{},{},{}",
204                esc(&s.id),
205                esc(&s.pool_id),
206                s.date,
207                esc(&s.from_account_id),
208                esc(&s.to_account_id),
209                s.amount,
210                esc(&s.currency),
211            )?;
212        }
213
214        w.flush()?;
215        Ok(data.len())
216    }
217
218    /// Export hedging instruments to `hedging_instruments.csv`.
219    pub fn export_hedging_instruments(&self, data: &[HedgingInstrument]) -> SynthResult<usize> {
220        let path = self.output_dir.join("hedging_instruments.csv");
221        let file = File::create(&path)?;
222        let mut w = BufWriter::with_capacity(256 * 1024, file);
223
224        writeln!(
225            w,
226            "id,instrument_type,notional_amount,currency,currency_pair,fixed_rate,floating_index,strike_rate,trade_date,maturity_date,counterparty,fair_value,status"
227        )?;
228
229        for h in data {
230            writeln!(
231                w,
232                "{},{:?},{},{},{},{},{},{},{},{},{},{},{:?}",
233                esc(&h.id),
234                h.instrument_type,
235                h.notional_amount,
236                esc(&h.currency),
237                h.currency_pair.as_deref().unwrap_or(""),
238                h.fixed_rate.map(|r| r.to_string()).unwrap_or_default(),
239                h.floating_index.as_deref().unwrap_or(""),
240                h.strike_rate.map(|r| r.to_string()).unwrap_or_default(),
241                h.trade_date,
242                h.maturity_date,
243                esc(&h.counterparty),
244                h.fair_value,
245                h.status,
246            )?;
247        }
248
249        w.flush()?;
250        Ok(data.len())
251    }
252
253    /// Export hedge relationships to `hedge_relationships.csv`.
254    pub fn export_hedge_relationships(&self, data: &[HedgeRelationship]) -> SynthResult<usize> {
255        let path = self.output_dir.join("hedge_relationships.csv");
256        let file = File::create(&path)?;
257        let mut w = BufWriter::with_capacity(256 * 1024, file);
258
259        writeln!(
260            w,
261            "id,hedged_item_type,hedged_item_description,hedging_instrument_id,hedge_type,designation_date,effectiveness_test_method,effectiveness_ratio,is_effective,ineffectiveness_amount"
262        )?;
263
264        for r in data {
265            writeln!(
266                w,
267                "{},{:?},{},{},{:?},{},{:?},{},{},{}",
268                esc(&r.id),
269                r.hedged_item_type,
270                esc(&r.hedged_item_description),
271                esc(&r.hedging_instrument_id),
272                r.hedge_type,
273                r.designation_date,
274                r.effectiveness_test_method,
275                r.effectiveness_ratio,
276                r.is_effective,
277                r.ineffectiveness_amount,
278            )?;
279        }
280
281        w.flush()?;
282        Ok(data.len())
283    }
284
285    /// Export debt instruments to `debt_instruments.csv`.
286    pub fn export_debt_instruments(&self, data: &[DebtInstrument]) -> SynthResult<usize> {
287        let path = self.output_dir.join("debt_instruments.csv");
288        let file = File::create(&path)?;
289        let mut w = BufWriter::with_capacity(256 * 1024, file);
290
291        writeln!(
292            w,
293            "id,entity_id,instrument_type,lender,principal,currency,interest_rate,rate_type,origination_date,maturity_date,drawn_amount,facility_limit"
294        )?;
295
296        for d in data {
297            writeln!(
298                w,
299                "{},{},{:?},{},{},{},{},{:?},{},{},{},{}",
300                esc(&d.id),
301                esc(&d.entity_id),
302                d.instrument_type,
303                esc(&d.lender),
304                d.principal,
305                esc(&d.currency),
306                d.interest_rate,
307                d.rate_type,
308                d.origination_date,
309                d.maturity_date,
310                d.drawn_amount,
311                d.facility_limit,
312            )?;
313        }
314
315        w.flush()?;
316        Ok(data.len())
317    }
318
319    /// Export debt covenants to `debt_covenants.csv`.
320    pub fn export_debt_covenants(&self, instruments: &[DebtInstrument]) -> SynthResult<usize> {
321        let path = self.output_dir.join("debt_covenants.csv");
322        let file = File::create(&path)?;
323        let mut w = BufWriter::with_capacity(256 * 1024, file);
324
325        writeln!(
326            w,
327            "id,debt_instrument_id,covenant_type,threshold,measurement_frequency,actual_value,measurement_date,is_compliant,headroom,waiver_obtained"
328        )?;
329
330        let mut count = 0;
331        for instrument in instruments {
332            for c in &instrument.covenants {
333                writeln!(
334                    w,
335                    "{},{},{:?},{},{:?},{},{},{},{},{}",
336                    esc(&c.id),
337                    esc(&instrument.id),
338                    c.covenant_type,
339                    c.threshold,
340                    c.measurement_frequency,
341                    c.actual_value,
342                    c.measurement_date,
343                    c.is_compliant,
344                    c.headroom,
345                    c.waiver_obtained,
346                )?;
347                count += 1;
348            }
349        }
350
351        w.flush()?;
352        Ok(count)
353    }
354
355    /// Export amortization schedules to `amortization_schedules.csv`.
356    pub fn export_amortization_schedules(
357        &self,
358        instruments: &[DebtInstrument],
359    ) -> SynthResult<usize> {
360        let path = self.output_dir.join("amortization_schedules.csv");
361        let file = File::create(&path)?;
362        let mut w = BufWriter::with_capacity(256 * 1024, file);
363
364        writeln!(
365            w,
366            "debt_instrument_id,date,principal_payment,interest_payment,total_payment,balance_after"
367        )?;
368
369        let mut count = 0;
370        for instrument in instruments {
371            for p in &instrument.amortization_schedule {
372                writeln!(
373                    w,
374                    "{},{},{},{},{},{}",
375                    esc(&instrument.id),
376                    p.date,
377                    p.principal_payment,
378                    p.interest_payment,
379                    p.total_payment(),
380                    p.balance_after,
381                )?;
382                count += 1;
383            }
384        }
385
386        w.flush()?;
387        Ok(count)
388    }
389
390    /// Export bank guarantees to `bank_guarantees.csv`.
391    pub fn export_bank_guarantees(&self, data: &[BankGuarantee]) -> SynthResult<usize> {
392        let path = self.output_dir.join("bank_guarantees.csv");
393        let file = File::create(&path)?;
394        let mut w = BufWriter::with_capacity(256 * 1024, file);
395
396        writeln!(
397            w,
398            "id,entity_id,guarantee_type,amount,currency,beneficiary,issuing_bank,issue_date,expiry_date,status,linked_contract_id,linked_project_id"
399        )?;
400
401        for g in data {
402            writeln!(
403                w,
404                "{},{},{:?},{},{},{},{},{},{},{:?},{},{}",
405                esc(&g.id),
406                esc(&g.entity_id),
407                g.guarantee_type,
408                g.amount,
409                esc(&g.currency),
410                esc(&g.beneficiary),
411                esc(&g.issuing_bank),
412                g.issue_date,
413                g.expiry_date,
414                g.status,
415                g.linked_contract_id.as_deref().unwrap_or(""),
416                g.linked_project_id.as_deref().unwrap_or(""),
417            )?;
418        }
419
420        w.flush()?;
421        Ok(data.len())
422    }
423
424    /// Export netting runs to `netting_runs.csv`.
425    pub fn export_netting_runs(&self, data: &[NettingRun]) -> SynthResult<usize> {
426        let path = self.output_dir.join("netting_runs.csv");
427        let file = File::create(&path)?;
428        let mut w = BufWriter::with_capacity(256 * 1024, file);
429
430        writeln!(
431            w,
432            "id,netting_date,cycle,gross_receivables,gross_payables,net_settlement,settlement_currency,savings,savings_pct,participant_count"
433        )?;
434
435        for n in data {
436            writeln!(
437                w,
438                "{},{},{:?},{},{},{},{},{},{},{}",
439                esc(&n.id),
440                n.netting_date,
441                n.cycle,
442                n.gross_receivables,
443                n.gross_payables,
444                n.net_settlement,
445                esc(&n.settlement_currency),
446                n.savings(),
447                n.savings_pct(),
448                n.participating_entities.len(),
449            )?;
450        }
451
452        w.flush()?;
453        Ok(data.len())
454    }
455
456    /// Export netting positions to `netting_positions.csv`.
457    pub fn export_netting_positions(&self, data: &[NettingRun]) -> SynthResult<usize> {
458        let path = self.output_dir.join("netting_positions.csv");
459        let file = File::create(&path)?;
460        let mut w = BufWriter::with_capacity(256 * 1024, file);
461
462        writeln!(
463            w,
464            "netting_run_id,entity_id,gross_receivable,gross_payable,net_position,settlement_direction"
465        )?;
466
467        let mut count = 0;
468        for run in data {
469            for pos in &run.positions {
470                writeln!(
471                    w,
472                    "{},{},{},{},{},{:?}",
473                    esc(&run.id),
474                    esc(&pos.entity_id),
475                    pos.gross_receivable,
476                    pos.gross_payable,
477                    pos.net_position,
478                    pos.settlement_direction,
479                )?;
480                count += 1;
481            }
482        }
483
484        w.flush()?;
485        Ok(count)
486    }
487
488    /// Export anomaly labels to `treasury_anomaly_labels.csv`.
489    pub fn export_anomaly_labels(&self, data: &[TreasuryAnomalyLabelRow]) -> SynthResult<usize> {
490        let path = self.output_dir.join("treasury_anomaly_labels.csv");
491        let file = File::create(&path)?;
492        let mut w = BufWriter::with_capacity(256 * 1024, file);
493
494        writeln!(
495            w,
496            "id,anomaly_type,severity,document_type,document_id,description,original_value,anomalous_value"
497        )?;
498
499        for a in data {
500            writeln!(
501                w,
502                "{},{},{},{},{},{},{},{}",
503                esc(&a.id),
504                esc(&a.anomaly_type),
505                esc(&a.severity),
506                esc(&a.document_type),
507                esc(&a.document_id),
508                esc(&a.description),
509                esc(&a.original_value),
510                esc(&a.anomalous_value),
511            )?;
512        }
513
514        w.flush()?;
515        Ok(data.len())
516    }
517}
518
519/// Escape a string for CSV output.
520fn esc(s: &str) -> String {
521    if s.contains(',') || s.contains('"') || s.contains('\n') || s.contains('\r') {
522        format!("\"{}\"", s.replace('"', "\"\""))
523    } else {
524        s.to_string()
525    }
526}
527
528// ---------------------------------------------------------------------------
529// Tests
530// ---------------------------------------------------------------------------
531
532#[cfg(test)]
533mod tests {
534    use super::*;
535    use chrono::NaiveDate;
536    use rust_decimal_macros::dec;
537    use tempfile::TempDir;
538
539    use datasynth_core::models::{
540        AmortizationPayment, CovenantType, DebtCovenant, DebtType, EffectivenessMethod, Frequency,
541        HedgeInstrumentType, HedgeType, HedgedItemType, InterestRateType, NettingCycle,
542        NettingPosition, PayOrReceive, TreasuryCashFlowCategory,
543    };
544
545    fn d(s: &str) -> NaiveDate {
546        NaiveDate::parse_from_str(s, "%Y-%m-%d").unwrap()
547    }
548
549    #[test]
550    fn test_export_cash_positions() {
551        let temp = TempDir::new().unwrap();
552        let exporter = TreasuryExporter::new(temp.path());
553
554        let positions = vec![
555            CashPosition::new(
556                "CP-001",
557                "C001",
558                "BA-001",
559                "USD",
560                d("2025-01-15"),
561                dec!(100000),
562                dec!(5000),
563                dec!(2000),
564            ),
565            CashPosition::new(
566                "CP-002",
567                "C001",
568                "BA-001",
569                "USD",
570                d("2025-01-16"),
571                dec!(103000),
572                dec!(0),
573                dec!(1000),
574            ),
575        ];
576
577        let count = exporter.export_cash_positions(&positions).unwrap();
578        assert_eq!(count, 2);
579
580        let content = std::fs::read_to_string(temp.path().join("cash_positions.csv")).unwrap();
581        assert_eq!(content.lines().count(), 3); // header + 2 rows
582        assert!(content.contains("CP-001"));
583        assert!(content.contains("CP-002"));
584        assert!(content.contains("opening_balance"));
585    }
586
587    #[test]
588    fn test_export_cash_forecasts_and_items() {
589        let temp = TempDir::new().unwrap();
590        let exporter = TreasuryExporter::new(temp.path());
591
592        let items = vec![datasynth_core::models::CashForecastItem {
593            id: "CFI-001".to_string(),
594            date: d("2025-02-15"),
595            category: TreasuryCashFlowCategory::ArCollection,
596            amount: dec!(50000),
597            probability: dec!(0.90),
598            source_document_type: Some("SalesOrder".to_string()),
599            source_document_id: Some("SO-001".to_string()),
600        }];
601        let forecasts = vec![CashForecast::new(
602            "CF-001",
603            "C001",
604            "USD",
605            d("2025-01-31"),
606            90,
607            items,
608            dec!(0.90),
609        )];
610
611        let fc_count = exporter.export_cash_forecasts(&forecasts).unwrap();
612        let fi_count = exporter.export_cash_forecast_items(&forecasts).unwrap();
613
614        assert_eq!(fc_count, 1);
615        assert_eq!(fi_count, 1);
616
617        let fc_content = std::fs::read_to_string(temp.path().join("cash_forecasts.csv")).unwrap();
618        assert!(fc_content.contains("CF-001"));
619
620        let fi_content =
621            std::fs::read_to_string(temp.path().join("cash_forecast_items.csv")).unwrap();
622        assert!(fi_content.contains("CFI-001"));
623        assert!(fi_content.contains("CF-001")); // parent reference
624    }
625
626    #[test]
627    fn test_export_hedging_and_relationships() {
628        let temp = TempDir::new().unwrap();
629        let exporter = TreasuryExporter::new(temp.path());
630
631        let instruments = vec![HedgingInstrument::new(
632            "HI-001",
633            HedgeInstrumentType::FxForward,
634            dec!(1000000),
635            "EUR",
636            d("2025-01-01"),
637            d("2025-06-30"),
638            "Deutsche Bank",
639        )
640        .with_currency_pair("EUR/USD")
641        .with_fixed_rate(dec!(1.0850))];
642
643        let relationships = vec![HedgeRelationship::new(
644            "HR-001",
645            HedgedItemType::ForecastedTransaction,
646            "EUR receivables Q2",
647            "HI-001",
648            HedgeType::CashFlowHedge,
649            d("2025-01-01"),
650            EffectivenessMethod::Regression,
651            dec!(0.95),
652        )];
653
654        let hi_count = exporter.export_hedging_instruments(&instruments).unwrap();
655        let hr_count = exporter.export_hedge_relationships(&relationships).unwrap();
656
657        assert_eq!(hi_count, 1);
658        assert_eq!(hr_count, 1);
659    }
660
661    #[test]
662    fn test_export_debt_with_covenants_and_amortization() {
663        let temp = TempDir::new().unwrap();
664        let exporter = TreasuryExporter::new(temp.path());
665
666        let instruments = vec![DebtInstrument::new(
667            "DEBT-001",
668            "C001",
669            DebtType::TermLoan,
670            "First Bank",
671            dec!(1000000),
672            "USD",
673            dec!(0.05),
674            InterestRateType::Fixed,
675            d("2025-01-01"),
676            d("2026-01-01"),
677        )
678        .with_amortization_schedule(vec![AmortizationPayment {
679            date: d("2025-06-30"),
680            principal_payment: dec!(500000),
681            interest_payment: dec!(25000),
682            balance_after: dec!(500000),
683        }])
684        .with_covenant(DebtCovenant::new(
685            "COV-001",
686            CovenantType::DebtToEbitda,
687            dec!(3.5),
688            Frequency::Quarterly,
689            dec!(2.5),
690            d("2025-03-31"),
691        ))];
692
693        let di_count = exporter.export_debt_instruments(&instruments).unwrap();
694        let dc_count = exporter.export_debt_covenants(&instruments).unwrap();
695        let as_count = exporter
696            .export_amortization_schedules(&instruments)
697            .unwrap();
698
699        assert_eq!(di_count, 1);
700        assert_eq!(dc_count, 1);
701        assert_eq!(as_count, 1);
702
703        let dc_csv = std::fs::read_to_string(temp.path().join("debt_covenants.csv")).unwrap();
704        assert!(dc_csv.contains("DEBT-001")); // parent reference
705        assert!(dc_csv.contains("COV-001"));
706    }
707
708    #[test]
709    fn test_export_netting() {
710        let temp = TempDir::new().unwrap();
711        let exporter = TreasuryExporter::new(temp.path());
712
713        let runs = vec![NettingRun::new(
714            "NR-001",
715            d("2025-01-31"),
716            NettingCycle::Monthly,
717            "USD",
718            vec![
719                NettingPosition {
720                    entity_id: "C001".to_string(),
721                    gross_receivable: dec!(100000),
722                    gross_payable: dec!(60000),
723                    net_position: dec!(40000),
724                    settlement_direction: PayOrReceive::Receive,
725                },
726                NettingPosition {
727                    entity_id: "C002".to_string(),
728                    gross_receivable: dec!(60000),
729                    gross_payable: dec!(100000),
730                    net_position: dec!(-40000),
731                    settlement_direction: PayOrReceive::Pay,
732                },
733            ],
734        )];
735
736        let nr_count = exporter.export_netting_runs(&runs).unwrap();
737        let np_count = exporter.export_netting_positions(&runs).unwrap();
738
739        assert_eq!(nr_count, 1);
740        assert_eq!(np_count, 2);
741    }
742
743    #[test]
744    fn test_export_anomaly_labels() {
745        let temp = TempDir::new().unwrap();
746        let exporter = TreasuryExporter::new(temp.path());
747
748        let labels = vec![TreasuryAnomalyLabelRow {
749            id: "TANOM-001".to_string(),
750            anomaly_type: "hedge_ineffectiveness".to_string(),
751            severity: "high".to_string(),
752            document_type: "hedge_relationship".to_string(),
753            document_id: "HR-001".to_string(),
754            description: "Ratio 0.72 outside corridor".to_string(),
755            original_value: "0.95".to_string(),
756            anomalous_value: "0.72".to_string(),
757        }];
758
759        let count = exporter.export_anomaly_labels(&labels).unwrap();
760        assert_eq!(count, 1);
761
762        let content =
763            std::fs::read_to_string(temp.path().join("treasury_anomaly_labels.csv")).unwrap();
764        assert!(content.contains("TANOM-001"));
765        assert!(content.contains("hedge_ineffectiveness"));
766    }
767
768    #[test]
769    fn test_export_summary_total() {
770        let summary = TreasuryExportSummary {
771            cash_positions_count: 30,
772            cash_forecasts_count: 1,
773            cash_forecast_items_count: 15,
774            cash_pool_sweeps_count: 20,
775            hedging_instruments_count: 5,
776            hedge_relationships_count: 5,
777            debt_instruments_count: 2,
778            debt_covenants_count: 4,
779            amortization_schedules_count: 40,
780            bank_guarantees_count: 3,
781            netting_runs_count: 1,
782            netting_positions_count: 4,
783            anomaly_labels_count: 3,
784        };
785        assert_eq!(summary.total(), 133);
786    }
787}