use crate::error::FormsError;
use crate::map::{Form8949Map, PartMap};
use crate::verify::{verify_8949, Geo, Placement};
use crate::{fmt_date, pdf};
use btctax_core::forms::Form8949Row;
use btctax_core::Form8949Part;
use rust_decimal::Decimal;
pub struct PartData {
pub rows: Vec<[String; 8]>,
pub totals: [String; 4],
pub adj_nonzero: bool,
}
const COL_D: usize = 3;
const COL_E: usize = 4;
const COL_G: usize = 6;
const COL_H: usize = 7;
fn row_cells(r: &Form8949Row) -> Result<[String; 8], FormsError> {
Ok([
r.description.clone(),
fmt_date(r.date_acquired)?,
fmt_date(r.date_sold)?,
r.proceeds.to_string(),
r.cost_basis.to_string(),
r.adjustment_code.clone(),
if r.adjustment_amount.is_zero() {
String::new()
} else {
r.adjustment_amount.to_string()
},
r.gain.to_string(),
])
}
pub fn part_data(rows: &[&Form8949Row]) -> Result<PartData, FormsError> {
let mut out = Vec::with_capacity(rows.len());
let (mut sp, mut sc, mut sg, mut sh) =
(Decimal::ZERO, Decimal::ZERO, Decimal::ZERO, Decimal::ZERO);
for r in rows {
out.push(row_cells(r)?);
sp += r.proceeds;
sc += r.cost_basis;
sg += r.adjustment_amount;
sh += r.gain;
}
Ok(PartData {
rows: out,
totals: [
sp.to_string(),
sc.to_string(),
sg.to_string(),
sh.to_string(),
],
adj_nonzero: !sg.is_zero(),
})
}
fn place_part(
part: &PartMap,
data: &PartData,
writes: &mut Vec<(String, pdf::FieldValue)>,
placements: &mut Vec<Placement>,
) {
if data.rows.is_empty() {
return; }
writes.push((
part.box_field.clone(),
pdf::FieldValue::Check {
on: part.box_on.clone(),
},
));
placements.push(Placement {
fqn: part.box_field.clone(),
geo: Geo::Check,
});
for (ri, cells) in data.rows.iter().enumerate() {
for (ci, value) in cells.iter().enumerate() {
if value.is_empty() {
continue; }
let fqn = part.rows[ri][ci].clone();
writes.push((fqn.clone(), pdf::FieldValue::Text(value.clone())));
placements.push(Placement {
fqn,
geo: Geo::Data { row: ri, col: ci },
});
}
}
let totals = [
(COL_D, &part.totals.proceeds_d, true),
(COL_E, &part.totals.cost_e, true),
(COL_G, &part.totals.adj_g, data.adj_nonzero),
(COL_H, &part.totals.gain_h, true),
];
let vals = [
&data.totals[0],
&data.totals[1],
&data.totals[2],
&data.totals[3],
];
for (i, (col, fqn, include)) in totals.into_iter().enumerate() {
if !include {
continue;
}
writes.push((fqn.clone(), pdf::FieldValue::Text(vals[i].clone())));
placements.push(Placement {
fqn: fqn.clone(),
geo: Geo::Total { col },
});
}
}
pub fn fill_8949_parts(
short: &PartData,
long: &PartData,
map: &Form8949Map,
) -> Result<Vec<u8>, FormsError> {
fill_8949_parts_inner(short, long, map, None)
}
pub fn fill_8949_parts_with_identity(
short: &PartData,
long: &PartData,
map: &Form8949Map,
header: &btctax_core::tax::packet::ReturnHeader,
) -> Result<Vec<u8>, FormsError> {
fill_8949_parts_inner(short, long, map, Some(header))
}
fn fill_8949_parts_inner(
short: &PartData,
long: &PartData,
map: &Form8949Map,
filer: Option<&btctax_core::tax::packet::ReturnHeader>,
) -> Result<Vec<u8>, FormsError> {
if short.rows.len() > map.rows_per_page {
return Err(FormsError::Overflow {
part: "Part I",
rows: short.rows.len(),
capacity: map.rows_per_page,
});
}
if long.rows.len() > map.rows_per_page {
return Err(FormsError::Overflow {
part: "Part II",
rows: long.rows.len(),
capacity: map.rows_per_page,
});
}
let mut writes: Vec<(String, pdf::FieldValue)> = Vec::new();
let mut placements: Vec<Placement> = Vec::new();
if let Some(p) = map.part("short") {
place_part(p, short, &mut writes, &mut placements);
}
if let Some(p) = map.part("long") {
place_part(p, long, &mut writes, &mut placements);
}
let mut doc = pdf::load(pdf::f8949_pdf(map.year)?)?;
let blank_fields = pdf::collect_fields(&doc)?;
if let Some(header) = filer {
for cells in [&map.identity_page1, &map.identity_page2] {
let cells = cells.as_ref().ok_or_else(|| {
FormsError::Geometry(format!(
"the {} Form 8949 map has no [identity] block — a full return cannot file an \
unnamed Form 8949",
map.year
))
})?;
let ssn = crate::cells::render_ssn(
&header.taxpayer.ssn,
blank_fields
.iter()
.find(|f| f.fqn == cells.ssn)
.and_then(|f| f.max_len),
)?;
writes.push((
cells.name.clone(),
pdf::FieldValue::Text(header.name_line.clone()),
));
placements.push(Placement {
fqn: cells.name.clone(),
geo: crate::verify::Geo::Check,
});
writes.push((cells.ssn.clone(), pdf::FieldValue::Text(ssn)));
placements.push(Placement {
fqn: cells.ssn.clone(),
geo: crate::verify::Geo::Check,
});
}
}
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 check = pdf::load(&bytes)?;
let fields = pdf::collect_fields(&check)?;
verify_8949(&check, &fields, &placements, &map.table_token)?;
if pdf_has_xfa(&check)? {
return Err(FormsError::Structure(
"output still carries /XFA after fill".into(),
));
}
Ok(bytes)
}
pub fn pdf_has_xfa(doc: &lopdf::Document) -> Result<bool, FormsError> {
let acro = match doc.catalog()?.get(b"AcroForm") {
Ok(lopdf::Object::Reference(id)) => doc.get_dictionary(*id)?,
Ok(lopdf::Object::Dictionary(d)) => d,
_ => return Ok(false),
};
Ok(acro.has(b"XFA"))
}
pub fn split_parts(rows: &[Form8949Row]) -> (Vec<&Form8949Row>, Vec<&Form8949Row>) {
let mut st = Vec::new();
let mut lt = Vec::new();
for r in rows {
match r.part {
Form8949Part::ShortTerm => st.push(r),
Form8949Part::LongTerm => lt.push(r),
}
}
(st, lt)
}