use crate::cells::{page_of, push_identity, push_money, push_money_opt};
use crate::error::FormsError;
use crate::map::Form8995AMap;
use crate::pdf;
use crate::verify::{verify_flat, FlatPlacement};
use btctax_core::tax::packet::ReturnHeader;
use btctax_core::tax::qbi_a::{Form8995APartIToIii, Form8995APartIv};
use btctax_core::Usd;
const COL_MID: usize = 0;
const COL_AMOUNT: usize = 1;
const COL_A: usize = 2;
const COL_LN: usize = 3;
const F8995A_CLUSTERS: &[(f32, f32)] = &[
(410.0, 482.0),
(504.0, 576.0),
(360.0, 432.0),
(266.0, 338.0),
];
const GRP_PART_IV: u32 = 0;
const GRP_PART_II: u32 = 1;
const GRP_PART_III: u32 = 2;
fn assert_paren_magnitudes(p: &Form8995APartIv) -> Result<(), FormsError> {
for (line, v) in [("29", p.line29), ("40", p.line40)] {
if v < Usd::ZERO {
return Err(FormsError::Geometry(format!(
"Form 8995-A line {line} is a PARENTHESIZED box holding {v}: the form's own \
parentheses supply the minus sign, so a negative value renders as a positive number \
on a filed return. Store the magnitude."
)));
}
}
Ok(())
}
pub fn fill_form_8995a_with_map(
p: &Form8995APartIv,
parts_i_to_iii: Option<&Form8995APartIToIii>,
header: &ReturnHeader,
map: &Form8995AMap,
) -> Result<Vec<u8>, FormsError> {
assert_paren_magnitudes(p)?;
let mut doc = pdf::load(pdf::f8995a_pdf(map.year)?)?;
let blank_fields = pdf::collect_fields(&doc)?;
let mut writes: Vec<(String, pdf::FieldValue)> = Vec::new();
let mut placements: Vec<FlatPlacement> = Vec::new();
let plan: [(&crate::map::MoneyCell, Usd, usize); 13] = [
(&map.line27, p.line27, COL_MID),
(&map.line28, p.line28, COL_MID),
(&map.line29, p.line29, COL_MID), (&map.line30, p.line30, COL_MID),
(&map.line31, p.line31, COL_MID),
(&map.line32, p.line32, COL_AMOUNT),
(&map.line33, p.line33, COL_MID),
(&map.line34, p.line34, COL_MID),
(&map.line35, p.line35, COL_AMOUNT),
(&map.line36, p.line36, COL_AMOUNT),
(&map.line37, p.line37, COL_AMOUNT),
(&map.line39, p.line39, COL_AMOUNT),
(&map.line40, p.line40, COL_AMOUNT), ];
for (ord, (cell, value, col)) in plan.iter().enumerate() {
push_money(
&mut writes,
&mut placements,
cell,
*value,
*col,
Some((GRP_PART_IV, ord as u32)),
);
}
push_money_opt(
&mut writes,
&mut placements,
&map.line38,
p.line38,
COL_AMOUNT,
None,
);
if let Some(f) = parts_i_to_iii {
if f.part_ii.line2 > Usd::ZERO && f.part_i.col_a_name.trim().is_empty() {
return Err(FormsError::Geometry(
"Form 8995-A Part II line 2 is non-zero but the trade or business has no name: \
Part I column (a) is \"Trade, business, or aggregation name\", so this would file a \
qualified business income component for a business the return never names."
.into(),
));
}
let a = &f.part_i;
writes.push((
map.part1_row_a.name.clone(),
pdf::FieldValue::Text(a.col_a_name.clone()),
));
placements.push(FlatPlacement::free(
map.part1_row_a.name.clone(),
page_of(&map.part1_row_a.name),
));
let proprietor = header.proprietor.as_ref().ok_or_else(|| {
FormsError::Geometry(
"Form 8995-A Part I lists a trade or business but the return names no proprietor to \
file it under"
.into(),
)
})?;
let tin_max_len = blank_fields
.iter()
.find(|f| f.fqn == map.part1_row_a.tin)
.ok_or_else(|| FormsError::MapFieldMissing(map.part1_row_a.tin.clone()))?
.max_len;
writes.push((
map.part1_row_a.tin.clone(),
pdf::FieldValue::Text(crate::cells::render_ssn(&proprietor.ssn, tin_max_len)?),
));
placements.push(FlatPlacement::free(
map.part1_row_a.tin.clone(),
page_of(&map.part1_row_a.tin),
));
for (c, on) in [
(
&map.part1_row_a.specified_service,
a.col_b_specified_service,
),
(&map.part1_row_a.aggregation, a.col_c_aggregation),
(&map.part1_row_a.patron, a.col_e_patron),
] {
if on {
writes.push((c.field.clone(), pdf::FieldValue::Check { on: c.on.clone() }));
placements.push(FlatPlacement::check(c.field.clone(), page_of(&c.field)));
}
}
let ii = &f.part_ii;
let m2 = &map.part2_col_a;
let unconditional: [(&crate::map::MoneyCell, Usd); 13] = [
(&m2.line2, ii.line2),
(&m2.line3, ii.line3),
(&m2.line4, ii.line4),
(&m2.line5, ii.line5),
(&m2.line6, ii.line6),
(&m2.line7, ii.line7),
(&m2.line8, ii.line8),
(&m2.line9, ii.line9),
(&m2.line10, ii.line10),
(&m2.line11, ii.line11),
(&m2.line13, ii.line13),
(&m2.line15, ii.line15),
(&m2.line16, ii.line16),
];
let ord_of = |cell: &crate::map::MoneyCell| -> u32 {
for (n, c) in (2u32..=16).zip([
&m2.line2, &m2.line3, &m2.line4, &m2.line5, &m2.line6, &m2.line7, &m2.line8,
&m2.line9, &m2.line10, &m2.line11, &m2.line12, &m2.line13, &m2.line14, &m2.line15,
&m2.line16,
]) {
if std::ptr::eq(c, cell) {
return n;
}
}
unreachable!("every Part II cell is in the line list")
};
for (cell, value) in unconditional {
push_money(
&mut writes,
&mut placements,
cell,
value,
COL_A,
Some((GRP_PART_II, ord_of(cell))),
);
}
push_money_opt(
&mut writes,
&mut placements,
&m2.line12,
ii.line12,
COL_A,
Some((GRP_PART_II, 12)),
);
push_money_opt(
&mut writes,
&mut placements,
&m2.line14,
ii.line14,
COL_A,
Some((GRP_PART_II, 14)),
);
if let Some(iii) = f.part_iii.as_ref() {
let m3 = &map.part3_col_a;
for (cell, value, col, ord) in [
(&m3.line17, iii.line17, COL_A, 17u32),
(&m3.line18, iii.line18, COL_A, 18),
(&m3.line19, iii.line19, COL_A, 19),
(&m3.line20, iii.line20, COL_LN, 20),
(&m3.line21, iii.line21, COL_LN, 21),
(&m3.line22, iii.line22, COL_LN, 22),
(&m3.line23, iii.line23, COL_LN, 23),
(&m3.line25, iii.line25, COL_A, 25),
(&m3.line26, iii.line26, COL_A, 26),
] {
push_money(
&mut writes,
&mut placements,
cell,
value,
col,
Some((GRP_PART_III, ord)),
);
}
push_money(
&mut writes,
&mut placements,
&m3.line24,
(iii.line24_ratio * rust_decimal::Decimal::from(100)).normalize(),
COL_LN,
Some((GRP_PART_III, 24)),
);
}
}
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, F8995A_CLUSTERS)?;
Ok(bytes)
}