use crate::cells::{push_identity, push_literal, push_money, render_ssn};
use crate::error::FormsError;
use crate::map::Form8995Map;
use crate::pdf;
use crate::verify::{verify_flat, FlatPlacement};
use btctax_core::tax::packet::ReturnHeader;
use btctax_core::tax::qbi::Form8995Lines;
use btctax_core::Usd;
const F8995_COL_MID: usize = 0;
const F8995_COL_AMOUNT: usize = 1;
const F8995_COL_ROW1_BUSINESS: usize = 2;
const F8995_COL_ROW1_TIN: usize = 3;
const F8995_COL_ROW1_QBI: usize = 4;
const F8995_CLUSTERS: &[(f32, f32)] = &[
(410.0, 482.0), (504.0, 576.0), (226.0, 235.0), (435.0, 443.0), (525.0, 533.0), ];
fn assert_paren_magnitudes(lines: &Form8995Lines) -> Result<(), FormsError> {
for (line, v) in [
("7", lines.line7),
("16", lines.line16),
("17", lines.line17),
] {
if v < Usd::ZERO {
return Err(FormsError::Geometry(format!(
"Form 8995 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."
)));
}
}
Ok(())
}
fn assert_row1_is_one_row(blank: &[pdf::Field], map: &Form8995Map) -> Result<(), FormsError> {
let field = |cell: &crate::map::MoneyCell| -> Result<&pdf::Field, FormsError> {
let fqn = match cell {
crate::map::MoneyCell::Single(f) => f,
crate::map::MoneyCell::Pair(mp) => &mp.dollars_field,
};
blank
.iter()
.find(|f| &f.fqn == fqn)
.ok_or_else(|| FormsError::MapFieldMissing(fqn.clone()))
};
let a = field(&map.row1_business)?;
let [_, y0, _, y1] = a.rect.ok_or_else(|| {
FormsError::Geometry("Form 8995 row 1i(a) has no /Rect to band the row with".into())
})?;
let (lo, hi) = (y0.min(y1), y0.max(y1));
for (what, cell) in [("(b) TIN", &map.row1_tin), ("(c) QBI", &map.row1_qbi)] {
let f = field(cell)?;
let cy = f
.cy()
.ok_or_else(|| FormsError::MapFieldMissing(f.fqn.clone()))?;
if cy < lo || cy > hi {
return Err(FormsError::Geometry(format!(
"Form 8995 Part I row 1i is not one ROW: cell {what} sits at y {cy:.1}, outside row 1i's \
band [{lo:.1}, {hi:.1}] (taken from the full-height (a) cell). A row whose name and \
income are on different lines names the WRONG business."
)));
}
}
Ok(())
}
pub fn fill_form_8995_with_map(
lines: &Form8995Lines,
header: &ReturnHeader,
map: &Form8995Map,
) -> Result<Vec<u8>, FormsError> {
assert_paren_magnitudes(lines)?;
let mut doc = pdf::load(pdf::f8995_pdf(map.year)?)?;
let blank_fields = pdf::collect_fields(&doc)?;
assert_row1_is_one_row(&blank_fields, map)?;
let mut writes: Vec<(String, pdf::FieldValue)> = Vec::new();
let mut placements: Vec<FlatPlacement> = Vec::new();
if lines.line2 > Usd::ZERO {
if lines.business_name.trim().is_empty() {
return Err(FormsError::Geometry(
"Form 8995 line 2 is non-zero but the trade or business has no name: line 2 is \"Total \
qualified business income or (loss). Combine lines 1i through 1v, column (c)\", so this \
would file a total over an EMPTY column, claiming a §199A deduction for a business the \
return never names."
.into(),
));
}
let proprietor = header.proprietor.as_ref().ok_or_else(|| {
FormsError::Geometry(
"Form 8995 has trade-or-business QBI but the return names no proprietor to file the \
business under"
.into(),
)
})?;
push_literal(
&mut writes,
&mut placements,
&map.row1_business,
&lines.business_name,
F8995_COL_ROW1_BUSINESS,
);
let tin_fqn = map.row1_tin.fields()[0];
let tin_max_len = blank_fields
.iter()
.find(|f| f.fqn == tin_fqn)
.ok_or_else(|| FormsError::MapFieldMissing(tin_fqn.to_string()))?
.max_len;
push_literal(
&mut writes,
&mut placements,
&map.row1_tin,
&render_ssn(&proprietor.ssn, tin_max_len)?,
F8995_COL_ROW1_TIN,
);
push_money(
&mut writes,
&mut placements,
&map.row1_qbi,
lines.line2,
F8995_COL_ROW1_QBI,
Some((0, 0)),
);
}
let plan: [(Usd, usize); 15] = [
(lines.line2, F8995_COL_MID), (lines.line4, F8995_COL_MID), (lines.line5, F8995_COL_AMOUNT), (lines.line6, F8995_COL_MID), (lines.line7, F8995_COL_MID), (lines.line8, F8995_COL_MID), (lines.line9, F8995_COL_AMOUNT), (lines.line10, F8995_COL_AMOUNT), (lines.line11, F8995_COL_MID), (lines.line12, F8995_COL_MID), (lines.line13, F8995_COL_MID), (lines.line14, F8995_COL_AMOUNT), (lines.line15, F8995_COL_AMOUNT), (lines.line16, F8995_COL_AMOUNT), (lines.line17, F8995_COL_AMOUNT), ];
for (ord, (cell, (value, col))) in map.lines().iter().zip(plan).enumerate() {
push_money(
&mut writes,
&mut placements,
cell,
value,
col,
Some((0, ord as u32 + 1)),
);
}
push_identity(
&mut writes,
&mut placements,
&map.identity,
&header.name_line,
&header.taxpayer.ssn,
&blank_fields,
)?;
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_flat(&check, &fields, &placements, F8995_CLUSTERS)?;
Ok(bytes)
}