use crate::compat::Instant;
use rust_decimal::Decimal;
use rust_decimal_macros::dec;
use serde::{Deserialize, Serialize};
use crate::error::CorpFinanceError;
use crate::types::*;
use crate::CorpFinanceResult;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ConsiderationType {
AllCash,
AllStock,
Mixed {
cash_pct: Rate,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MergerInput {
pub acquirer_name: String,
pub acquirer_net_income: Money,
pub acquirer_shares_outstanding: Decimal,
pub acquirer_share_price: Money,
pub acquirer_tax_rate: Rate,
pub target_name: String,
pub target_net_income: Money,
pub target_shares_outstanding: Decimal,
pub target_share_price: Money,
pub offer_price_per_share: Money,
pub consideration: ConsiderationType,
pub revenue_synergies: Option<Money>,
pub cost_synergies: Option<Money>,
pub synergy_phase_in_pct: Option<Rate>,
pub integration_costs: Option<Money>,
pub debt_financing_rate: Option<Rate>,
pub foregone_interest_rate: Option<Rate>,
pub goodwill_amortisation: Option<Money>,
pub transaction_fees: Option<Money>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MergerOutput {
pub deal_value: Money,
pub premium_pct: Rate,
pub premium_amount: Money,
pub acquirer_eps_standalone: Money,
pub pro_forma_eps: Money,
pub eps_accretion_dilution: Money,
pub eps_accretion_dilution_pct: Rate,
pub is_accretive: bool,
pub exchange_ratio: Option<Decimal>,
pub new_shares_issued: Option<Decimal>,
pub pro_forma_shares: Decimal,
pub pro_forma_net_income: Money,
pub combined_net_income_pre_synergies: Money,
pub synergy_impact: Money,
pub financing_cost: Money,
pub breakeven_synergies: Money,
}
pub fn analyze_merger(input: &MergerInput) -> CorpFinanceResult<ComputationOutput<MergerOutput>> {
let start = Instant::now();
let mut warnings: Vec<String> = Vec::new();
validate_input(input)?;
let zero = Decimal::ZERO;
let deal_value = input.offer_price_per_share * input.target_shares_outstanding;
let premium_amount = input.offer_price_per_share - input.target_share_price;
let premium_pct = premium_amount / input.target_share_price;
if premium_pct < zero {
warnings.push("Offer price is below current target share price (negative premium)".into());
}
let acquirer_eps_standalone = input.acquirer_net_income / input.acquirer_shares_outstanding;
let (financing_cost, new_shares_issued, exchange_ratio) =
compute_consideration(input, deal_value, &mut warnings);
let pro_forma_shares = input.acquirer_shares_outstanding + new_shares_issued.unwrap_or(zero);
let combined_net_income_pre_synergies = input.acquirer_net_income + input.target_net_income;
let synergy_impact = compute_synergy_impact(input, &mut warnings);
let pro_forma_net_income = combined_net_income_pre_synergies - financing_cost + synergy_impact;
let pro_forma_eps = pro_forma_net_income / pro_forma_shares;
let eps_accretion_dilution = pro_forma_eps - acquirer_eps_standalone;
let eps_accretion_dilution_pct = if acquirer_eps_standalone != zero {
eps_accretion_dilution / acquirer_eps_standalone
} else {
zero
};
let is_accretive = eps_accretion_dilution >= zero;
let breakeven_synergies = compute_breakeven_synergies(
input,
combined_net_income_pre_synergies,
financing_cost,
acquirer_eps_standalone,
pro_forma_shares,
);
let output = MergerOutput {
deal_value,
premium_pct,
premium_amount,
acquirer_eps_standalone,
pro_forma_eps,
eps_accretion_dilution,
eps_accretion_dilution_pct,
is_accretive,
exchange_ratio,
new_shares_issued,
pro_forma_shares,
pro_forma_net_income,
combined_net_income_pre_synergies,
synergy_impact,
financing_cost,
breakeven_synergies,
};
let elapsed = start.elapsed().as_micros() as u64;
Ok(with_metadata(
"M&A Accretion/Dilution Analysis",
&serde_json::json!({
"acquirer": input.acquirer_name,
"target": input.target_name,
"consideration": format!("{:?}", input.consideration),
"offer_price": input.offer_price_per_share.to_string(),
}),
warnings,
elapsed,
output,
))
}
fn validate_input(input: &MergerInput) -> CorpFinanceResult<()> {
if input.acquirer_shares_outstanding <= Decimal::ZERO {
return Err(CorpFinanceError::InvalidInput {
field: "acquirer_shares_outstanding".into(),
reason: "Acquirer shares outstanding must be positive".into(),
});
}
if input.target_shares_outstanding <= Decimal::ZERO {
return Err(CorpFinanceError::InvalidInput {
field: "target_shares_outstanding".into(),
reason: "Target shares outstanding must be positive".into(),
});
}
if input.acquirer_share_price <= Decimal::ZERO {
return Err(CorpFinanceError::InvalidInput {
field: "acquirer_share_price".into(),
reason: "Acquirer share price must be positive".into(),
});
}
if input.target_share_price <= Decimal::ZERO {
return Err(CorpFinanceError::InvalidInput {
field: "target_share_price".into(),
reason: "Target share price must be positive".into(),
});
}
if input.offer_price_per_share <= Decimal::ZERO {
return Err(CorpFinanceError::InvalidInput {
field: "offer_price_per_share".into(),
reason: "Offer price per share must be positive".into(),
});
}
if input.acquirer_tax_rate < Decimal::ZERO || input.acquirer_tax_rate > dec!(1) {
return Err(CorpFinanceError::InvalidInput {
field: "acquirer_tax_rate".into(),
reason: "Tax rate must be between 0 and 1".into(),
});
}
if let ConsiderationType::Mixed { cash_pct } = &input.consideration {
if *cash_pct < Decimal::ZERO || *cash_pct > dec!(1) {
return Err(CorpFinanceError::InvalidInput {
field: "consideration.cash_pct".into(),
reason: "Cash percentage must be between 0 and 1".into(),
});
}
}
Ok(())
}
fn compute_consideration(
input: &MergerInput,
deal_value: Money,
warnings: &mut Vec<String>,
) -> (Money, Option<Decimal>, Option<Decimal>) {
let one = dec!(1);
let zero = Decimal::ZERO;
let after_tax_multiplier = one - input.acquirer_tax_rate;
match &input.consideration {
ConsiderationType::AllCash => {
let debt_cost = input
.debt_financing_rate
.map(|r| deal_value * r * after_tax_multiplier)
.unwrap_or(zero);
let foregone_cost = input
.foregone_interest_rate
.map(|r| deal_value * r * after_tax_multiplier)
.unwrap_or(zero);
if input.debt_financing_rate.is_none() && input.foregone_interest_rate.is_none() {
warnings.push(
"All-cash deal with no financing rate specified; financing cost is zero".into(),
);
}
let financing_cost = debt_cost + foregone_cost;
(financing_cost, None, None)
}
ConsiderationType::AllStock => {
let exchange_ratio = input.offer_price_per_share / input.acquirer_share_price;
let new_shares = input.target_shares_outstanding * exchange_ratio;
(zero, Some(new_shares), Some(exchange_ratio))
}
ConsiderationType::Mixed { cash_pct } => {
let cash_portion = deal_value * *cash_pct;
let debt_cost = input
.debt_financing_rate
.map(|r| cash_portion * r * after_tax_multiplier)
.unwrap_or(zero);
let foregone_cost = input
.foregone_interest_rate
.map(|r| cash_portion * r * after_tax_multiplier)
.unwrap_or(zero);
let financing_cost = debt_cost + foregone_cost;
let exchange_ratio = input.offer_price_per_share / input.acquirer_share_price;
let stock_pct = one - *cash_pct;
let new_shares = input.target_shares_outstanding * exchange_ratio * stock_pct;
(financing_cost, Some(new_shares), Some(exchange_ratio))
}
}
}
fn compute_synergy_impact(input: &MergerInput, warnings: &mut Vec<String>) -> Money {
let one = dec!(1);
let zero = Decimal::ZERO;
let gross_synergies =
input.cost_synergies.unwrap_or(zero) + input.revenue_synergies.unwrap_or(zero);
let phase_in = input.synergy_phase_in_pct.unwrap_or(one);
let after_tax_synergies = gross_synergies * phase_in * (one - input.acquirer_tax_rate);
let integration = input.integration_costs.unwrap_or(zero);
let goodwill = input.goodwill_amortisation.unwrap_or(zero);
let fees = input.transaction_fees.unwrap_or(zero);
if gross_synergies == zero && (integration > zero || goodwill > zero || fees > zero) {
warnings.push("No synergies specified but integration costs / fees are present".into());
}
after_tax_synergies - integration - goodwill - fees
}
fn compute_breakeven_synergies(
input: &MergerInput,
combined_ni: Money,
financing_cost: Money,
standalone_eps: Money,
pro_forma_shares: Decimal,
) -> Money {
let one = dec!(1);
let zero = Decimal::ZERO;
let phase_in = input.synergy_phase_in_pct.unwrap_or(one);
let after_tax_multiplier = (one - input.acquirer_tax_rate) * phase_in;
if after_tax_multiplier == zero {
return zero;
}
let integration = input.integration_costs.unwrap_or(zero);
let goodwill = input.goodwill_amortisation.unwrap_or(zero);
let fees = input.transaction_fees.unwrap_or(zero);
let target_ni = standalone_eps * pro_forma_shares;
let numerator = target_ni - combined_ni + financing_cost + integration + goodwill + fees;
let breakeven = numerator / after_tax_multiplier;
if breakeven < zero {
zero
} else {
breakeven
}
}
#[cfg(test)]
mod tests {
use super::*;
use rust_decimal_macros::dec;
fn base_input() -> MergerInput {
MergerInput {
acquirer_name: "AcquirerCo".into(),
acquirer_net_income: dec!(500),
acquirer_shares_outstanding: dec!(100),
acquirer_share_price: dec!(50),
acquirer_tax_rate: dec!(0.25),
target_name: "TargetCo".into(),
target_net_income: dec!(100),
target_shares_outstanding: dec!(50),
target_share_price: dec!(20),
offer_price_per_share: dec!(25),
consideration: ConsiderationType::AllCash,
revenue_synergies: None,
cost_synergies: None,
synergy_phase_in_pct: None,
integration_costs: None,
debt_financing_rate: Some(dec!(0.05)),
foregone_interest_rate: None,
goodwill_amortisation: None,
transaction_fees: None,
}
}
#[test]
fn test_all_cash_accretive() {
let input = base_input();
let result = analyze_merger(&input).unwrap();
let out = &result.result;
assert_eq!(out.deal_value, dec!(1250));
assert_eq!(out.financing_cost, dec!(46.875));
assert_eq!(out.combined_net_income_pre_synergies, dec!(600));
assert_eq!(out.pro_forma_net_income, dec!(553.125));
assert_eq!(out.pro_forma_shares, dec!(100));
assert!(out.new_shares_issued.is_none());
assert_eq!(out.acquirer_eps_standalone, dec!(5));
assert_eq!(out.pro_forma_eps, dec!(5.53125));
assert!(out.is_accretive);
assert!(out.eps_accretion_dilution > Decimal::ZERO);
}
#[test]
fn test_all_stock_dilutive() {
let mut input = base_input();
input.consideration = ConsiderationType::AllStock;
let result = analyze_merger(&input).unwrap();
let out = &result.result;
assert_eq!(out.exchange_ratio.unwrap(), dec!(0.5));
assert_eq!(out.new_shares_issued.unwrap(), dec!(25));
assert_eq!(out.pro_forma_shares, dec!(125));
assert_eq!(out.financing_cost, Decimal::ZERO);
assert_eq!(out.pro_forma_net_income, dec!(600));
assert_eq!(out.pro_forma_eps, dec!(4.8));
assert!(!out.is_accretive);
assert_eq!(out.eps_accretion_dilution, dec!(-0.2));
}
#[test]
fn test_all_stock_accretive() {
let mut input = base_input();
input.acquirer_share_price = dec!(80);
input.consideration = ConsiderationType::AllStock;
let result = analyze_merger(&input).unwrap();
let out = &result.result;
assert_eq!(out.exchange_ratio.unwrap(), dec!(0.3125));
assert_eq!(out.new_shares_issued.unwrap(), dec!(15.625));
assert_eq!(out.pro_forma_shares, dec!(115.625));
assert!(out.is_accretive);
assert!(out.pro_forma_eps > dec!(5));
}
#[test]
fn test_mixed_consideration() {
let mut input = base_input();
input.consideration = ConsiderationType::Mixed {
cash_pct: dec!(0.5),
};
input.debt_financing_rate = Some(dec!(0.05));
let result = analyze_merger(&input).unwrap();
let out = &result.result;
assert_eq!(out.deal_value, dec!(1250));
assert_eq!(out.financing_cost, dec!(23.4375));
assert_eq!(out.exchange_ratio.unwrap(), dec!(0.5));
assert_eq!(out.new_shares_issued.unwrap(), dec!(12.5));
assert_eq!(out.pro_forma_shares, dec!(112.5));
assert_eq!(out.pro_forma_net_income, dec!(576.5625));
assert_eq!(out.pro_forma_eps, dec!(5.125));
assert!(out.is_accretive);
}
#[test]
fn test_synergies_make_accretive() {
let mut input = base_input();
input.consideration = ConsiderationType::AllStock;
let result_no_syn = analyze_merger(&input).unwrap();
assert!(!result_no_syn.result.is_accretive);
input.cost_synergies = Some(dec!(50));
input.synergy_phase_in_pct = Some(dec!(1));
let result = analyze_merger(&input).unwrap();
let out = &result.result;
assert_eq!(out.synergy_impact, dec!(37.5));
assert_eq!(out.pro_forma_net_income, dec!(637.5));
assert_eq!(out.pro_forma_eps, dec!(5.1));
assert!(out.is_accretive);
}
#[test]
fn test_premium_calculation() {
let input = base_input();
let result = analyze_merger(&input).unwrap();
let out = &result.result;
assert_eq!(out.premium_pct, dec!(0.25));
assert_eq!(out.premium_amount, dec!(5));
}
#[test]
fn test_exchange_ratio() {
let mut input = base_input();
input.consideration = ConsiderationType::AllStock;
input.offer_price_per_share = dec!(30);
input.acquirer_share_price = dec!(60);
let result = analyze_merger(&input).unwrap();
let out = &result.result;
assert_eq!(out.exchange_ratio.unwrap(), dec!(0.5));
assert_eq!(out.new_shares_issued.unwrap(), dec!(25));
assert_eq!(out.pro_forma_shares, dec!(125));
}
#[test]
fn test_breakeven_synergies() {
let mut input = base_input();
input.consideration = ConsiderationType::AllStock;
let result = analyze_merger(&input).unwrap();
let out = &result.result;
assert!(out.breakeven_synergies > Decimal::ZERO);
let mut verify_input = input.clone();
verify_input.cost_synergies = Some(out.breakeven_synergies);
verify_input.synergy_phase_in_pct = Some(dec!(1));
let verify_result = analyze_merger(&verify_input).unwrap();
let eps_diff = (verify_result.result.pro_forma_eps
- verify_result.result.acquirer_eps_standalone)
.abs();
assert!(
eps_diff < dec!(0.0001),
"Breakeven synergies did not produce EPS-neutral result; diff = {eps_diff}"
);
}
#[test]
fn test_zero_shares_error() {
let mut input = base_input();
input.acquirer_shares_outstanding = Decimal::ZERO;
let result = analyze_merger(&input);
assert!(result.is_err());
let err = result.unwrap_err();
match err {
CorpFinanceError::InvalidInput { field, .. } => {
assert_eq!(field, "acquirer_shares_outstanding");
}
other => panic!("Expected InvalidInput error, got: {other}"),
}
let mut input2 = base_input();
input2.target_shares_outstanding = Decimal::ZERO;
assert!(analyze_merger(&input2).is_err());
}
#[test]
fn test_foregone_interest() {
let mut input = base_input();
input.debt_financing_rate = Some(dec!(0.05));
input.foregone_interest_rate = Some(dec!(0.02));
let result = analyze_merger(&input).unwrap();
let out = &result.result;
assert_eq!(out.financing_cost, dec!(65.625));
}
#[test]
fn test_adjustments_reduce_earnings() {
let mut input = base_input();
input.cost_synergies = Some(dec!(100));
input.synergy_phase_in_pct = Some(dec!(1));
input.integration_costs = Some(dec!(10));
input.goodwill_amortisation = Some(dec!(5));
input.transaction_fees = Some(dec!(3));
let result = analyze_merger(&input).unwrap();
let out = &result.result;
assert_eq!(out.synergy_impact, dec!(57));
}
#[test]
fn test_methodology_string() {
let input = base_input();
let result = analyze_merger(&input).unwrap();
assert_eq!(result.methodology, "M&A Accretion/Dilution Analysis");
}
}