using System.Globalization; using System.Text; using DevExpress.Office; using DevExpress.XtraRichEdit; using DevExpress.XtraRichEdit.API.Native; using Microsoft.EntityFrameworkCore; using ROLAC.API.Data; using ROLAC.API.DTOs.Payee; using ROLAC.API.Entities; namespace ROLAC.API.Services; /// /// Produces recipient-facing 1099 outputs: a plain-paper Copy B 1099-NEC PDF (rendered with the /// DevExpress RichEdit/Office API, mirroring CheckPrintService) and a filing-data CSV. /// public class Form1099FormService : I1099FormService { private readonly IForm1099ReportService _report; private readonly IPayee1099Service _payees; private readonly AppDbContext _db; public Form1099FormService(IForm1099ReportService report, IPayee1099Service payees, AppDbContext db) { _report = report; _payees = payees; _db = db; } public async Task<(Stream stream, string contentType, string fileName)> RenderCopyBAsync(int payeeId, int taxYear) { var payee = await _payees.GetByIdAsync(payeeId) ?? throw new InvalidOperationException($"Payee {payeeId} not found."); var church = await _db.ChurchProfiles.AsNoTracking().OrderBy(x => x.Id).FirstOrDefaultAsync() ?? new ChurchProfile { Name = "Church" }; // Box 1 (Nonemployee compensation) = sum of this payee's NEC-1 payments for the year. var detail = await _report.GetRecipientDetailAsync(payeeId, taxYear); var box1Nec = detail?.Payments .Where(payment => payment.BoxCode == Entities.Form1099.BoxNec1) .Sum(payment => payment.Amount) ?? 0m; using var server = new RichEditDocumentServer(); var document = server.Document; document.BeginUpdate(); try { document.Unit = DocumentUnit.Inch; var section = document.Sections[0]; section.Page.Width = 8.5f; section.Page.Height = 11f; section.Margins.Left = section.Margins.Right = 0.8f; section.Margins.Top = section.Margins.Bottom = 0.8f; document.AppendHtmlText(BuildCopyBHtml(church, payee, taxYear, box1Nec)); } finally { document.EndUpdate(); } var stream = new MemoryStream(); server.ExportToPdf(stream); stream.Position = 0; return (stream, "application/pdf", $"1099-NEC-{payeeId}-{taxYear}.pdf"); } public async Task<(Stream stream, string contentType, string fileName)> ExportFilingCsvAsync(int taxYear) { var summary = await _report.GetAnnualSummaryAsync(taxYear); var builder = new StringBuilder(); builder.AppendLine("LegalName,TinLast4,W9Status,Box1_NEC,Box1_Rents,Total,MeetsThreshold"); foreach (var row in summary.Rows) { builder.AppendLine(string.Join(",", Csv(row.LegalName), Csv(row.TinLast4 ?? ""), Csv(row.W9Status), row.NecTotal.ToString(CultureInfo.InvariantCulture), row.RentsTotal.ToString(CultureInfo.InvariantCulture), row.GrandTotal.ToString(CultureInfo.InvariantCulture), row.MeetsThreshold ? "Y" : "N")); } var bytes = Encoding.UTF8.GetBytes(builder.ToString()); return (new MemoryStream(bytes), "text/csv", $"1099-filing-{taxYear}.csv"); static string Csv(string value) => value.Contains(',') || value.Contains('"') ? "\"" + value.Replace("\"", "\"\"") + "\"" : value; } private static string BuildCopyBHtml(ChurchProfile church, Payee1099Dto payee, int taxYear, decimal box1Nec) { var payerAddress = JoinAddress(church.Address, church.City, church.State, church.ZipCode); var recipientAddress = JoinAddress( JoinLines(payee.AddressLine1, payee.AddressLine2), payee.City, payee.State, payee.Zip); // ChurchProfile has no payer EIN/TIN field, so the payer-TIN box is labelled but left blank. var maskedTin = string.IsNullOrWhiteSpace(payee.TinLast4) ? "" : $"***-**-{payee.TinLast4}"; return "
" + $"

Form 1099-NEC — Copy B (For Recipient)

" + $"

Tax Year {taxYear}
Nonemployee Compensation

" + "" + "" + "" + "" + "" + "" + "
" + "PAYER’s name, address
" + $"{Encode(church.Name)}
{payerAddress}" + "
" + "PAYER’s TIN (EIN)
 " + "
" + "RECIPIENT’s name, address
" + $"{Encode(payee.LegalName)}
{recipientAddress}" + "
" + $"RECIPIENT’s TIN
{Encode(maskedTin)}" + "
" + "Box 1 — Nonemployee compensation
" + $"{Encode(FormatCurrency(box1Nec))}" + "
" + "

" + "This is important tax information and is being furnished to the recipient. " + "Recipient’s taxpayer identification number is shown masked for security." + "

" + "
"; } private static string Encode(string? text) => System.Net.WebUtility.HtmlEncode(text ?? ""); private static string FormatCurrency(decimal amount) => amount.ToString("C2", CultureInfo.GetCultureInfo("en-US")); private static string? JoinLines(string? line1, string? line2) { var parts = new[] { line1, line2 }.Where(part => !string.IsNullOrWhiteSpace(part)); var joined = string.Join(", ", parts); return string.IsNullOrWhiteSpace(joined) ? null : joined; } // Builds an HTML address block; each text part is HTML-encoded and the line break (
) is literal. private static string JoinAddress(string? address, string? city, string? state, string? zip) { var cityLine = string.Join(", ", new[] { city, string.Join(" ", new[] { state, zip }.Where(part => !string.IsNullOrWhiteSpace(part))) } .Where(part => !string.IsNullOrWhiteSpace(part))); var lines = new[] { address, cityLine } .Where(part => !string.IsNullOrWhiteSpace(part)) .Select(Encode); return string.Join("
", lines); } }