using System.Globalization; using System.Text; using ROLAC.API.DTOs.Finance; using ROLAC.API.Services; using Xunit; namespace ROLAC.API.Tests.Services; public class Form1099FormServiceTests { /// Stub report service: only GetAnnualSummaryAsync is exercised by the CSV export. private sealed class StubReportService : IForm1099ReportService { private readonly Form1099SummaryDto _summary; public StubReportService(Form1099SummaryDto summary) => _summary = summary; public Task GetAnnualSummaryAsync(int taxYear) => Task.FromResult(_summary); public Task> GetBoxesAsync() => throw new NotImplementedException(); public Task GetRecipientDetailAsync(int payeeId, int taxYear) => throw new NotImplementedException(); } private static Form1099FormService BuildService(Form1099SummaryDto summary) => // IPayee1099Service and AppDbContext are only used by RenderCopyBAsync, not by the CSV path. new Form1099FormService(new StubReportService(summary), payees: null!, db: null!); [Fact] public async Task ExportFilingCsvAsync_WritesHeaderRowPerRecipientAndInvariantNumbers() { var summary = new Form1099SummaryDto { TaxYear = 2026, Rows = { new Form1099RecipientRowDto { PayeeId = 1, LegalName = "Acme, LLC", TinLast4 = "1234", W9Status = "OnFile", NecTotal = 1234.50m, RentsTotal = 0m, GrandTotal = 1234.50m, MeetsThreshold = true }, new Form1099RecipientRowDto { PayeeId = 2, LegalName = "Bob Smith", TinLast4 = "9876", W9Status = "Missing", NecTotal = 100m, RentsTotal = 50m, GrandTotal = 150m, MeetsThreshold = false }, } }; var service = BuildService(summary); var (stream, contentType, fileName) = await service.ExportFilingCsvAsync(2026); Assert.Equal("text/csv", contentType); Assert.Equal("1099-filing-2026.csv", fileName); using var reader = new StreamReader(stream, Encoding.UTF8); var text = await reader.ReadToEndAsync(); var lines = text.Split(new[] { "\r\n", "\n" }, StringSplitOptions.RemoveEmptyEntries); // Header + one data line per row. Assert.Equal(3, lines.Length); Assert.Equal("LegalName,TinLast4,W9Status,Box1_NEC,Box1_Rents,Total,MeetsThreshold", lines[0]); // A value containing a comma is quoted. Assert.StartsWith("\"Acme, LLC\",1234,OnFile,", lines[1]); // Invariant numeric formatting (period decimal separator) and Y/N threshold flag. Assert.Contains("1234.50", lines[1]); Assert.EndsWith(",Y", lines[1]); Assert.EndsWith(",N", lines[2]); // Sanity: the period really is the invariant separator regardless of current culture. Assert.Equal("1234.50", 1234.50m.ToString(CultureInfo.InvariantCulture)); } }