btctax_forms/lib.rs
1//! **btctax-forms** — fill the OFFICIAL IRS fillable PDFs (Form 8949 + Schedule D) from btctax's
2//! already-computed tax data. Offline, deterministic, and **geometry-verified**: every fill is read
3//! back from its own serialized bytes and each value's widget `/Rect` is checked against a
4//! map-independent column/row band re-derived from the PDF itself. A mis-mapped cell fails closed.
5//!
6//! ## Sub-project 1 (TY2025)
7//! - **Form 8949** — Bitcoin under **Box I** (short-term) / **Box L** (long-term), the 1099-DA
8//! revision (NOT Box C/F, which read "other than digital asset transactions"). 11 rows per part
9//! per page.
10//! - **Schedule D** — lines 3 & 7 (ST), 10 & 15 (LT), 16 (total), QOF = No. Lines 17-22 are scoped
11//! out (the caller prints a notice).
12//! - These are **static XFA-hybrid** PDFs: the fill removes the `/AcroForm` `/XFA` layer (else
13//! Acrobat shows blank), sets `/NeedAppearances`, and pins determinism (drops `/Info` dates + the
14//! trailer `/ID`).
15//!
16//! The tax data is REUSED verbatim from the projection (`btctax_core::form_8949` /
17//! `btctax_core::schedule_d`) — this crate never recomputes gains.
18
19mod cells;
20mod error;
21mod fill8949;
22mod fill8949_full;
23mod form1040;
24mod form1040_full;
25mod form8283;
26mod form8959;
27mod form8960;
28mod form8995;
29mod map;
30mod overflow;
31mod packet;
32mod pdf;
33mod schedule23;
34mod schedule_a;
35mod schedule_b;
36mod schedule_c;
37mod schedule_d;
38mod schedule_d_full;
39mod schedule_se;
40mod schedule_se_full;
41mod transcribe;
42mod verify;
43mod watermark;
44
45pub use error::FormsError;
46pub use form1040::{Form1040Fill, Form1040Inputs};
47pub use map::{
48 Form1040Map, Form8283Map, Form8949Map, Form8959Map, Form8960Map, Form8995Map, Schedule1Map,
49 Schedule2Map, Schedule3Map, ScheduleAMap, ScheduleBMap, ScheduleCMap, ScheduleDMap,
50 ScheduleSeMap,
51};
52pub use schedule_se::SE_FLOOR;
53
54use btctax_core::conventions::{TaxDate, Usd};
55use btctax_core::{Form8949Row, ScheduleDTotals};
56use time::macros::format_description;
57
58/// The tax years this build bundles forms + maps for. Each fill dispatches to the year's committed
59/// map + bundled PDF via the `Map::for_year` constructors; an unlisted year fails closed with
60/// [`FormsError::UnsupportedYear`].
61pub const SUPPORTED_YEARS: &[i32] = &[2017, 2024, 2025];
62
63/// Format a date as **MM/DD/YYYY** — Form 8949's native date format for columns (b)/(c).
64pub(crate) fn fmt_date(d: TaxDate) -> Result<String, FormsError> {
65 let fmt = format_description!("[month]/[day]/[year]");
66 d.format(&fmt)
67 .map_err(|e| FormsError::Structure(format!("date format: {e}")))
68}
69
70/// Format money exactly as the `form8949.csv` / `schedule_d.csv` do — the raw `Decimal` Display
71/// (native scale, no `$`, no thousands separators). Keeping this identical to the CSV is what lets
72/// `schedule_d_totals_match_form8949_and_csv` cross-check the three artifacts.
73pub(crate) fn fmt_money(d: Usd) -> String {
74 d.to_string()
75}
76
77/// Fill **Form 8949** (Part I + Part II) for `year` from the projection rows and return the PDF
78/// bytes. The box is year-aware: Bitcoin is filed under the digital-asset Box I/L from TY2025, and
79/// the securities Box C/F on the pre-2025 revisions. Parts that overflow the revision's grid (11 rows
80/// per part on the 2025 form, 14 on 2024/2017) paginate: ⌈rows/grid⌉ page copies per part, each with
81/// its own totals; the copies are merged with per-copy field renaming so no two share a value. Every
82/// copy is geometry-verified before merge.
83pub fn fill_form_8949(rows: &[Form8949Row], year: i32) -> Result<Vec<u8>, FormsError> {
84 let map = Form8949Map::for_year(year)?;
85 let cap = map.rows_per_page;
86 let (st, lt) = fill8949::split_parts(rows);
87 let n_pages = div_ceil(st.len(), cap).max(div_ceil(lt.len(), cap)).max(1);
88
89 if n_pages == 1 {
90 let short = fill8949::part_data(&st)?;
91 let long = fill8949::part_data(<)?;
92 return fill8949::fill_8949_parts(&short, &long, &map);
93 }
94
95 let mut copies = Vec::with_capacity(n_pages);
96 for k in 0..n_pages {
97 let st_chunk: Vec<&Form8949Row> = st.iter().skip(k * cap).take(cap).copied().collect();
98 let lt_chunk: Vec<&Form8949Row> = lt.iter().skip(k * cap).take(cap).copied().collect();
99 let short = fill8949::part_data(&st_chunk)?;
100 let long = fill8949::part_data(<_chunk)?;
101 // Each copy is filled on ORIGINAL names and geometry-verified here (fails closed).
102 copies.push(fill8949::fill_8949_parts(&short, &long, &map)?);
103 }
104 overflow::merge_copies(&copies)
105}
106
107fn div_ceil(n: usize, d: usize) -> usize {
108 n.div_ceil(d)
109}
110
111/// Stamp a diagonal `DRAFT — ESTIMATE, NOT FOR FILING` watermark on every page of a filled form.
112/// Applied by the CLI when the ledger is pseudo-reconciled (an estimate). The overlay carries its own
113/// embedded standard font resource, orthogonal to `/NeedAppearances`.
114pub fn stamp_draft_watermark(pdf_bytes: &[u8]) -> Result<Vec<u8>, FormsError> {
115 watermark::stamp_draft(pdf_bytes)
116}
117
118/// Fill **Schedule D** for `year` from the part totals and return the PDF bytes.
119pub fn fill_schedule_d(totals: &ScheduleDTotals, year: i32) -> Result<Vec<u8>, FormsError> {
120 let map = ScheduleDMap::for_year(year)?;
121 schedule_d::fill_schedule_d_totals(totals, &map)
122}
123
124/// Fill **Schedule SE** (Form 1040) for `year` from the computed §1401 `SeTaxResult`, the filer's
125/// Form W-2 Social Security wages (line 8a), and the year's Social Security wage base (line 7).
126/// Returns `Ok(None)` when net SE earnings are **below the $400 floor** (no SE tax owed — the form is
127/// not written). Line 12 = SS + regular Medicare only (the 0.9% Additional Medicare Tax is a Form 8959
128/// item, not on Schedule SE); when `se.addl > 0` the caller prints a Form 8959 advisory.
129pub fn fill_schedule_se(
130 se: &btctax_core::SeTaxResult,
131 w2_ss_wages: Usd,
132 ss_wage_base: Usd,
133 year: i32,
134) -> Result<Option<Vec<u8>>, FormsError> {
135 let map = ScheduleSeMap::for_year(year)?;
136 schedule_se::fill_schedule_se_with_map(se, w2_ss_wages, ss_wage_base, &map)
137}
138
139/// Fill **Form 8959** (Additional Medicare Tax) for `year` from the core-derived line chain
140/// (`btctax_core::tax::other_taxes::form_8959_lines`). Returns `Ok(None)` when the form is not
141/// required — line 18 (the tax) AND line 24 (the withholding reconciliation) are both zero.
142///
143/// Line 24 is not redundant with line 18: an employer withholds the 0.9% on ITS OWN wages over
144/// $200,000 with no knowledge of a spouse or a second job, so a taxpayer who owes NO Additional
145/// Medicare Tax can still have had some withheld — and that excess is a credit on 1040 line 25c.
146/// Skipping the form on line 18 alone would silently forfeit it.
147pub fn fill_form_8959(
148 lines: &btctax_core::tax::other_taxes::Form8959Lines,
149 header: &btctax_core::tax::packet::ReturnHeader,
150 year: i32,
151) -> Result<Option<Vec<u8>>, FormsError> {
152 let map = Form8959Map::for_year(year)?;
153 form8959::fill_form_8959_with_map(lines, header, &map)
154}
155
156/// Fill **Form 8960** (Net Investment Income Tax, §1411) for `year` from the core-derived line chain
157/// (`btctax_core::tax::other_taxes::form_8960_lines`, which returns `None` when no NIIT is owed —
158/// there is then no form to file).
159pub fn fill_form_8960(
160 lines: &btctax_core::tax::other_taxes::Form8960Lines,
161 header: &btctax_core::tax::packet::ReturnHeader,
162 year: i32,
163) -> Result<Vec<u8>, FormsError> {
164 let map = Form8960Map::for_year(year)?;
165 form8960::fill_form_8960_with_map(lines, header, &map)
166}
167
168/// Fill **Form 8995** (QBI deduction, simplified) for `year` from the core-derived line chain
169/// (`btctax_core::tax::qbi::form_8995_lines`, which returns `None` when there is no QBI).
170///
171/// FAILS CLOSED if a parenthesized cell (line 7/16/17) carries a negative value: the form pre-prints
172/// the parentheses, so a negative would render as a POSITIVE number on the filed return.
173pub fn fill_form_8995(
174 lines: &btctax_core::tax::qbi::Form8995Lines,
175 header: &btctax_core::tax::packet::ReturnHeader,
176 year: i32,
177) -> Result<Vec<u8>, FormsError> {
178 let map = Form8995Map::for_year(year)?;
179 form8995::fill_form_8995_with_map(lines, header, &map)
180}
181
182/// Fill **Schedule 1** (Additional Income and Adjustments to Income) for `year` from the core-derived
183/// printed chain (`btctax_core::tax::printed::schedule_1_lines`, which returns `None` when there is
184/// neither additional income nor an adjustment — the schedule is then not filed).
185pub fn fill_schedule_1(
186 lines: &btctax_core::tax::printed::Schedule1Lines,
187 header: &btctax_core::tax::packet::ReturnHeader,
188 year: i32,
189) -> Result<Vec<u8>, FormsError> {
190 let map = Schedule1Map::for_year(year)?;
191 schedule23::fill_schedule_1_with_map(lines, header, &map)
192}
193
194/// Fill **Schedule 2** (Additional Taxes) for `year` from the core-derived printed chain
195/// (`btctax_core::tax::printed::schedule_2_lines`, which returns `None` when there are no other
196/// taxes to report — the schedule is then not filed).
197pub fn fill_schedule_2(
198 lines: &btctax_core::tax::printed::Schedule2Lines,
199 header: &btctax_core::tax::packet::ReturnHeader,
200 year: i32,
201) -> Result<Vec<u8>, FormsError> {
202 let map = Schedule2Map::for_year(year)?;
203 schedule23::fill_schedule_2_with_map(lines, header, &map)
204}
205
206/// Fill **Schedule 3** (Additional Credits and Payments) for `year` from the core-derived printed
207/// chain (`btctax_core::tax::printed::schedule_3_lines`, which returns `None` when there is neither a
208/// foreign tax credit nor an excess-Social-Security credit).
209pub fn fill_schedule_3(
210 lines: &btctax_core::tax::printed::Schedule3Lines,
211 header: &btctax_core::tax::packet::ReturnHeader,
212 year: i32,
213) -> Result<Vec<u8>, FormsError> {
214 let map = Schedule3Map::for_year(year)?;
215 schedule23::fill_schedule_3_with_map(lines, header, &map)
216}
217
218/// Fill **Schedule A** (Itemized Deductions) for `year` from the core-derived printed chain
219/// (`btctax_core::tax::printed::schedule_a_lines`, which returns `None` unless the return actually
220/// itemizes — Schedule A is computed even when the standard deduction wins, but only FILED when it is
221/// the deduction claimed).
222pub fn fill_schedule_a(
223 lines: &btctax_core::tax::printed::ScheduleALines,
224 header: &btctax_core::tax::packet::ReturnHeader,
225 year: i32,
226) -> Result<Vec<u8>, FormsError> {
227 let map = ScheduleAMap::for_year(year)?;
228 schedule_a::fill_schedule_a_with_map(lines, header, &map)
229}
230
231/// Fill the **FULL-RETURN Form 1040** for `year` from the core-derived printed chain
232/// (`btctax_core::tax::printed::form_1040_lines`) — every line, not just the capital-gain cluster.
233///
234/// This is NOT [`fill_form_1040_capgains`], which writes only line 7 + the Digital-Asset question for
235/// the crypto-slice export. That one stays: for a year with no `ReturnInputs` it is what the filer
236/// wants.
237pub fn fill_form_1040_full(
238 lines: &btctax_core::tax::printed::Form1040Lines,
239 header: &btctax_core::tax::packet::ReturnHeader,
240 status: btctax_core::tax::types::FilingStatus,
241 year: i32,
242) -> Result<Vec<u8>, FormsError> {
243 let map = Form1040Map::for_year(year)?;
244 form1040_full::fill_form_1040_full_with_map(lines, header, status, &map)
245}
246
247/// Fill the **FULL-RETURN Schedule D** for `year` from the core-derived printed chain
248/// (`btctax_core::tax::printed::schedule_d_lines`), including Part III's SPEC §7.2 routing.
249///
250/// This is NOT [`fill_schedule_d`], which is the **crypto-slice** fill: that one writes only lines
251/// 3/7/10/15/16 from the ledger totals, with no line 13 (1099-DIV box-2a capital-gain distributions)
252/// and no lines 6/14 (capital-loss carryovers). For a crypto-only year that is complete and correct.
253/// For a full return it would be a complete-LOOKING form with income missing.
254///
255/// FAILS CLOSED if a parenthesized cell (line 6/14/21) carries a negative: the form pre-prints the
256/// parentheses, so a negative renders as a POSITIVE number — turning a capital loss into a gain.
257pub fn fill_schedule_d_full(
258 lines: &btctax_core::tax::printed::ScheduleDLines,
259 header: &btctax_core::tax::packet::ReturnHeader,
260 year: i32,
261) -> Result<Vec<u8>, FormsError> {
262 let map = ScheduleDMap::for_year(year)?;
263 schedule_d_full::fill_schedule_d_full_with_map(lines, header, &map)
264}
265
266/// Fill the **full-return Form 8283** for `year` — whole-dollar rows plus the FILER's identity block
267/// (which the crypto slice never writes). `Ok(None)` when there are no donation rows.
268pub fn fill_form_8283_full(
269 printed: &btctax_core::tax::printed::Printed8283Rows,
270 header: &btctax_core::tax::packet::ReturnHeader,
271 year: i32,
272) -> Result<Option<Vec<u8>>, FormsError> {
273 let map = Form8283Map::for_year(year)?;
274 form8283::fill_form_8283_full(printed, header, &map)
275}
276
277/// Fill the **full-return Form 8949** for `year` from the core-derived printed chain
278/// (`btctax_core::tax::printed::form_8949_printed`). Whole dollars, and column (h) is DERIVED from the
279/// printed (d) − (e). Schedule D lines 3/10 ARE this form's printed column totals.
280pub fn fill_8949_full(
281 printed: &btctax_core::tax::printed::Printed8949,
282 header: &btctax_core::tax::packet::ReturnHeader,
283 year: i32,
284) -> Result<Vec<u8>, FormsError> {
285 let map = Form8949Map::for_year(year)?;
286 fill8949_full::fill_8949_full_with_map(printed, header, &map)
287}
288
289/// Fill the **full-return Schedule SE** for `year` from the core-derived printed chain
290/// (`btctax_core::tax::printed::schedule_se_lines`). Whole dollars — the crypto slice's
291/// `fill_schedule_se` keeps its exact-cents rendering and is untouched.
292pub fn fill_schedule_se_full(
293 lines: &btctax_core::tax::printed::ScheduleSeLines,
294 header: &btctax_core::tax::packet::ReturnHeader,
295 year: i32,
296) -> Result<Vec<u8>, FormsError> {
297 let map = ScheduleSeMap::for_year(year)?;
298 schedule_se_full::fill_schedule_se_full_with_map(lines, header, &map)
299}
300
301/// Fill **Schedule B** (Interest and Ordinary Dividends) for `year` from the core-derived printed
302/// chain (`btctax_core::tax::printed::schedule_b_lines`, which returns `None` when Schedule B is not
303/// required — interest and dividends both at or under $1,500 and no declared foreign account).
304///
305/// REFUSES when there are more payers than the form has rows (14 interest / 15 dividend). Truncating
306/// the list would leave a form whose printed rows do not add up to its own total.
307pub fn fill_schedule_b(
308 lines: &btctax_core::tax::printed::ScheduleBLines,
309 header: &btctax_core::tax::packet::ReturnHeader,
310 year: i32,
311) -> Result<Vec<u8>, FormsError> {
312 let map = ScheduleBMap::for_year(year)?;
313 schedule_b::fill_schedule_b_with_map(lines, header, &map)
314}
315
316/// Fill **Schedule C** (Profit or Loss From Business) for `year` from the core-derived printed chain
317/// (`btctax_core::tax::printed::schedule_c_lines`, which returns `None` when there is no crypto trade
318/// or business).
319pub fn fill_schedule_c(
320 lines: &btctax_core::tax::printed::ScheduleCLines,
321 header: &btctax_core::tax::packet::ReturnHeader,
322 year: i32,
323) -> Result<Vec<u8>, FormsError> {
324 let map = ScheduleCMap::for_year(year)?;
325 schedule_c::fill_schedule_c_with_map(lines, header, &map)
326}
327
328/// Fill **Form 8283** (Noncash Charitable Contributions, Rev. 12-2025) for `year` from the projected
329/// donation rows + `DonationDetails`. Returns `Ok(None)` when there are no donations in the year.
330/// Fills the donee/appraiser IDENTITY + per-row property data (and, for Section B, checks the "k
331/// Digital assets" property-type box); leaves BLANK every OTHER party's declaration/signature (a
332/// LOUD partial-scope notice is the caller's). More rows than a section holds (4 in Section A / 3 in
333/// Section B) overflow onto additional form copies via [`overflow::merge_copies`].
334pub fn fill_form_8283(
335 rows: &[btctax_core::Form8283Row],
336 year: i32,
337) -> Result<Option<Vec<u8>>, FormsError> {
338 let map = Form8283Map::for_year(year)?;
339 form8283::fill_form_8283(rows, &map)
340}
341
342/// Fill the capital-gains cells of **Form 1040** for `year`: line 7a (only when Schedule D is ACTIVE
343/// and line 16 ≥ 0; active-and-zero → "-0-") and the Digital-Asset Yes/No question (YES iff there is
344/// btctax-evidenced qualifying activity). Returns `Ok(None)` — **skip the whole 1040** — when there is
345/// no reportable activity (the DA answer would be blank and there is no 7a value). 7b checkboxes are
346/// left untouched; a NET LOSS leaves 7a blank (the §1211 line-21 cap is the filer's). A partial-scope
347/// notice enumerating exactly what was filled is the caller's.
348pub fn fill_form_1040_capgains(
349 inputs: &Form1040Inputs,
350 year: i32,
351) -> Result<Option<Form1040Fill>, FormsError> {
352 let map = Form1040Map::for_year(year)?;
353 form1040::fill_form_1040_capgains(inputs, &map)
354}
355
356/// **[I5]** How many rows might belong on a SEPARATE broker-reported Form 8949 — i.e. disposals on an
357/// exchange that may have issued broker basis reporting. The separate boxes and this export's filed
358/// boxes are year-aware (see `btctax-cli`'s `broker_reporting_advisory`): a 1099-B / Box A/B/D/E, filed
359/// under C/F pre-TY2025; a 1099-DA / Box G/H/J/K, filed under I/L from TY2025. A non-zero count is a
360/// loud advisory, not a refusal.
361pub fn rows_possibly_broker_reported(rows: &[Form8949Row]) -> usize {
362 rows.iter().filter(|r| r.box_needs_review).count()
363}
364
365// ── Internals exposed for the KATs (fault injection needs a corruptible map + the verifier). ──────
366#[doc(hidden)]
367pub use packet::{fill_full_return, NamedForm};
368
369pub mod testonly {
370 pub use crate::cells::fmt_money_pair;
371 pub use crate::fill8949::{fill_8949_parts, part_data, pdf_has_xfa, split_parts, PartData};
372 pub use crate::fill8949_full::fill_8949_full_with_map;
373 pub use crate::form1040::{fill_form_1040_capgains as fill_1040_with_map, Form1040Fill};
374 pub use crate::form1040_full::fill_form_1040_full_with_map;
375 pub use crate::form8283::fill_form_8283 as fill_8283_with_map;
376 pub use crate::form8959::fill_form_8959_with_map;
377 pub use crate::form8960::fill_form_8960_with_map;
378 pub use crate::form8995::fill_form_8995_with_map;
379 // The committed map TOML, for the line-keyed inverse transcriber (`extract_lines`). Downstream
380 // read-back tests need the map itself, not just its parsed struct.
381 pub use crate::map::{
382 AmountCols, Form1040Map, Form8283Map, Form8949Map, Form8959Map, Form8960Map, Form8995Map,
383 MoneyCell, MoneyPair, PartMap, Schedule1Map, Schedule2Map, Schedule3Map, ScheduleAMap,
384 ScheduleBMap, ScheduleCMap, ScheduleDMap, ScheduleSeMap,
385 };
386 pub use crate::map::{
387 F1040_MAP_2024, F8283_MAP_2024, F8949_MAP_2024, F8959_MAP_2024, F8960_MAP_2024,
388 F8995_MAP_2024, SCHEDULE_1_MAP_2024, SCHEDULE_2_MAP_2024, SCHEDULE_3_MAP_2024,
389 SCHEDULE_A_MAP_2024, SCHEDULE_B_MAP_2024, SCHEDULE_C_MAP_2024, SCHEDULE_D_MAP_2024,
390 SCHEDULE_SE_MAP_2024,
391 };
392 pub use crate::pdf::{
393 button_on_states, checkbox_on, collect_fields, index, load, text_value, Field,
394 F1040_PDF_2017, F1040_PDF_2024, F1040_PDF_2025, F8283_PDF_2017, F8283_PDF_2024,
395 F8283_PDF_2025, F8949_PDF_2017, F8949_PDF_2024, F8949_PDF_2025, F8959_PDF_2024,
396 SCHEDULE_D_PDF_2017, SCHEDULE_D_PDF_2024, SCHEDULE_D_PDF_2025, SCHEDULE_SE_PDF_2017,
397 SCHEDULE_SE_PDF_2024, SCHEDULE_SE_PDF_2025,
398 };
399 pub use crate::schedule23::{
400 fill_schedule_1_with_map, fill_schedule_2_with_map, fill_schedule_3_with_map,
401 };
402 pub use crate::schedule_a::fill_schedule_a_with_map;
403 pub use crate::schedule_b::fill_schedule_b_with_map;
404 pub use crate::schedule_c::fill_schedule_c_with_map;
405 pub use crate::schedule_d::fill_schedule_d_totals;
406 pub use crate::schedule_d_full::fill_schedule_d_full_with_map;
407 pub use crate::schedule_se::fill_schedule_se_with_map;
408 pub use crate::schedule_se_full::fill_schedule_se_full_with_map;
409 pub use crate::transcribe::extract_lines;
410 pub use crate::verify::{
411 no_unmapped_filled, topmost_yes_no_pair, verify_8949, verify_flat, FlatPlacement, Geo,
412 Placement,
413 };
414}