use std::path::{Path, PathBuf};
use clap::Args;
use retiretui_engine::optimize::LadderStep;
use retiretui_engine::optimize::{
BracketSweep, OptimizeOptions, OptimizedLadder, ladder_overlay, optimize_conversions,
sweep_brackets,
};
use retiretui_engine::params::TaxTables;
use retiretui_engine::plan::{Dollars, Plan};
use retiretui_engine::project::{Projection, Summary};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::commands::project::OutputFormat;
use crate::commands::table::{align, display_dollars, summary_table};
#[derive(Args)]
pub struct OptimizeArgs {
pub plan: PathBuf,
#[arg(long, required = true)]
pub from: Vec<String>,
#[arg(long)]
pub to: String,
#[arg(long)]
pub bracket: Option<f64>,
#[command(flatten)]
pub constraints: LadderConstraints,
#[arg(long, requires = "bracket")]
pub write: Option<PathBuf>,
#[arg(long)]
pub nominal: bool,
#[arg(long, value_enum, default_value_t = OutputFormat::Table)]
pub format: OutputFormat,
#[arg(long)]
pub tax_dir: Vec<PathBuf>,
}
#[derive(Args, Deserialize, JsonSchema)]
pub struct LadderConstraints {
#[arg(long)]
pub start_year: Option<i16>,
#[arg(long)]
pub end_year: Option<i16>,
#[arg(long)]
pub annual_max: Option<Dollars>,
#[arg(long)]
pub total_max: Option<Dollars>,
#[arg(long, default_value_t = 0)]
#[serde(default)]
pub headroom: Dollars,
#[arg(long)]
pub irmaa_tier: Option<u8>,
#[arg(long)]
pub max_magi: Option<Dollars>,
}
impl LadderConstraints {
pub fn options(&self, sources: &[String], destination: &str) -> OptimizeOptions {
OptimizeOptions {
sources: sources.to_vec(),
destination: destination.to_owned(),
start_year: self.start_year,
end_year: self.end_year,
annual_max: self.annual_max,
total_max: self.total_max,
headroom: self.headroom,
irmaa_tier: self.irmaa_tier,
max_magi: self.max_magi,
}
}
}
#[derive(Serialize, JsonSchema)]
pub struct SweepReply {
pub baseline: Summary,
pub brackets: Vec<SweepEntry>,
}
#[derive(Serialize, JsonSchema)]
pub struct SweepEntry {
pub bracket_rate: f64,
pub total_converted: Dollars,
pub optimized: Summary,
}
impl SweepReply {
pub fn new(sweep: &BracketSweep, deflated: bool) -> Self {
Self {
baseline: sweep.baseline.summary(deflated),
brackets: sweep
.brackets
.iter()
.map(|bracket| SweepEntry {
bracket_rate: bracket.rate,
total_converted: bracket.converted(deflated),
optimized: bracket.optimized.summary(deflated),
})
.collect(),
}
}
}
#[derive(Serialize, JsonSchema)]
pub struct LadderReply {
pub steps: Vec<LadderStep>,
pub total_converted: Dollars,
pub baseline: Summary,
pub optimized: Summary,
}
impl LadderReply {
pub fn new(ladder: &OptimizedLadder, deflated: bool) -> Self {
Self {
steps: ladder.ladder.steps.clone(),
total_converted: ladder.ladder.converted(deflated),
baseline: ladder.baseline.summary(deflated),
optimized: ladder.ladder.optimized.summary(deflated),
}
}
}
pub fn run(args: &OptimizeArgs) -> anyhow::Result<()> {
let tables = crate::commands::load_tables(&args.tax_dir)?;
let plan = crate::commands::load_validated_plan(&args.plan, &tables)?;
match args.bracket {
Some(percent) => run_single(&plan, &tables, percent / 100.0, args),
None => run_sweep(&plan, &tables, args),
}
}
fn run_sweep(plan: &Plan, tables: &TaxTables, args: &OptimizeArgs) -> anyhow::Result<()> {
let sweep = sweep_brackets(
plan,
tables,
&args.constraints.options(&args.from, &args.to),
)
.map_err(|issues| anyhow::Error::msg(crate::commands::issue_listing(&issues)))?;
let deflated = !args.nominal;
match args.format {
OutputFormat::Json => {
let reply = SweepReply::new(&sweep, deflated);
println!("{}", serde_json::to_string_pretty(&reply)?);
}
OutputFormat::Table => print!("{}", sweep_table(&sweep, deflated)),
}
Ok(())
}
fn sweep_table(sweep: &BracketSweep, deflated: bool) -> String {
let mut rows = vec![(
vec!["baseline".to_owned(), "0".to_owned()],
sweep.baseline.summary(deflated),
)];
rows.extend(sweep.brackets.iter().map(|bracket| {
let cells = vec![
format!("{:.0}%", bracket.rate * 100.0),
bracket.converted(deflated).to_string(),
];
(cells, bracket.optimized.summary(deflated))
}));
summary_table(&["bracket", "converted"], rows)
}
fn run_single(
plan: &Plan,
tables: &TaxTables,
rate: f64,
args: &OptimizeArgs,
) -> anyhow::Result<()> {
let options = args.constraints.options(&args.from, &args.to);
let ladder = optimize_conversions(plan, tables, &options, rate)
.map_err(|issues| anyhow::Error::msg(crate::commands::issue_listing(&issues)))?;
let deflated = !args.nominal;
match args.format {
OutputFormat::Json => {
let reply = LadderReply::new(&ladder, deflated);
println!("{}", serde_json::to_string_pretty(&reply)?);
}
OutputFormat::Table => {
print!(
"{}",
ladder_table(&ladder.ladder.steps, &ladder.ladder.optimized, args.nominal)
);
println!();
print!("{}", comparison_table(&ladder, deflated));
}
}
if let Some(out) = &args.write {
write_overlay(out, &args.plan, plan, &options, &ladder)?;
println!("wrote {}", out.display());
}
Ok(())
}
fn ladder_table(steps: &[LadderStep], optimized: &Projection, nominal: bool) -> String {
let header: Vec<String> = ["year", "source", "amount", "taxable"]
.map(str::to_owned)
.into();
let rows: Vec<Vec<String>> = steps
.iter()
.map(|step| {
let row = optimized.years.iter().find(|row| row.year == step.year);
let deflator = row.map_or(1.0, |row| row.deflator);
let taxable = row.map_or(0, |row| row.taxes.ordinary_taxable);
vec![
step.year.to_string(),
step.source.clone(),
display_dollars(step.amount, deflator, nominal),
display_dollars(taxable, deflator, nominal),
]
})
.collect();
align(&header, &rows)
}
fn comparison_table(ladder: &OptimizedLadder, deflated: bool) -> String {
let baseline = vec!["baseline".to_owned(), "0".to_owned()];
let optimized = vec![
"optimized".to_owned(),
ladder.ladder.converted(deflated).to_string(),
];
summary_table(
&["plan", "converted"],
vec![
(baseline, ladder.baseline.summary(deflated)),
(optimized, ladder.ladder.optimized.summary(deflated)),
],
)
}
fn write_overlay(
out: &Path,
plan_path: &Path,
plan: &Plan,
options: &OptimizeOptions,
ladder: &OptimizedLadder,
) -> anyhow::Result<()> {
let base = crate::commands::overlay_base(out, plan_path)?;
let overlay = ladder_overlay(&base, plan, options, &ladder.ladder.steps)?;
crate::commands::write_atomic(out, &overlay)?;
Ok(())
}