1use crate::error::FormsError;
9use crate::map::{Form8949Map, PartMap};
10use crate::verify::{verify_8949, Geo, Placement};
11use crate::{fmt_date, pdf};
12use btctax_core::forms::Form8949Row;
13use btctax_core::Form8949Part;
14use rust_decimal::Decimal;
15
16pub struct PartData {
18 pub rows: Vec<[String; 8]>,
20 pub totals: [String; 4],
22 pub adj_nonzero: bool,
24}
25
26const COL_D: usize = 3;
28const COL_E: usize = 4;
29const COL_G: usize = 6;
30const COL_H: usize = 7;
31
32fn row_cells(r: &Form8949Row) -> Result<[String; 8], FormsError> {
36 Ok([
37 r.description.clone(),
38 fmt_date(r.date_acquired)?,
39 fmt_date(r.date_sold)?,
40 r.proceeds.to_string(),
41 r.cost_basis.to_string(),
42 r.adjustment_code.clone(),
43 if r.adjustment_amount.is_zero() {
44 String::new()
45 } else {
46 r.adjustment_amount.to_string()
47 },
48 r.gain.to_string(),
49 ])
50}
51
52pub fn part_data(rows: &[&Form8949Row]) -> Result<PartData, FormsError> {
54 let mut out = Vec::with_capacity(rows.len());
55 let (mut sp, mut sc, mut sg, mut sh) =
56 (Decimal::ZERO, Decimal::ZERO, Decimal::ZERO, Decimal::ZERO);
57 for r in rows {
58 out.push(row_cells(r)?);
59 sp += r.proceeds;
60 sc += r.cost_basis;
61 sg += r.adjustment_amount;
62 sh += r.gain;
63 }
64 Ok(PartData {
65 rows: out,
66 totals: [
67 sp.to_string(),
68 sc.to_string(),
69 sg.to_string(),
70 sh.to_string(),
71 ],
72 adj_nonzero: !sg.is_zero(),
73 })
74}
75
76fn place_part(
78 part: &PartMap,
79 data: &PartData,
80 writes: &mut Vec<(String, pdf::FieldValue)>,
81 placements: &mut Vec<Placement>,
82) {
83 if data.rows.is_empty() {
84 return; }
86 writes.push((
88 part.box_field.clone(),
89 pdf::FieldValue::Check {
90 on: part.box_on.clone(),
91 },
92 ));
93 placements.push(Placement {
94 fqn: part.box_field.clone(),
95 geo: Geo::Check,
96 });
97 for (ri, cells) in data.rows.iter().enumerate() {
99 for (ci, value) in cells.iter().enumerate() {
100 if value.is_empty() {
101 continue; }
103 let fqn = part.rows[ri][ci].clone();
104 writes.push((fqn.clone(), pdf::FieldValue::Text(value.clone())));
105 placements.push(Placement {
106 fqn,
107 geo: Geo::Data { row: ri, col: ci },
108 });
109 }
110 }
111 let totals = [
113 (COL_D, &part.totals.proceeds_d, true),
114 (COL_E, &part.totals.cost_e, true),
115 (COL_G, &part.totals.adj_g, data.adj_nonzero),
116 (COL_H, &part.totals.gain_h, true),
117 ];
118 let vals = [
119 &data.totals[0],
120 &data.totals[1],
121 &data.totals[2],
122 &data.totals[3],
123 ];
124 for (i, (col, fqn, include)) in totals.into_iter().enumerate() {
125 if !include {
126 continue;
127 }
128 writes.push((fqn.clone(), pdf::FieldValue::Text(vals[i].clone())));
129 placements.push(Placement {
130 fqn: fqn.clone(),
131 geo: Geo::Total { col },
132 });
133 }
134}
135
136pub fn fill_8949_parts(
141 short: &PartData,
142 long: &PartData,
143 map: &Form8949Map,
144) -> Result<Vec<u8>, FormsError> {
145 fill_8949_parts_inner(short, long, map, None)
146}
147
148pub fn fill_8949_parts_with_identity(
152 short: &PartData,
153 long: &PartData,
154 map: &Form8949Map,
155 header: &btctax_core::tax::packet::ReturnHeader,
156) -> Result<Vec<u8>, FormsError> {
157 fill_8949_parts_inner(short, long, map, Some(header))
158}
159
160fn fill_8949_parts_inner(
161 short: &PartData,
162 long: &PartData,
163 map: &Form8949Map,
164 filer: Option<&btctax_core::tax::packet::ReturnHeader>,
165) -> Result<Vec<u8>, FormsError> {
166 if short.rows.len() > map.rows_per_page {
167 return Err(FormsError::Overflow {
168 part: "Part I",
169 rows: short.rows.len(),
170 capacity: map.rows_per_page,
171 });
172 }
173 if long.rows.len() > map.rows_per_page {
174 return Err(FormsError::Overflow {
175 part: "Part II",
176 rows: long.rows.len(),
177 capacity: map.rows_per_page,
178 });
179 }
180
181 let mut writes: Vec<(String, pdf::FieldValue)> = Vec::new();
182 let mut placements: Vec<Placement> = Vec::new();
183 if let Some(p) = map.part("short") {
184 place_part(p, short, &mut writes, &mut placements);
185 }
186 if let Some(p) = map.part("long") {
187 place_part(p, long, &mut writes, &mut placements);
188 }
189
190 let mut doc = pdf::load(pdf::f8949_pdf(map.year)?)?;
191 let blank_fields = pdf::collect_fields(&doc)?;
192
193 if let Some(header) = filer {
196 for cells in [&map.identity_page1, &map.identity_page2] {
197 let cells = cells.as_ref().ok_or_else(|| {
198 FormsError::Geometry(format!(
199 "the {} Form 8949 map has no [identity] block — a full return cannot file an \
200 unnamed Form 8949",
201 map.year
202 ))
203 })?;
204 let ssn = crate::cells::render_ssn(
205 &header.taxpayer.ssn,
206 blank_fields
207 .iter()
208 .find(|f| f.fqn == cells.ssn)
209 .and_then(|f| f.max_len),
210 )?;
211 writes.push((
212 cells.name.clone(),
213 pdf::FieldValue::Text(header.name_line.clone()),
214 ));
215 placements.push(Placement {
216 fqn: cells.name.clone(),
217 geo: crate::verify::Geo::Check,
218 });
219 writes.push((cells.ssn.clone(), pdf::FieldValue::Text(ssn)));
220 placements.push(Placement {
221 fqn: cells.ssn.clone(),
222 geo: crate::verify::Geo::Check,
223 });
224 }
225 }
226
227 let index = pdf::index(&blank_fields);
228 pdf::drop_xfa_and_set_needappearances(&mut doc)?;
229 pdf::apply_writes(&mut doc, &index, &writes)?;
230 pdf::strip_nondeterminism(&mut doc);
231 let bytes = pdf::save(&mut doc)?;
232
233 let check = pdf::load(&bytes)?;
235 let fields = pdf::collect_fields(&check)?;
236 verify_8949(&check, &fields, &placements, &map.table_token)?;
237 if pdf_has_xfa(&check)? {
239 return Err(FormsError::Structure(
240 "output still carries /XFA after fill".into(),
241 ));
242 }
243 Ok(bytes)
244}
245
246pub fn pdf_has_xfa(doc: &lopdf::Document) -> Result<bool, FormsError> {
248 let acro = match doc.catalog()?.get(b"AcroForm") {
249 Ok(lopdf::Object::Reference(id)) => doc.get_dictionary(*id)?,
250 Ok(lopdf::Object::Dictionary(d)) => d,
251 _ => return Ok(false),
252 };
253 Ok(acro.has(b"XFA"))
254}
255
256pub fn split_parts(rows: &[Form8949Row]) -> (Vec<&Form8949Row>, Vec<&Form8949Row>) {
259 let mut st = Vec::new();
260 let mut lt = Vec::new();
261 for r in rows {
262 match r.part {
263 Form8949Part::ShortTerm => st.push(r),
264 Form8949Part::LongTerm => lt.push(r),
265 }
266 }
267 (st, lt)
268}