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)]
533#[allow(clippy::unwrap_used)]
534mod tests {
535    use super::*;
536    use chrono::NaiveDate;
537    use rust_decimal_macros::dec;
538    use tempfile::TempDir;
539
540    use datasynth_core::models::{
541        AmortizationPayment, CovenantType, DebtCovenant, DebtType, EffectivenessMethod, Frequency,
542        HedgeInstrumentType, HedgeType, HedgedItemType, InterestRateType, NettingCycle,
543        NettingPosition, PayOrReceive, TreasuryCashFlowCategory,
544    };
545
546    fn d(s: &str) -> NaiveDate {
547        NaiveDate::parse_from_str(s, "%Y-%m-%d").unwrap()
548    }
549
550    #[test]
551    fn test_export_cash_positions() {
552        let temp = TempDir::new().unwrap();
553        let exporter = TreasuryExporter::new(temp.path());
554
555        let positions = vec![
556            CashPosition::new(
557                "CP-001",
558                "C001",
559                "BA-001",
560                "USD",
561                d("2025-01-15"),
562                dec!(100000),
563                dec!(5000),
564                dec!(2000),
565            ),
566            CashPosition::new(
567                "CP-002",
568                "C001",
569                "BA-001",
570                "USD",
571                d("2025-01-16"),
572                dec!(103000),
573                dec!(0),
574                dec!(1000),
575            ),
576        ];
577
578        let count = exporter.export_cash_positions(&positions).unwrap();
579        assert_eq!(count, 2);
580
581        let content = std::fs::read_to_string(temp.path().join("cash_positions.csv")).unwrap();
582        assert_eq!(content.lines().count(), 3); // header + 2 rows
583        assert!(content.contains("CP-001"));
584        assert!(content.contains("CP-002"));
585        assert!(content.contains("opening_balance"));
586    }
587
588    #[test]
589    fn test_export_cash_forecasts_and_items() {
590        let temp = TempDir::new().unwrap();
591        let exporter = TreasuryExporter::new(temp.path());
592
593        let items = vec![datasynth_core::models::CashForecastItem {
594            id: "CFI-001".to_string(),
595            date: d("2025-02-15"),
596            category: TreasuryCashFlowCategory::ArCollection,
597            amount: dec!(50000),
598            probability: dec!(0.90),
599            source_document_type: Some("SalesOrder".to_string()),
600            source_document_id: Some("SO-001".to_string()),
601        }];
602        let forecasts = vec![CashForecast::new(
603            "CF-001",
604            "C001",
605            "USD",
606            d("2025-01-31"),
607            90,
608            items,
609            dec!(0.90),
610        )];
611
612        let fc_count = exporter.export_cash_forecasts(&forecasts).unwrap();
613        let fi_count = exporter.export_cash_forecast_items(&forecasts).unwrap();
614
615        assert_eq!(fc_count, 1);
616        assert_eq!(fi_count, 1);
617
618        let fc_content = std::fs::read_to_string(temp.path().join("cash_forecasts.csv")).unwrap();
619        assert!(fc_content.contains("CF-001"));
620
621        let fi_content =
622            std::fs::read_to_string(temp.path().join("cash_forecast_items.csv")).unwrap();
623        assert!(fi_content.contains("CFI-001"));
624        assert!(fi_content.contains("CF-001")); // parent reference
625    }
626
627    #[test]
628    fn test_export_hedging_and_relationships() {
629        let temp = TempDir::new().unwrap();
630        let exporter = TreasuryExporter::new(temp.path());
631
632        let instruments = vec![HedgingInstrument::new(
633            "HI-001",
634            HedgeInstrumentType::FxForward,
635            dec!(1000000),
636            "EUR",
637            d("2025-01-01"),
638            d("2025-06-30"),
639            "Deutsche Bank",
640        )
641        .with_currency_pair("EUR/USD")
642        .with_fixed_rate(dec!(1.0850))];
643
644        let relationships = vec![HedgeRelationship::new(
645            "HR-001",
646            HedgedItemType::ForecastedTransaction,
647            "EUR receivables Q2",
648            "HI-001",
649            HedgeType::CashFlowHedge,
650            d("2025-01-01"),
651            EffectivenessMethod::Regression,
652            dec!(0.95),
653        )];
654
655        let hi_count = exporter.export_hedging_instruments(&instruments).unwrap();
656        let hr_count = exporter.export_hedge_relationships(&relationships).unwrap();
657
658        assert_eq!(hi_count, 1);
659        assert_eq!(hr_count, 1);
660    }
661
662    #[test]
663    fn test_export_debt_with_covenants_and_amortization() {
664        let temp = TempDir::new().unwrap();
665        let exporter = TreasuryExporter::new(temp.path());
666
667        let instruments = vec![DebtInstrument::new(
668            "DEBT-001",
669            "C001",
670            DebtType::TermLoan,
671            "First Bank",
672            dec!(1000000),
673            "USD",
674            dec!(0.05),
675            InterestRateType::Fixed,
676            d("2025-01-01"),
677            d("2026-01-01"),
678        )
679        .with_amortization_schedule(vec![AmortizationPayment {
680            date: d("2025-06-30"),
681            principal_payment: dec!(500000),
682            interest_payment: dec!(25000),
683            balance_after: dec!(500000),
684        }])
685        .with_covenant(DebtCovenant::new(
686            "COV-001",
687            CovenantType::DebtToEbitda,
688            dec!(3.5),
689            Frequency::Quarterly,
690            dec!(2.5),
691            d("2025-03-31"),
692        ))];
693
694        let di_count = exporter.export_debt_instruments(&instruments).unwrap();
695        let dc_count = exporter.export_debt_covenants(&instruments).unwrap();
696        let as_count = exporter
697            .export_amortization_schedules(&instruments)
698            .unwrap();
699
700        assert_eq!(di_count, 1);
701        assert_eq!(dc_count, 1);
702        assert_eq!(as_count, 1);
703
704        let dc_csv = std::fs::read_to_string(temp.path().join("debt_covenants.csv")).unwrap();
705        assert!(dc_csv.contains("DEBT-001")); // parent reference
706        assert!(dc_csv.contains("COV-001"));
707    }
708
709    #[test]
710    fn test_export_netting() {
711        let temp = TempDir::new().unwrap();
712        let exporter = TreasuryExporter::new(temp.path());
713
714        let runs = vec![NettingRun::new(
715            "NR-001",
716            d("2025-01-31"),
717            NettingCycle::Monthly,
718            "USD",
719            vec![
720                NettingPosition {
721                    entity_id: "C001".to_string(),
722                    gross_receivable: dec!(100000),
723                    gross_payable: dec!(60000),
724                    net_position: dec!(40000),
725                    settlement_direction: PayOrReceive::Receive,
726                },
727                NettingPosition {
728                    entity_id: "C002".to_string(),
729                    gross_receivable: dec!(60000),
730                    gross_payable: dec!(100000),
731                    net_position: dec!(-40000),
732                    settlement_direction: PayOrReceive::Pay,
733                },
734            ],
735        )];
736
737        let nr_count = exporter.export_netting_runs(&runs).unwrap();
738        let np_count = exporter.export_netting_positions(&runs).unwrap();
739
740        assert_eq!(nr_count, 1);
741        assert_eq!(np_count, 2);
742    }
743
744    #[test]
745    fn test_export_anomaly_labels() {
746        let temp = TempDir::new().unwrap();
747        let exporter = TreasuryExporter::new(temp.path());
748
749        let labels = vec![TreasuryAnomalyLabelRow {
750            id: "TANOM-001".to_string(),
751            anomaly_type: "hedge_ineffectiveness".to_string(),
752            severity: "high".to_string(),
753            document_type: "hedge_relationship".to_string(),
754            document_id: "HR-001".to_string(),
755            description: "Ratio 0.72 outside corridor".to_string(),
756            original_value: "0.95".to_string(),
757            anomalous_value: "0.72".to_string(),
758        }];
759
760        let count = exporter.export_anomaly_labels(&labels).unwrap();
761        assert_eq!(count, 1);
762
763        let content =
764            std::fs::read_to_string(temp.path().join("treasury_anomaly_labels.csv")).unwrap();
765        assert!(content.contains("TANOM-001"));
766        assert!(content.contains("hedge_ineffectiveness"));
767    }
768
769    #[test]
770    fn test_export_summary_total() {
771        let summary = TreasuryExportSummary {
772            cash_positions_count: 30,
773            cash_forecasts_count: 1,
774            cash_forecast_items_count: 15,
775            cash_pool_sweeps_count: 20,
776            hedging_instruments_count: 5,
777            hedge_relationships_count: 5,
778            debt_instruments_count: 2,
779            debt_covenants_count: 4,
780            amortization_schedules_count: 40,
781            bank_guarantees_count: 3,
782            netting_runs_count: 1,
783            netting_positions_count: 4,
784            anomaly_labels_count: 3,
785        };
786        assert_eq!(summary.total(), 133);
787    }
788}