//! Fidelity reconciler for the `audit` command. //! //! Fidelity exports a single "all accounts" positions CSV. Parsing //! lives in `brokerage/fidelity.zig`; this module wires that parser //! into the shared per-account comparison engine in `common.zig`. //! Because the Fidelity export is a plain per-account positions list, //! the only Fidelity-specific knowledge here is "use the Fidelity CSV //! parser" and "the institution key is `fidelity`" — everything else //! (comparison, display) is shared. const std = @import("std"); const zfin = @import("../../root.zig"); const analysis = @import("../../analytics/analysis.zig"); const Date = @import("../../Date.zig"); const common = @import("common.zig"); const fidelity_parser = @import("../../brokerage/fidelity.zig"); /// Parse a Fidelity positions CSV and reconcile it against the /// portfolio. Returns owned `AccountComparison` results (free each /// `.comparisons` slice, then the results slice). String fields in /// the results borrow from `csv_data`, which must outlive them. /// /// Propagates the parser's errors (`EmptyFile` / `UnexpectedHeader`) /// and allocation failures so the caller can decide between a /// user-facing message (explicit `--fidelity`) and silently skipping /// the file (flagless auto-reconcile). pub fn reconcile( allocator: std.mem.Allocator, portfolio: zfin.Portfolio, csv_data: []const u8, account_map: analysis.AccountMap, prices: std.StringHashMap(f64), as_of: Date, ) ![]common.AccountComparison { const positions = try fidelity_parser.parseCsv(allocator, csv_data); // The result strings borrow from `csv_data`, not from this slice, // so freeing the slice array here is safe. defer allocator.free(positions); return common.compareAccounts(allocator, portfolio, positions, account_map, "fidelity", prices, as_of); } // ── Tests ──────────────────────────────────────────────────── const portfolio_mod = @import("../../models/portfolio.zig"); test "reconcile: parses Fidelity CSV and matches against portfolio" { const allocator = std.testing.allocator; const csv = "Account Number,Account Name,Symbol,Description,Quantity,Last Price,Last Price Change,Current Value,Today's Gain/Loss Dollar,Today's Gain/Loss Percent,Total Gain/Loss Dollar,Total Gain/Loss Percent,Percent Of Account,Cost Basis Total,Average Cost Basis,Type\n" ++ "Z123,Individual,AAPL,APPLE INC,100,$150.00,+$2.00,$15000.00,+$200.00,+1.35%,+$5000.00,+50.00%,100%,$10000.00,$100.00,Margin,\n"; var lots = [_]portfolio_mod.Lot{ .{ .symbol = "AAPL", .shares = 100, .open_date = Date.fromYmd(2024, 1, 1), .open_price = 100.0, .account = "Sample Brokerage" }, }; const portfolio = portfolio_mod.Portfolio{ .lots = &lots, .allocator = allocator }; var entries = [_]analysis.AccountTaxEntry{ .{ .account = "Sample Brokerage", .tax_type = .taxable, .institution = "fidelity", .account_number = "Z123" }, }; const acct_map = analysis.AccountMap{ .entries = &entries, .allocator = allocator }; var prices = std.StringHashMap(f64).init(allocator); defer prices.deinit(); try prices.put("AAPL", 150.0); const results = try reconcile(allocator, portfolio, csv, acct_map, prices, Date.fromYmd(2026, 5, 8)); defer { for (results) |r| allocator.free(r.comparisons); allocator.free(results); } try std.testing.expectEqual(@as(usize, 1), results.len); try std.testing.expectEqualStrings("Sample Brokerage", results[0].account_name); // 100 shares @ $150 on both sides → no discrepancy. try std.testing.expect(!results[0].has_discrepancies); } test "reconcile: propagates parser errors" { const allocator = std.testing.allocator; const portfolio = portfolio_mod.Portfolio{ .lots = &.{}, .allocator = allocator }; var entries = [_]analysis.AccountTaxEntry{}; const acct_map = analysis.AccountMap{ .entries = &entries, .allocator = allocator }; var prices = std.StringHashMap(f64).init(allocator); defer prices.deinit(); try std.testing.expectError( error.EmptyFile, reconcile(allocator, portfolio, "", acct_map, prices, Date.fromYmd(2026, 5, 8)), ); }