use chrono::{Datelike, NaiveDate};
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum TransferPricingMethod {
#[default]
CostPlus,
ComparableUncontrolled,
ResalePrice,
TransactionalNetMargin,
ProfitSplit,
FixedFee,
}
impl TransferPricingMethod {
pub fn typical_margin_range(&self) -> (Decimal, Decimal) {
match self {
Self::CostPlus => (Decimal::new(3, 2), Decimal::new(15, 2)), Self::ComparableUncontrolled => (Decimal::ZERO, Decimal::ZERO), Self::ResalePrice => (Decimal::new(10, 2), Decimal::new(30, 2)), Self::TransactionalNetMargin => (Decimal::new(2, 2), Decimal::new(10, 2)), Self::ProfitSplit => (Decimal::new(40, 2), Decimal::new(60, 2)), Self::FixedFee => (Decimal::ZERO, Decimal::ZERO), }
}
pub fn is_cost_based(&self) -> bool {
matches!(self, Self::CostPlus | Self::TransactionalNetMargin)
}
pub fn requires_comparables(&self) -> bool {
matches!(
self,
Self::ComparableUncontrolled | Self::ResalePrice | Self::TransactionalNetMargin
)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TransferPricingPolicy {
pub policy_id: String,
pub name: String,
pub method: TransferPricingMethod,
pub markup_percent: Decimal,
pub min_markup_percent: Option<Decimal>,
pub max_markup_percent: Option<Decimal>,
pub applicable_transaction_types: Vec<String>,
pub effective_date: NaiveDate,
pub end_date: Option<NaiveDate>,
pub fee_currency: Option<String>,
pub fixed_fee_amount: Option<Decimal>,
pub documentation_requirements: DocumentationLevel,
pub requires_annual_benchmarking: bool,
}
impl TransferPricingPolicy {
pub fn new_cost_plus(
policy_id: String,
name: String,
markup_percent: Decimal,
effective_date: NaiveDate,
) -> Self {
Self {
policy_id,
name,
method: TransferPricingMethod::CostPlus,
markup_percent,
min_markup_percent: None,
max_markup_percent: None,
applicable_transaction_types: Vec::new(),
effective_date,
end_date: None,
fee_currency: None,
fixed_fee_amount: None,
documentation_requirements: DocumentationLevel::Standard,
requires_annual_benchmarking: false,
}
}
pub fn new_fixed_fee(
policy_id: String,
name: String,
fee_amount: Decimal,
currency: String,
effective_date: NaiveDate,
) -> Self {
Self {
policy_id,
name,
method: TransferPricingMethod::FixedFee,
markup_percent: Decimal::ZERO,
min_markup_percent: None,
max_markup_percent: None,
applicable_transaction_types: Vec::new(),
effective_date,
end_date: None,
fee_currency: Some(currency),
fixed_fee_amount: Some(fee_amount),
documentation_requirements: DocumentationLevel::Standard,
requires_annual_benchmarking: false,
}
}
pub fn is_active_on(&self, date: NaiveDate) -> bool {
date >= self.effective_date && self.end_date.is_none_or(|end| date <= end)
}
pub fn calculate_transfer_price(&self, cost: Decimal) -> Decimal {
match self.method {
TransferPricingMethod::CostPlus => {
cost * (Decimal::ONE + self.markup_percent / Decimal::from(100))
}
TransferPricingMethod::FixedFee => self.fixed_fee_amount.unwrap_or(Decimal::ZERO),
TransferPricingMethod::ResalePrice => {
cost / (Decimal::ONE - self.markup_percent / Decimal::from(100))
}
TransferPricingMethod::TransactionalNetMargin => {
cost * (Decimal::ONE + self.markup_percent / Decimal::from(100))
}
TransferPricingMethod::ProfitSplit => {
let industry_margin = Decimal::new(15, 2); let total_profit = cost * industry_margin;
let seller_share = total_profit * self.markup_percent / Decimal::from(100);
cost + seller_share
}
TransferPricingMethod::ComparableUncontrolled => {
cost * (Decimal::ONE + self.markup_percent / Decimal::from(100))
}
}
}
pub fn calculate_markup(&self, cost: Decimal) -> Decimal {
self.calculate_transfer_price(cost) - cost
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum DocumentationLevel {
Minimal,
#[default]
Standard,
Comprehensive,
CbCR,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TransferPriceCalculation {
pub policy_id: String,
pub base_amount: Decimal,
pub transfer_price: Decimal,
pub markup_amount: Decimal,
pub effective_markup_percent: Decimal,
pub currency: String,
pub calculation_date: NaiveDate,
pub is_arms_length: bool,
}
impl TransferPriceCalculation {
pub fn new(
policy: &TransferPricingPolicy,
base_amount: Decimal,
currency: String,
calculation_date: NaiveDate,
) -> Self {
let transfer_price = policy.calculate_transfer_price(base_amount);
let markup_amount = transfer_price - base_amount;
let effective_markup_percent = if base_amount != Decimal::ZERO {
(markup_amount / base_amount) * Decimal::from(100)
} else {
Decimal::ZERO
};
let is_arms_length = match (policy.min_markup_percent, policy.max_markup_percent) {
(Some(min), Some(max)) => {
effective_markup_percent >= min && effective_markup_percent <= max
}
_ => true, };
Self {
policy_id: policy.policy_id.clone(),
base_amount,
transfer_price,
markup_amount,
effective_markup_percent,
currency,
calculation_date,
is_arms_length,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ArmsLengthRange {
pub lower_quartile: Decimal,
pub median: Decimal,
pub upper_quartile: Decimal,
pub iqr: Decimal,
pub comparable_count: usize,
pub study_date: NaiveDate,
pub validity_months: u32,
}
impl ArmsLengthRange {
pub fn new(
lower_quartile: Decimal,
median: Decimal,
upper_quartile: Decimal,
comparable_count: usize,
study_date: NaiveDate,
) -> Self {
Self {
lower_quartile,
median,
upper_quartile,
iqr: upper_quartile - lower_quartile,
comparable_count,
study_date,
validity_months: 36, }
}
pub fn is_within_range(&self, margin: Decimal) -> bool {
margin >= self.lower_quartile && margin <= self.upper_quartile
}
pub fn is_valid_on(&self, date: NaiveDate) -> bool {
let months_elapsed = (date.year() - self.study_date.year()) * 12
+ (date.month() as i32 - self.study_date.month() as i32);
months_elapsed >= 0 && (months_elapsed as u32) <= self.validity_months
}
pub fn get_adjustment(&self, margin: Decimal) -> Option<Decimal> {
if margin < self.lower_quartile || margin > self.upper_quartile {
Some(self.median - margin)
} else {
None
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TransferPricingAdjustment {
pub adjustment_id: String,
pub policy_id: String,
pub seller_company: String,
pub buyer_company: String,
pub fiscal_year: i32,
pub original_amount: Decimal,
pub adjusted_amount: Decimal,
pub adjustment_amount: Decimal,
pub currency: String,
pub adjustment_reason: AdjustmentReason,
pub adjustment_date: NaiveDate,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum AdjustmentReason {
YearEndTrueUp,
BenchmarkingUpdate,
TaxAuthorityAdjustment,
ApaPricing,
CompetentAuthority,
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
use rust_decimal_macros::dec;
#[test]
fn test_cost_plus_calculation() {
let policy = TransferPricingPolicy::new_cost_plus(
"TP001".to_string(),
"Standard Cost Plus".to_string(),
dec!(5), NaiveDate::from_ymd_opt(2022, 1, 1).unwrap(),
);
let cost = dec!(1000);
let transfer_price = policy.calculate_transfer_price(cost);
assert_eq!(transfer_price, dec!(1050));
let markup = policy.calculate_markup(cost);
assert_eq!(markup, dec!(50));
}
#[test]
fn test_fixed_fee_policy() {
let policy = TransferPricingPolicy::new_fixed_fee(
"TP002".to_string(),
"Management Fee".to_string(),
dec!(50000),
"USD".to_string(),
NaiveDate::from_ymd_opt(2022, 1, 1).unwrap(),
);
assert_eq!(policy.calculate_transfer_price(dec!(0)), dec!(50000));
assert_eq!(policy.calculate_transfer_price(dec!(100000)), dec!(50000));
}
#[test]
fn test_transfer_price_calculation() {
let policy = TransferPricingPolicy::new_cost_plus(
"TP001".to_string(),
"Cost Plus 8%".to_string(),
dec!(8),
NaiveDate::from_ymd_opt(2022, 1, 1).unwrap(),
);
let calc = TransferPriceCalculation::new(
&policy,
dec!(10000),
"USD".to_string(),
NaiveDate::from_ymd_opt(2022, 6, 15).unwrap(),
);
assert_eq!(calc.base_amount, dec!(10000));
assert_eq!(calc.transfer_price, dec!(10800));
assert_eq!(calc.markup_amount, dec!(800));
assert_eq!(calc.effective_markup_percent, dec!(8));
assert!(calc.is_arms_length);
}
#[test]
fn test_arms_length_range() {
let range = ArmsLengthRange::new(
dec!(3),
dec!(5),
dec!(8),
15,
NaiveDate::from_ymd_opt(2022, 1, 1).unwrap(),
);
assert!(range.is_within_range(dec!(5)));
assert!(range.is_within_range(dec!(3)));
assert!(range.is_within_range(dec!(8)));
assert!(!range.is_within_range(dec!(2)));
assert!(!range.is_within_range(dec!(10)));
assert_eq!(range.get_adjustment(dec!(1)), Some(dec!(4))); assert_eq!(range.get_adjustment(dec!(5)), None); }
#[test]
fn test_policy_active_date() {
let mut policy = TransferPricingPolicy::new_cost_plus(
"TP001".to_string(),
"Test Policy".to_string(),
dec!(5),
NaiveDate::from_ymd_opt(2022, 1, 1).unwrap(),
);
policy.end_date = Some(NaiveDate::from_ymd_opt(2023, 12, 31).unwrap());
assert!(policy.is_active_on(NaiveDate::from_ymd_opt(2022, 6, 15).unwrap()));
assert!(!policy.is_active_on(NaiveDate::from_ymd_opt(2021, 12, 31).unwrap()));
assert!(!policy.is_active_on(NaiveDate::from_ymd_opt(2024, 1, 1).unwrap()));
}
}