diff --git a/API/ROLAC.API.Tests/ROLAC.API.Tests.csproj b/API/ROLAC.API.Tests/ROLAC.API.Tests.csproj
index ffa3eaa..59db6a3 100644
--- a/API/ROLAC.API.Tests/ROLAC.API.Tests.csproj
+++ b/API/ROLAC.API.Tests/ROLAC.API.Tests.csproj
@@ -22,6 +22,7 @@
+
diff --git a/API/ROLAC.API.Tests/Services/Form1099FormServiceTests.cs b/API/ROLAC.API.Tests/Services/Form1099FormServiceTests.cs
new file mode 100644
index 0000000..b4eaf40
--- /dev/null
+++ b/API/ROLAC.API.Tests/Services/Form1099FormServiceTests.cs
@@ -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
+{
+ /// 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));
+ }
+}
diff --git a/API/ROLAC.API.Tests/Services/Form1099ReportServiceTests.cs b/API/ROLAC.API.Tests/Services/Form1099ReportServiceTests.cs
new file mode 100644
index 0000000..90e68a1
--- /dev/null
+++ b/API/ROLAC.API.Tests/Services/Form1099ReportServiceTests.cs
@@ -0,0 +1,106 @@
+using Microsoft.AspNetCore.Http;
+using Microsoft.EntityFrameworkCore;
+using Moq;
+using System.Security.Claims;
+using ROLAC.API.Data;
+using ROLAC.API.Data.Interceptors;
+using ROLAC.API.Entities;
+using ROLAC.API.Services;
+using Xunit;
+
+namespace ROLAC.API.Tests.Services;
+
+public class Form1099ReportServiceTests
+{
+ private static AppDbContext NewDb()
+ {
+ var httpContext = new DefaultHttpContext { User = new(new ClaimsIdentity(new[] { new Claim(ClaimTypes.NameIdentifier, "t") })) };
+ var accessorMock = new Mock();
+ accessorMock.Setup(x => x.HttpContext).Returns(httpContext);
+ return new AppDbContext(new DbContextOptionsBuilder()
+ .UseInMemoryDatabase(Guid.NewGuid().ToString())
+ .AddInterceptors(new AuditSaveChangesInterceptor(new ROLAC.API.Services.Logging.CurrentUserAccessor(accessorMock.Object))).Options);
+ }
+
+ private static AppDbContext Seeded(out int necSubId, out int rentSubId, out int salarySubId)
+ {
+ var db = NewDb();
+ db.Ministries.Add(new Ministry { Id = 1, Name_en = "Admin", DefaultFunctionalClass = "Program" });
+ var nec = new Form1099Box { Id = 1, BoxCode = Form1099.BoxNec1, Name_en = "NEC", FormType = "1099-NEC", SortOrder = 1 };
+ var rent = new Form1099Box { Id = 2, BoxCode = Form1099.BoxMisc1, Name_en = "Rent", FormType = "1099-MISC", SortOrder = 2 };
+ db.Form1099Boxes.AddRange(nec, rent);
+ db.ExpenseCategoryGroups.Add(new ExpenseCategoryGroup { Id = 1, Name_en = "Personnel" });
+ db.ExpenseCategoryGroups.Add(new ExpenseCategoryGroup { Id = 2, Name_en = "Facility" });
+ db.ExpenseSubCategories.Add(new ExpenseSubCategory { Id = 1, GroupId = 1, Name_en = "Contract Labor", Form1099BoxId = 1 });
+ db.ExpenseSubCategories.Add(new ExpenseSubCategory { Id = 2, GroupId = 2, Name_en = "Rent", Form1099BoxId = 2 });
+ db.ExpenseSubCategories.Add(new ExpenseSubCategory { Id = 3, GroupId = 1, Name_en = "Salary & Wages", Form1099BoxId = null });
+ db.SaveChanges();
+ necSubId = 1; rentSubId = 2; salarySubId = 3;
+ return db;
+ }
+
+ private static void AddPaidExpense(AppDbContext db, int payeeId, int subId, int groupId, decimal amount, DateOnly paidOn)
+ {
+ var e = new Expense
+ {
+ MinistryId = 1, Type = "VendorPayment", Status = "Paid", PayeeId = payeeId,
+ Amount = amount, Description = "x", ExpenseDate = paidOn,
+ PaidAt = new DateTimeOffset(paidOn.ToDateTime(TimeOnly.MinValue), TimeSpan.Zero),
+ Lines = [ new ExpenseLine { CategoryGroupId = groupId, SubCategoryId = subId, Amount = amount } ],
+ };
+ db.Expenses.Add(e);
+ db.SaveChanges();
+ }
+
+ [Fact]
+ public async Task Sums_tracked_recipient_by_box_and_flags_threshold_and_w9()
+ {
+ var db = Seeded(out var necSub, out var rentSub, out _);
+ db.Payee1099s.Add(new Payee1099 { Id = 10, LegalName = "Pat Player", Is1099Tracked = true, W9Status = "Missing" });
+ db.SaveChanges();
+ AddPaidExpense(db, 10, necSub, 1, 700m, new DateOnly(2026, 3, 1));
+ AddPaidExpense(db, 10, rentSub, 2, 500m, new DateOnly(2026, 4, 1));
+
+ var svc = new Form1099ReportService(db);
+ var sum = await svc.GetAnnualSummaryAsync(2026);
+
+ var row = Assert.Single(sum.Rows);
+ Assert.Equal(700m, row.NecTotal);
+ Assert.Equal(500m, row.RentsTotal);
+ Assert.Equal(1200m, row.GrandTotal);
+ Assert.True(row.MeetsThreshold);
+ Assert.True(row.W9Missing);
+ Assert.Equal(1, sum.RecipientsAtThreshold);
+ Assert.Equal(1, sum.RecipientsMissingW9);
+ }
+
+ [Fact]
+ public async Task Excludes_untracked_recipients_and_unmapped_and_wrong_year()
+ {
+ var db = Seeded(out var necSub, out _, out var salarySub);
+ db.Payee1099s.Add(new Payee1099 { Id = 10, LegalName = "Tracked Tim", Is1099Tracked = true, W9Status = "OnFile" });
+ db.Payee1099s.Add(new Payee1099 { Id = 11, LegalName = "Corp Inc", Is1099Tracked = false, W9Status = "OnFile" });
+ db.SaveChanges();
+ AddPaidExpense(db, 11, necSub, 1, 5000m, new DateOnly(2026, 5, 1)); // untracked
+ AddPaidExpense(db, 10, salarySub, 1, 5000m, new DateOnly(2026, 6, 1)); // unmapped box
+ AddPaidExpense(db, 10, necSub, 1, 5000m, new DateOnly(2025, 6, 1)); // wrong year
+
+ var sum = await new Form1099ReportService(db).GetAnnualSummaryAsync(2026);
+ Assert.Empty(sum.Rows);
+ }
+
+ [Fact]
+ public async Task Threshold_flag_is_false_below_600()
+ {
+ var db = Seeded(out var necSub, out _, out _);
+ db.Payee1099s.Add(new Payee1099 { Id = 10, LegalName = "Small Sam", Is1099Tracked = true, W9Status = "OnFile" });
+ db.SaveChanges();
+ AddPaidExpense(db, 10, necSub, 1, 599.99m, new DateOnly(2026, 7, 1));
+
+ var sum = await new Form1099ReportService(db).GetAnnualSummaryAsync(2026);
+ var row = Assert.Single(sum.Rows);
+ Assert.False(row.MeetsThreshold);
+ Assert.False(row.W9Missing);
+ Assert.Equal(0, sum.RecipientsAtThreshold);
+ }
+}
diff --git a/API/ROLAC.API.Tests/Services/Payee1099ServiceTests.cs b/API/ROLAC.API.Tests/Services/Payee1099ServiceTests.cs
new file mode 100644
index 0000000..f3bb20f
--- /dev/null
+++ b/API/ROLAC.API.Tests/Services/Payee1099ServiceTests.cs
@@ -0,0 +1,112 @@
+using Microsoft.AspNetCore.DataProtection;
+using Microsoft.AspNetCore.Http;
+using Microsoft.EntityFrameworkCore;
+using Moq;
+using System.Security.Claims;
+using ROLAC.API.Data;
+using ROLAC.API.Data.Interceptors;
+using ROLAC.API.DTOs.Payee;
+using ROLAC.API.Entities;
+using ROLAC.API.Services;
+using ROLAC.API.Services.Logging;
+using ROLAC.API.Services.Security;
+using ROLAC.API.Services.Storage;
+using Xunit;
+
+namespace ROLAC.API.Tests.Services;
+
+public class Payee1099ServiceTests
+{
+ // Minimal in-memory IFileStorage (mirrors the ExpenseServiceTests fake).
+ private sealed class FakeStorage : IFileStorage
+ {
+ public Dictionary Files = new();
+ public Task SaveAsync(Stream c, string p, CancellationToken ct = default)
+ { using var ms = new MemoryStream(); c.CopyTo(ms); Files[p] = ms.ToArray(); return Task.FromResult(p); }
+ public Task OpenReadAsync(string p, CancellationToken ct = default)
+ => Task.FromResult(Files.TryGetValue(p, out var b) ? new MemoryStream(b) : null);
+ public Task DeleteAsync(string p, CancellationToken ct = default) { Files.Remove(p); return Task.CompletedTask; }
+ }
+
+ private static (Payee1099Service svc, AppDbContext db) Build()
+ {
+ var httpContext = new DefaultHttpContext { User = new(new ClaimsIdentity(new[] { new Claim(ClaimTypes.NameIdentifier, "test-user") })) };
+ var accessorMock = new Mock();
+ accessorMock.Setup(x => x.HttpContext).Returns(httpContext);
+ var db = new AppDbContext(new DbContextOptionsBuilder()
+ .UseInMemoryDatabase(Guid.NewGuid().ToString())
+ .AddInterceptors(new AuditSaveChangesInterceptor(new CurrentUserAccessor(accessorMock.Object))).Options);
+ var tin = new TinProtector(DataProtectionProvider.Create("ROLAC.Tests"));
+ return (new Payee1099Service(db, tin, new FakeStorage()), db);
+ }
+
+ [Fact]
+ public async Task Create_encrypts_tin_and_stores_last4_only_in_clear()
+ {
+ var (svc, db) = Build();
+ var id = await svc.CreateAsync(new SavePayee1099Request
+ { LegalName = "Pat Player", TinType = "SSN", Tin = "123-45-6789", W9Status = "OnFile" });
+
+ var saved = await db.Payee1099s.FindAsync(id);
+ Assert.NotNull(saved);
+ Assert.Equal("6789", saved!.TinLast4);
+ Assert.NotNull(saved.TinEncrypted);
+ Assert.DoesNotContain("123-45-6789", saved.TinEncrypted!);
+ Assert.Equal("123-45-6789", await svc.RevealTinAsync(id));
+ }
+
+ [Fact]
+ public async Task Update_with_null_tin_keeps_existing_ciphertext()
+ {
+ var (svc, db) = Build();
+ var id = await svc.CreateAsync(new SavePayee1099Request { LegalName = "X", Tin = "11-2223333" });
+ var before = (await db.Payee1099s.FindAsync(id))!.TinEncrypted;
+
+ await svc.UpdateAsync(id, new SavePayee1099Request { LegalName = "X renamed", Tin = null });
+
+ var after = await db.Payee1099s.FindAsync(id);
+ Assert.Equal("X renamed", after!.LegalName);
+ Assert.Equal(before, after.TinEncrypted);
+ Assert.Equal("3333", after.TinLast4);
+ }
+
+ [Fact]
+ public async Task List_dto_masks_tin_to_last4()
+ {
+ var (svc, _) = Build();
+ await svc.CreateAsync(new SavePayee1099Request { LegalName = "Y", Tin = "999-88-7777" });
+ var list = await svc.GetAllAsync(includeInactive: true);
+ var item = Assert.Single(list);
+ Assert.Equal("7777", item.TinLast4);
+ }
+
+ [Fact]
+ public async Task Delete_is_soft_and_hides_from_list()
+ {
+ var (svc, _) = Build();
+ var id = await svc.CreateAsync(new SavePayee1099Request { LegalName = "Z" });
+ await svc.DeleteAsync(id);
+ Assert.Empty(await svc.GetAllAsync(includeInactive: true));
+ }
+
+ [Fact]
+ public async Task SaveW9_records_document_and_round_trips_bytes()
+ {
+ var (svc, _) = Build();
+ var id = await svc.CreateAsync(new SavePayee1099Request { LegalName = "W9 Payee" });
+
+ var bytes = new byte[] { 1, 2, 3, 4, 5 };
+ await svc.SaveW9Async(id, new MemoryStream(bytes), "w9.pdf");
+
+ var dto = await svc.GetByIdAsync(id);
+ Assert.NotNull(dto);
+ Assert.True(dto!.HasW9Document);
+
+ var opened = await svc.OpenW9Async(id);
+ Assert.NotNull(opened);
+ Assert.Equal("application/pdf", opened!.Value.contentType);
+ using var ms = new MemoryStream();
+ await opened.Value.stream.CopyToAsync(ms);
+ Assert.Equal(bytes, ms.ToArray());
+ }
+}
diff --git a/API/ROLAC.API.Tests/Services/TinProtectorTests.cs b/API/ROLAC.API.Tests/Services/TinProtectorTests.cs
new file mode 100644
index 0000000..44ba817
--- /dev/null
+++ b/API/ROLAC.API.Tests/Services/TinProtectorTests.cs
@@ -0,0 +1,30 @@
+using Microsoft.AspNetCore.DataProtection;
+using ROLAC.API.Services.Security;
+using Xunit;
+
+namespace ROLAC.API.Tests.Services;
+
+public class TinProtectorTests
+{
+ private static TinProtector Build() =>
+ new TinProtector(DataProtectionProvider.Create("ROLAC.Tests"));
+
+ [Fact]
+ public void Protect_then_Unprotect_round_trips()
+ {
+ var p = Build();
+ var cipher = p.Protect("123-45-6789");
+ Assert.NotEqual("123-45-6789", cipher);
+ Assert.Equal("123-45-6789", p.Unprotect(cipher));
+ }
+
+ [Theory]
+ [InlineData("123-45-6789", "6789")]
+ [InlineData("12-3456789", "6789")]
+ [InlineData("7", "7")]
+ public void Last4_keeps_only_trailing_digits(string raw, string expected)
+ => Assert.Equal(expected, TinProtector.Last4(raw));
+
+ [Fact]
+ public void Last4_of_null_is_null() => Assert.Null(TinProtector.Last4(null));
+}
diff --git a/API/ROLAC.API/Authorization/Modules.cs b/API/ROLAC.API/Authorization/Modules.cs
index fa0ce49..03066d7 100644
--- a/API/ROLAC.API/Authorization/Modules.cs
+++ b/API/ROLAC.API/Authorization/Modules.cs
@@ -17,6 +17,7 @@ public static class Modules
public const string Ministries = "Ministries";
public const string FinanceDashboard = "FinanceDashboard";
public const string Form990Report = "Form990Report";
+ public const string Form1099 = "Form1099";
public const string MonthlyStatements = "MonthlyStatements";
public const string ChurchProfile = "ChurchProfile";
public const string Disbursements = "Disbursements";
@@ -39,6 +40,7 @@ public static class Modules
Ministries,
FinanceDashboard,
Form990Report,
+ Form1099,
MonthlyStatements,
ChurchProfile,
Disbursements,
diff --git a/API/ROLAC.API/Controllers/Form1099ReportController.cs b/API/ROLAC.API/Controllers/Form1099ReportController.cs
new file mode 100644
index 0000000..c83617e
--- /dev/null
+++ b/API/ROLAC.API/Controllers/Form1099ReportController.cs
@@ -0,0 +1,44 @@
+using Microsoft.AspNetCore.Mvc;
+using ROLAC.API.Authorization;
+using ROLAC.API.Services;
+
+namespace ROLAC.API.Controllers;
+
+[ApiController]
+[Route("api/form1099-report")]
+[HasPermission(Modules.Form1099, PermissionActions.Read)]
+public class Form1099ReportController : ControllerBase
+{
+ private readonly IForm1099ReportService _svc;
+ private readonly I1099FormService _form;
+ public Form1099ReportController(IForm1099ReportService svc, I1099FormService form)
+ {
+ _svc = svc;
+ _form = form;
+ }
+
+ [HttpGet("boxes")]
+ public async Task Boxes() => Ok(await _svc.GetBoxesAsync());
+
+ [HttpGet("summary")]
+ public async Task Summary([FromQuery] int taxYear)
+ => Ok(await _svc.GetAnnualSummaryAsync(taxYear));
+
+ [HttpGet("recipient/{payeeId:int}")]
+ public async Task Recipient(int payeeId, [FromQuery] int taxYear)
+ => await _svc.GetRecipientDetailAsync(payeeId, taxYear) is { } d ? Ok(d) : NotFound();
+
+ [HttpGet("recipient/{payeeId:int}/copy-b")]
+ public async Task CopyB(int payeeId, [FromQuery] int taxYear)
+ {
+ var (stream, contentType, fileName) = await _form.RenderCopyBAsync(payeeId, taxYear);
+ return File(stream, contentType, fileName);
+ }
+
+ [HttpGet("export-csv")]
+ public async Task ExportCsv([FromQuery] int taxYear)
+ {
+ var (stream, contentType, fileName) = await _form.ExportFilingCsvAsync(taxYear);
+ return File(stream, contentType, fileName);
+ }
+}
diff --git a/API/ROLAC.API/Controllers/Payee1099Controller.cs b/API/ROLAC.API/Controllers/Payee1099Controller.cs
new file mode 100644
index 0000000..dd6a93c
--- /dev/null
+++ b/API/ROLAC.API/Controllers/Payee1099Controller.cs
@@ -0,0 +1,71 @@
+using Microsoft.AspNetCore.Mvc;
+using ROLAC.API.Authorization;
+using ROLAC.API.DTOs.Payee;
+using ROLAC.API.Services;
+
+namespace ROLAC.API.Controllers;
+
+[ApiController]
+[Route("api/payee-1099")]
+[HasPermission(Modules.Form1099, PermissionActions.Read)]
+public class Payee1099Controller : ControllerBase
+{
+ private readonly IPayee1099Service _svc;
+ public Payee1099Controller(IPayee1099Service svc) => _svc = svc;
+
+ [HttpGet]
+ public async Task GetAll([FromQuery] bool includeInactive = false)
+ => Ok(await _svc.GetAllAsync(includeInactive));
+
+ [HttpGet("{id:int}")]
+ public async Task GetById(int id)
+ => await _svc.GetByIdAsync(id) is { } dto ? Ok(dto) : NotFound();
+
+ [HttpPost]
+ [HasPermission(Modules.Form1099, PermissionActions.Write)]
+ public async Task Create([FromBody] SavePayee1099Request r)
+ => Ok(new { id = await _svc.CreateAsync(r) });
+
+ [HttpPut("{id:int}")]
+ [HasPermission(Modules.Form1099, PermissionActions.Write)]
+ public async Task Update(int id, [FromBody] SavePayee1099Request r)
+ { await _svc.UpdateAsync(id, r); return NoContent(); }
+
+ [HttpDelete("{id:int}")]
+ [HasPermission(Modules.Form1099, PermissionActions.Delete)]
+ public async Task Delete(int id)
+ { await _svc.DeleteAsync(id); return NoContent(); }
+
+ // Full TIN reveal is gated on Write (a stronger right than Read).
+ [HttpGet("{id:int}/tin")]
+ [HasPermission(Modules.Form1099, PermissionActions.Write)]
+ public async Task RevealTin(int id)
+ => Ok(new { tin = await _svc.RevealTinAsync(id) });
+
+ // Mirrors the expense-receipt upload: multipart form file, size-limited, type-checked.
+ [HttpPost("{id:int}/w9")]
+ [HasPermission(Modules.Form1099, PermissionActions.Write)]
+ [RequestSizeLimit(10_485_760)]
+ public async Task UploadW9(int id, IFormFile file)
+ {
+ if (file is null || file.Length == 0) return BadRequest(new { message = "No file." });
+ var allowed = new[] { "image/jpeg", "image/png", "image/webp", "application/pdf" };
+ if (!allowed.Contains(file.ContentType)) return BadRequest(new { message = "Unsupported file type." });
+ try
+ {
+ await using var stream = file.OpenReadStream();
+ await _svc.SaveW9Async(id, stream, file.FileName);
+ return NoContent();
+ }
+ catch (KeyNotFoundException) { return NotFound(); }
+ }
+
+ // Class-level Read gate covers viewing the stored W-9 (mirrors the receipt GET).
+ [HttpGet("{id:int}/w9")]
+ public async Task GetW9(int id)
+ {
+ var result = await _svc.OpenW9Async(id);
+ if (result is null) return NotFound();
+ return File(result.Value.stream, result.Value.contentType);
+ }
+}
diff --git a/API/ROLAC.API/DTOs/Disbursement/ChurchProfileDtos.cs b/API/ROLAC.API/DTOs/Disbursement/ChurchProfileDtos.cs
index 749e8c4..6882cf7 100644
--- a/API/ROLAC.API/DTOs/Disbursement/ChurchProfileDtos.cs
+++ b/API/ROLAC.API/DTOs/Disbursement/ChurchProfileDtos.cs
@@ -16,6 +16,7 @@ public class ChurchProfileDto
public string? BankName { get; set; }
public string? BankAccountNumber { get; set; }
public string? BankRoutingNumber { get; set; }
+ public string? PayerEin { get; set; }
public int NextCheckNumber { get; set; }
public string AiProvider { get; set; } = "Claude";
public string? ClaudeModel { get; set; }
@@ -38,6 +39,7 @@ public class UpdateChurchProfileRequest
[MaxLength(200)] public string? BankName { get; set; }
[MaxLength(50)] public string? BankAccountNumber { get; set; }
[MaxLength(50)] public string? BankRoutingNumber { get; set; }
+ [MaxLength(20)] public string? PayerEin { get; set; }
[Range(1, int.MaxValue)] public int NextCheckNumber { get; set; }
[MaxLength(20)] public string AiProvider { get; set; } = "Claude";
[MaxLength(100)] public string? ClaudeModel { get; set; }
diff --git a/API/ROLAC.API/DTOs/Expense/ExpenseCategoryDtos.cs b/API/ROLAC.API/DTOs/Expense/ExpenseCategoryDtos.cs
index 78e1250..6efcc5b 100644
--- a/API/ROLAC.API/DTOs/Expense/ExpenseCategoryDtos.cs
+++ b/API/ROLAC.API/DTOs/Expense/ExpenseCategoryDtos.cs
@@ -11,6 +11,8 @@ public class ExpenseSubCategoryDto
public bool IsActive { get; set; }
public int? Form990LineId { get; set; }
public string? Form990LineCode { get; set; }
+ public int? Form1099BoxId { get; set; }
+ public string? Form1099BoxCode { get; set; }
}
public class ExpenseCategoryGroupDto
@@ -22,6 +24,8 @@ public class ExpenseCategoryGroupDto
public bool IsActive { get; set; }
public int? Form990LineId { get; set; }
public string? Form990LineCode { get; set; }
+ public int? Form1099BoxId { get; set; }
+ public string? Form1099BoxCode { get; set; }
public List SubCategories { get; set; } = [];
}
@@ -31,6 +35,7 @@ public class CreateExpenseGroupRequest
[MaxLength(200)] public string? Name_zh { get; set; }
public int SortOrder { get; set; }
public int? Form990LineId { get; set; }
+ public int? Form1099BoxId { get; set; }
}
public class UpdateExpenseGroupRequest : CreateExpenseGroupRequest
{
@@ -44,6 +49,7 @@ public class CreateExpenseSubCategoryRequest
[MaxLength(200)] public string? Name_zh { get; set; }
public int SortOrder { get; set; }
public int? Form990LineId { get; set; }
+ public int? Form1099BoxId { get; set; }
}
public class UpdateExpenseSubCategoryRequest : CreateExpenseSubCategoryRequest
{
diff --git a/API/ROLAC.API/DTOs/Expense/ExpenseDtos.cs b/API/ROLAC.API/DTOs/Expense/ExpenseDtos.cs
index 9527125..2584854 100644
--- a/API/ROLAC.API/DTOs/Expense/ExpenseDtos.cs
+++ b/API/ROLAC.API/DTOs/Expense/ExpenseDtos.cs
@@ -35,6 +35,7 @@ public class ExpenseListItemDto
public string? ReviewedByName { get; set; } // resolved Member full name, email fallback
public DateTimeOffset? ReviewedAt { get; set; }
public string? ReviewNotes { get; set; } // reject reason (or approval note)
+ public int? PayeeId { get; set; }
}
public class ExpenseDto : ExpenseListItemDto
@@ -66,6 +67,7 @@ public class CreateExpenseRequest
[MaxLength(50)] public string? CheckNumber { get; set; }
[Required] public DateOnly ExpenseDate { get; set; }
public string? Notes { get; set; }
+ public int? PayeeId { get; set; }
}
public class UpdateExpenseRequest : CreateExpenseRequest { }
diff --git a/API/ROLAC.API/DTOs/Finance/Form1099ReportDtos.cs b/API/ROLAC.API/DTOs/Finance/Form1099ReportDtos.cs
new file mode 100644
index 0000000..52fca56
--- /dev/null
+++ b/API/ROLAC.API/DTOs/Finance/Form1099ReportDtos.cs
@@ -0,0 +1,52 @@
+namespace ROLAC.API.DTOs.Finance;
+
+public class Form1099BoxDto
+{
+ public int Id { get; set; }
+ public string BoxCode { get; set; } = "";
+ public string Name_en { get; set; } = "";
+ public string? Name_zh { get; set; }
+ public string FormType { get; set; } = "";
+ public int SortOrder { get; set; }
+}
+
+public class Form1099RecipientRowDto
+{
+ public int PayeeId { get; set; }
+ public string LegalName { get; set; } = "";
+ public string? TinLast4 { get; set; }
+ public string W9Status { get; set; } = "";
+ public decimal NecTotal { get; set; }
+ public decimal RentsTotal { get; set; }
+ public decimal GrandTotal { get; set; }
+ public bool MeetsThreshold { get; set; }
+ public bool W9Missing { get; set; }
+}
+
+public class Form1099SummaryDto
+{
+ public int TaxYear { get; set; }
+ public List Rows { get; set; } = [];
+ public decimal TotalReportable { get; set; }
+ public int RecipientsAtThreshold { get; set; }
+ public int RecipientsMissingW9 { get; set; }
+}
+
+public class Form1099PaymentDto
+{
+ public string PaidDate { get; set; } = "";
+ public string Description { get; set; } = "";
+ public string CategoryName { get; set; } = "";
+ public string BoxCode { get; set; } = "";
+ public decimal Amount { get; set; }
+}
+
+public class Form1099RecipientDetailDto
+{
+ public int PayeeId { get; set; }
+ public string LegalName { get; set; } = "";
+ public string? TinLast4 { get; set; }
+ public string W9Status { get; set; } = "";
+ public int TaxYear { get; set; }
+ public List Payments { get; set; } = [];
+}
diff --git a/API/ROLAC.API/DTOs/Payee/Payee1099Dtos.cs b/API/ROLAC.API/DTOs/Payee/Payee1099Dtos.cs
new file mode 100644
index 0000000..481e952
--- /dev/null
+++ b/API/ROLAC.API/DTOs/Payee/Payee1099Dtos.cs
@@ -0,0 +1,54 @@
+using System.ComponentModel.DataAnnotations;
+namespace ROLAC.API.DTOs.Payee;
+
+public class Payee1099ListItemDto
+{
+ public int Id { get; set; }
+ public string LegalName { get; set; } = "";
+ public string? DisplayName { get; set; }
+ public int? MemberId { get; set; }
+ public string? MemberName { get; set; }
+ public string TaxClassification { get; set; } = "";
+ public bool Is1099Tracked { get; set; }
+ public string? TinType { get; set; }
+ public string? TinLast4 { get; set; }
+ public string W9Status { get; set; } = "";
+ public bool IsActive { get; set; }
+}
+
+public class Payee1099Dto : Payee1099ListItemDto
+{
+ public string? AddressLine1 { get; set; }
+ public string? AddressLine2 { get; set; }
+ public string? City { get; set; }
+ public string? State { get; set; }
+ public string? Zip { get; set; }
+ public string? Email { get; set; }
+ public string? Phone { get; set; }
+ public string? W9ReceivedDate { get; set; }
+ public bool HasW9Document { get; set; }
+ public string? Notes { get; set; }
+}
+
+public class SavePayee1099Request
+{
+ [Required, MaxLength(200)] public string LegalName { get; set; } = "";
+ [MaxLength(200)] public string? DisplayName { get; set; }
+ public int? MemberId { get; set; }
+ [Required, MaxLength(40)] public string TaxClassification { get; set; } = "Individual";
+ public bool Is1099Tracked { get; set; } = true;
+ [MaxLength(10)] public string? TinType { get; set; }
+ /// Plain TIN; null = leave unchanged on update. Encrypted server-side.
+ public string? Tin { get; set; }
+ [MaxLength(100)] public string? AddressLine1 { get; set; }
+ [MaxLength(100)] public string? AddressLine2 { get; set; }
+ [MaxLength(60)] public string? City { get; set; }
+ [MaxLength(2)] public string? State { get; set; }
+ [MaxLength(10)] public string? Zip { get; set; }
+ [MaxLength(120)] public string? Email { get; set; }
+ [MaxLength(40)] public string? Phone { get; set; }
+ [MaxLength(20)] public string W9Status { get; set; } = "Missing";
+ public DateOnly? W9ReceivedDate { get; set; }
+ public bool IsActive { get; set; } = true;
+ public string? Notes { get; set; }
+}
diff --git a/API/ROLAC.API/Data/AppDbContext.cs b/API/ROLAC.API/Data/AppDbContext.cs
index 37eaddf..7c946bd 100644
--- a/API/ROLAC.API/Data/AppDbContext.cs
+++ b/API/ROLAC.API/Data/AppDbContext.cs
@@ -21,6 +21,8 @@ public class AppDbContext : IdentityDbContext
public DbSet ExpenseCategoryGroups => Set();
public DbSet ExpenseSubCategories => Set();
public DbSet Form990ExpenseLines => Set();
+ public DbSet Payee1099s => Set();
+ public DbSet Form1099Boxes => Set();
public DbSet Expenses => Set();
public DbSet ExpenseLines => Set();
public DbSet ExpenseSnapshots => Set();
@@ -218,6 +220,45 @@ public class AppDbContext : IdentityDbContext
entity.HasIndex(e => e.LineCode).IsUnique();
});
+ // ── Form1099Box (1099 reporting box catalog) ──────────────────────────
+ builder.Entity(entity =>
+ {
+ entity.Property(e => e.BoxCode).HasMaxLength(10).IsRequired();
+ entity.Property(e => e.Name_en).HasMaxLength(200).IsRequired();
+ entity.Property(e => e.Name_zh).HasMaxLength(200);
+ entity.Property(e => e.FormType).HasMaxLength(20).IsRequired();
+ entity.Property(e => e.CreatedBy).HasMaxLength(450);
+ entity.Property(e => e.UpdatedBy).HasMaxLength(450);
+ entity.HasIndex(e => e.BoxCode).IsUnique();
+ });
+
+ // ── Payee1099 (1099 recipient master) ────────────────────────────────
+ builder.Entity(entity =>
+ {
+ entity.HasQueryFilter(p => !p.IsDeleted);
+ entity.Property(e => e.LegalName).HasMaxLength(200).IsRequired();
+ entity.Property(e => e.DisplayName).HasMaxLength(200);
+ entity.Property(e => e.TaxClassification).HasMaxLength(40).IsRequired();
+ entity.Property(e => e.TinType).HasMaxLength(10);
+ entity.Property(e => e.TinLast4).HasMaxLength(4);
+ entity.Property(e => e.State).HasMaxLength(2);
+ entity.Property(e => e.Zip).HasMaxLength(10);
+ entity.Property(e => e.W9Status).HasMaxLength(20).HasDefaultValue(Form1099.W9Status.Missing);
+ entity.Property(e => e.AddressLine1).HasMaxLength(200);
+ entity.Property(e => e.AddressLine2).HasMaxLength(200);
+ entity.Property(e => e.City).HasMaxLength(100);
+ entity.Property(e => e.Email).HasMaxLength(200);
+ entity.Property(e => e.Phone).HasMaxLength(30);
+ entity.Property(e => e.Notes).HasMaxLength(500);
+ entity.Property(e => e.W9BlobPath).HasMaxLength(500);
+ entity.Property(e => e.TinEncrypted).HasMaxLength(500);
+ entity.Property(e => e.CreatedBy).HasMaxLength(450);
+ entity.Property(e => e.UpdatedBy).HasMaxLength(450);
+ entity.Property(e => e.DeletedBy).HasMaxLength(450);
+ entity.HasOne(e => e.Member).WithMany()
+ .HasForeignKey(e => e.MemberId).OnDelete(DeleteBehavior.SetNull);
+ });
+
// ── ExpenseCategoryGroup ─────────────────────────────────────────────
builder.Entity(entity =>
{
@@ -227,6 +268,8 @@ public class AppDbContext : IdentityDbContext
entity.Property(e => e.UpdatedBy).HasMaxLength(450);
entity.HasOne(e => e.Form990Line).WithMany()
.HasForeignKey(e => e.Form990LineId).OnDelete(DeleteBehavior.SetNull);
+ entity.HasOne(e => e.Form1099Box).WithMany()
+ .HasForeignKey(e => e.Form1099BoxId).OnDelete(DeleteBehavior.SetNull);
});
// ── ExpenseSubCategory ───────────────────────────────────────────────
@@ -240,6 +283,8 @@ public class AppDbContext : IdentityDbContext
.HasForeignKey(e => e.GroupId).OnDelete(DeleteBehavior.Restrict);
entity.HasOne(e => e.Form990Line).WithMany()
.HasForeignKey(e => e.Form990LineId).OnDelete(DeleteBehavior.SetNull);
+ entity.HasOne(e => e.Form1099Box).WithMany()
+ .HasForeignKey(e => e.Form1099BoxId).OnDelete(DeleteBehavior.SetNull);
});
// ── Expense ──────────────────────────────────────────────────────────
@@ -270,6 +315,8 @@ public class AppDbContext : IdentityDbContext
.HasForeignKey(e => e.MinistryId).OnDelete(DeleteBehavior.Restrict);
entity.HasOne(e => e.Member).WithMany()
.HasForeignKey(e => e.MemberId).OnDelete(DeleteBehavior.SetNull);
+ entity.HasOne(e => e.Payee).WithMany()
+ .HasForeignKey(e => e.PayeeId).OnDelete(DeleteBehavior.SetNull);
});
// ── ExpenseLine (category breakdown of one Expense) ──────────────────
@@ -346,6 +393,7 @@ public class AppDbContext : IdentityDbContext
entity.Property(e => e.BankName).HasMaxLength(200);
entity.Property(e => e.BankAccountNumber).HasMaxLength(50);
entity.Property(e => e.BankRoutingNumber).HasMaxLength(50);
+ entity.Property(e => e.PayerEin).HasMaxLength(20);
entity.Property(e => e.NameZh).HasMaxLength(200);
entity.Property(e => e.Phone).HasMaxLength(50);
entity.Property(e => e.Email).HasMaxLength(200);
diff --git a/API/ROLAC.API/Data/DbSeeder.cs b/API/ROLAC.API/Data/DbSeeder.cs
index e7ec773..b4d880f 100644
--- a/API/ROLAC.API/Data/DbSeeder.cs
+++ b/API/ROLAC.API/Data/DbSeeder.cs
@@ -137,6 +137,23 @@ public static class DbSeeder
("Other", "Gifts", "24"),
];
+ private static readonly (string Code, string En, string Zh, string FormType, int Sort)[] Form1099BoxSeed =
+ [
+ (Form1099.BoxNec1, "Nonemployee compensation", "非員工報酬", "1099-NEC", 1),
+ (Form1099.BoxMisc1, "Rents", "租金", "1099-MISC", 2),
+ ];
+
+ // Only service/rent subcategories get a box. Everything else stays unmapped (not reportable).
+ private static readonly (string GroupEn, string SubEn, string Code)[] Form1099SubMappingSeed =
+ [
+ ("Personnel", "Honorarium", Form1099.BoxNec1),
+ ("Personnel", "Contract Labor", Form1099.BoxNec1),
+ ("Professional Services", "Legal", Form1099.BoxNec1),
+ ("Professional Services", "Accounting & Audit", Form1099.BoxNec1),
+ ("Professional Services", "Other Professional", Form1099.BoxNec1),
+ ("Facility", "Rent", Form1099.BoxMisc1),
+ ];
+
// One-time corrections for subcategories that were mapped to the WRONG line in an earlier
// seed. The normal mapping loop below only fills NULLs, so it cannot fix an existing bad
// value — this block does. Idempotent: each row fires only while the subcategory still holds
@@ -190,6 +207,11 @@ public static class DbSeeder
("finance", Modules.ChurchProfile, true, true, false, false),
("finance", Modules.Disbursements, true, true, true, true),
("finance", Modules.Form990Report, true, false, false, false),
+ // Form1099 — finance manages recipients and tracks filings; pastor and board_member
+ // get read-only oversight (same pattern as Form990Report). No Approve semantics.
+ ("finance", Modules.Form1099, true, true, true, false),
+ ("pastor", Modules.Form1099, true, false, false, false),
+ ("board_member", Modules.Form1099, true, false, false, false),
// Logs — read-only. System logs are technical (pastor only); audit logs have
// governance value, so finance and board members can read them too.
@@ -375,6 +397,25 @@ public static class DbSeeder
await db.SaveChangesAsync();
}
+ public static async Task SeedForm1099BoxesAsync(AppDbContext db)
+ {
+ foreach (var (code, en, zh, formType, sort) in Form1099BoxSeed)
+ if (!await db.Form1099Boxes.AnyAsync(b => b.BoxCode == code))
+ db.Form1099Boxes.Add(new Form1099Box
+ { BoxCode = code, Name_en = en, Name_zh = zh, FormType = formType, SortOrder = sort, IsActive = true });
+ await db.SaveChangesAsync();
+
+ var boxesByCode = await db.Form1099Boxes.ToDictionaryAsync(b => b.BoxCode, b => b.Id);
+ var subs = await db.ExpenseSubCategories.Include(s => s.Group).ToListAsync();
+ foreach (var (groupEn, subEn, code) in Form1099SubMappingSeed)
+ {
+ var sub = subs.FirstOrDefault(s => s.Group!.Name_en == groupEn && s.Name_en == subEn);
+ if (sub is not null && sub.Form1099BoxId is null && boxesByCode.TryGetValue(code, out var boxId))
+ sub.Form1099BoxId = boxId;
+ }
+ await db.SaveChangesAsync();
+ }
+
public static async Task SeedChurchProfileAsync(AppDbContext db)
{
// Singleton row used by the disbursement module (issuer info + check counter).
@@ -454,6 +495,7 @@ public static class DbSeeder
await SeedMinistriesAsync(db);
await SeedExpenseCategoriesAsync(db);
await SeedForm990ExpenseLinesAsync(db);
+ await SeedForm1099BoxesAsync(db);
await SeedChurchProfileAsync(db);
await SeedSiteSettingAsync(db);
await SeedNotificationSettingAsync(db, config);
diff --git a/API/ROLAC.API/Entities/ChurchProfile.cs b/API/ROLAC.API/Entities/ChurchProfile.cs
index a7157f4..1b17d92 100644
--- a/API/ROLAC.API/Entities/ChurchProfile.cs
+++ b/API/ROLAC.API/Entities/ChurchProfile.cs
@@ -21,6 +21,9 @@ public class ChurchProfile : AuditableEntity, IAuditable
public string? BankAccountNumber { get; set; }
public string? BankRoutingNumber { get; set; }
+ /// Payer EIN printed on Form 1099-NEC Copy B; the church's own public business identifier.
+ public string? PayerEin { get; set; }
+
// ── AI assist provider settings (editable via Church Profile → AI 設定 tab) ──
public string AiProvider { get; set; } = "Claude"; // "Claude" | "Gemini"
public string? ClaudeModel { get; set; } = "claude-haiku-4-5-20251001";
diff --git a/API/ROLAC.API/Entities/Expense.cs b/API/ROLAC.API/Entities/Expense.cs
index 3597676..3da2229 100644
--- a/API/ROLAC.API/Entities/Expense.cs
+++ b/API/ROLAC.API/Entities/Expense.cs
@@ -11,6 +11,7 @@ public class Expense : SoftDeleteEntity, IAuditable
public string Description { get; set; } = null!;
public string? VendorName { get; set; }
public int? MemberId { get; set; }
+ public int? PayeeId { get; set; } // 1099 recipient attribution (header-level)
public string? CheckNumber { get; set; }
public DateOnly ExpenseDate { get; set; }
public string? ReceiptBlobPath { get; set; }
@@ -25,5 +26,6 @@ public class Expense : SoftDeleteEntity, IAuditable
public Ministry? Ministry { get; set; }
public Member? Member { get; set; }
+ public Payee1099? Payee { get; set; }
public List Lines { get; set; } = new();
}
diff --git a/API/ROLAC.API/Entities/ExpenseCategoryGroup.cs b/API/ROLAC.API/Entities/ExpenseCategoryGroup.cs
index 7349323..ac87791 100644
--- a/API/ROLAC.API/Entities/ExpenseCategoryGroup.cs
+++ b/API/ROLAC.API/Entities/ExpenseCategoryGroup.cs
@@ -12,5 +12,8 @@ public class ExpenseCategoryGroup : AuditableEntity, IAuditable
public int? Form990LineId { get; set; }
public Form990ExpenseLine? Form990Line { get; set; }
+ public int? Form1099BoxId { get; set; } // null = not 1099-reportable
+ public Form1099Box? Form1099Box { get; set; }
+
public List SubCategories { get; set; } = [];
}
diff --git a/API/ROLAC.API/Entities/ExpenseSubCategory.cs b/API/ROLAC.API/Entities/ExpenseSubCategory.cs
index 0c20a3a..0719d0d 100644
--- a/API/ROLAC.API/Entities/ExpenseSubCategory.cs
+++ b/API/ROLAC.API/Entities/ExpenseSubCategory.cs
@@ -13,5 +13,8 @@ public class ExpenseSubCategory : AuditableEntity, IAuditable
public int? Form990LineId { get; set; }
public Form990ExpenseLine? Form990Line { get; set; }
+ public int? Form1099BoxId { get; set; } // null = not 1099-reportable
+ public Form1099Box? Form1099Box { get; set; }
+
public ExpenseCategoryGroup? Group { get; set; }
}
diff --git a/API/ROLAC.API/Entities/Form1099.cs b/API/ROLAC.API/Entities/Form1099.cs
new file mode 100644
index 0000000..dc683a4
--- /dev/null
+++ b/API/ROLAC.API/Entities/Form1099.cs
@@ -0,0 +1,20 @@
+namespace ROLAC.API.Entities;
+
+/// Shared 1099 constants. Box codes match Form1099Box.BoxCode seed values.
+public static class Form1099
+{
+ /// IRS reporting threshold (USD) per box, per recipient, per calendar year.
+ public const decimal ReportingThreshold = 600m;
+
+ public const string BoxNec1 = "NEC-1"; // Nonemployee compensation
+ public const string BoxMisc1 = "MISC-1"; // Rents
+
+ public static class W9Status
+ {
+ public const string Missing = "Missing";
+ public const string Requested = "Requested";
+ public const string OnFile = "OnFile";
+ public const string Expired = "Expired";
+ public static readonly IReadOnlyList All = [Missing, Requested, OnFile, Expired];
+ }
+}
diff --git a/API/ROLAC.API/Entities/Form1099Box.cs b/API/ROLAC.API/Entities/Form1099Box.cs
new file mode 100644
index 0000000..58b0a8c
--- /dev/null
+++ b/API/ROLAC.API/Entities/Form1099Box.cs
@@ -0,0 +1,14 @@
+using ROLAC.API.Entities.Base;
+namespace ROLAC.API.Entities;
+
+/// A 1099 reporting box, e.g. "NEC-1 — Nonemployee compensation".
+public class Form1099Box : AuditableEntity, IAuditable
+{
+ public int Id { get; set; }
+ public string BoxCode { get; set; } = null!; // "NEC-1", "MISC-1"
+ public string Name_en { get; set; } = null!;
+ public string? Name_zh { get; set; }
+ public string FormType { get; set; } = "1099-NEC"; // "1099-NEC" | "1099-MISC"
+ public int SortOrder { get; set; }
+ public bool IsActive { get; set; } = true;
+}
diff --git a/API/ROLAC.API/Entities/Payee1099.cs b/API/ROLAC.API/Entities/Payee1099.cs
new file mode 100644
index 0000000..2238ae1
--- /dev/null
+++ b/API/ROLAC.API/Entities/Payee1099.cs
@@ -0,0 +1,32 @@
+using ROLAC.API.Entities.Base;
+namespace ROLAC.API.Entities;
+
+///
+/// A 1099 recipient (independent contractor / vendor). Holds W-9 data and an encrypted TIN.
+/// Optionally linked to a Member (e.g. a part-time co-worker paid as a contractor).
+///
+public class Payee1099 : SoftDeleteEntity, IAuditable
+{
+ public int Id { get; set; }
+ public string LegalName { get; set; } = null!; // name on the W-9
+ public string? DisplayName { get; set; } // friendly / DBA
+ public int? MemberId { get; set; }
+ public Member? Member { get; set; }
+ public string TaxClassification { get; set; } = "Individual"; // drives Is1099Tracked default
+ public bool Is1099Tracked { get; set; } = true;
+ public string? TinType { get; set; } // "SSN" | "EIN"
+ public string? TinEncrypted { get; set; } // Data-Protection ciphertext
+ public string? TinLast4 { get; set; }
+ public string? AddressLine1 { get; set; }
+ public string? AddressLine2 { get; set; }
+ public string? City { get; set; }
+ public string? State { get; set; }
+ public string? Zip { get; set; }
+ public string? Email { get; set; }
+ public string? Phone { get; set; }
+ public string W9Status { get; set; } = Form1099.W9Status.Missing;
+ public DateOnly? W9ReceivedDate { get; set; }
+ public string? W9BlobPath { get; set; }
+ public bool IsActive { get; set; } = true;
+ public string? Notes { get; set; }
+}
diff --git a/API/ROLAC.API/Migrations/20260626001416_AddForm1099RecipientTracking.Designer.cs b/API/ROLAC.API/Migrations/20260626001416_AddForm1099RecipientTracking.Designer.cs
new file mode 100644
index 0000000..0af6a1f
--- /dev/null
+++ b/API/ROLAC.API/Migrations/20260626001416_AddForm1099RecipientTracking.Designer.cs
@@ -0,0 +1,2676 @@
+//
+using System;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+using ROLAC.API.Data;
+
+#nullable disable
+
+namespace ROLAC.API.Migrations
+{
+ [DbContext(typeof(AppDbContext))]
+ [Migration("20260626001416_AddForm1099RecipientTracking")]
+ partial class AddForm1099RecipientTracking
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "8.0.11")
+ .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+ NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("ClaimType")
+ .HasColumnType("text");
+
+ b.Property("ClaimValue")
+ .HasColumnType("text");
+
+ b.Property("RoleId")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.HasKey("Id");
+
+ b.HasIndex("RoleId");
+
+ b.ToTable("AspNetRoleClaims", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("ClaimType")
+ .HasColumnType("text");
+
+ b.Property("ClaimValue")
+ .HasColumnType("text");
+
+ b.Property("UserId")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("AspNetUserClaims", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b =>
+ {
+ b.Property("LoginProvider")
+ .HasColumnType("text");
+
+ b.Property("ProviderKey")
+ .HasColumnType("text");
+
+ b.Property("ProviderDisplayName")
+ .HasColumnType("text");
+
+ b.Property("UserId")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.HasKey("LoginProvider", "ProviderKey");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("AspNetUserLogins", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b =>
+ {
+ b.Property("UserId")
+ .HasColumnType("text");
+
+ b.Property("RoleId")
+ .HasColumnType("text");
+
+ b.HasKey("UserId", "RoleId");
+
+ b.HasIndex("RoleId");
+
+ b.ToTable("AspNetUserRoles", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b =>
+ {
+ b.Property("UserId")
+ .HasColumnType("text");
+
+ b.Property("LoginProvider")
+ .HasColumnType("text");
+
+ b.Property("Name")
+ .HasColumnType("text");
+
+ b.Property("Value")
+ .HasColumnType("text");
+
+ b.HasKey("UserId", "LoginProvider", "Name");
+
+ b.ToTable("AspNetUserTokens", (string)null);
+ });
+
+ modelBuilder.Entity("ROLAC.API.Entities.AppRole", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("text");
+
+ b.Property("ConcurrencyStamp")
+ .IsConcurrencyToken()
+ .HasColumnType("text");
+
+ b.Property("Description")
+ .HasMaxLength(500)
+ .HasColumnType("character varying(500)");
+
+ b.Property("Name")
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)");
+
+ b.Property("NormalizedName")
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("NormalizedName")
+ .IsUnique()
+ .HasDatabaseName("RoleNameIndex");
+
+ b.ToTable("AspNetRoles", (string)null);
+ });
+
+ modelBuilder.Entity("ROLAC.API.Entities.AppUser", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("text");
+
+ b.Property("AccessFailedCount")
+ .HasColumnType("integer");
+
+ b.Property("ConcurrencyStamp")
+ .IsConcurrencyToken()
+ .HasColumnType("text");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("Email")
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)");
+
+ b.Property("EmailConfirmed")
+ .HasColumnType("boolean");
+
+ b.Property("IsActive")
+ .HasColumnType("boolean");
+
+ b.Property("LanguagePreference")
+ .IsRequired()
+ .ValueGeneratedOnAdd()
+ .HasMaxLength(10)
+ .HasColumnType("character varying(10)")
+ .HasDefaultValue("en");
+
+ b.Property("LastLoginAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("LockoutEnabled")
+ .HasColumnType("boolean");
+
+ b.Property("LockoutEnd")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("MemberId")
+ .HasColumnType("integer");
+
+ b.Property("NormalizedEmail")
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)");
+
+ b.Property("NormalizedUserName")
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)");
+
+ b.Property("PasswordHash")
+ .HasColumnType("text");
+
+ b.Property("PhoneNumber")
+ .HasColumnType("text");
+
+ b.Property("PhoneNumberConfirmed")
+ .HasColumnType("boolean");
+
+ b.Property("SecurityStamp")
+ .HasColumnType("text");
+
+ b.Property("TwoFactorEnabled")
+ .HasColumnType("boolean");
+
+ b.Property("UserName")
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("MemberId")
+ .IsUnique()
+ .HasFilter("\"MemberId\" IS NOT NULL");
+
+ b.HasIndex("NormalizedEmail")
+ .HasDatabaseName("EmailIndex");
+
+ b.HasIndex("NormalizedUserName")
+ .IsUnique()
+ .HasDatabaseName("UserNameIndex");
+
+ b.ToTable("AspNetUsers", (string)null);
+ });
+
+ modelBuilder.Entity("ROLAC.API.Entities.Check", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("Amount")
+ .HasColumnType("decimal(18,2)");
+
+ b.Property("CheckDate")
+ .HasColumnType("date");
+
+ b.Property("CheckNumber")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("character varying(50)");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("CreatedBy")
+ .IsRequired()
+ .HasMaxLength(450)
+ .HasColumnType("character varying(450)");
+
+ b.Property("DeletedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("DeletedBy")
+ .HasMaxLength(450)
+ .HasColumnType("character varying(450)");
+
+ b.Property("IsDeleted")
+ .HasColumnType("boolean");
+
+ b.Property("IssuedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("IssuedBy")
+ .IsRequired()
+ .HasMaxLength(450)
+ .HasColumnType("character varying(450)");
+
+ b.Property("MemberId")
+ .HasColumnType("integer");
+
+ b.Property("Memo")
+ .HasMaxLength(500)
+ .HasColumnType("character varying(500)");
+
+ b.Property("PayeeAddress")
+ .HasMaxLength(500)
+ .HasColumnType("character varying(500)");
+
+ b.Property("PayeeCity")
+ .HasMaxLength(100)
+ .HasColumnType("character varying(100)");
+
+ b.Property("PayeeName")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("character varying(200)");
+
+ b.Property("PayeeState")
+ .HasMaxLength(50)
+ .HasColumnType("character varying(50)");
+
+ b.Property("PayeeType")
+ .IsRequired()
+ .HasMaxLength(20)
+ .HasColumnType("character varying(20)");
+
+ b.Property("PayeeZip")
+ .HasMaxLength(20)
+ .HasColumnType("character varying(20)");
+
+ b.Property("ReceiptCapturedBy")
+ .HasMaxLength(450)
+ .HasColumnType("character varying(450)");
+
+ b.Property("ReceiptSignatureBlobPath")
+ .HasMaxLength(500)
+ .HasColumnType("character varying(500)");
+
+ b.Property("ReceiptSignedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("ReceiptSignedName")
+ .HasMaxLength(200)
+ .HasColumnType("character varying(200)");
+
+ b.Property("Status")
+ .IsRequired()
+ .ValueGeneratedOnAdd()
+ .HasMaxLength(20)
+ .HasColumnType("character varying(20)")
+ .HasDefaultValue("Issued");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("UpdatedBy")
+ .IsRequired()
+ .HasMaxLength(450)
+ .HasColumnType("character varying(450)");
+
+ b.Property("VoidReason")
+ .HasMaxLength(500)
+ .HasColumnType("character varying(500)");
+
+ b.Property("VoidedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("VoidedBy")
+ .HasMaxLength(450)
+ .HasColumnType("character varying(450)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("CheckDate");
+
+ b.HasIndex("CheckNumber")
+ .IsUnique()
+ .HasFilter("\"IsDeleted\" = false");
+
+ b.HasIndex("MemberId");
+
+ b.HasIndex("Status")
+ .HasFilter("\"IsDeleted\" = false");
+
+ b.ToTable("Checks");
+ });
+
+ modelBuilder.Entity("ROLAC.API.Entities.CheckLine", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("Amount")
+ .HasColumnType("decimal(18,2)");
+
+ b.Property("CheckId")
+ .HasColumnType("integer");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("CreatedBy")
+ .IsRequired()
+ .HasMaxLength(450)
+ .HasColumnType("character varying(450)");
+
+ b.Property("Description")
+ .IsRequired()
+ .HasMaxLength(500)
+ .HasColumnType("character varying(500)");
+
+ b.Property("ExpenseId")
+ .HasColumnType("integer");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("UpdatedBy")
+ .IsRequired()
+ .HasMaxLength(450)
+ .HasColumnType("character varying(450)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("CheckId");
+
+ b.HasIndex("ExpenseId");
+
+ b.ToTable("CheckLines");
+ });
+
+ modelBuilder.Entity("ROLAC.API.Entities.ChurchProfile", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("Address")
+ .HasMaxLength(500)
+ .HasColumnType("character varying(500)");
+
+ b.Property("AiProvider")
+ .IsRequired()
+ .ValueGeneratedOnAdd()
+ .HasMaxLength(20)
+ .HasColumnType("character varying(20)")
+ .HasDefaultValue("Claude");
+
+ b.Property("BankAccountNumber")
+ .HasMaxLength(50)
+ .HasColumnType("character varying(50)");
+
+ b.Property("BankName")
+ .HasMaxLength(200)
+ .HasColumnType("character varying(200)");
+
+ b.Property("BankRoutingNumber")
+ .HasMaxLength(50)
+ .HasColumnType("character varying(50)");
+
+ b.Property("City")
+ .HasMaxLength(100)
+ .HasColumnType("character varying(100)");
+
+ b.Property("ClaudeApiKey")
+ .HasMaxLength(500)
+ .HasColumnType("character varying(500)");
+
+ b.Property("ClaudeModel")
+ .ValueGeneratedOnAdd()
+ .HasMaxLength(100)
+ .HasColumnType("character varying(100)")
+ .HasDefaultValue("claude-haiku-4-5-20251001");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("CreatedBy")
+ .IsRequired()
+ .HasMaxLength(450)
+ .HasColumnType("character varying(450)");
+
+ b.Property("Email")
+ .HasMaxLength(200)
+ .HasColumnType("character varying(200)");
+
+ b.Property("GeminiApiKey")
+ .HasMaxLength(500)
+ .HasColumnType("character varying(500)");
+
+ b.Property("GeminiModel")
+ .ValueGeneratedOnAdd()
+ .HasMaxLength(100)
+ .HasColumnType("character varying(100)")
+ .HasDefaultValue("gemini-2.5-flash-lite");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("character varying(200)");
+
+ b.Property("NameZh")
+ .HasMaxLength(200)
+ .HasColumnType("character varying(200)");
+
+ b.Property("NextCheckNumber")
+ .HasColumnType("integer");
+
+ b.Property("Phone")
+ .HasMaxLength(50)
+ .HasColumnType("character varying(50)");
+
+ b.Property("State")
+ .HasMaxLength(50)
+ .HasColumnType("character varying(50)");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("UpdatedBy")
+ .IsRequired()
+ .HasMaxLength(450)
+ .HasColumnType("character varying(450)");
+
+ b.Property("Website")
+ .HasMaxLength(300)
+ .HasColumnType("character varying(300)");
+
+ b.Property("ZipCode")
+ .HasMaxLength(20)
+ .HasColumnType("character varying(20)");
+
+ b.Property("xmin")
+ .IsConcurrencyToken()
+ .ValueGeneratedOnAddOrUpdate()
+ .HasColumnType("xid")
+ .HasColumnName("xmin");
+
+ b.HasKey("Id");
+
+ b.ToTable("ChurchProfiles");
+ });
+
+ modelBuilder.Entity("ROLAC.API.Entities.Expense", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("Amount")
+ .HasColumnType("decimal(18,2)");
+
+ b.Property("CheckNumber")
+ .HasMaxLength(50)
+ .HasColumnType("character varying(50)");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("CreatedBy")
+ .IsRequired()
+ .HasMaxLength(450)
+ .HasColumnType("character varying(450)");
+
+ b.Property("DeletedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("DeletedBy")
+ .HasMaxLength(450)
+ .HasColumnType("character varying(450)");
+
+ b.Property("Description")
+ .IsRequired()
+ .HasMaxLength(500)
+ .HasColumnType("character varying(500)");
+
+ b.Property("ExpenseDate")
+ .HasColumnType("date");
+
+ b.Property("IsDeleted")
+ .HasColumnType("boolean");
+
+ b.Property("MemberId")
+ .HasColumnType("integer");
+
+ b.Property("MinistryId")
+ .HasColumnType("integer");
+
+ b.Property("Notes")
+ .HasColumnType("text");
+
+ b.Property("PaidAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("PaidBy")
+ .HasMaxLength(450)
+ .HasColumnType("character varying(450)");
+
+ b.Property("PayeeId")
+ .HasColumnType("integer");
+
+ b.Property("ReceiptBlobPath")
+ .HasMaxLength(500)
+ .HasColumnType("character varying(500)");
+
+ b.Property("ReviewNotes")
+ .HasMaxLength(500)
+ .HasColumnType("character varying(500)");
+
+ b.Property("ReviewedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("ReviewedBy")
+ .HasMaxLength(450)
+ .HasColumnType("character varying(450)");
+
+ b.Property("Status")
+ .IsRequired()
+ .ValueGeneratedOnAdd()
+ .HasMaxLength(30)
+ .HasColumnType("character varying(30)")
+ .HasDefaultValue("Draft");
+
+ b.Property("SubmittedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("SubmittedBy")
+ .HasMaxLength(450)
+ .HasColumnType("character varying(450)");
+
+ b.Property("Type")
+ .IsRequired()
+ .HasMaxLength(30)
+ .HasColumnType("character varying(30)");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("UpdatedBy")
+ .IsRequired()
+ .HasMaxLength(450)
+ .HasColumnType("character varying(450)");
+
+ b.Property("VendorName")
+ .HasMaxLength(200)
+ .HasColumnType("character varying(200)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ExpenseDate");
+
+ b.HasIndex("MemberId");
+
+ b.HasIndex("MinistryId");
+
+ b.HasIndex("PayeeId");
+
+ b.HasIndex("Status")
+ .HasFilter("\"IsDeleted\" = false");
+
+ b.ToTable("Expenses");
+ });
+
+ modelBuilder.Entity("ROLAC.API.Entities.ExpenseCategoryGroup", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("CreatedBy")
+ .IsRequired()
+ .HasMaxLength(450)
+ .HasColumnType("character varying(450)");
+
+ b.Property("Form1099BoxId")
+ .HasColumnType("integer");
+
+ b.Property("Form990LineId")
+ .HasColumnType("integer");
+
+ b.Property("IsActive")
+ .HasColumnType("boolean");
+
+ b.Property("Name_en")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("character varying(200)");
+
+ b.Property("Name_zh")
+ .HasMaxLength(200)
+ .HasColumnType("character varying(200)");
+
+ b.Property("SortOrder")
+ .HasColumnType("integer");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("UpdatedBy")
+ .IsRequired()
+ .HasMaxLength(450)
+ .HasColumnType("character varying(450)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Form1099BoxId");
+
+ b.HasIndex("Form990LineId");
+
+ b.ToTable("ExpenseCategoryGroups");
+ });
+
+ modelBuilder.Entity("ROLAC.API.Entities.ExpenseLine", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("Amount")
+ .HasColumnType("decimal(18,2)");
+
+ b.Property("CategoryGroupId")
+ .HasColumnType("integer");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("CreatedBy")
+ .IsRequired()
+ .HasMaxLength(450)
+ .HasColumnType("character varying(450)");
+
+ b.Property("Description")
+ .HasMaxLength(500)
+ .HasColumnType("character varying(500)");
+
+ b.Property("ExpenseId")
+ .HasColumnType("integer");
+
+ b.Property("FunctionalClass")
+ .HasMaxLength(20)
+ .HasColumnType("character varying(20)");
+
+ b.Property("SubCategoryId")
+ .HasColumnType("integer");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("UpdatedBy")
+ .IsRequired()
+ .HasMaxLength(450)
+ .HasColumnType("character varying(450)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("CategoryGroupId");
+
+ b.HasIndex("ExpenseId");
+
+ b.HasIndex("SubCategoryId");
+
+ b.ToTable("ExpenseLines");
+ });
+
+ modelBuilder.Entity("ROLAC.API.Entities.ExpenseSnapshot", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("CheckNumber")
+ .HasMaxLength(50)
+ .HasColumnType("character varying(50)");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("CreatedBy")
+ .IsRequired()
+ .HasMaxLength(450)
+ .HasColumnType("character varying(450)");
+
+ b.Property("DeletedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("DeletedBy")
+ .HasMaxLength(450)
+ .HasColumnType("character varying(450)");
+
+ b.Property("Description")
+ .IsRequired()
+ .HasMaxLength(500)
+ .HasColumnType("character varying(500)");
+
+ b.Property("IsDeleted")
+ .HasColumnType("boolean");
+
+ b.Property("MinistryId")
+ .HasColumnType("integer");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(150)
+ .HasColumnType("character varying(150)");
+
+ b.Property("Notes")
+ .HasColumnType("text");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("UpdatedBy")
+ .IsRequired()
+ .HasMaxLength(450)
+ .HasColumnType("character varying(450)");
+
+ b.Property("VendorName")
+ .HasMaxLength(200)
+ .HasColumnType("character varying(200)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("CreatedAt");
+
+ b.HasIndex("MinistryId");
+
+ b.ToTable("ExpenseSnapshots");
+ });
+
+ modelBuilder.Entity("ROLAC.API.Entities.ExpenseSnapshotLine", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("Amount")
+ .HasColumnType("decimal(18,2)");
+
+ b.Property("CategoryGroupId")
+ .HasColumnType("integer");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("CreatedBy")
+ .IsRequired()
+ .HasMaxLength(450)
+ .HasColumnType("character varying(450)");
+
+ b.Property("Description")
+ .HasMaxLength(500)
+ .HasColumnType("character varying(500)");
+
+ b.Property("FunctionalClass")
+ .HasMaxLength(20)
+ .HasColumnType("character varying(20)");
+
+ b.Property("SnapshotId")
+ .HasColumnType("integer");
+
+ b.Property("SubCategoryId")
+ .HasColumnType("integer");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("UpdatedBy")
+ .IsRequired()
+ .HasMaxLength(450)
+ .HasColumnType("character varying(450)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("CategoryGroupId");
+
+ b.HasIndex("SnapshotId");
+
+ b.HasIndex("SubCategoryId");
+
+ b.ToTable("ExpenseSnapshotLines");
+ });
+
+ modelBuilder.Entity("ROLAC.API.Entities.ExpenseSubCategory", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("CreatedBy")
+ .IsRequired()
+ .HasMaxLength(450)
+ .HasColumnType("character varying(450)");
+
+ b.Property