datasynth_core/models/intercompany/
transfer_pricing.rs

1//! Transfer pricing models and policies for intercompany transactions.
2
3use chrono::{Datelike, NaiveDate};
4use rust_decimal::Decimal;
5use serde::{Deserialize, Serialize};
6
7/// Transfer pricing method for intercompany transactions.
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
9#[serde(rename_all = "snake_case")]
10pub enum TransferPricingMethod {
11    /// Cost plus a markup percentage.
12    #[default]
13    CostPlus,
14    /// Comparable uncontrolled price (market-based).
15    ComparableUncontrolled,
16    /// Resale price minus a margin.
17    ResalePrice,
18    /// Transactional net margin method.
19    TransactionalNetMargin,
20    /// Profit split method.
21    ProfitSplit,
22    /// Fixed fee arrangement.
23    FixedFee,
24}
25
26impl TransferPricingMethod {
27    /// Get the typical markup/margin range for this method.
28    pub fn typical_margin_range(&self) -> (Decimal, Decimal) {
29        match self {
30            Self::CostPlus => (Decimal::new(3, 2), Decimal::new(15, 2)), // 3-15%
31            Self::ComparableUncontrolled => (Decimal::ZERO, Decimal::ZERO), // Market price
32            Self::ResalePrice => (Decimal::new(10, 2), Decimal::new(30, 2)), // 10-30%
33            Self::TransactionalNetMargin => (Decimal::new(2, 2), Decimal::new(10, 2)), // 2-10%
34            Self::ProfitSplit => (Decimal::new(40, 2), Decimal::new(60, 2)), // 40-60% split
35            Self::FixedFee => (Decimal::ZERO, Decimal::ZERO),            // Fixed amount
36        }
37    }
38
39    /// Check if this method is cost-based.
40    pub fn is_cost_based(&self) -> bool {
41        matches!(self, Self::CostPlus | Self::TransactionalNetMargin)
42    }
43
44    /// Check if this method requires comparable data.
45    pub fn requires_comparables(&self) -> bool {
46        matches!(
47            self,
48            Self::ComparableUncontrolled | Self::ResalePrice | Self::TransactionalNetMargin
49        )
50    }
51}
52
53/// A transfer pricing policy applicable to intercompany transactions.
54#[derive(Debug, Clone, Serialize, Deserialize)]
55pub struct TransferPricingPolicy {
56    /// Unique policy identifier.
57    pub policy_id: String,
58    /// Policy name/description.
59    pub name: String,
60    /// Transfer pricing method used.
61    pub method: TransferPricingMethod,
62    /// Markup or margin percentage (interpretation depends on method).
63    pub markup_percent: Decimal,
64    /// Minimum markup (for range-based policies).
65    pub min_markup_percent: Option<Decimal>,
66    /// Maximum markup (for range-based policies).
67    pub max_markup_percent: Option<Decimal>,
68    /// Transaction types this policy applies to.
69    pub applicable_transaction_types: Vec<String>,
70    /// Effective date of the policy.
71    pub effective_date: NaiveDate,
72    /// End date of the policy (if replaced).
73    pub end_date: Option<NaiveDate>,
74    /// Currency for fixed fee policies.
75    pub fee_currency: Option<String>,
76    /// Fixed fee amount (for FixedFee method).
77    pub fixed_fee_amount: Option<Decimal>,
78    /// Documentation requirements.
79    pub documentation_requirements: DocumentationLevel,
80    /// Whether annual benchmarking is required.
81    pub requires_annual_benchmarking: bool,
82}
83
84impl TransferPricingPolicy {
85    /// Create a new cost-plus policy.
86    pub fn new_cost_plus(
87        policy_id: String,
88        name: String,
89        markup_percent: Decimal,
90        effective_date: NaiveDate,
91    ) -> Self {
92        Self {
93            policy_id,
94            name,
95            method: TransferPricingMethod::CostPlus,
96            markup_percent,
97            min_markup_percent: None,
98            max_markup_percent: None,
99            applicable_transaction_types: Vec::new(),
100            effective_date,
101            end_date: None,
102            fee_currency: None,
103            fixed_fee_amount: None,
104            documentation_requirements: DocumentationLevel::Standard,
105            requires_annual_benchmarking: false,
106        }
107    }
108
109    /// Create a new fixed fee policy.
110    pub fn new_fixed_fee(
111        policy_id: String,
112        name: String,
113        fee_amount: Decimal,
114        currency: String,
115        effective_date: NaiveDate,
116    ) -> Self {
117        Self {
118            policy_id,
119            name,
120            method: TransferPricingMethod::FixedFee,
121            markup_percent: Decimal::ZERO,
122            min_markup_percent: None,
123            max_markup_percent: None,
124            applicable_transaction_types: Vec::new(),
125            effective_date,
126            end_date: None,
127            fee_currency: Some(currency),
128            fixed_fee_amount: Some(fee_amount),
129            documentation_requirements: DocumentationLevel::Standard,
130            requires_annual_benchmarking: false,
131        }
132    }
133
134    /// Check if the policy is active on a given date.
135    pub fn is_active_on(&self, date: NaiveDate) -> bool {
136        date >= self.effective_date && self.end_date.map_or(true, |end| date <= end)
137    }
138
139    /// Calculate the transfer price for a given cost.
140    pub fn calculate_transfer_price(&self, cost: Decimal) -> Decimal {
141        match self.method {
142            TransferPricingMethod::CostPlus => {
143                cost * (Decimal::ONE + self.markup_percent / Decimal::from(100))
144            }
145            TransferPricingMethod::FixedFee => self.fixed_fee_amount.unwrap_or(Decimal::ZERO),
146            TransferPricingMethod::ResalePrice => {
147                // For resale price, markup is actually a margin to subtract
148                cost / (Decimal::ONE - self.markup_percent / Decimal::from(100))
149            }
150            TransferPricingMethod::TransactionalNetMargin => {
151                cost * (Decimal::ONE + self.markup_percent / Decimal::from(100))
152            }
153            TransferPricingMethod::ProfitSplit => {
154                // Profit split: apply the markup_percent as the seller's profit share
155                // For example, if markup_percent is 50, seller keeps 50% of total profit
156                // We approximate total profit as a percentage of cost (typical 10-20% industry margin)
157                let industry_margin = Decimal::new(15, 2); // Assume 15% industry profit
158                let total_profit = cost * industry_margin;
159                let seller_share = total_profit * self.markup_percent / Decimal::from(100);
160                cost + seller_share
161            }
162            TransferPricingMethod::ComparableUncontrolled => {
163                // Comparable uncontrolled price: use market adjustment
164                // The markup_percent represents market price premium/discount vs cost
165                // Positive = market price above cost, negative = below cost
166                cost * (Decimal::ONE + self.markup_percent / Decimal::from(100))
167            }
168        }
169    }
170
171    /// Calculate the markup amount for a given cost.
172    pub fn calculate_markup(&self, cost: Decimal) -> Decimal {
173        self.calculate_transfer_price(cost) - cost
174    }
175}
176
177/// Level of documentation required for transfer pricing compliance.
178#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
179#[serde(rename_all = "snake_case")]
180pub enum DocumentationLevel {
181    /// Minimal documentation.
182    Minimal,
183    /// Standard documentation.
184    #[default]
185    Standard,
186    /// Comprehensive documentation (for high-risk transactions).
187    Comprehensive,
188    /// Country-by-country reporting level.
189    CbCR,
190}
191
192/// Result of a transfer pricing calculation.
193#[derive(Debug, Clone, Serialize, Deserialize)]
194pub struct TransferPriceCalculation {
195    /// The policy used for calculation.
196    pub policy_id: String,
197    /// Original cost/base amount.
198    pub base_amount: Decimal,
199    /// Calculated transfer price.
200    pub transfer_price: Decimal,
201    /// Markup/margin amount.
202    pub markup_amount: Decimal,
203    /// Effective markup percentage.
204    pub effective_markup_percent: Decimal,
205    /// Currency of the amounts.
206    pub currency: String,
207    /// Date of calculation.
208    pub calculation_date: NaiveDate,
209    /// Whether the price is within arm's length range.
210    pub is_arms_length: bool,
211}
212
213impl TransferPriceCalculation {
214    /// Create a new transfer price calculation.
215    pub fn new(
216        policy: &TransferPricingPolicy,
217        base_amount: Decimal,
218        currency: String,
219        calculation_date: NaiveDate,
220    ) -> Self {
221        let transfer_price = policy.calculate_transfer_price(base_amount);
222        let markup_amount = transfer_price - base_amount;
223        let effective_markup_percent = if base_amount != Decimal::ZERO {
224            (markup_amount / base_amount) * Decimal::from(100)
225        } else {
226            Decimal::ZERO
227        };
228
229        // Check if within arm's length range
230        let is_arms_length = match (policy.min_markup_percent, policy.max_markup_percent) {
231            (Some(min), Some(max)) => {
232                effective_markup_percent >= min && effective_markup_percent <= max
233            }
234            _ => true, // No range specified, assume compliant
235        };
236
237        Self {
238            policy_id: policy.policy_id.clone(),
239            base_amount,
240            transfer_price,
241            markup_amount,
242            effective_markup_percent,
243            currency,
244            calculation_date,
245            is_arms_length,
246        }
247    }
248}
249
250/// Arm's length range for benchmarking.
251#[derive(Debug, Clone, Serialize, Deserialize)]
252pub struct ArmsLengthRange {
253    /// Lower quartile (25th percentile).
254    pub lower_quartile: Decimal,
255    /// Median (50th percentile).
256    pub median: Decimal,
257    /// Upper quartile (75th percentile).
258    pub upper_quartile: Decimal,
259    /// Interquartile range.
260    pub iqr: Decimal,
261    /// Number of comparables used.
262    pub comparable_count: usize,
263    /// Date of the benchmarking study.
264    pub study_date: NaiveDate,
265    /// Validity period in months.
266    pub validity_months: u32,
267}
268
269impl ArmsLengthRange {
270    /// Create a new arm's length range from benchmarking data.
271    pub fn new(
272        lower_quartile: Decimal,
273        median: Decimal,
274        upper_quartile: Decimal,
275        comparable_count: usize,
276        study_date: NaiveDate,
277    ) -> Self {
278        Self {
279            lower_quartile,
280            median,
281            upper_quartile,
282            iqr: upper_quartile - lower_quartile,
283            comparable_count,
284            study_date,
285            validity_months: 36, // Typical 3-year validity
286        }
287    }
288
289    /// Check if a margin falls within the arm's length range.
290    pub fn is_within_range(&self, margin: Decimal) -> bool {
291        margin >= self.lower_quartile && margin <= self.upper_quartile
292    }
293
294    /// Check if the range is still valid.
295    pub fn is_valid_on(&self, date: NaiveDate) -> bool {
296        let months_elapsed = (date.year() - self.study_date.year()) * 12
297            + (date.month() as i32 - self.study_date.month() as i32);
298        months_elapsed >= 0 && (months_elapsed as u32) <= self.validity_months
299    }
300
301    /// Get adjustment needed to bring margin within range.
302    pub fn get_adjustment(&self, margin: Decimal) -> Option<Decimal> {
303        if margin < self.lower_quartile || margin > self.upper_quartile {
304            Some(self.median - margin)
305        } else {
306            None
307        }
308    }
309}
310
311/// Transfer pricing adjustment for year-end true-up.
312#[derive(Debug, Clone, Serialize, Deserialize)]
313pub struct TransferPricingAdjustment {
314    /// Adjustment identifier.
315    pub adjustment_id: String,
316    /// Related policy.
317    pub policy_id: String,
318    /// Seller company.
319    pub seller_company: String,
320    /// Buyer company.
321    pub buyer_company: String,
322    /// Fiscal year of adjustment.
323    pub fiscal_year: i32,
324    /// Original aggregate transfer prices.
325    pub original_amount: Decimal,
326    /// Adjusted aggregate transfer prices.
327    pub adjusted_amount: Decimal,
328    /// Net adjustment amount.
329    pub adjustment_amount: Decimal,
330    /// Currency.
331    pub currency: String,
332    /// Reason for adjustment.
333    pub adjustment_reason: AdjustmentReason,
334    /// Date of adjustment.
335    pub adjustment_date: NaiveDate,
336}
337
338/// Reason for transfer pricing adjustment.
339#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
340#[serde(rename_all = "snake_case")]
341pub enum AdjustmentReason {
342    /// Year-end true-up to target margin.
343    YearEndTrueUp,
344    /// Benchmarking study update.
345    BenchmarkingUpdate,
346    /// Tax authority adjustment.
347    TaxAuthorityAdjustment,
348    /// Advance pricing agreement.
349    ApaPricing,
350    /// Competent authority resolution.
351    CompetentAuthority,
352}
353
354#[cfg(test)]
355mod tests {
356    use super::*;
357    use rust_decimal_macros::dec;
358
359    #[test]
360    fn test_cost_plus_calculation() {
361        let policy = TransferPricingPolicy::new_cost_plus(
362            "TP001".to_string(),
363            "Standard Cost Plus".to_string(),
364            dec!(5), // 5% markup
365            NaiveDate::from_ymd_opt(2022, 1, 1).unwrap(),
366        );
367
368        let cost = dec!(1000);
369        let transfer_price = policy.calculate_transfer_price(cost);
370        assert_eq!(transfer_price, dec!(1050));
371
372        let markup = policy.calculate_markup(cost);
373        assert_eq!(markup, dec!(50));
374    }
375
376    #[test]
377    fn test_fixed_fee_policy() {
378        let policy = TransferPricingPolicy::new_fixed_fee(
379            "TP002".to_string(),
380            "Management Fee".to_string(),
381            dec!(50000),
382            "USD".to_string(),
383            NaiveDate::from_ymd_opt(2022, 1, 1).unwrap(),
384        );
385
386        // Fixed fee ignores cost
387        assert_eq!(policy.calculate_transfer_price(dec!(0)), dec!(50000));
388        assert_eq!(policy.calculate_transfer_price(dec!(100000)), dec!(50000));
389    }
390
391    #[test]
392    fn test_transfer_price_calculation() {
393        let policy = TransferPricingPolicy::new_cost_plus(
394            "TP001".to_string(),
395            "Cost Plus 8%".to_string(),
396            dec!(8),
397            NaiveDate::from_ymd_opt(2022, 1, 1).unwrap(),
398        );
399
400        let calc = TransferPriceCalculation::new(
401            &policy,
402            dec!(10000),
403            "USD".to_string(),
404            NaiveDate::from_ymd_opt(2022, 6, 15).unwrap(),
405        );
406
407        assert_eq!(calc.base_amount, dec!(10000));
408        assert_eq!(calc.transfer_price, dec!(10800));
409        assert_eq!(calc.markup_amount, dec!(800));
410        assert_eq!(calc.effective_markup_percent, dec!(8));
411        assert!(calc.is_arms_length);
412    }
413
414    #[test]
415    fn test_arms_length_range() {
416        let range = ArmsLengthRange::new(
417            dec!(3),
418            dec!(5),
419            dec!(8),
420            15,
421            NaiveDate::from_ymd_opt(2022, 1, 1).unwrap(),
422        );
423
424        assert!(range.is_within_range(dec!(5)));
425        assert!(range.is_within_range(dec!(3)));
426        assert!(range.is_within_range(dec!(8)));
427        assert!(!range.is_within_range(dec!(2)));
428        assert!(!range.is_within_range(dec!(10)));
429
430        // Check adjustment
431        assert_eq!(range.get_adjustment(dec!(1)), Some(dec!(4))); // Need to increase by 4
432        assert_eq!(range.get_adjustment(dec!(5)), None); // Within range
433    }
434
435    #[test]
436    fn test_policy_active_date() {
437        let mut policy = TransferPricingPolicy::new_cost_plus(
438            "TP001".to_string(),
439            "Test Policy".to_string(),
440            dec!(5),
441            NaiveDate::from_ymd_opt(2022, 1, 1).unwrap(),
442        );
443        policy.end_date = Some(NaiveDate::from_ymd_opt(2023, 12, 31).unwrap());
444
445        assert!(policy.is_active_on(NaiveDate::from_ymd_opt(2022, 6, 15).unwrap()));
446        assert!(!policy.is_active_on(NaiveDate::from_ymd_opt(2021, 12, 31).unwrap()));
447        assert!(!policy.is_active_on(NaiveDate::from_ymd_opt(2024, 1, 1).unwrap()));
448    }
449}