1use chrono::NaiveDate;
10use rust_decimal::Decimal;
11use rust_decimal_macros::dec;
12use std::collections::HashMap;
13
14#[derive(Debug, Clone, PartialEq, Eq, Hash)]
16pub struct CurrencyPair {
17 pub from_currency: String,
19 pub to_currency: String,
21}
22
23impl CurrencyPair {
24 #[allow(clippy::too_many_arguments)]
26 pub fn new(from: &str, to: &str) -> Self {
27 Self {
28 from_currency: from.to_uppercase(),
29 to_currency: to.to_uppercase(),
30 }
31 }
32
33 pub fn inverse(&self) -> Self {
35 Self {
36 from_currency: self.to_currency.clone(),
37 to_currency: self.from_currency.clone(),
38 }
39 }
40
41 pub fn as_string(&self) -> String {
43 format!("{}/{}", self.from_currency, self.to_currency)
44 }
45
46 pub fn is_same_currency(&self) -> bool {
48 self.from_currency == self.to_currency
49 }
50}
51
52impl std::fmt::Display for CurrencyPair {
53 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
54 write!(f, "{}/{}", self.from_currency, self.to_currency)
55 }
56}
57
58#[derive(Debug, Clone, PartialEq, Eq, Hash)]
60pub enum RateType {
61 Spot,
63 Closing,
65 Average,
67 Budget,
69 Historical,
71 Negotiated,
73 Custom(String),
75}
76
77impl std::fmt::Display for RateType {
78 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
79 match self {
80 RateType::Spot => write!(f, "SPOT"),
81 RateType::Closing => write!(f, "CLOSING"),
82 RateType::Average => write!(f, "AVERAGE"),
83 RateType::Budget => write!(f, "BUDGET"),
84 RateType::Historical => write!(f, "HISTORICAL"),
85 RateType::Negotiated => write!(f, "NEGOTIATED"),
86 RateType::Custom(s) => write!(f, "{}", s),
87 }
88 }
89}
90
91#[derive(Debug, Clone)]
93pub struct FxRate {
94 pub pair: CurrencyPair,
96 pub rate_type: RateType,
98 pub effective_date: NaiveDate,
100 pub rate: Decimal,
102 pub inverse_rate: Decimal,
104 pub source: String,
106 pub valid_until: Option<NaiveDate>,
108}
109
110impl FxRate {
111 pub fn new(
113 from_currency: &str,
114 to_currency: &str,
115 rate_type: RateType,
116 effective_date: NaiveDate,
117 rate: Decimal,
118 source: &str,
119 ) -> Self {
120 let inverse = if rate > Decimal::ZERO {
121 (Decimal::ONE / rate).round_dp(6)
122 } else {
123 Decimal::ZERO
124 };
125
126 Self {
127 pair: CurrencyPair::new(from_currency, to_currency),
128 rate_type,
129 effective_date,
130 rate,
131 inverse_rate: inverse,
132 source: source.to_string(),
133 valid_until: None,
134 }
135 }
136
137 pub fn with_validity(mut self, valid_until: NaiveDate) -> Self {
139 self.valid_until = Some(valid_until);
140 self
141 }
142
143 pub fn convert(&self, amount: Decimal) -> Decimal {
145 (amount * self.rate).round_dp(2)
146 }
147
148 pub fn convert_inverse(&self, amount: Decimal) -> Decimal {
150 (amount * self.inverse_rate).round_dp(2)
151 }
152
153 pub fn is_valid_on(&self, date: NaiveDate) -> bool {
155 if date < self.effective_date {
156 return false;
157 }
158 if let Some(valid_until) = self.valid_until {
159 date <= valid_until
160 } else {
161 true
162 }
163 }
164}
165
166#[derive(Debug, Clone, Default)]
168pub struct FxRateTable {
169 rates: HashMap<(String, String), Vec<FxRate>>,
171 pub base_currency: String,
173}
174
175impl FxRateTable {
176 pub fn new(base_currency: &str) -> Self {
178 Self {
179 rates: HashMap::new(),
180 base_currency: base_currency.to_uppercase(),
181 }
182 }
183
184 pub fn add_rate(&mut self, rate: FxRate) {
186 let key = (
187 format!("{}_{}", rate.pair.from_currency, rate.pair.to_currency),
188 rate.rate_type.to_string(),
189 );
190 self.rates.entry(key).or_default().push(rate);
191 }
192
193 pub fn get_rate(
195 &self,
196 from_currency: &str,
197 to_currency: &str,
198 rate_type: &RateType,
199 date: NaiveDate,
200 ) -> Option<&FxRate> {
201 if from_currency.to_uppercase() == to_currency.to_uppercase() {
203 return None;
204 }
205
206 let key = (
207 format!(
208 "{}_{}",
209 from_currency.to_uppercase(),
210 to_currency.to_uppercase()
211 ),
212 rate_type.to_string(),
213 );
214
215 self.rates.get(&key).and_then(|rates| {
216 rates
217 .iter()
218 .filter(|r| r.is_valid_on(date))
219 .max_by_key(|r| r.effective_date)
220 })
221 }
222
223 pub fn get_closing_rate(
225 &self,
226 from_currency: &str,
227 to_currency: &str,
228 date: NaiveDate,
229 ) -> Option<&FxRate> {
230 self.get_rate(from_currency, to_currency, &RateType::Closing, date)
231 }
232
233 pub fn get_average_rate(
235 &self,
236 from_currency: &str,
237 to_currency: &str,
238 date: NaiveDate,
239 ) -> Option<&FxRate> {
240 self.get_rate(from_currency, to_currency, &RateType::Average, date)
241 }
242
243 pub fn get_spot_rate(
245 &self,
246 from_currency: &str,
247 to_currency: &str,
248 date: NaiveDate,
249 ) -> Option<&FxRate> {
250 self.get_rate(from_currency, to_currency, &RateType::Spot, date)
251 }
252
253 pub fn convert(
255 &self,
256 amount: Decimal,
257 from_currency: &str,
258 to_currency: &str,
259 rate_type: &RateType,
260 date: NaiveDate,
261 ) -> Option<Decimal> {
262 if from_currency.to_uppercase() == to_currency.to_uppercase() {
263 return Some(amount);
264 }
265
266 if let Some(rate) = self.get_rate(from_currency, to_currency, rate_type, date) {
268 return Some(rate.convert(amount));
269 }
270
271 if let Some(rate) = self.get_rate(to_currency, from_currency, rate_type, date) {
273 return Some(rate.convert_inverse(amount));
274 }
275
276 if from_currency.to_uppercase() != self.base_currency
278 && to_currency.to_uppercase() != self.base_currency
279 {
280 let to_base = self.get_rate(from_currency, &self.base_currency, rate_type, date);
281 let from_base = self.get_rate(&self.base_currency, to_currency, rate_type, date);
282
283 if let (Some(r1), Some(r2)) = (to_base, from_base) {
284 let base_amount = r1.convert(amount);
285 return Some(r2.convert(base_amount));
286 }
287 }
288
289 None
290 }
291
292 pub fn get_all_rates(&self, from_currency: &str, to_currency: &str) -> Vec<&FxRate> {
294 self.rates
295 .iter()
296 .filter(|((pair, _), _)| {
297 *pair
298 == format!(
299 "{}_{}",
300 from_currency.to_uppercase(),
301 to_currency.to_uppercase()
302 )
303 })
304 .flat_map(|(_, rates)| rates.iter())
305 .collect()
306 }
307
308 pub fn len(&self) -> usize {
310 self.rates.values().map(|v| v.len()).sum()
311 }
312
313 pub fn is_empty(&self) -> bool {
315 self.rates.is_empty()
316 }
317}
318
319#[derive(Debug, Clone, PartialEq, Eq)]
321pub enum TranslationMethod {
322 CurrentRate,
324 Temporal,
326 MonetaryNonMonetary,
328}
329
330#[derive(Debug, Clone, PartialEq, Eq)]
332pub enum TranslationAccountType {
333 Asset,
335 Liability,
337 Equity,
339 Revenue,
341 Expense,
343 RetainedEarnings,
345 CommonStock,
347 AdditionalPaidInCapital,
349}
350
351#[derive(Debug, Clone)]
353pub struct TranslatedAmount {
354 pub local_amount: Decimal,
356 pub local_currency: String,
358 pub group_amount: Decimal,
360 pub group_currency: String,
362 pub rate_used: Decimal,
364 pub rate_type: RateType,
366 pub translation_date: NaiveDate,
368}
369
370#[derive(Debug, Clone)]
372pub struct CTAEntry {
373 pub entry_id: String,
375 pub company_code: String,
377 pub local_currency: String,
379 pub group_currency: String,
381 pub fiscal_year: i32,
383 pub fiscal_period: u8,
385 pub period_end_date: NaiveDate,
387 pub cta_amount: Decimal,
389 pub opening_rate: Decimal,
391 pub closing_rate: Decimal,
393 pub average_rate: Decimal,
395 pub opening_net_assets_local: Decimal,
397 pub closing_net_assets_local: Decimal,
399 pub net_income_local: Decimal,
401 pub components: Vec<CTAComponent>,
403}
404
405#[derive(Debug, Clone)]
407pub struct CTAComponent {
408 pub description: String,
410 pub local_amount: Decimal,
412 pub rate: Decimal,
414 pub group_amount: Decimal,
416}
417
418impl CTAEntry {
419 pub fn new(
421 entry_id: String,
422 company_code: String,
423 local_currency: String,
424 group_currency: String,
425 fiscal_year: i32,
426 fiscal_period: u8,
427 period_end_date: NaiveDate,
428 ) -> Self {
429 Self {
430 entry_id,
431 company_code,
432 local_currency,
433 group_currency,
434 fiscal_year,
435 fiscal_period,
436 period_end_date,
437 cta_amount: Decimal::ZERO,
438 opening_rate: Decimal::ONE,
439 closing_rate: Decimal::ONE,
440 average_rate: Decimal::ONE,
441 opening_net_assets_local: Decimal::ZERO,
442 closing_net_assets_local: Decimal::ZERO,
443 net_income_local: Decimal::ZERO,
444 components: Vec::new(),
445 }
446 }
447
448 pub fn calculate_current_rate_method(&mut self) {
454 let closing_translated = self.closing_net_assets_local * self.closing_rate;
455 let opening_translated = self.opening_net_assets_local * self.opening_rate;
456 let income_translated = self.net_income_local * self.average_rate;
457
458 self.cta_amount = closing_translated - opening_translated - income_translated;
459
460 self.components = vec![
461 CTAComponent {
462 description: "Closing net assets at closing rate".to_string(),
463 local_amount: self.closing_net_assets_local,
464 rate: self.closing_rate,
465 group_amount: closing_translated,
466 },
467 CTAComponent {
468 description: "Opening net assets at opening rate".to_string(),
469 local_amount: self.opening_net_assets_local,
470 rate: self.opening_rate,
471 group_amount: opening_translated,
472 },
473 CTAComponent {
474 description: "Net income at average rate".to_string(),
475 local_amount: self.net_income_local,
476 rate: self.average_rate,
477 group_amount: income_translated,
478 },
479 ];
480 }
481}
482
483#[derive(Debug, Clone)]
485pub struct RealizedFxGainLoss {
486 pub document_number: String,
488 pub company_code: String,
490 pub transaction_date: NaiveDate,
492 pub settlement_date: NaiveDate,
494 pub transaction_currency: String,
496 pub local_currency: String,
498 pub original_amount: Decimal,
500 pub original_local_amount: Decimal,
502 pub settlement_local_amount: Decimal,
504 pub gain_loss: Decimal,
506 pub transaction_rate: Decimal,
508 pub settlement_rate: Decimal,
510}
511
512impl RealizedFxGainLoss {
513 #[allow(clippy::too_many_arguments)]
515 pub fn new(
516 document_number: String,
517 company_code: String,
518 transaction_date: NaiveDate,
519 settlement_date: NaiveDate,
520 transaction_currency: String,
521 local_currency: String,
522 original_amount: Decimal,
523 transaction_rate: Decimal,
524 settlement_rate: Decimal,
525 ) -> Self {
526 let original_local = (original_amount * transaction_rate).round_dp(2);
527 let settlement_local = (original_amount * settlement_rate).round_dp(2);
528 let gain_loss = settlement_local - original_local;
529
530 Self {
531 document_number,
532 company_code,
533 transaction_date,
534 settlement_date,
535 transaction_currency,
536 local_currency,
537 original_amount,
538 original_local_amount: original_local,
539 settlement_local_amount: settlement_local,
540 gain_loss,
541 transaction_rate,
542 settlement_rate,
543 }
544 }
545
546 pub fn is_gain(&self) -> bool {
548 self.gain_loss > Decimal::ZERO
549 }
550}
551
552#[derive(Debug, Clone)]
554pub struct UnrealizedFxGainLoss {
555 pub revaluation_id: String,
557 pub company_code: String,
559 pub revaluation_date: NaiveDate,
561 pub account_code: String,
563 pub document_number: String,
565 pub transaction_currency: String,
567 pub local_currency: String,
569 pub open_amount: Decimal,
571 pub book_value_local: Decimal,
573 pub revalued_local: Decimal,
575 pub gain_loss: Decimal,
577 pub original_rate: Decimal,
579 pub revaluation_rate: Decimal,
581}
582
583pub mod currencies {
585 pub const USD: &str = "USD";
586 pub const EUR: &str = "EUR";
587 pub const GBP: &str = "GBP";
588 pub const JPY: &str = "JPY";
589 pub const CHF: &str = "CHF";
590 pub const CAD: &str = "CAD";
591 pub const AUD: &str = "AUD";
592 pub const CNY: &str = "CNY";
593 pub const INR: &str = "INR";
594 pub const BRL: &str = "BRL";
595 pub const MXN: &str = "MXN";
596 pub const KRW: &str = "KRW";
597 pub const SGD: &str = "SGD";
598 pub const HKD: &str = "HKD";
599 pub const SEK: &str = "SEK";
600 pub const NOK: &str = "NOK";
601 pub const DKK: &str = "DKK";
602 pub const PLN: &str = "PLN";
603 pub const ZAR: &str = "ZAR";
604 pub const THB: &str = "THB";
605}
606
607pub fn base_rates_usd() -> HashMap<String, Decimal> {
609 let mut rates = HashMap::new();
610 rates.insert("EUR".to_string(), dec!(1.10)); rates.insert("GBP".to_string(), dec!(1.27)); rates.insert("JPY".to_string(), dec!(0.0067)); rates.insert("CHF".to_string(), dec!(1.13)); rates.insert("CAD".to_string(), dec!(0.74)); rates.insert("AUD".to_string(), dec!(0.65)); rates.insert("CNY".to_string(), dec!(0.14)); rates.insert("INR".to_string(), dec!(0.012)); rates.insert("BRL".to_string(), dec!(0.20)); rates.insert("MXN".to_string(), dec!(0.058)); rates.insert("KRW".to_string(), dec!(0.00075)); rates.insert("SGD".to_string(), dec!(0.75)); rates.insert("HKD".to_string(), dec!(0.128)); rates.insert("SEK".to_string(), dec!(0.095)); rates.insert("NOK".to_string(), dec!(0.093)); rates.insert("DKK".to_string(), dec!(0.147)); rates.insert("PLN".to_string(), dec!(0.25)); rates.insert("ZAR".to_string(), dec!(0.053)); rates.insert("THB".to_string(), dec!(0.028)); rates
630}
631
632#[cfg(test)]
633mod tests {
634 use super::*;
635
636 #[test]
637 fn test_currency_pair() {
638 let pair = CurrencyPair::new("EUR", "USD");
639 assert_eq!(pair.from_currency, "EUR");
640 assert_eq!(pair.to_currency, "USD");
641 assert_eq!(pair.as_string(), "EUR/USD");
642
643 let inverse = pair.inverse();
644 assert_eq!(inverse.from_currency, "USD");
645 assert_eq!(inverse.to_currency, "EUR");
646 }
647
648 #[test]
649 fn test_fx_rate_conversion() {
650 let rate = FxRate::new(
651 "EUR",
652 "USD",
653 RateType::Spot,
654 NaiveDate::from_ymd_opt(2024, 1, 15).unwrap(),
655 dec!(1.10),
656 "ECB",
657 );
658
659 let converted = rate.convert(dec!(100));
660 assert_eq!(converted, dec!(110.00));
661
662 let inverse = rate.convert_inverse(dec!(110));
663 assert_eq!(inverse, dec!(100.00));
664 }
665
666 #[test]
667 fn test_fx_rate_table() {
668 let mut table = FxRateTable::new("USD");
669
670 table.add_rate(FxRate::new(
671 "EUR",
672 "USD",
673 RateType::Spot,
674 NaiveDate::from_ymd_opt(2024, 1, 15).unwrap(),
675 dec!(1.10),
676 "ECB",
677 ));
678
679 let converted = table.convert(
680 dec!(100),
681 "EUR",
682 "USD",
683 &RateType::Spot,
684 NaiveDate::from_ymd_opt(2024, 1, 15).unwrap(),
685 );
686
687 assert_eq!(converted, Some(dec!(110.00)));
688 }
689
690 #[test]
691 fn test_cta_calculation() {
692 let mut cta = CTAEntry::new(
693 "CTA-001".to_string(),
694 "1200".to_string(),
695 "EUR".to_string(),
696 "USD".to_string(),
697 2024,
698 12,
699 NaiveDate::from_ymd_opt(2024, 12, 31).unwrap(),
700 );
701
702 cta.opening_net_assets_local = dec!(1000000);
703 cta.closing_net_assets_local = dec!(1100000);
704 cta.net_income_local = dec!(100000);
705 cta.opening_rate = dec!(1.08);
706 cta.closing_rate = dec!(1.12);
707 cta.average_rate = dec!(1.10);
708
709 cta.calculate_current_rate_method();
710
711 assert_eq!(cta.cta_amount, dec!(42000));
716 }
717}