#[cfg(feature = "office")]
use crate::office::types::{Slide, SlideDeckSpec, WorkbookProperties};
#[cfg(feature = "office")]
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[cfg_attr(feature = "schema_gen", derive(schemars::JsonSchema))]
pub struct IcPresentationInput {
pub deal_name: String,
pub recommendation: String,
pub date: String,
pub presenter: String,
pub investment_thesis: Vec<String>,
pub key_metrics: Vec<(String, String)>,
pub returns_table: Vec<Vec<String>>,
pub risks: Vec<String>,
pub mitigants: Vec<String>,
pub timeline: Vec<(String, String)>,
}
#[cfg(feature = "office")]
pub fn ic_presentation_to_deck(input: &IcPresentationInput) -> SlideDeckSpec {
let mut slides = Vec::with_capacity(9);
slides.push(Slide::Title {
title: format!("IC Presentation: {}", input.deal_name),
subtitle: Some(format!(
"Presented by {} on {}",
input.presenter, input.date
)),
});
slides.push(Slide::Content {
title: "Recommendation".into(),
bullets: vec![input.recommendation.clone()],
});
slides.push(Slide::Content {
title: "Investment Thesis".into(),
bullets: input.investment_thesis.clone(),
});
slides.push(Slide::Table {
title: "Key Metrics".into(),
headers: vec!["Metric".into(), "Value".into()],
rows: input
.key_metrics
.iter()
.map(|(k, v)| vec![k.clone(), v.clone()])
.collect(),
});
slides.push(build_returns_slide(&input.returns_table));
if !input.risks.is_empty() || !input.mitigants.is_empty() {
slides.push(Slide::Section {
heading: "Risks & Mitigants".into(),
});
}
if !input.risks.is_empty() {
slides.push(Slide::Content {
title: "Risks".into(),
bullets: input.risks.clone(),
});
}
if !input.mitigants.is_empty() {
slides.push(Slide::Content {
title: "Mitigants".into(),
bullets: input.mitigants.clone(),
});
}
if !input.timeline.is_empty() {
slides.push(Slide::Table {
title: "Process Timeline".into(),
headers: vec!["Milestone".into(), "Date".into()],
rows: input
.timeline
.iter()
.map(|(m, d)| vec![m.clone(), d.clone()])
.collect(),
});
}
SlideDeckSpec {
slides,
properties: WorkbookProperties {
title: Some(format!("IC Presentation — {}", input.deal_name)),
author: Some(input.presenter.clone()),
company: None,
subject: None,
},
}
}
#[cfg(feature = "office")]
fn build_returns_slide(returns_table: &[Vec<String>]) -> Slide {
if returns_table.len() >= 2 {
Slide::Table {
title: "Returns Analysis".into(),
headers: returns_table[0].clone(),
rows: returns_table[1..].to_vec(),
}
} else {
Slide::Table {
title: "Returns Analysis".into(),
headers: vec!["Scenario".into(), "MOIC".into(), "IRR".into()],
rows: vec![],
}
}
}
#[cfg(all(test, feature = "office"))]
mod tests {
use super::*;
fn minimal_input() -> IcPresentationInput {
IcPresentationInput {
deal_name: "Acme Holdings".into(),
recommendation: "APPROVE — $250M EQUITY CHECK".into(),
date: "2026-05-08".into(),
presenter: "Jane Smith".into(),
investment_thesis: vec!["Market-leading platform with 30% EBITDA margins".into()],
key_metrics: vec![("EV/EBITDA".into(), "8.5x".into())],
returns_table: vec![
vec!["Scenario".into(), "MOIC".into(), "IRR".into()],
vec!["Base".into(), "2.5x".into(), "22%".into()],
],
risks: vec!["Customer concentration >40% in top-3 accounts".into()],
mitigants: vec!["Long-term MSAs with 3-year renewal cycles".into()],
timeline: vec![("Sign LOI".into(), "2026-05-15".into())],
}
}
#[test]
fn ic_presentation_to_deck_basic() {
let input = minimal_input();
let deck = ic_presentation_to_deck(&input);
assert!(
deck.slides.len() >= 8,
"expected at least 8 slides, got {}",
deck.slides.len()
);
match &deck.slides[0] {
Slide::Title { title, .. } => {
assert!(
title.contains("Acme Holdings"),
"title slide must contain deal_name; got: {title}"
);
}
other => panic!("slide 0 should be Title, got {other:?}"),
}
match &deck.slides[1] {
Slide::Content { title, .. } => {
assert_eq!(title, "Recommendation", "slide 1 title mismatch");
}
other => panic!("slide 1 should be Content, got {other:?}"),
}
assert!(
deck.properties
.title
.as_deref()
.unwrap_or("")
.contains("Acme Holdings"),
"workbook title must contain deal_name"
);
assert_eq!(
deck.properties.author.as_deref(),
Some("Jane Smith"),
"author should be presenter"
);
}
#[test]
fn ic_presentation_to_deck_round_trips_through_writer() {
use crate::office::pptx::write_slide_deck;
use tempfile::tempdir;
let input = minimal_input();
let deck = ic_presentation_to_deck(&input);
let dir = tempdir().expect("tempdir creation failed");
let path = dir.path().join("ic_presentation.pptx");
let result = write_slide_deck(&deck, &path).expect("write_slide_deck failed");
assert!(path.exists(), "output file must exist");
assert!(
result.bytes_written > 0,
"expected nonzero bytes written, got {}",
result.bytes_written
);
let bytes = std::fs::read(&path).expect("failed to read output file");
assert_eq!(
&bytes[..4],
b"PK\x03\x04",
"pptx must begin with ZIP magic bytes PK\\x03\\x04"
);
}
}