use crate::cells::{push_identity, push_money};
use crate::error::FormsError;
use crate::map::{MoneyCell, ScheduleDMap};
use crate::pdf;
use crate::verify::{verify_flat, FlatPlacement};
use btctax_core::tax::packet::ReturnHeader;
use btctax_core::tax::printed::{ScheduleDLines, ScheduleDRouting};
use btctax_core::Usd;
const COL_PROCEEDS_D: usize = 0;
const COL_COST_E: usize = 1;
const COL_AMOUNT_H: usize = 2;
const SCHEDULE_D_CLUSTERS: &[(f32, f32)] = &[(288.0, 360.0), (360.0, 432.0), (489.0, 576.0)];
const GRP_P1_AMOUNT: u32 = 0;
const GRP_P1_D: u32 = 1;
const GRP_P1_E: u32 = 2;
const GRP_P2_AMOUNT: u32 = 3;
fn assert_paren_magnitudes(lines: &ScheduleDLines) -> Result<(), FormsError> {
let mut cells = vec![("6", lines.line6), ("14", lines.line14)];
if let ScheduleDRouting::NetLoss { line21, .. } = lines.routing {
cells.push(("21", line21));
}
for (line, v) in cells {
if v < Usd::ZERO {
return Err(FormsError::Geometry(format!(
"Schedule D line {line} is a PARENTHESIZED box (the form prints the minus sign), so it \
must carry a positive magnitude — got {v}. Writing this would render as a POSITIVE \
number on the filed return, turning a capital loss into a gain."
)));
}
}
Ok(())
}
fn need<'a, T>(cell: &'a Option<T>, what: &str, year: i32) -> Result<&'a T, FormsError> {
cell.as_ref().ok_or_else(|| {
FormsError::Geometry(format!(
"the TY{year} Schedule D map has no `{what}` — the full-return fill needs it. Full-return \
v1 is TY2024-only."
))
})
}
pub fn fill_schedule_d_full_with_map(
lines: &ScheduleDLines,
header: &ReturnHeader,
map: &ScheduleDMap,
) -> Result<Vec<u8>, FormsError> {
assert_paren_magnitudes(lines)?;
let y = map.year;
let mut writes: Vec<(String, pdf::FieldValue)> = Vec::new();
let mut placements: Vec<FlatPlacement> = Vec::new();
let p1_amounts: [(&MoneyCell, Usd); 7] = [
(&MoneyCell::Single(map.line3.gain_h.clone()), lines.line3_h),
(need(&map.line6, "line6", y)?, lines.line6), (&MoneyCell::Single(map.line7_h.clone()), lines.line7),
(
&MoneyCell::Single(map.line10.gain_h.clone()),
lines.line10_h,
),
(need(&map.line13, "line13", y)?, lines.line13),
(need(&map.line14, "line14", y)?, lines.line14), (&MoneyCell::Single(map.line15_h.clone()), lines.line15),
];
for (ord, (cell, value)) in p1_amounts.iter().enumerate() {
push_money(
&mut writes,
&mut placements,
cell,
*value,
COL_AMOUNT_H,
Some((GRP_P1_AMOUNT, ord as u32)),
);
}
for (ord, (fqn, value)) in [
(&map.line3.proceeds_d, lines.line3_d),
(&map.line10.proceeds_d, lines.line10_d),
]
.iter()
.enumerate()
{
push_money(
&mut writes,
&mut placements,
&MoneyCell::Single((*fqn).clone()),
*value,
COL_PROCEEDS_D,
Some((GRP_P1_D, ord as u32)),
);
}
for (ord, (fqn, value)) in [
(&map.line3.cost_e, lines.line3_e),
(&map.line10.cost_e, lines.line10_e),
]
.iter()
.enumerate()
{
push_money(
&mut writes,
&mut placements,
&MoneyCell::Single((*fqn).clone()),
*value,
COL_COST_E,
Some((GRP_P1_E, ord as u32)),
);
}
let mut p2_ord = 0u32;
let mut push_p2 = |cell: &MoneyCell,
value: Usd,
writes: &mut Vec<(String, pdf::FieldValue)>,
placements: &mut Vec<FlatPlacement>| {
push_money(
writes,
placements,
cell,
value,
COL_AMOUNT_H,
Some((GRP_P2_AMOUNT, p2_ord)),
);
p2_ord += 1;
};
push_p2(
&MoneyCell::Single(map.line16_h.clone()),
lines.line16,
&mut writes,
&mut placements,
);
let check = |pair: &crate::map::YesNoPair,
yes: bool,
writes: &mut Vec<(String, pdf::FieldValue)>,
placements: &mut Vec<FlatPlacement>| {
let choice = if yes { &pair.yes } else { &pair.no };
writes.push((
choice.field.clone(),
pdf::FieldValue::Check {
on: choice.on.clone(),
},
));
placements.push(FlatPlacement::check(choice.field.clone(), 1));
};
match lines.routing {
ScheduleDRouting::BothGains => {
check(
need(&map.line17, "line17", y)?,
true,
&mut writes,
&mut placements,
);
push_p2(
need(&map.line18, "line18", y)?,
Usd::ZERO,
&mut writes,
&mut placements,
);
push_p2(
need(&map.line19, "line19", y)?,
Usd::ZERO,
&mut writes,
&mut placements,
);
check(
need(&map.line20, "line20", y)?,
true,
&mut writes,
&mut placements,
);
}
ScheduleDRouting::ShortGainLongLoss { line22_yes } => {
check(
need(&map.line17, "line17", y)?,
false,
&mut writes,
&mut placements,
);
check(
need(&map.line22, "line22", y)?,
line22_yes,
&mut writes,
&mut placements,
);
}
ScheduleDRouting::NetLoss { line21, line22_yes } => {
push_p2(
need(&map.line21, "line21", y)?,
line21,
&mut writes,
&mut placements,
);
check(
need(&map.line22, "line22", y)?,
line22_yes,
&mut writes,
&mut placements,
);
}
ScheduleDRouting::Zero { line22_yes } => {
check(
need(&map.line22, "line22", y)?,
line22_yes,
&mut writes,
&mut placements,
);
}
}
let mut doc = pdf::load(pdf::schedule_d_pdf(map.year)?)?;
let blank_fields = pdf::collect_fields(&doc)?;
let identity = map.identity.as_ref().ok_or_else(|| {
FormsError::Geometry(format!(
"the {} Schedule D map has no [identity] block — a full return cannot file an unnamed schedule",
map.year
))
})?;
push_identity(
&mut writes,
&mut placements,
identity,
&header.name_line,
&header.taxpayer.ssn,
&blank_fields,
)?;
if let Some(qof_no) = &map.qof_no {
writes.push((
qof_no.field.clone(),
pdf::FieldValue::Check {
on: qof_no.on.clone(),
},
));
placements.push(FlatPlacement::check(
qof_no.field.clone(),
crate::cells::page_of(&qof_no.field),
));
}
let index = pdf::index(&blank_fields);
pdf::drop_xfa_and_set_needappearances(&mut doc)?;
pdf::apply_writes(&mut doc, &index, &writes)?;
pdf::strip_nondeterminism(&mut doc);
let bytes = pdf::save(&mut doc)?;
let doc2 = pdf::load(&bytes)?;
let fields = pdf::collect_fields(&doc2)?;
verify_flat(&doc2, &fields, &placements, SCHEDULE_D_CLUSTERS)?;
Ok(bytes)
}