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