feat(1099): add I1099FormService with filing CSV export + Copy B PDF
Adds I1099FormService and Form1099FormService: an IRIS/accountant filing-data CSV (one row per reportable recipient) and a plain-paper recipient Copy B 1099-NEC PDF rendered via the DevExpress RichEdit/Office API (mirroring CheckPrintService). Includes a CSV-export unit test over a stub report service. Service lives in namespace ROLAC.API.Services (not ...Services.Form1099) to avoid shadowing the ROLAC.API.Entities.Form1099 constants class. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
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
|
||||
{
|
||||
/// <summary>Stub report service: only GetAnnualSummaryAsync is exercised by the CSV export.</summary>
|
||||
private sealed class StubReportService : IForm1099ReportService
|
||||
{
|
||||
private readonly Form1099SummaryDto _summary;
|
||||
public StubReportService(Form1099SummaryDto summary) => _summary = summary;
|
||||
|
||||
public Task<Form1099SummaryDto> GetAnnualSummaryAsync(int taxYear) => Task.FromResult(_summary);
|
||||
public Task<List<Form1099BoxDto>> GetBoxesAsync() => throw new NotImplementedException();
|
||||
public Task<Form1099RecipientDetailDto?> 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));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user