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 form1040;
23mod form8283;
24mod map;
25mod overflow;
26mod pdf;
27mod schedule_d;
28mod schedule_se;
29mod verify;
30mod watermark;
31
32pub use error::FormsError;
33pub use form1040::{Form1040Fill, Form1040Inputs};
34pub use map::{Form1040Map, Form8283Map, Form8949Map, ScheduleDMap, ScheduleSeMap};
35pub use schedule_se::SE_FLOOR;
36
37use btctax_core::conventions::{TaxDate, Usd};
38use btctax_core::{Form8949Row, ScheduleDTotals};
39use time::macros::format_description;
40
41/// The tax years this build bundles forms + maps for. Each fill dispatches to the year's committed
42/// map + bundled PDF via the `Map::for_year` constructors; an unlisted year fails closed with
43/// [`FormsError::UnsupportedYear`].
44pub const SUPPORTED_YEARS: &[i32] = &[2017, 2024, 2025];
45
46/// Format a date as **MM/DD/YYYY** — Form 8949's native date format for columns (b)/(c).
47pub(crate) fn fmt_date(d: TaxDate) -> Result<String, FormsError> {
48 let fmt = format_description!("[month]/[day]/[year]");
49 d.format(&fmt)
50 .map_err(|e| FormsError::Structure(format!("date format: {e}")))
51}
52
53/// Format money exactly as the `form8949.csv` / `schedule_d.csv` do — the raw `Decimal` Display
54/// (native scale, no `$`, no thousands separators). Keeping this identical to the CSV is what lets
55/// `schedule_d_totals_match_form8949_and_csv` cross-check the three artifacts.
56pub(crate) fn fmt_money(d: Usd) -> String {
57 d.to_string()
58}
59
60/// Fill **Form 8949** (Part I + Part II) for `year` from the projection rows and return the PDF
61/// bytes. Bitcoin is filed under Box I/L. Parts with > 11 rows paginate: ⌈rows/11⌉ page copies per
62/// part, each with its own totals; the copies are merged with per-copy field renaming so no two share
63/// a value. Every copy is geometry-verified before merge.
64pub fn fill_form_8949(rows: &[Form8949Row], year: i32) -> Result<Vec<u8>, FormsError> {
65 let map = Form8949Map::for_year(year)?;
66 let cap = map.rows_per_page;
67 let (st, lt) = fill8949::split_parts(rows);
68 let n_pages = div_ceil(st.len(), cap).max(div_ceil(lt.len(), cap)).max(1);
69
70 if n_pages == 1 {
71 let short = fill8949::part_data(&st)?;
72 let long = fill8949::part_data(<)?;
73 return fill8949::fill_8949_parts(&short, &long, &map);
74 }
75
76 let mut copies = Vec::with_capacity(n_pages);
77 for k in 0..n_pages {
78 let st_chunk: Vec<&Form8949Row> = st.iter().skip(k * cap).take(cap).copied().collect();
79 let lt_chunk: Vec<&Form8949Row> = lt.iter().skip(k * cap).take(cap).copied().collect();
80 let short = fill8949::part_data(&st_chunk)?;
81 let long = fill8949::part_data(<_chunk)?;
82 // Each copy is filled on ORIGINAL names and geometry-verified here (fails closed).
83 copies.push(fill8949::fill_8949_parts(&short, &long, &map)?);
84 }
85 overflow::merge_copies(&copies)
86}
87
88fn div_ceil(n: usize, d: usize) -> usize {
89 n.div_ceil(d)
90}
91
92/// Stamp a diagonal `DRAFT — ESTIMATE, NOT FOR FILING` watermark on every page of a filled form.
93/// Applied by the CLI when the ledger is pseudo-reconciled (an estimate). The overlay carries its own
94/// embedded standard font resource, orthogonal to `/NeedAppearances`.
95pub fn stamp_draft_watermark(pdf_bytes: &[u8]) -> Result<Vec<u8>, FormsError> {
96 watermark::stamp_draft(pdf_bytes)
97}
98
99/// Fill **Schedule D** for `year` from the part totals and return the PDF bytes.
100pub fn fill_schedule_d(totals: &ScheduleDTotals, year: i32) -> Result<Vec<u8>, FormsError> {
101 let map = ScheduleDMap::for_year(year)?;
102 schedule_d::fill_schedule_d_totals(totals, &map)
103}
104
105/// Fill **Schedule SE** (Form 1040) for `year` from the computed §1401 `SeTaxResult`, the filer's
106/// Form W-2 Social Security wages (line 8a), and the year's Social Security wage base (line 7).
107/// Returns `Ok(None)` when net SE earnings are **below the $400 floor** (no SE tax owed — the form is
108/// not written). Line 12 = SS + regular Medicare only (the 0.9% Additional Medicare Tax is a Form 8959
109/// item, not on Schedule SE); when `se.addl > 0` the caller prints a Form 8959 advisory.
110pub fn fill_schedule_se(
111 se: &btctax_core::SeTaxResult,
112 w2_ss_wages: Usd,
113 ss_wage_base: Usd,
114 year: i32,
115) -> Result<Option<Vec<u8>>, FormsError> {
116 let map = ScheduleSeMap::for_year(year)?;
117 schedule_se::fill_schedule_se_with_map(se, w2_ss_wages, ss_wage_base, &map)
118}
119
120/// Fill **Form 8283** (Noncash Charitable Contributions, Rev. 12-2025) for `year` from the projected
121/// donation rows + `DonationDetails`. Returns `Ok(None)` when there are no donations in the year.
122/// Fills the donee/appraiser IDENTITY + per-row property data (and, for Section B, checks the "k
123/// Digital assets" property-type box); leaves BLANK every OTHER party's declaration/signature (a
124/// LOUD partial-scope notice is the caller's). More rows than a section holds (4 in Section A / 3 in
125/// Section B) overflow onto additional form copies via [`overflow::merge_copies`].
126pub fn fill_form_8283(
127 rows: &[btctax_core::Form8283Row],
128 year: i32,
129) -> Result<Option<Vec<u8>>, FormsError> {
130 let map = Form8283Map::for_year(year)?;
131 form8283::fill_form_8283(rows, &map)
132}
133
134/// Fill the capital-gains cells of **Form 1040** for `year`: line 7a (only when Schedule D is ACTIVE
135/// and line 16 ≥ 0; active-and-zero → "-0-") and the Digital-Asset Yes/No question (YES iff there is
136/// btctax-evidenced qualifying activity). Returns `Ok(None)` — **skip the whole 1040** — when there is
137/// no reportable activity (the DA answer would be blank and there is no 7a value). 7b checkboxes are
138/// left untouched; a NET LOSS leaves 7a blank (the §1211 line-21 cap is the filer's). A partial-scope
139/// notice enumerating exactly what was filled is the caller's.
140pub fn fill_form_1040_capgains(
141 inputs: &Form1040Inputs,
142 year: i32,
143) -> Result<Option<Form1040Fill>, FormsError> {
144 let map = Form1040Map::for_year(year)?;
145 form1040::fill_form_1040_capgains(inputs, &map)
146}
147
148/// **[I5]** How many rows might belong on a SEPARATE 1099-DA-reported Form 8949 (Box G/H/J/K) — i.e.
149/// disposals on an exchange that may have issued broker basis reporting. SP1 files EVERY Bitcoin row
150/// under Box I/L and says so; a non-zero count is a loud advisory, not a refusal.
151pub fn rows_possibly_broker_reported(rows: &[Form8949Row]) -> usize {
152 rows.iter().filter(|r| r.box_needs_review).count()
153}
154
155// ── Internals exposed for the KATs (fault injection needs a corruptible map + the verifier). ──────
156#[doc(hidden)]
157pub mod testonly {
158 pub use crate::cells::fmt_money_pair;
159 pub use crate::fill8949::{fill_8949_parts, part_data, pdf_has_xfa, split_parts, PartData};
160 pub use crate::form1040::{fill_form_1040_capgains as fill_1040_with_map, Form1040Fill};
161 pub use crate::form8283::fill_form_8283 as fill_8283_with_map;
162 pub use crate::map::{
163 AmountCols, Form1040Map, Form8283Map, Form8949Map, MoneyCell, MoneyPair, PartMap,
164 ScheduleDMap, ScheduleSeMap,
165 };
166 pub use crate::pdf::{
167 button_on_states, checkbox_on, collect_fields, index, load, text_value, Field,
168 F1040_PDF_2017, F1040_PDF_2024, F1040_PDF_2025, F8283_PDF_2017, F8283_PDF_2024,
169 F8283_PDF_2025, F8949_PDF_2017, F8949_PDF_2024, F8949_PDF_2025, SCHEDULE_D_PDF_2017,
170 SCHEDULE_D_PDF_2024, SCHEDULE_D_PDF_2025, SCHEDULE_SE_PDF_2017, SCHEDULE_SE_PDF_2024,
171 SCHEDULE_SE_PDF_2025,
172 };
173 pub use crate::schedule_d::fill_schedule_d_totals;
174 pub use crate::schedule_se::fill_schedule_se_with_map;
175 pub use crate::verify::{
176 no_unmapped_filled, topmost_yes_no_pair, verify_8949, verify_flat, FlatPlacement, Geo,
177 Placement,
178 };
179}