use crate::cells::{page_of, push_money, render_ssn};
use crate::error::FormsError;
use crate::map::{CheckChoice, Form1040HeaderCells, Form1040Map, MoneyCell};
use crate::pdf;
use crate::verify::{verify_flat, FlatPlacement};
use btctax_core::tax::packet::ReturnHeader;
use btctax_core::tax::printed::Form1040Lines;
use btctax_core::tax::types::FilingStatus;
use btctax_core::Usd;
const COL_SUBLINE: usize = 0; const COL_MID: usize = 1; const COL_AMOUNT: usize = 2;
const F1040_CLUSTERS: &[(f32, f32)] = &[(252.0, 324.0), (410.0, 482.0), (504.0, 576.0)];
const GRP_P1_AMOUNT: u32 = 0;
const GRP_P1_SUBLINE: u32 = 1;
const GRP_P2_AMOUNT: u32 = 2;
const GRP_P2_MID: u32 = 3;
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} Form 1040 map has no `{what}` — the full-return fill needs it. Full-return \
v1 is TY2024-only."
))
})
}
fn filing_status_box(
map: &Form1040Map,
status: FilingStatus,
year: i32,
) -> Result<&CheckChoice, FormsError> {
let fs = need(&map.filing_status, "filing_status", year)?;
Ok(match status {
FilingStatus::Single => &fs.single,
FilingStatus::HoH => &fs.hoh,
FilingStatus::Mfj => &fs.mfj,
FilingStatus::Mfs => &fs.mfs,
FilingStatus::Qss => &fs.qss,
})
}
pub fn fill_form_1040_full_with_map(
lines: &Form1040Lines,
header: &ReturnHeader,
status: FilingStatus,
map: &Form1040Map,
) -> Result<Vec<u8>, FormsError> {
let y = map.year;
let mut writes: Vec<(String, pdf::FieldValue)> = Vec::new();
let mut placements: Vec<FlatPlacement> = Vec::new();
let h = map.header.as_ref().ok_or_else(|| {
FormsError::Geometry(format!(
"the TY{y} 1040 map has no [header] block — a full return cannot file an unnamed 1040"
))
})?;
let blank = pdf::load(pdf::f1040_pdf(y)?)?;
let blank_fields = pdf::collect_fields(&blank)?;
push_header_block(
&mut writes,
&mut placements,
h,
header,
status,
&blank_fields,
)?;
let p1: [(&MoneyCell, Usd); 13] = [
(need(&map.line1a, "line1a", y)?, lines.line1a),
(need(&map.line1z, "line1z", y)?, lines.line1z),
(need(&map.line2b, "line2b", y)?, lines.line2b),
(need(&map.line3b, "line3b", y)?, lines.line3b),
(&map.line7a, lines.line7), (need(&map.line8, "line8", y)?, lines.line8),
(need(&map.line9, "line9", y)?, lines.line9),
(need(&map.line10, "line10", y)?, lines.line10),
(need(&map.line11, "line11", y)?, lines.line11),
(need(&map.line12, "line12", y)?, lines.line12),
(need(&map.line13, "line13", y)?, lines.line13),
(need(&map.line14, "line14", y)?, lines.line14),
(need(&map.line15, "line15", y)?, lines.line15),
];
for (ord, (cell, value)) in p1.iter().enumerate() {
push_money(
&mut writes,
&mut placements,
cell,
*value,
COL_AMOUNT,
Some((GRP_P1_AMOUNT, ord as u32)),
);
}
push_money(
&mut writes,
&mut placements,
need(&map.line2a, "line2a", y)?,
lines.line2a,
COL_SUBLINE,
Some((GRP_P1_SUBLINE, 0)),
);
push_money(
&mut writes,
&mut placements,
need(&map.line3a, "line3a", y)?,
lines.line3a,
COL_SUBLINE,
Some((GRP_P1_SUBLINE, 1)),
);
let p2_amount: [(&MoneyCell, Usd); 15] = [
(need(&map.line16, "line16", y)?, lines.line16),
(need(&map.line17, "line17", y)?, lines.line17),
(need(&map.line18, "line18", y)?, lines.line18),
(need(&map.line19, "line19", y)?, lines.line19),
(need(&map.line20, "line20", y)?, lines.line20),
(need(&map.line21, "line21", y)?, lines.line21),
(need(&map.line22, "line22", y)?, lines.line22),
(need(&map.line23, "line23", y)?, lines.line23),
(need(&map.line24, "line24", y)?, lines.line24),
(need(&map.line25d, "line25d", y)?, lines.line25d),
(need(&map.line26, "line26", y)?, lines.line26),
(need(&map.line32, "line32", y)?, lines.line32),
(need(&map.line33, "line33", y)?, lines.line33),
(need(&map.line34, "line34", y)?, lines.line34),
(need(&map.line35a, "line35a", y)?, lines.line34), ];
for (ord, (cell, value)) in p2_amount.iter().enumerate() {
push_money(
&mut writes,
&mut placements,
cell,
*value,
COL_AMOUNT,
Some((GRP_P2_AMOUNT, ord as u32)),
);
}
push_money(
&mut writes,
&mut placements,
need(&map.line37, "line37", y)?,
lines.line37,
COL_AMOUNT,
Some((GRP_P2_AMOUNT, p2_amount.len() as u32)),
);
let p2_mid: [(&MoneyCell, Usd); 4] = [
(need(&map.line25a, "line25a", y)?, lines.line25a),
(need(&map.line25b, "line25b", y)?, lines.line25b),
(need(&map.line25c, "line25c", y)?, lines.line25c),
(need(&map.line31, "line31", y)?, lines.line31),
];
for (ord, (cell, value)) in p2_mid.iter().enumerate() {
push_money(
&mut writes,
&mut placements,
cell,
*value,
COL_MID,
Some((GRP_P2_MID, ord as u32)),
);
}
let fs = filing_status_box(map, status, y)?;
writes.push((
fs.field.clone(),
pdf::FieldValue::Check { on: fs.on.clone() },
));
placements.push(FlatPlacement::check(fs.field.clone(), 0));
if lines.digital_asset_yes {
let da = map
.da_yes
.as_ref()
.ok_or_else(|| FormsError::Geometry(format!("the TY{y} 1040 map has no `da_yes`")))?;
writes.push((
da.field.clone(),
pdf::FieldValue::Check { on: da.on.clone() },
));
placements.push(FlatPlacement::check(da.field.clone(), 0));
}
let mut doc = pdf::load(pdf::f1040_pdf(y)?)?;
let index = pdf::index(&pdf::collect_fields(&doc)?);
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, F1040_CLUSTERS)?;
Ok(bytes)
}
#[allow(clippy::too_many_arguments)]
fn push_header_block(
w: &mut Vec<(String, pdf::FieldValue)>,
p: &mut Vec<FlatPlacement>,
cells: &Form1040HeaderCells,
header: &ReturnHeader,
status: FilingStatus,
blank_fields: &[pdf::Field],
) -> Result<(), FormsError> {
let max_len_of = |fqn: &str| -> Option<usize> {
blank_fields
.iter()
.find(|f| f.fqn == fqn)
.and_then(|f| f.max_len)
};
let text = |w: &mut Vec<(String, pdf::FieldValue)>,
p: &mut Vec<FlatPlacement>,
fqn: &str,
value: &str| {
if value.is_empty() {
return; }
w.push((fqn.to_string(), pdf::FieldValue::Text(value.to_string())));
p.push(FlatPlacement::free(fqn.to_string(), page_of(fqn)));
};
let check = |w: &mut Vec<(String, pdf::FieldValue)>,
p: &mut Vec<FlatPlacement>,
c: &CheckChoice,
on: bool| {
if !on {
return; }
w.push((c.field.clone(), pdf::FieldValue::Check { on: c.on.clone() }));
p.push(FlatPlacement::check(c.field.clone(), page_of(&c.field)));
};
let t = &header.taxpayer;
text(w, p, &cells.taxpayer_first, &t.first_name);
text(w, p, &cells.taxpayer_last, &t.last_name);
text(
w,
p,
&cells.taxpayer_ssn,
&render_ssn(&t.ssn, max_len_of(&cells.taxpayer_ssn))?,
);
if let Some(sp) = &header.spouse {
text(w, p, &cells.spouse_first, &sp.first_name);
text(w, p, &cells.spouse_last, &sp.last_name);
text(
w,
p,
&cells.spouse_ssn,
&render_ssn(&sp.ssn, max_len_of(&cells.spouse_ssn))?,
);
if status == FilingStatus::Mfs {
text(w, p, &cells.mfs_spouse_name, &sp.full_name());
}
}
text(w, p, &cells.occupation_taxpayer, &t.occupation);
if let Some(sp) = &header.spouse {
text(w, p, &cells.occupation_spouse, &sp.occupation);
}
if let Some(pin) = &header.ip_pin {
text(w, p, &cells.ip_pin, pin.digits());
}
text(w, p, &cells.address_street, &header.address_street);
text(w, p, &cells.address_city, &header.address_city);
text(w, p, &cells.address_state, &header.address_state);
text(w, p, &cells.address_zip, &header.address_zip);
check(
w,
p,
&cells.presidential_taxpayer,
header.presidential_fund_taxpayer,
);
check(
w,
p,
&cells.presidential_spouse,
header.presidential_fund_spouse,
);
check(
w,
p,
&cells.claimed_dependent_taxpayer,
header.claimed_as_dependent_taxpayer,
);
check(
w,
p,
&cells.claimed_dependent_spouse,
header.claimed_as_dependent_spouse,
);
check(w, p, &cells.mfs_spouse_itemizes, header.mfs_spouse_itemizes);
let ab = header.aged_blind;
check(w, p, &cells.taxpayer_aged, ab.taxpayer_aged);
check(w, p, &cells.taxpayer_blind, ab.taxpayer_blind);
check(w, p, &cells.spouse_aged, ab.spouse_aged);
check(w, p, &cells.spouse_blind, ab.spouse_blind);
if header.dependents.len() > cells.dependent_rows.len() {
return Err(FormsError::Overflow {
part: "the 1040 dependents table",
rows: header.dependents.len(),
capacity: cells.dependent_rows.len(),
});
}
for (d, row) in header.dependents.iter().zip(&cells.dependent_rows) {
text(w, p, &row.name, &d.name);
text(w, p, &row.ssn, &render_ssn(&d.ssn, max_len_of(&row.ssn))?);
text(w, p, &row.relationship, &d.relationship);
}
Ok(())
}