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("Form1099BoxId") + .HasColumnType("integer"); + + b.Property("Form990LineId") + .HasColumnType("integer"); + + b.Property("GroupId") + .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.HasIndex("GroupId"); + + b.ToTable("ExpenseSubCategories"); + }); + + modelBuilder.Entity("ROLAC.API.Entities.FamilyUnit", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .IsRequired() + .HasColumnType("text"); + + b.Property("FamilyName_en") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("FamilyName_zh") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("FamilyUnits"); + }); + + modelBuilder.Entity("ROLAC.API.Entities.Form1099Box", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BoxCode") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(450) + .HasColumnType("character varying(450)"); + + b.Property("FormType") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + 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("BoxCode") + .IsUnique(); + + b.ToTable("Form1099Boxes"); + }); + + modelBuilder.Entity("ROLAC.API.Entities.Form990ExpenseLine", 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("IsActive") + .HasColumnType("boolean"); + + b.Property("LineCode") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + 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("LineCode") + .IsUnique(); + + b.ToTable("Form990ExpenseLines"); + }); + + modelBuilder.Entity("ROLAC.API.Entities.Giving", 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("GivingCategoryId") + .HasColumnType("integer"); + + b.Property("GivingDate") + .HasColumnType("date"); + + b.Property("IsAnonymous") + .HasColumnType("boolean"); + + b.Property("MemberId") + .HasColumnType("integer"); + + b.Property("Notes") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("OfferingSessionId") + .HasColumnType("integer"); + + b.Property("PayPalTransactionId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("PaymentMethod") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(450) + .HasColumnType("character varying(450)"); + + b.Property("ZelleReferenceCode") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.HasKey("Id"); + + b.HasIndex("GivingCategoryId"); + + b.HasIndex("GivingDate"); + + b.HasIndex("OfferingSessionId") + .HasFilter("\"OfferingSessionId\" IS NOT NULL"); + + b.HasIndex("MemberId", "GivingDate"); + + b.ToTable("Givings"); + }); + + modelBuilder.Entity("ROLAC.API.Entities.GivingCategory", 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("Description_en") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Description_zh") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + 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.ToTable("GivingCategories"); + }); + + modelBuilder.Entity("ROLAC.API.Entities.Logging.AuditLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Action") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("Category") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("Changes") + .HasColumnType("jsonb"); + + b.Property("CorrelationId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("EntityId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("EntityName") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("IpAddress") + .HasMaxLength(45) + .HasColumnType("character varying(45)"); + + b.Property("Level") + .HasColumnType("smallint"); + + b.Property("Summary") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.Property("UserEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("UserId") + .HasMaxLength(450) + .HasColumnType("character varying(450)"); + + b.HasKey("Id"); + + b.HasIndex("Action"); + + b.HasIndex("Timestamp"); + + b.HasIndex("UserId") + .HasFilter("\"UserId\" IS NOT NULL"); + + b.HasIndex("Category", "Timestamp"); + + b.HasIndex("EntityName", "EntityId"); + + b.ToTable("AuditLogs", (string)null); + }); + + modelBuilder.Entity("ROLAC.API.Entities.Logging.SystemLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Category") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("CorrelationId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("EventId") + .HasColumnType("integer"); + + b.Property("Exception") + .HasColumnType("text"); + + b.Property("HttpMethod") + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("IpAddress") + .HasMaxLength(45) + .HasColumnType("character varying(45)"); + + b.Property("Level") + .HasColumnType("smallint"); + + b.Property("Message") + .IsRequired() + .HasColumnType("text"); + + b.Property("RequestPath") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("StatusCode") + .HasColumnType("integer"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasMaxLength(450) + .HasColumnType("character varying(450)"); + + b.HasKey("Id"); + + b.HasIndex("Level"); + + b.HasIndex("Timestamp"); + + b.HasIndex("UserId") + .HasFilter("\"UserId\" IS NOT NULL"); + + b.HasIndex("Timestamp", "Level"); + + b.ToTable("SystemLogs", (string)null); + }); + + modelBuilder.Entity("ROLAC.API.Entities.MealAttendance", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AdultCount") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0); + + b.Property("AttendanceDate") + .HasColumnType("date"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(450) + .HasColumnType("character varying(450)"); + + b.Property("KidCount") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(450) + .HasColumnType("character varying(450)"); + + b.Property("YouthCount") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0); + + b.HasKey("Id"); + + b.HasIndex("AttendanceDate") + .IsUnique(); + + b.ToTable("MealAttendances"); + }); + + modelBuilder.Entity("ROLAC.API.Entities.Member", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Address") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("BaptismChurch") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("BaptismDate") + .HasColumnType("date"); + + b.Property("City") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Country") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasDefaultValue("USA"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(450) + .HasColumnType("character varying(450)"); + + b.Property("DateOfBirth") + .HasColumnType("date"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedBy") + .HasMaxLength(450) + .HasColumnType("character varying(450)"); + + b.Property("Email") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Entity") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("FamilyUnitId") + .HasColumnType("integer"); + + b.Property("FirstName_en") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("FirstName_zh") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Gender") + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("JoinDate") + .HasColumnType("date"); + + b.Property("LanguagePreference") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasDefaultValue("en"); + + b.Property("LastName_en") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("LastName_zh") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("NickName") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("PhoneCell") + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("PhoneHome") + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("PhotoBlobPath") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("State") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Status") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasDefaultValue("Member"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(450) + .HasColumnType("character varying(450)"); + + b.Property("ZipCode") + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .HasFilter("\"Email\" IS NOT NULL"); + + b.HasIndex("FamilyUnitId"); + + b.HasIndex("Status") + .HasFilter("\"IsDeleted\" = false"); + + b.ToTable("Members"); + }); + + modelBuilder.Entity("ROLAC.API.Entities.Ministry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DefaultFunctionalClass") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasDefaultValue("Program"); + + b.Property("Description_en") + .HasColumnType("text"); + + b.Property("Description_zh") + .HasColumnType("text"); + + 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.HasKey("Id"); + + b.ToTable("Ministries"); + }); + + modelBuilder.Entity("ROLAC.API.Entities.MonthlyStatement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BankStatementBalance") + .HasColumnType("decimal(18,2)"); + + b.Property("CalculatedClosingBalance") + .HasColumnType("decimal(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(450) + .HasColumnType("character varying(450)"); + + b.Property("Difference") + .HasColumnType("decimal(18,2)"); + + b.Property("FinalizedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FinalizedBy") + .HasMaxLength(450) + .HasColumnType("character varying(450)"); + + b.Property("IsFinalized") + .HasColumnType("boolean"); + + b.Property("Month") + .HasColumnType("integer"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("OpeningBalance") + .HasColumnType("decimal(18,2)"); + + b.Property("TotalExpenses") + .HasColumnType("decimal(18,2)"); + + b.Property("TotalGiving") + .HasColumnType("decimal(18,2)"); + + b.Property("TotalOtherIncome") + .HasColumnType("decimal(18,2)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(450) + .HasColumnType("character varying(450)"); + + b.Property("Year") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Year", "Month") + .IsUnique(); + + b.ToTable("MonthlyStatements"); + }); + + modelBuilder.Entity("ROLAC.API.Entities.NotificationSetting", 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("EnableEmail") + .HasColumnType("boolean"); + + b.Property("EnableLine") + .HasColumnType("boolean"); + + b.Property("FromAddress") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("FromName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("LineChannelAccessToken") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("LineChannelSecret") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("SmtpHost") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("SmtpPassword") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("SmtpPort") + .HasColumnType("integer"); + + b.Property("SmtpUseSsl") + .HasColumnType("boolean"); + + b.Property("SmtpUser") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(450) + .HasColumnType("character varying(450)"); + + b.HasKey("Id"); + + b.ToTable("NotificationSettings"); + }); + + modelBuilder.Entity("ROLAC.API.Entities.Notifications.LineBindingCode", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Code") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("ConsumedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("MemberId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Code"); + + b.HasIndex("MemberId"); + + b.ToTable("LineBindingCodes"); + }); + + modelBuilder.Entity("ROLAC.API.Entities.Notifications.MemberChannelBinding", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BoundAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Channel") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("ExternalId") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("MemberId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Channel", "ExternalId") + .IsUnique(); + + b.HasIndex("MemberId", "Channel") + .IsUnique(); + + b.ToTable("MemberChannelBindings"); + }); + + modelBuilder.Entity("ROLAC.API.Entities.Notifications.MessagingGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Channel") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("ExternalId") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Name") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("RegisteredAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("Channel", "ExternalId") + .IsUnique(); + + b.ToTable("MessagingGroups"); + }); + + modelBuilder.Entity("ROLAC.API.Entities.Notifications.NotificationLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Body") + .IsRequired() + .HasColumnType("text"); + + b.Property("Channel") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("Error") + .HasColumnType("text"); + + b.Property("MemberId") + .HasColumnType("integer"); + + b.Property("MessagingGroupId") + .HasColumnType("integer"); + + b.Property("SentAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SentByUserId") + .IsRequired() + .HasMaxLength(450) + .HasColumnType("character varying(450)"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("Subject") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("TargetExternalId") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("TargetType") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.HasKey("Id"); + + b.HasIndex("Channel"); + + b.HasIndex("MemberId"); + + b.HasIndex("MessagingGroupId"); + + b.HasIndex("SentAt"); + + b.ToTable("NotificationLogs"); + }); + + modelBuilder.Entity("ROLAC.API.Entities.OfferingSession", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CashTotal") + .HasColumnType("decimal(18,2)"); + + b.Property("CheckTotal") + .HasColumnType("decimal(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(450) + .HasColumnType("character varying(450)"); + + b.Property("Difference") + .HasColumnType("decimal(18,2)"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("ProofPdfPath") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReconciledAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReconciledBy") + .HasMaxLength(450) + .HasColumnType("character varying(450)"); + + b.Property("SessionDate") + .HasColumnType("date"); + + b.Property("Status") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasDefaultValue("Draft"); + + b.Property("SubmittedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SubmittedBy") + .HasMaxLength(450) + .HasColumnType("character varying(450)"); + + b.Property("SystemTotal") + .HasColumnType("decimal(18,2)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(450) + .HasColumnType("character varying(450)"); + + b.HasKey("Id"); + + b.HasIndex("SessionDate") + .IsUnique(); + + b.ToTable("OfferingSessions"); + }); + + modelBuilder.Entity("ROLAC.API.Entities.Payee1099", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AddressLine1") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("AddressLine2") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("City") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + 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("DisplayName") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Email") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Is1099Tracked") + .HasColumnType("boolean"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("LegalName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("MemberId") + .HasColumnType("integer"); + + b.Property("Notes") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Phone") + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("State") + .HasMaxLength(2) + .HasColumnType("character varying(2)"); + + b.Property("TaxClassification") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("TinEncrypted") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("TinLast4") + .HasMaxLength(4) + .HasColumnType("character varying(4)"); + + b.Property("TinType") + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(450) + .HasColumnType("character varying(450)"); + + b.Property("W9BlobPath") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("W9ReceivedDate") + .HasColumnType("date"); + + b.Property("W9Status") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasDefaultValue("Missing"); + + b.Property("Zip") + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.HasKey("Id"); + + b.HasIndex("MemberId"); + + b.ToTable("Payee1099s"); + }); + + modelBuilder.Entity("ROLAC.API.Entities.RefreshToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeviceInfo") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IpAddress") + .HasMaxLength(45) + .HasColumnType("character varying(45)"); + + b.Property("ReplacedByHash") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("RevokedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("TokenHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("UserId") + .IsRequired() + .HasMaxLength(450) + .HasColumnType("character varying(450)"); + + b.HasKey("Id"); + + b.HasIndex("TokenHash") + .IsUnique(); + + b.HasIndex("UserId"); + + b.ToTable("RefreshTokens"); + }); + + modelBuilder.Entity("ROLAC.API.Entities.RolePermission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CanApprove") + .HasColumnType("boolean"); + + b.Property("CanDelete") + .HasColumnType("boolean"); + + b.Property("CanRead") + .HasColumnType("boolean"); + + b.Property("CanWrite") + .HasColumnType("boolean"); + + b.Property("Module") + .IsRequired() + .HasMaxLength(60) + .HasColumnType("character varying(60)"); + + b.Property("RoleId") + .IsRequired() + .HasMaxLength(450) + .HasColumnType("character varying(450)"); + + b.HasKey("Id"); + + b.HasIndex("RoleId", "Module") + .IsUnique(); + + b.ToTable("RolePermissions"); + }); + + modelBuilder.Entity("ROLAC.API.Entities.SiteSetting", 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("Currency") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("DateFormat") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("DefaultLanguage") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("SiteTitle") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("SiteTitleZh") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("TimeZone") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(450) + .HasColumnType("character varying(450)"); + + b.HasKey("Id"); + + b.ToTable("SiteSettings"); + }); + + modelBuilder.Entity("ROLAC.API.Entities.UserInvitation", 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("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("RevokedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("TokenHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("UsedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .IsRequired() + .HasMaxLength(450) + .HasColumnType("character varying(450)"); + + b.HasKey("Id"); + + b.HasIndex("TokenHash") + .IsUnique(); + + b.HasIndex("UserId"); + + b.ToTable("UserInvitations"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("ROLAC.API.Entities.AppRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("ROLAC.API.Entities.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("ROLAC.API.Entities.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("ROLAC.API.Entities.AppRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ROLAC.API.Entities.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("ROLAC.API.Entities.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ROLAC.API.Entities.Check", b => + { + b.HasOne("ROLAC.API.Entities.Member", "Member") + .WithMany() + .HasForeignKey("MemberId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Member"); + }); + + modelBuilder.Entity("ROLAC.API.Entities.CheckLine", b => + { + b.HasOne("ROLAC.API.Entities.Check", "Check") + .WithMany("Lines") + .HasForeignKey("CheckId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ROLAC.API.Entities.Expense", "Expense") + .WithMany() + .HasForeignKey("ExpenseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Check"); + + b.Navigation("Expense"); + }); + + modelBuilder.Entity("ROLAC.API.Entities.Expense", b => + { + b.HasOne("ROLAC.API.Entities.Member", "Member") + .WithMany() + .HasForeignKey("MemberId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("ROLAC.API.Entities.Ministry", "Ministry") + .WithMany() + .HasForeignKey("MinistryId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ROLAC.API.Entities.Payee1099", "Payee") + .WithMany() + .HasForeignKey("PayeeId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Member"); + + b.Navigation("Ministry"); + + b.Navigation("Payee"); + }); + + modelBuilder.Entity("ROLAC.API.Entities.ExpenseCategoryGroup", b => + { + b.HasOne("ROLAC.API.Entities.Form1099Box", "Form1099Box") + .WithMany() + .HasForeignKey("Form1099BoxId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("ROLAC.API.Entities.Form990ExpenseLine", "Form990Line") + .WithMany() + .HasForeignKey("Form990LineId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Form1099Box"); + + b.Navigation("Form990Line"); + }); + + modelBuilder.Entity("ROLAC.API.Entities.ExpenseLine", b => + { + b.HasOne("ROLAC.API.Entities.ExpenseCategoryGroup", "CategoryGroup") + .WithMany() + .HasForeignKey("CategoryGroupId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ROLAC.API.Entities.Expense", "Expense") + .WithMany("Lines") + .HasForeignKey("ExpenseId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ROLAC.API.Entities.ExpenseSubCategory", "SubCategory") + .WithMany() + .HasForeignKey("SubCategoryId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("CategoryGroup"); + + b.Navigation("Expense"); + + b.Navigation("SubCategory"); + }); + + modelBuilder.Entity("ROLAC.API.Entities.ExpenseSnapshot", b => + { + b.HasOne("ROLAC.API.Entities.Ministry", "Ministry") + .WithMany() + .HasForeignKey("MinistryId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Ministry"); + }); + + modelBuilder.Entity("ROLAC.API.Entities.ExpenseSnapshotLine", b => + { + b.HasOne("ROLAC.API.Entities.ExpenseCategoryGroup", "CategoryGroup") + .WithMany() + .HasForeignKey("CategoryGroupId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ROLAC.API.Entities.ExpenseSnapshot", "Snapshot") + .WithMany("Lines") + .HasForeignKey("SnapshotId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ROLAC.API.Entities.ExpenseSubCategory", "SubCategory") + .WithMany() + .HasForeignKey("SubCategoryId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("CategoryGroup"); + + b.Navigation("Snapshot"); + + b.Navigation("SubCategory"); + }); + + modelBuilder.Entity("ROLAC.API.Entities.ExpenseSubCategory", b => + { + b.HasOne("ROLAC.API.Entities.Form1099Box", "Form1099Box") + .WithMany() + .HasForeignKey("Form1099BoxId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("ROLAC.API.Entities.Form990ExpenseLine", "Form990Line") + .WithMany() + .HasForeignKey("Form990LineId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("ROLAC.API.Entities.ExpenseCategoryGroup", "Group") + .WithMany("SubCategories") + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Form1099Box"); + + b.Navigation("Form990Line"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("ROLAC.API.Entities.Giving", b => + { + b.HasOne("ROLAC.API.Entities.GivingCategory", "GivingCategory") + .WithMany() + .HasForeignKey("GivingCategoryId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ROLAC.API.Entities.Member", "Member") + .WithMany() + .HasForeignKey("MemberId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("ROLAC.API.Entities.OfferingSession", "OfferingSession") + .WithMany("Givings") + .HasForeignKey("OfferingSessionId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GivingCategory"); + + b.Navigation("Member"); + + b.Navigation("OfferingSession"); + }); + + modelBuilder.Entity("ROLAC.API.Entities.Member", b => + { + b.HasOne("ROLAC.API.Entities.FamilyUnit", "FamilyUnit") + .WithMany() + .HasForeignKey("FamilyUnitId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("FamilyUnit"); + }); + + modelBuilder.Entity("ROLAC.API.Entities.Notifications.LineBindingCode", b => + { + b.HasOne("ROLAC.API.Entities.Member", "Member") + .WithMany() + .HasForeignKey("MemberId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Member"); + }); + + modelBuilder.Entity("ROLAC.API.Entities.Notifications.MemberChannelBinding", b => + { + b.HasOne("ROLAC.API.Entities.Member", "Member") + .WithMany() + .HasForeignKey("MemberId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Member"); + }); + + modelBuilder.Entity("ROLAC.API.Entities.Notifications.NotificationLog", b => + { + b.HasOne("ROLAC.API.Entities.Member", "Member") + .WithMany() + .HasForeignKey("MemberId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("ROLAC.API.Entities.Notifications.MessagingGroup", "MessagingGroup") + .WithMany() + .HasForeignKey("MessagingGroupId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Member"); + + b.Navigation("MessagingGroup"); + }); + + modelBuilder.Entity("ROLAC.API.Entities.Payee1099", b => + { + b.HasOne("ROLAC.API.Entities.Member", "Member") + .WithMany() + .HasForeignKey("MemberId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Member"); + }); + + modelBuilder.Entity("ROLAC.API.Entities.RefreshToken", b => + { + b.HasOne("ROLAC.API.Entities.AppUser", "User") + .WithMany("RefreshTokens") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("ROLAC.API.Entities.RolePermission", b => + { + b.HasOne("ROLAC.API.Entities.AppRole", "Role") + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Role"); + }); + + modelBuilder.Entity("ROLAC.API.Entities.UserInvitation", b => + { + b.HasOne("ROLAC.API.Entities.AppUser", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("ROLAC.API.Entities.AppUser", b => + { + b.Navigation("RefreshTokens"); + }); + + modelBuilder.Entity("ROLAC.API.Entities.Check", b => + { + b.Navigation("Lines"); + }); + + modelBuilder.Entity("ROLAC.API.Entities.Expense", b => + { + b.Navigation("Lines"); + }); + + modelBuilder.Entity("ROLAC.API.Entities.ExpenseCategoryGroup", b => + { + b.Navigation("SubCategories"); + }); + + modelBuilder.Entity("ROLAC.API.Entities.ExpenseSnapshot", b => + { + b.Navigation("Lines"); + }); + + modelBuilder.Entity("ROLAC.API.Entities.OfferingSession", b => + { + b.Navigation("Givings"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/API/ROLAC.API/Migrations/20260626001416_AddForm1099RecipientTracking.cs b/API/ROLAC.API/Migrations/20260626001416_AddForm1099RecipientTracking.cs new file mode 100644 index 0000000..c3e19a7 --- /dev/null +++ b/API/ROLAC.API/Migrations/20260626001416_AddForm1099RecipientTracking.cs @@ -0,0 +1,197 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace ROLAC.API.Migrations +{ + /// + public partial class AddForm1099RecipientTracking : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Form1099BoxId", + table: "ExpenseSubCategories", + type: "integer", + nullable: true); + + migrationBuilder.AddColumn( + name: "PayeeId", + table: "Expenses", + type: "integer", + nullable: true); + + migrationBuilder.AddColumn( + name: "Form1099BoxId", + table: "ExpenseCategoryGroups", + type: "integer", + nullable: true); + + migrationBuilder.CreateTable( + name: "Form1099Boxes", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + BoxCode = table.Column(type: "character varying(10)", maxLength: 10, nullable: false), + Name_en = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), + Name_zh = table.Column(type: "character varying(200)", maxLength: 200, nullable: true), + FormType = table.Column(type: "character varying(20)", maxLength: 20, nullable: false), + SortOrder = table.Column(type: "integer", nullable: false), + IsActive = table.Column(type: "boolean", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + CreatedBy = table.Column(type: "character varying(450)", maxLength: 450, nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedBy = table.Column(type: "character varying(450)", maxLength: 450, nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Form1099Boxes", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Payee1099s", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + LegalName = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), + DisplayName = table.Column(type: "character varying(200)", maxLength: 200, nullable: true), + MemberId = table.Column(type: "integer", nullable: true), + TaxClassification = table.Column(type: "character varying(40)", maxLength: 40, nullable: false), + Is1099Tracked = table.Column(type: "boolean", nullable: false), + TinType = table.Column(type: "character varying(10)", maxLength: 10, nullable: true), + TinEncrypted = table.Column(type: "character varying(500)", maxLength: 500, nullable: true), + TinLast4 = table.Column(type: "character varying(4)", maxLength: 4, nullable: true), + AddressLine1 = table.Column(type: "character varying(200)", maxLength: 200, nullable: true), + AddressLine2 = table.Column(type: "character varying(200)", maxLength: 200, nullable: true), + City = table.Column(type: "character varying(100)", maxLength: 100, nullable: true), + State = table.Column(type: "character varying(2)", maxLength: 2, nullable: true), + Zip = table.Column(type: "character varying(10)", maxLength: 10, nullable: true), + Email = table.Column(type: "character varying(200)", maxLength: 200, nullable: true), + Phone = table.Column(type: "character varying(30)", maxLength: 30, nullable: true), + W9Status = table.Column(type: "character varying(20)", maxLength: 20, nullable: false, defaultValue: "Missing"), + W9ReceivedDate = table.Column(type: "date", nullable: true), + W9BlobPath = table.Column(type: "character varying(500)", maxLength: 500, nullable: true), + IsActive = table.Column(type: "boolean", nullable: false), + Notes = table.Column(type: "character varying(500)", maxLength: 500, nullable: true), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + CreatedBy = table.Column(type: "character varying(450)", maxLength: 450, nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedBy = table.Column(type: "character varying(450)", maxLength: 450, nullable: false), + IsDeleted = table.Column(type: "boolean", nullable: false), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true), + DeletedBy = table.Column(type: "character varying(450)", maxLength: 450, nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Payee1099s", x => x.Id); + table.ForeignKey( + name: "FK_Payee1099s_Members_MemberId", + column: x => x.MemberId, + principalTable: "Members", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + }); + + migrationBuilder.CreateIndex( + name: "IX_ExpenseSubCategories_Form1099BoxId", + table: "ExpenseSubCategories", + column: "Form1099BoxId"); + + migrationBuilder.CreateIndex( + name: "IX_Expenses_PayeeId", + table: "Expenses", + column: "PayeeId"); + + migrationBuilder.CreateIndex( + name: "IX_ExpenseCategoryGroups_Form1099BoxId", + table: "ExpenseCategoryGroups", + column: "Form1099BoxId"); + + migrationBuilder.CreateIndex( + name: "IX_Form1099Boxes_BoxCode", + table: "Form1099Boxes", + column: "BoxCode", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_Payee1099s_MemberId", + table: "Payee1099s", + column: "MemberId"); + + migrationBuilder.AddForeignKey( + name: "FK_ExpenseCategoryGroups_Form1099Boxes_Form1099BoxId", + table: "ExpenseCategoryGroups", + column: "Form1099BoxId", + principalTable: "Form1099Boxes", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + + migrationBuilder.AddForeignKey( + name: "FK_Expenses_Payee1099s_PayeeId", + table: "Expenses", + column: "PayeeId", + principalTable: "Payee1099s", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + + migrationBuilder.AddForeignKey( + name: "FK_ExpenseSubCategories_Form1099Boxes_Form1099BoxId", + table: "ExpenseSubCategories", + column: "Form1099BoxId", + principalTable: "Form1099Boxes", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_ExpenseCategoryGroups_Form1099Boxes_Form1099BoxId", + table: "ExpenseCategoryGroups"); + + migrationBuilder.DropForeignKey( + name: "FK_Expenses_Payee1099s_PayeeId", + table: "Expenses"); + + migrationBuilder.DropForeignKey( + name: "FK_ExpenseSubCategories_Form1099Boxes_Form1099BoxId", + table: "ExpenseSubCategories"); + + migrationBuilder.DropTable( + name: "Form1099Boxes"); + + migrationBuilder.DropTable( + name: "Payee1099s"); + + migrationBuilder.DropIndex( + name: "IX_ExpenseSubCategories_Form1099BoxId", + table: "ExpenseSubCategories"); + + migrationBuilder.DropIndex( + name: "IX_Expenses_PayeeId", + table: "Expenses"); + + migrationBuilder.DropIndex( + name: "IX_ExpenseCategoryGroups_Form1099BoxId", + table: "ExpenseCategoryGroups"); + + migrationBuilder.DropColumn( + name: "Form1099BoxId", + table: "ExpenseSubCategories"); + + migrationBuilder.DropColumn( + name: "PayeeId", + table: "Expenses"); + + migrationBuilder.DropColumn( + name: "Form1099BoxId", + table: "ExpenseCategoryGroups"); + } + } +} diff --git a/API/ROLAC.API/Migrations/20260626012205_AddPayerEinToChurchProfile.Designer.cs b/API/ROLAC.API/Migrations/20260626012205_AddPayerEinToChurchProfile.Designer.cs new file mode 100644 index 0000000..4a60b37 --- /dev/null +++ b/API/ROLAC.API/Migrations/20260626012205_AddPayerEinToChurchProfile.Designer.cs @@ -0,0 +1,2680 @@ +// +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("20260626012205_AddPayerEinToChurchProfile")] + partial class AddPayerEinToChurchProfile + { + /// + 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("PayerEin") + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + 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("Form1099BoxId") + .HasColumnType("integer"); + + b.Property("Form990LineId") + .HasColumnType("integer"); + + b.Property("GroupId") + .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.HasIndex("GroupId"); + + b.ToTable("ExpenseSubCategories"); + }); + + modelBuilder.Entity("ROLAC.API.Entities.FamilyUnit", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .IsRequired() + .HasColumnType("text"); + + b.Property("FamilyName_en") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("FamilyName_zh") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("FamilyUnits"); + }); + + modelBuilder.Entity("ROLAC.API.Entities.Form1099Box", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BoxCode") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(450) + .HasColumnType("character varying(450)"); + + b.Property("FormType") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + 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("BoxCode") + .IsUnique(); + + b.ToTable("Form1099Boxes"); + }); + + modelBuilder.Entity("ROLAC.API.Entities.Form990ExpenseLine", 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("IsActive") + .HasColumnType("boolean"); + + b.Property("LineCode") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + 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("LineCode") + .IsUnique(); + + b.ToTable("Form990ExpenseLines"); + }); + + modelBuilder.Entity("ROLAC.API.Entities.Giving", 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("GivingCategoryId") + .HasColumnType("integer"); + + b.Property("GivingDate") + .HasColumnType("date"); + + b.Property("IsAnonymous") + .HasColumnType("boolean"); + + b.Property("MemberId") + .HasColumnType("integer"); + + b.Property("Notes") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("OfferingSessionId") + .HasColumnType("integer"); + + b.Property("PayPalTransactionId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("PaymentMethod") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(450) + .HasColumnType("character varying(450)"); + + b.Property("ZelleReferenceCode") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.HasKey("Id"); + + b.HasIndex("GivingCategoryId"); + + b.HasIndex("GivingDate"); + + b.HasIndex("OfferingSessionId") + .HasFilter("\"OfferingSessionId\" IS NOT NULL"); + + b.HasIndex("MemberId", "GivingDate"); + + b.ToTable("Givings"); + }); + + modelBuilder.Entity("ROLAC.API.Entities.GivingCategory", 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("Description_en") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Description_zh") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + 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.ToTable("GivingCategories"); + }); + + modelBuilder.Entity("ROLAC.API.Entities.Logging.AuditLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Action") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("Category") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("Changes") + .HasColumnType("jsonb"); + + b.Property("CorrelationId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("EntityId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("EntityName") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("IpAddress") + .HasMaxLength(45) + .HasColumnType("character varying(45)"); + + b.Property("Level") + .HasColumnType("smallint"); + + b.Property("Summary") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.Property("UserEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("UserId") + .HasMaxLength(450) + .HasColumnType("character varying(450)"); + + b.HasKey("Id"); + + b.HasIndex("Action"); + + b.HasIndex("Timestamp"); + + b.HasIndex("UserId") + .HasFilter("\"UserId\" IS NOT NULL"); + + b.HasIndex("Category", "Timestamp"); + + b.HasIndex("EntityName", "EntityId"); + + b.ToTable("AuditLogs", (string)null); + }); + + modelBuilder.Entity("ROLAC.API.Entities.Logging.SystemLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Category") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("CorrelationId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("EventId") + .HasColumnType("integer"); + + b.Property("Exception") + .HasColumnType("text"); + + b.Property("HttpMethod") + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("IpAddress") + .HasMaxLength(45) + .HasColumnType("character varying(45)"); + + b.Property("Level") + .HasColumnType("smallint"); + + b.Property("Message") + .IsRequired() + .HasColumnType("text"); + + b.Property("RequestPath") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("StatusCode") + .HasColumnType("integer"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasMaxLength(450) + .HasColumnType("character varying(450)"); + + b.HasKey("Id"); + + b.HasIndex("Level"); + + b.HasIndex("Timestamp"); + + b.HasIndex("UserId") + .HasFilter("\"UserId\" IS NOT NULL"); + + b.HasIndex("Timestamp", "Level"); + + b.ToTable("SystemLogs", (string)null); + }); + + modelBuilder.Entity("ROLAC.API.Entities.MealAttendance", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AdultCount") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0); + + b.Property("AttendanceDate") + .HasColumnType("date"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(450) + .HasColumnType("character varying(450)"); + + b.Property("KidCount") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(450) + .HasColumnType("character varying(450)"); + + b.Property("YouthCount") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0); + + b.HasKey("Id"); + + b.HasIndex("AttendanceDate") + .IsUnique(); + + b.ToTable("MealAttendances"); + }); + + modelBuilder.Entity("ROLAC.API.Entities.Member", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Address") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("BaptismChurch") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("BaptismDate") + .HasColumnType("date"); + + b.Property("City") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Country") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasDefaultValue("USA"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(450) + .HasColumnType("character varying(450)"); + + b.Property("DateOfBirth") + .HasColumnType("date"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedBy") + .HasMaxLength(450) + .HasColumnType("character varying(450)"); + + b.Property("Email") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Entity") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("FamilyUnitId") + .HasColumnType("integer"); + + b.Property("FirstName_en") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("FirstName_zh") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Gender") + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("JoinDate") + .HasColumnType("date"); + + b.Property("LanguagePreference") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasDefaultValue("en"); + + b.Property("LastName_en") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("LastName_zh") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("NickName") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("PhoneCell") + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("PhoneHome") + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("PhotoBlobPath") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("State") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Status") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasDefaultValue("Member"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(450) + .HasColumnType("character varying(450)"); + + b.Property("ZipCode") + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .HasFilter("\"Email\" IS NOT NULL"); + + b.HasIndex("FamilyUnitId"); + + b.HasIndex("Status") + .HasFilter("\"IsDeleted\" = false"); + + b.ToTable("Members"); + }); + + modelBuilder.Entity("ROLAC.API.Entities.Ministry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DefaultFunctionalClass") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasDefaultValue("Program"); + + b.Property("Description_en") + .HasColumnType("text"); + + b.Property("Description_zh") + .HasColumnType("text"); + + 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.HasKey("Id"); + + b.ToTable("Ministries"); + }); + + modelBuilder.Entity("ROLAC.API.Entities.MonthlyStatement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BankStatementBalance") + .HasColumnType("decimal(18,2)"); + + b.Property("CalculatedClosingBalance") + .HasColumnType("decimal(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(450) + .HasColumnType("character varying(450)"); + + b.Property("Difference") + .HasColumnType("decimal(18,2)"); + + b.Property("FinalizedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FinalizedBy") + .HasMaxLength(450) + .HasColumnType("character varying(450)"); + + b.Property("IsFinalized") + .HasColumnType("boolean"); + + b.Property("Month") + .HasColumnType("integer"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("OpeningBalance") + .HasColumnType("decimal(18,2)"); + + b.Property("TotalExpenses") + .HasColumnType("decimal(18,2)"); + + b.Property("TotalGiving") + .HasColumnType("decimal(18,2)"); + + b.Property("TotalOtherIncome") + .HasColumnType("decimal(18,2)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(450) + .HasColumnType("character varying(450)"); + + b.Property("Year") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Year", "Month") + .IsUnique(); + + b.ToTable("MonthlyStatements"); + }); + + modelBuilder.Entity("ROLAC.API.Entities.NotificationSetting", 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("EnableEmail") + .HasColumnType("boolean"); + + b.Property("EnableLine") + .HasColumnType("boolean"); + + b.Property("FromAddress") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("FromName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("LineChannelAccessToken") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("LineChannelSecret") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("SmtpHost") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("SmtpPassword") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("SmtpPort") + .HasColumnType("integer"); + + b.Property("SmtpUseSsl") + .HasColumnType("boolean"); + + b.Property("SmtpUser") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(450) + .HasColumnType("character varying(450)"); + + b.HasKey("Id"); + + b.ToTable("NotificationSettings"); + }); + + modelBuilder.Entity("ROLAC.API.Entities.Notifications.LineBindingCode", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Code") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("ConsumedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("MemberId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Code"); + + b.HasIndex("MemberId"); + + b.ToTable("LineBindingCodes"); + }); + + modelBuilder.Entity("ROLAC.API.Entities.Notifications.MemberChannelBinding", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BoundAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Channel") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("ExternalId") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("MemberId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Channel", "ExternalId") + .IsUnique(); + + b.HasIndex("MemberId", "Channel") + .IsUnique(); + + b.ToTable("MemberChannelBindings"); + }); + + modelBuilder.Entity("ROLAC.API.Entities.Notifications.MessagingGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Channel") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("ExternalId") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Name") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("RegisteredAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("Channel", "ExternalId") + .IsUnique(); + + b.ToTable("MessagingGroups"); + }); + + modelBuilder.Entity("ROLAC.API.Entities.Notifications.NotificationLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Body") + .IsRequired() + .HasColumnType("text"); + + b.Property("Channel") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("Error") + .HasColumnType("text"); + + b.Property("MemberId") + .HasColumnType("integer"); + + b.Property("MessagingGroupId") + .HasColumnType("integer"); + + b.Property("SentAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SentByUserId") + .IsRequired() + .HasMaxLength(450) + .HasColumnType("character varying(450)"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("Subject") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("TargetExternalId") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("TargetType") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.HasKey("Id"); + + b.HasIndex("Channel"); + + b.HasIndex("MemberId"); + + b.HasIndex("MessagingGroupId"); + + b.HasIndex("SentAt"); + + b.ToTable("NotificationLogs"); + }); + + modelBuilder.Entity("ROLAC.API.Entities.OfferingSession", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CashTotal") + .HasColumnType("decimal(18,2)"); + + b.Property("CheckTotal") + .HasColumnType("decimal(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(450) + .HasColumnType("character varying(450)"); + + b.Property("Difference") + .HasColumnType("decimal(18,2)"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("ProofPdfPath") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReconciledAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReconciledBy") + .HasMaxLength(450) + .HasColumnType("character varying(450)"); + + b.Property("SessionDate") + .HasColumnType("date"); + + b.Property("Status") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasDefaultValue("Draft"); + + b.Property("SubmittedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SubmittedBy") + .HasMaxLength(450) + .HasColumnType("character varying(450)"); + + b.Property("SystemTotal") + .HasColumnType("decimal(18,2)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(450) + .HasColumnType("character varying(450)"); + + b.HasKey("Id"); + + b.HasIndex("SessionDate") + .IsUnique(); + + b.ToTable("OfferingSessions"); + }); + + modelBuilder.Entity("ROLAC.API.Entities.Payee1099", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AddressLine1") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("AddressLine2") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("City") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + 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("DisplayName") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Email") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Is1099Tracked") + .HasColumnType("boolean"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("LegalName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("MemberId") + .HasColumnType("integer"); + + b.Property("Notes") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Phone") + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("State") + .HasMaxLength(2) + .HasColumnType("character varying(2)"); + + b.Property("TaxClassification") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("TinEncrypted") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("TinLast4") + .HasMaxLength(4) + .HasColumnType("character varying(4)"); + + b.Property("TinType") + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(450) + .HasColumnType("character varying(450)"); + + b.Property("W9BlobPath") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("W9ReceivedDate") + .HasColumnType("date"); + + b.Property("W9Status") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasDefaultValue("Missing"); + + b.Property("Zip") + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.HasKey("Id"); + + b.HasIndex("MemberId"); + + b.ToTable("Payee1099s"); + }); + + modelBuilder.Entity("ROLAC.API.Entities.RefreshToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeviceInfo") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IpAddress") + .HasMaxLength(45) + .HasColumnType("character varying(45)"); + + b.Property("ReplacedByHash") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("RevokedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("TokenHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("UserId") + .IsRequired() + .HasMaxLength(450) + .HasColumnType("character varying(450)"); + + b.HasKey("Id"); + + b.HasIndex("TokenHash") + .IsUnique(); + + b.HasIndex("UserId"); + + b.ToTable("RefreshTokens"); + }); + + modelBuilder.Entity("ROLAC.API.Entities.RolePermission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CanApprove") + .HasColumnType("boolean"); + + b.Property("CanDelete") + .HasColumnType("boolean"); + + b.Property("CanRead") + .HasColumnType("boolean"); + + b.Property("CanWrite") + .HasColumnType("boolean"); + + b.Property("Module") + .IsRequired() + .HasMaxLength(60) + .HasColumnType("character varying(60)"); + + b.Property("RoleId") + .IsRequired() + .HasMaxLength(450) + .HasColumnType("character varying(450)"); + + b.HasKey("Id"); + + b.HasIndex("RoleId", "Module") + .IsUnique(); + + b.ToTable("RolePermissions"); + }); + + modelBuilder.Entity("ROLAC.API.Entities.SiteSetting", 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("Currency") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("DateFormat") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("DefaultLanguage") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("SiteTitle") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("SiteTitleZh") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("TimeZone") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(450) + .HasColumnType("character varying(450)"); + + b.HasKey("Id"); + + b.ToTable("SiteSettings"); + }); + + modelBuilder.Entity("ROLAC.API.Entities.UserInvitation", 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("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("RevokedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("TokenHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("UsedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .IsRequired() + .HasMaxLength(450) + .HasColumnType("character varying(450)"); + + b.HasKey("Id"); + + b.HasIndex("TokenHash") + .IsUnique(); + + b.HasIndex("UserId"); + + b.ToTable("UserInvitations"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("ROLAC.API.Entities.AppRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("ROLAC.API.Entities.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("ROLAC.API.Entities.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("ROLAC.API.Entities.AppRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ROLAC.API.Entities.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("ROLAC.API.Entities.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ROLAC.API.Entities.Check", b => + { + b.HasOne("ROLAC.API.Entities.Member", "Member") + .WithMany() + .HasForeignKey("MemberId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Member"); + }); + + modelBuilder.Entity("ROLAC.API.Entities.CheckLine", b => + { + b.HasOne("ROLAC.API.Entities.Check", "Check") + .WithMany("Lines") + .HasForeignKey("CheckId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ROLAC.API.Entities.Expense", "Expense") + .WithMany() + .HasForeignKey("ExpenseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Check"); + + b.Navigation("Expense"); + }); + + modelBuilder.Entity("ROLAC.API.Entities.Expense", b => + { + b.HasOne("ROLAC.API.Entities.Member", "Member") + .WithMany() + .HasForeignKey("MemberId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("ROLAC.API.Entities.Ministry", "Ministry") + .WithMany() + .HasForeignKey("MinistryId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ROLAC.API.Entities.Payee1099", "Payee") + .WithMany() + .HasForeignKey("PayeeId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Member"); + + b.Navigation("Ministry"); + + b.Navigation("Payee"); + }); + + modelBuilder.Entity("ROLAC.API.Entities.ExpenseCategoryGroup", b => + { + b.HasOne("ROLAC.API.Entities.Form1099Box", "Form1099Box") + .WithMany() + .HasForeignKey("Form1099BoxId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("ROLAC.API.Entities.Form990ExpenseLine", "Form990Line") + .WithMany() + .HasForeignKey("Form990LineId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Form1099Box"); + + b.Navigation("Form990Line"); + }); + + modelBuilder.Entity("ROLAC.API.Entities.ExpenseLine", b => + { + b.HasOne("ROLAC.API.Entities.ExpenseCategoryGroup", "CategoryGroup") + .WithMany() + .HasForeignKey("CategoryGroupId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ROLAC.API.Entities.Expense", "Expense") + .WithMany("Lines") + .HasForeignKey("ExpenseId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ROLAC.API.Entities.ExpenseSubCategory", "SubCategory") + .WithMany() + .HasForeignKey("SubCategoryId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("CategoryGroup"); + + b.Navigation("Expense"); + + b.Navigation("SubCategory"); + }); + + modelBuilder.Entity("ROLAC.API.Entities.ExpenseSnapshot", b => + { + b.HasOne("ROLAC.API.Entities.Ministry", "Ministry") + .WithMany() + .HasForeignKey("MinistryId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Ministry"); + }); + + modelBuilder.Entity("ROLAC.API.Entities.ExpenseSnapshotLine", b => + { + b.HasOne("ROLAC.API.Entities.ExpenseCategoryGroup", "CategoryGroup") + .WithMany() + .HasForeignKey("CategoryGroupId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ROLAC.API.Entities.ExpenseSnapshot", "Snapshot") + .WithMany("Lines") + .HasForeignKey("SnapshotId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ROLAC.API.Entities.ExpenseSubCategory", "SubCategory") + .WithMany() + .HasForeignKey("SubCategoryId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("CategoryGroup"); + + b.Navigation("Snapshot"); + + b.Navigation("SubCategory"); + }); + + modelBuilder.Entity("ROLAC.API.Entities.ExpenseSubCategory", b => + { + b.HasOne("ROLAC.API.Entities.Form1099Box", "Form1099Box") + .WithMany() + .HasForeignKey("Form1099BoxId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("ROLAC.API.Entities.Form990ExpenseLine", "Form990Line") + .WithMany() + .HasForeignKey("Form990LineId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("ROLAC.API.Entities.ExpenseCategoryGroup", "Group") + .WithMany("SubCategories") + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Form1099Box"); + + b.Navigation("Form990Line"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("ROLAC.API.Entities.Giving", b => + { + b.HasOne("ROLAC.API.Entities.GivingCategory", "GivingCategory") + .WithMany() + .HasForeignKey("GivingCategoryId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ROLAC.API.Entities.Member", "Member") + .WithMany() + .HasForeignKey("MemberId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("ROLAC.API.Entities.OfferingSession", "OfferingSession") + .WithMany("Givings") + .HasForeignKey("OfferingSessionId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GivingCategory"); + + b.Navigation("Member"); + + b.Navigation("OfferingSession"); + }); + + modelBuilder.Entity("ROLAC.API.Entities.Member", b => + { + b.HasOne("ROLAC.API.Entities.FamilyUnit", "FamilyUnit") + .WithMany() + .HasForeignKey("FamilyUnitId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("FamilyUnit"); + }); + + modelBuilder.Entity("ROLAC.API.Entities.Notifications.LineBindingCode", b => + { + b.HasOne("ROLAC.API.Entities.Member", "Member") + .WithMany() + .HasForeignKey("MemberId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Member"); + }); + + modelBuilder.Entity("ROLAC.API.Entities.Notifications.MemberChannelBinding", b => + { + b.HasOne("ROLAC.API.Entities.Member", "Member") + .WithMany() + .HasForeignKey("MemberId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Member"); + }); + + modelBuilder.Entity("ROLAC.API.Entities.Notifications.NotificationLog", b => + { + b.HasOne("ROLAC.API.Entities.Member", "Member") + .WithMany() + .HasForeignKey("MemberId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("ROLAC.API.Entities.Notifications.MessagingGroup", "MessagingGroup") + .WithMany() + .HasForeignKey("MessagingGroupId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Member"); + + b.Navigation("MessagingGroup"); + }); + + modelBuilder.Entity("ROLAC.API.Entities.Payee1099", b => + { + b.HasOne("ROLAC.API.Entities.Member", "Member") + .WithMany() + .HasForeignKey("MemberId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Member"); + }); + + modelBuilder.Entity("ROLAC.API.Entities.RefreshToken", b => + { + b.HasOne("ROLAC.API.Entities.AppUser", "User") + .WithMany("RefreshTokens") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("ROLAC.API.Entities.RolePermission", b => + { + b.HasOne("ROLAC.API.Entities.AppRole", "Role") + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Role"); + }); + + modelBuilder.Entity("ROLAC.API.Entities.UserInvitation", b => + { + b.HasOne("ROLAC.API.Entities.AppUser", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("ROLAC.API.Entities.AppUser", b => + { + b.Navigation("RefreshTokens"); + }); + + modelBuilder.Entity("ROLAC.API.Entities.Check", b => + { + b.Navigation("Lines"); + }); + + modelBuilder.Entity("ROLAC.API.Entities.Expense", b => + { + b.Navigation("Lines"); + }); + + modelBuilder.Entity("ROLAC.API.Entities.ExpenseCategoryGroup", b => + { + b.Navigation("SubCategories"); + }); + + modelBuilder.Entity("ROLAC.API.Entities.ExpenseSnapshot", b => + { + b.Navigation("Lines"); + }); + + modelBuilder.Entity("ROLAC.API.Entities.OfferingSession", b => + { + b.Navigation("Givings"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/API/ROLAC.API/Migrations/20260626012205_AddPayerEinToChurchProfile.cs b/API/ROLAC.API/Migrations/20260626012205_AddPayerEinToChurchProfile.cs new file mode 100644 index 0000000..21055d7 --- /dev/null +++ b/API/ROLAC.API/Migrations/20260626012205_AddPayerEinToChurchProfile.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace ROLAC.API.Migrations +{ + /// + public partial class AddPayerEinToChurchProfile : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "PayerEin", + table: "ChurchProfiles", + type: "character varying(20)", + maxLength: 20, + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "PayerEin", + table: "ChurchProfiles"); + } + } +} diff --git a/API/ROLAC.API/Migrations/AppDbContextModelSnapshot.cs b/API/ROLAC.API/Migrations/AppDbContextModelSnapshot.cs index 341dcd8..f252461 100644 --- a/API/ROLAC.API/Migrations/AppDbContextModelSnapshot.cs +++ b/API/ROLAC.API/Migrations/AppDbContextModelSnapshot.cs @@ -506,6 +506,10 @@ namespace ROLAC.API.Migrations b.Property("NextCheckNumber") .HasColumnType("integer"); + b.Property("PayerEin") + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + b.Property("Phone") .HasMaxLength(50) .HasColumnType("character varying(50)"); @@ -598,6 +602,9 @@ namespace ROLAC.API.Migrations .HasMaxLength(450) .HasColumnType("character varying(450)"); + b.Property("PayeeId") + .HasColumnType("integer"); + b.Property("ReceiptBlobPath") .HasMaxLength(500) .HasColumnType("character varying(500)"); @@ -652,6 +659,8 @@ namespace ROLAC.API.Migrations b.HasIndex("MinistryId"); + b.HasIndex("PayeeId"); + b.HasIndex("Status") .HasFilter("\"IsDeleted\" = false"); @@ -674,6 +683,9 @@ namespace ROLAC.API.Migrations .HasMaxLength(450) .HasColumnType("character varying(450)"); + b.Property("Form1099BoxId") + .HasColumnType("integer"); + b.Property("Form990LineId") .HasColumnType("integer"); @@ -702,6 +714,8 @@ namespace ROLAC.API.Migrations b.HasKey("Id"); + b.HasIndex("Form1099BoxId"); + b.HasIndex("Form990LineId"); b.ToTable("ExpenseCategoryGroups"); @@ -900,6 +914,9 @@ namespace ROLAC.API.Migrations .HasMaxLength(450) .HasColumnType("character varying(450)"); + b.Property("Form1099BoxId") + .HasColumnType("integer"); + b.Property("Form990LineId") .HasColumnType("integer"); @@ -931,6 +948,8 @@ namespace ROLAC.API.Migrations b.HasKey("Id"); + b.HasIndex("Form1099BoxId"); + b.HasIndex("Form990LineId"); b.HasIndex("GroupId"); @@ -976,6 +995,63 @@ namespace ROLAC.API.Migrations b.ToTable("FamilyUnits"); }); + modelBuilder.Entity("ROLAC.API.Entities.Form1099Box", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BoxCode") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(450) + .HasColumnType("character varying(450)"); + + b.Property("FormType") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + 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("BoxCode") + .IsUnique(); + + b.ToTable("Form1099Boxes"); + }); + modelBuilder.Entity("ROLAC.API.Entities.Form990ExpenseLine", b => { b.Property("Id") @@ -1925,6 +2001,128 @@ namespace ROLAC.API.Migrations b.ToTable("OfferingSessions"); }); + modelBuilder.Entity("ROLAC.API.Entities.Payee1099", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AddressLine1") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("AddressLine2") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("City") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + 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("DisplayName") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Email") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Is1099Tracked") + .HasColumnType("boolean"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("LegalName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("MemberId") + .HasColumnType("integer"); + + b.Property("Notes") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Phone") + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("State") + .HasMaxLength(2) + .HasColumnType("character varying(2)"); + + b.Property("TaxClassification") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("TinEncrypted") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("TinLast4") + .HasMaxLength(4) + .HasColumnType("character varying(4)"); + + b.Property("TinType") + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(450) + .HasColumnType("character varying(450)"); + + b.Property("W9BlobPath") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("W9ReceivedDate") + .HasColumnType("date"); + + b.Property("W9Status") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasDefaultValue("Missing"); + + b.Property("Zip") + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.HasKey("Id"); + + b.HasIndex("MemberId"); + + b.ToTable("Payee1099s"); + }); + modelBuilder.Entity("ROLAC.API.Entities.RefreshToken", b => { b.Property("Id") @@ -2208,18 +2406,32 @@ namespace ROLAC.API.Migrations .OnDelete(DeleteBehavior.Restrict) .IsRequired(); + b.HasOne("ROLAC.API.Entities.Payee1099", "Payee") + .WithMany() + .HasForeignKey("PayeeId") + .OnDelete(DeleteBehavior.SetNull); + b.Navigation("Member"); b.Navigation("Ministry"); + + b.Navigation("Payee"); }); modelBuilder.Entity("ROLAC.API.Entities.ExpenseCategoryGroup", b => { + b.HasOne("ROLAC.API.Entities.Form1099Box", "Form1099Box") + .WithMany() + .HasForeignKey("Form1099BoxId") + .OnDelete(DeleteBehavior.SetNull); + b.HasOne("ROLAC.API.Entities.Form990ExpenseLine", "Form990Line") .WithMany() .HasForeignKey("Form990LineId") .OnDelete(DeleteBehavior.SetNull); + b.Navigation("Form1099Box"); + b.Navigation("Form990Line"); }); @@ -2290,6 +2502,11 @@ namespace ROLAC.API.Migrations modelBuilder.Entity("ROLAC.API.Entities.ExpenseSubCategory", b => { + b.HasOne("ROLAC.API.Entities.Form1099Box", "Form1099Box") + .WithMany() + .HasForeignKey("Form1099BoxId") + .OnDelete(DeleteBehavior.SetNull); + b.HasOne("ROLAC.API.Entities.Form990ExpenseLine", "Form990Line") .WithMany() .HasForeignKey("Form990LineId") @@ -2301,6 +2518,8 @@ namespace ROLAC.API.Migrations .OnDelete(DeleteBehavior.Restrict) .IsRequired(); + b.Navigation("Form1099Box"); + b.Navigation("Form990Line"); b.Navigation("Group"); @@ -2380,6 +2599,16 @@ namespace ROLAC.API.Migrations b.Navigation("MessagingGroup"); }); + modelBuilder.Entity("ROLAC.API.Entities.Payee1099", b => + { + b.HasOne("ROLAC.API.Entities.Member", "Member") + .WithMany() + .HasForeignKey("MemberId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Member"); + }); + modelBuilder.Entity("ROLAC.API.Entities.RefreshToken", b => { b.HasOne("ROLAC.API.Entities.AppUser", "User") diff --git a/API/ROLAC.API/Program.cs b/API/ROLAC.API/Program.cs index 8f701c3..ee7da4e 100644 --- a/API/ROLAC.API/Program.cs +++ b/API/ROLAC.API/Program.cs @@ -15,6 +15,7 @@ using ROLAC.API.Json; using ROLAC.API.Middleware; using ROLAC.API.Services; using ROLAC.API.Services.Logging; +using ROLAC.API.Services.Security; var builder = WebApplication.CreateBuilder(args); var config = builder.Configuration; @@ -157,6 +158,11 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddDataProtection(); +builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); diff --git a/API/ROLAC.API/Services/ChurchProfileService.cs b/API/ROLAC.API/Services/ChurchProfileService.cs index 842dbf6..ed889eb 100644 --- a/API/ROLAC.API/Services/ChurchProfileService.cs +++ b/API/ROLAC.API/Services/ChurchProfileService.cs @@ -18,7 +18,7 @@ public class ChurchProfileService : IChurchProfileService Id = p.Id, Name = p.Name, NameZh = p.NameZh, Phone = p.Phone, Email = p.Email, Website = p.Website, Address = p.Address, City = p.City, State = p.State, ZipCode = p.ZipCode, BankName = p.BankName, BankAccountNumber = p.BankAccountNumber, - BankRoutingNumber = p.BankRoutingNumber, NextCheckNumber = p.NextCheckNumber, + BankRoutingNumber = p.BankRoutingNumber, PayerEin = p.PayerEin, NextCheckNumber = p.NextCheckNumber, AiProvider = p.AiProvider, ClaudeModel = p.ClaudeModel, ClaudeApiKeyMasked = Mask(p.ClaudeApiKey), @@ -33,7 +33,7 @@ public class ChurchProfileService : IChurchProfileService p.Name = r.Name; p.NameZh = r.NameZh; p.Phone = r.Phone; p.Email = r.Email; p.Website = r.Website; p.Address = r.Address; p.City = r.City; p.State = r.State; p.ZipCode = r.ZipCode; p.BankName = r.BankName; p.BankAccountNumber = r.BankAccountNumber; - p.BankRoutingNumber = r.BankRoutingNumber; p.NextCheckNumber = r.NextCheckNumber; + p.BankRoutingNumber = r.BankRoutingNumber; p.PayerEin = r.PayerEin; p.NextCheckNumber = r.NextCheckNumber; p.AiProvider = string.IsNullOrWhiteSpace(r.AiProvider) ? "Claude" : r.AiProvider; p.ClaudeModel = r.ClaudeModel; p.GeminiModel = r.GeminiModel; diff --git a/API/ROLAC.API/Services/ExpenseCategoryService.cs b/API/ROLAC.API/Services/ExpenseCategoryService.cs index ae6bb87..90967b0 100644 --- a/API/ROLAC.API/Services/ExpenseCategoryService.cs +++ b/API/ROLAC.API/Services/ExpenseCategoryService.cs @@ -25,25 +25,32 @@ public class ExpenseCategoryService : IExpenseCategoryService var lineCodes = await _db.Form990ExpenseLines.AsNoTracking() .ToDictionaryAsync(l => l.Id, l => l.LineCode); + var boxCodes = await _db.Form1099Boxes.AsNoTracking() + .ToDictionaryAsync(b => b.Id, b => b.BoxCode); + return groups.Select(g => new ExpenseCategoryGroupDto { Id = g.Id, Name_en = g.Name_en, Name_zh = g.Name_zh, SortOrder = g.SortOrder, IsActive = g.IsActive, Form990LineId = g.Form990LineId, Form990LineCode = g.Form990LineId.HasValue ? lineCodes.GetValueOrDefault(g.Form990LineId.Value) : null, + Form1099BoxId = g.Form1099BoxId, + Form1099BoxCode = g.Form1099BoxId.HasValue ? boxCodes.GetValueOrDefault(g.Form1099BoxId.Value) : null, SubCategories = subs.Where(s => s.GroupId == g.Id).Select(s => new ExpenseSubCategoryDto { Id = s.Id, GroupId = s.GroupId, Name_en = s.Name_en, Name_zh = s.Name_zh, SortOrder = s.SortOrder, IsActive = s.IsActive, Form990LineId = s.Form990LineId, Form990LineCode = s.Form990LineId.HasValue ? lineCodes.GetValueOrDefault(s.Form990LineId.Value) : null, + Form1099BoxId = s.Form1099BoxId, + Form1099BoxCode = s.Form1099BoxId.HasValue ? boxCodes.GetValueOrDefault(s.Form1099BoxId.Value) : null, }).ToList(), }).ToList(); } public async Task CreateGroupAsync(CreateExpenseGroupRequest r) { - var g = new ExpenseCategoryGroup { Name_en = r.Name_en, Name_zh = r.Name_zh, SortOrder = r.SortOrder, IsActive = true, Form990LineId = r.Form990LineId }; + var g = new ExpenseCategoryGroup { Name_en = r.Name_en, Name_zh = r.Name_zh, SortOrder = r.SortOrder, IsActive = true, Form990LineId = r.Form990LineId, Form1099BoxId = r.Form1099BoxId }; _db.ExpenseCategoryGroups.Add(g); await _db.SaveChangesAsync(); return g.Id; @@ -53,7 +60,7 @@ public class ExpenseCategoryService : IExpenseCategoryService { var g = await _db.ExpenseCategoryGroups.FindAsync(id) ?? throw new KeyNotFoundException($"ExpenseCategoryGroup {id} not found."); - g.Name_en = r.Name_en; g.Name_zh = r.Name_zh; g.SortOrder = r.SortOrder; g.IsActive = r.IsActive; g.Form990LineId = r.Form990LineId; + g.Name_en = r.Name_en; g.Name_zh = r.Name_zh; g.SortOrder = r.SortOrder; g.IsActive = r.IsActive; g.Form990LineId = r.Form990LineId; g.Form1099BoxId = r.Form1099BoxId; await _db.SaveChangesAsync(); } @@ -69,7 +76,7 @@ public class ExpenseCategoryService : IExpenseCategoryService { var exists = await _db.ExpenseCategoryGroups.AnyAsync(g => g.Id == r.GroupId); if (!exists) throw new KeyNotFoundException($"ExpenseCategoryGroup {r.GroupId} not found."); - var s = new ExpenseSubCategory { GroupId = r.GroupId, Name_en = r.Name_en, Name_zh = r.Name_zh, SortOrder = r.SortOrder, IsActive = true, Form990LineId = r.Form990LineId }; + var s = new ExpenseSubCategory { GroupId = r.GroupId, Name_en = r.Name_en, Name_zh = r.Name_zh, SortOrder = r.SortOrder, IsActive = true, Form990LineId = r.Form990LineId, Form1099BoxId = r.Form1099BoxId }; _db.ExpenseSubCategories.Add(s); await _db.SaveChangesAsync(); return s.Id; @@ -79,7 +86,7 @@ public class ExpenseCategoryService : IExpenseCategoryService { var s = await _db.ExpenseSubCategories.FindAsync(id) ?? throw new KeyNotFoundException($"ExpenseSubCategory {id} not found."); - s.GroupId = r.GroupId; s.Name_en = r.Name_en; s.Name_zh = r.Name_zh; s.SortOrder = r.SortOrder; s.IsActive = r.IsActive; s.Form990LineId = r.Form990LineId; + s.GroupId = r.GroupId; s.Name_en = r.Name_en; s.Name_zh = r.Name_zh; s.SortOrder = r.SortOrder; s.IsActive = r.IsActive; s.Form990LineId = r.Form990LineId; s.Form1099BoxId = r.Form1099BoxId; await _db.SaveChangesAsync(); } diff --git a/API/ROLAC.API/Services/ExpenseService.cs b/API/ROLAC.API/Services/ExpenseService.cs index 5c2bfe3..3ce5249 100644 --- a/API/ROLAC.API/Services/ExpenseService.cs +++ b/API/ROLAC.API/Services/ExpenseService.cs @@ -120,6 +120,7 @@ public class ExpenseService : IExpenseService ReviewedByName = e.ReviewedBy != null ? reviewerNames.GetValueOrDefault(e.ReviewedBy) : null, ReviewedAt = e.ReviewedAt, ReviewNotes = e.ReviewNotes, + PayeeId = e.PayeeId, }; }).ToList(); @@ -211,6 +212,7 @@ public class ExpenseService : IExpenseService CheckNumber = e.CheckNumber, Notes = e.Notes, ReviewNotes = e.ReviewNotes, ReviewedByName = reviewerName, ReviewedAt = e.ReviewedAt, SubmittedBy = e.SubmittedBy, SubmittedAt = e.SubmittedAt, PaidAt = e.PaidAt, + PayeeId = e.PayeeId, Lines = lineDtos, }; } @@ -273,6 +275,7 @@ public class ExpenseService : IExpenseService e.VendorName = null; } + e.PayeeId = r.PayeeId; _db.Expenses.Add(e); await _db.SaveChangesAsync(); return e.Id; @@ -294,7 +297,7 @@ public class ExpenseService : IExpenseService throw new InvalidOperationException("You can only edit your own draft, pending, or rejected reimbursements."); e.MinistryId = r.MinistryId; e.Description = r.Description; e.CheckNumber = r.CheckNumber; - e.ExpenseDate = r.ExpenseDate; e.Notes = r.Notes; + e.ExpenseDate = r.ExpenseDate; e.Notes = r.Notes; e.PayeeId = r.PayeeId; if (e.Type == "VendorPayment") e.VendorName = r.VendorName; // Replace the line set wholesale (lines are owned by the header), recompute the total. diff --git a/API/ROLAC.API/Services/Form1099/Form1099FormService.cs b/API/ROLAC.API/Services/Form1099/Form1099FormService.cs new file mode 100644 index 0000000..4c7d199 --- /dev/null +++ b/API/ROLAC.API/Services/Form1099/Form1099FormService.cs @@ -0,0 +1,160 @@ +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); + + var payerEin = string.IsNullOrWhiteSpace(church.PayerEin) ? "" : church.PayerEin; + 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)
{Encode(payerEin)}" + + "
" + + "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); + } +} diff --git a/API/ROLAC.API/Services/Form1099/I1099FormService.cs b/API/ROLAC.API/Services/Form1099/I1099FormService.cs new file mode 100644 index 0000000..91e96b9 --- /dev/null +++ b/API/ROLAC.API/Services/Form1099/I1099FormService.cs @@ -0,0 +1,10 @@ +namespace ROLAC.API.Services; + +public interface I1099FormService +{ + /// Recipient Copy B 1099-NEC PDF for one payee/year (plain paper). + Task<(Stream stream, string contentType, string fileName)> RenderCopyBAsync(int payeeId, int taxYear); + + /// Filing-data CSV (one row per reportable recipient) for IRIS/accountant. + Task<(Stream stream, string contentType, string fileName)> ExportFilingCsvAsync(int taxYear); +} diff --git a/API/ROLAC.API/Services/Form1099ReportService.cs b/API/ROLAC.API/Services/Form1099ReportService.cs new file mode 100644 index 0000000..e237e8e --- /dev/null +++ b/API/ROLAC.API/Services/Form1099ReportService.cs @@ -0,0 +1,134 @@ +using Microsoft.EntityFrameworkCore; +using ROLAC.API.Data; +using ROLAC.API.DTOs.Finance; +using ROLAC.API.Entities; + +namespace ROLAC.API.Services; + +/// +/// Read-only aggregation producing the year-end 1099 recipient summary. CASH BASIS: +/// only Paid expenses whose PaidAt falls in the tax year, attributed to a tracked payee, +/// on a line whose category maps to a 1099 box (sub ?? group). Unmapped lines are excluded. +/// +public class Form1099ReportService : IForm1099ReportService +{ + private readonly AppDbContext _db; + public Form1099ReportService(AppDbContext db) => _db = db; + + public async Task> GetBoxesAsync() => + await _db.Form1099Boxes.AsNoTracking().Where(b => b.IsActive) + .OrderBy(b => b.SortOrder) + .Select(b => new Form1099BoxDto + { + Id = b.Id, BoxCode = b.BoxCode, Name_en = b.Name_en, + Name_zh = b.Name_zh, FormType = b.FormType, SortOrder = b.SortOrder, + }).ToListAsync(); + + /// + /// Pulls the reportable expense lines for the tax year and materializes them (anonymous + /// projection -> ToListAsync -> in-memory map), mirroring Form990ReportService so the SQL + /// translation stays simple on Npgsql. The tax year is a half-open UTC range + /// [Jan 1 taxYear, Jan 1 taxYear+1), deterministic regardless of server timezone and matching + /// how Expense.PaidAt is written (midnight UTC). Unmapped lines (no 1099 box) are dropped here + /// so callers always receive reportable lines. + /// + private async Task> LoadReportableLinesAsync(int taxYear) + { + var start = new DateTimeOffset(new DateTime(taxYear, 1, 1), TimeSpan.Zero); + var end = start.AddYears(1); + var raw = await ( + from e in _db.Expenses.Where(e => e.Status == "Paid" && e.PaidAt != null + && e.PaidAt >= start && e.PaidAt < end && e.PayeeId != null) + join p in _db.Payee1099s.Where(p => p.Is1099Tracked) on e.PayeeId equals p.Id + join l in _db.ExpenseLines on e.Id equals l.ExpenseId + join sub in _db.ExpenseSubCategories on l.SubCategoryId equals sub.Id + join grp in _db.ExpenseCategoryGroups on l.CategoryGroupId equals grp.Id + select new + { + PayeeId = p.Id, + p.LegalName, + p.TinLast4, + p.W9Status, + PaidAt = e.PaidAt!.Value, + e.Description, + GroupName = grp.Name_en, + SubName = sub.Name_en, + l.Amount, + BoxId = sub.Form1099BoxId ?? grp.Form1099BoxId, + }).ToListAsync(); + + return raw.Where(x => x.BoxId != null) + .Select(x => new PaidLine + { + PayeeId = x.PayeeId, + LegalName = x.LegalName, + TinLast4 = x.TinLast4, + W9Status = x.W9Status, + PaidAt = x.PaidAt, + Description = x.Description, + CategoryName = x.GroupName + " / " + x.SubName, + Amount = x.Amount, + BoxId = x.BoxId, + }).ToList(); + } + + public async Task GetAnnualSummaryAsync(int taxYear) + { + var boxes = await _db.Form1099Boxes.AsNoTracking().ToDictionaryAsync(b => b.Id, b => b.BoxCode); + var lines = await LoadReportableLinesAsync(taxYear); + + var dto = new Form1099SummaryDto { TaxYear = taxYear }; + foreach (var g in lines.GroupBy(x => x.PayeeId)) + { + var first = g.First(); + var nec = g.Where(x => boxes.GetValueOrDefault(x.BoxId!.Value) == Form1099.BoxNec1).Sum(x => x.Amount); + var rents = g.Where(x => boxes.GetValueOrDefault(x.BoxId!.Value) == Form1099.BoxMisc1).Sum(x => x.Amount); + var w9Missing = first.W9Status != Form1099.W9Status.OnFile; + var meets = nec >= Form1099.ReportingThreshold || rents >= Form1099.ReportingThreshold; + dto.Rows.Add(new Form1099RecipientRowDto + { + PayeeId = first.PayeeId, LegalName = first.LegalName, TinLast4 = first.TinLast4, + W9Status = first.W9Status, NecTotal = nec, RentsTotal = rents, + GrandTotal = nec + rents, MeetsThreshold = meets, W9Missing = w9Missing, + }); + } + dto.Rows = dto.Rows.OrderByDescending(r => r.GrandTotal).ThenBy(r => r.LegalName).ToList(); + dto.TotalReportable = dto.Rows.Sum(r => r.GrandTotal); + dto.RecipientsAtThreshold = dto.Rows.Count(r => r.MeetsThreshold); + dto.RecipientsMissingW9 = dto.Rows.Count(r => r.W9Missing); + return dto; + } + + public async Task GetRecipientDetailAsync(int payeeId, int taxYear) + { + var payee = await _db.Payee1099s.AsNoTracking().FirstOrDefaultAsync(p => p.Id == payeeId); + if (payee is null) return null; + var boxes = await _db.Form1099Boxes.AsNoTracking().ToDictionaryAsync(b => b.Id, b => b.BoxCode); + var lines = (await LoadReportableLinesAsync(taxYear)).Where(x => x.PayeeId == payeeId).ToList(); + + return new Form1099RecipientDetailDto + { + PayeeId = payee.Id, LegalName = payee.LegalName, TinLast4 = payee.TinLast4, + W9Status = payee.W9Status, TaxYear = taxYear, + Payments = lines.OrderBy(x => x.PaidAt).Select(x => new Form1099PaymentDto + { + PaidDate = DateOnly.FromDateTime(x.PaidAt.Date).ToString("yyyy-MM-dd"), + Description = x.Description, CategoryName = x.CategoryName, + BoxCode = boxes.GetValueOrDefault(x.BoxId!.Value) ?? "", Amount = x.Amount, + }).ToList(), + }; + } + + private sealed class PaidLine + { + public int PayeeId { get; set; } + public string LegalName { get; set; } = ""; + public string? TinLast4 { get; set; } + public string W9Status { get; set; } = ""; + public DateTimeOffset PaidAt { get; set; } + public string Description { get; set; } = ""; + public string CategoryName { get; set; } = ""; + public decimal Amount { get; set; } + public int? BoxId { get; set; } + } +} diff --git a/API/ROLAC.API/Services/IForm1099ReportService.cs b/API/ROLAC.API/Services/IForm1099ReportService.cs new file mode 100644 index 0000000..64bf88d --- /dev/null +++ b/API/ROLAC.API/Services/IForm1099ReportService.cs @@ -0,0 +1,9 @@ +using ROLAC.API.DTOs.Finance; +namespace ROLAC.API.Services; + +public interface IForm1099ReportService +{ + Task> GetBoxesAsync(); + Task GetAnnualSummaryAsync(int taxYear); + Task GetRecipientDetailAsync(int payeeId, int taxYear); +} diff --git a/API/ROLAC.API/Services/IPayee1099Service.cs b/API/ROLAC.API/Services/IPayee1099Service.cs new file mode 100644 index 0000000..63e5d3e --- /dev/null +++ b/API/ROLAC.API/Services/IPayee1099Service.cs @@ -0,0 +1,17 @@ +using ROLAC.API.DTOs.Payee; +namespace ROLAC.API.Services; + +public interface IPayee1099Service +{ + Task> GetAllAsync(bool includeInactive); + Task GetByIdAsync(int id); + Task CreateAsync(SavePayee1099Request r); + Task UpdateAsync(int id, SavePayee1099Request r); + Task DeleteAsync(int id); + /// Full decrypted TIN. Caller must be authorized (gated at controller). + Task RevealTinAsync(int id); + /// Stores the uploaded W-9 blob and records its path. Throws KeyNotFoundException if the payee is missing. + Task SaveW9Async(int id, Stream content, string fileName); + /// Opens the stored W-9 blob; null when none is attached. + Task<(Stream stream, string contentType)?> OpenW9Async(int id); +} diff --git a/API/ROLAC.API/Services/Payee1099Service.cs b/API/ROLAC.API/Services/Payee1099Service.cs new file mode 100644 index 0000000..ae9fcc2 --- /dev/null +++ b/API/ROLAC.API/Services/Payee1099Service.cs @@ -0,0 +1,162 @@ +using Microsoft.EntityFrameworkCore; +using ROLAC.API.Data; +using ROLAC.API.DTOs.Payee; +using ROLAC.API.Entities; +using ROLAC.API.Services.Security; +using ROLAC.API.Services.Storage; + +namespace ROLAC.API.Services; + +public class Payee1099Service : IPayee1099Service +{ + private readonly AppDbContext _db; + private readonly ITinProtector _tin; + private readonly IFileStorage _storage; + + public Payee1099Service(AppDbContext db, ITinProtector tin, IFileStorage storage) + { + _db = db; + _tin = tin; + _storage = storage; + } + + public async Task> GetAllAsync(bool includeInactive) + { + var q = _db.Payee1099s.AsNoTracking().Include(p => p.Member).AsQueryable(); + if (!includeInactive) q = q.Where(p => p.IsActive); + return await q.OrderBy(p => p.LegalName).Select(p => new Payee1099ListItemDto + { + Id = p.Id, + LegalName = p.LegalName, + DisplayName = p.DisplayName, + MemberId = p.MemberId, + MemberName = p.Member != null ? p.Member.FirstName_en + " " + p.Member.LastName_en : null, + TaxClassification = p.TaxClassification, + Is1099Tracked = p.Is1099Tracked, + TinType = p.TinType, + TinLast4 = p.TinLast4, + W9Status = p.W9Status, + IsActive = p.IsActive, + }).ToListAsync(); + } + + public async Task GetByIdAsync(int id) + { + var p = await _db.Payee1099s.AsNoTracking().Include(x => x.Member).FirstOrDefaultAsync(x => x.Id == id); + if (p is null) return null; + return new Payee1099Dto + { + Id = p.Id, + LegalName = p.LegalName, + DisplayName = p.DisplayName, + MemberId = p.MemberId, + MemberName = p.Member != null ? $"{p.Member.FirstName_en} {p.Member.LastName_en}" : null, + TaxClassification = p.TaxClassification, + Is1099Tracked = p.Is1099Tracked, + TinType = p.TinType, + TinLast4 = p.TinLast4, + W9Status = p.W9Status, + IsActive = p.IsActive, + AddressLine1 = p.AddressLine1, + AddressLine2 = p.AddressLine2, + City = p.City, + State = p.State, + Zip = p.Zip, + Email = p.Email, + Phone = p.Phone, + W9ReceivedDate = p.W9ReceivedDate?.ToString("yyyy-MM-dd"), + HasW9Document = p.W9BlobPath != null, + Notes = p.Notes, + }; + } + + public async Task CreateAsync(SavePayee1099Request r) + { + var p = new Payee1099(); + Apply(p, r); + _db.Payee1099s.Add(p); + await _db.SaveChangesAsync(); + return p.Id; + } + + public async Task UpdateAsync(int id, SavePayee1099Request r) + { + var p = await _db.Payee1099s.FirstOrDefaultAsync(x => x.Id == id) + ?? throw new KeyNotFoundException($"Payee1099 {id} not found."); + Apply(p, r); + await _db.SaveChangesAsync(); + } + + public async Task DeleteAsync(int id) + { + var p = await _db.Payee1099s.FirstOrDefaultAsync(x => x.Id == id) + ?? throw new KeyNotFoundException($"Payee1099 {id} not found."); + p.IsDeleted = true; + p.DeletedAt = DateTimeOffset.UtcNow; + await _db.SaveChangesAsync(); + } + + public async Task RevealTinAsync(int id) + { + var p = await _db.Payee1099s.AsNoTracking().FirstOrDefaultAsync(x => x.Id == id); + return p?.TinEncrypted is null ? null : _tin.Unprotect(p.TinEncrypted); + } + + public async Task SaveW9Async(int id, Stream content, string fileName) + { + var p = await _db.Payee1099s.FirstOrDefaultAsync(x => x.Id == id) + ?? throw new KeyNotFoundException($"Payee1099 {id} not found."); + + // Mirror the expense-receipt blob convention: a stable per-record path under a feature folder, + // preserving the original extension. Re-uploads overwrite the prior blob. + var ext = Path.GetExtension(fileName); + var path = $"finance/w9/{p.Id}{ext}"; + if (p.W9BlobPath != null && p.W9BlobPath != path) + await _storage.DeleteAsync(p.W9BlobPath); + var saved = await _storage.SaveAsync(content, path); + p.W9BlobPath = saved; + await _db.SaveChangesAsync(); + } + + public async Task<(Stream stream, string contentType)?> OpenW9Async(int id) + { + var p = await _db.Payee1099s.AsNoTracking().FirstOrDefaultAsync(x => x.Id == id); + if (p?.W9BlobPath is null) return null; + var stream = await _storage.OpenReadAsync(p.W9BlobPath); + if (stream is null) return null; + var ext = Path.GetExtension(p.W9BlobPath).ToLowerInvariant(); + var contentType = ext switch + { + ".png" => "image/png", ".webp" => "image/webp", ".pdf" => "application/pdf", + _ => "image/jpeg", + }; + return (stream, contentType); + } + + // Maps request fields onto the entity. A null/blank Tin leaves the existing ciphertext untouched (update case). + private void Apply(Payee1099 p, SavePayee1099Request r) + { + p.LegalName = r.LegalName; + p.DisplayName = r.DisplayName; + p.MemberId = r.MemberId; + p.TaxClassification = r.TaxClassification; + p.Is1099Tracked = r.Is1099Tracked; + p.TinType = r.TinType; + p.AddressLine1 = r.AddressLine1; + p.AddressLine2 = r.AddressLine2; + p.City = r.City; + p.State = r.State; + p.Zip = r.Zip; + p.Email = r.Email; + p.Phone = r.Phone; + p.W9Status = r.W9Status; + p.W9ReceivedDate = r.W9ReceivedDate; + p.IsActive = r.IsActive; + p.Notes = r.Notes; + if (!string.IsNullOrWhiteSpace(r.Tin)) + { + p.TinEncrypted = _tin.Protect(r.Tin); + p.TinLast4 = TinProtector.Last4(r.Tin); + } + } +} diff --git a/API/ROLAC.API/Services/Security/ITinProtector.cs b/API/ROLAC.API/Services/Security/ITinProtector.cs new file mode 100644 index 0000000..2996bd3 --- /dev/null +++ b/API/ROLAC.API/Services/Security/ITinProtector.cs @@ -0,0 +1,8 @@ +namespace ROLAC.API.Services.Security; + +/// Reversible protection for taxpayer identification numbers (SSN/EIN). +public interface ITinProtector +{ + string Protect(string plaintext); + string Unprotect(string ciphertext); +} diff --git a/API/ROLAC.API/Services/Security/TinProtector.cs b/API/ROLAC.API/Services/Security/TinProtector.cs new file mode 100644 index 0000000..f29a399 --- /dev/null +++ b/API/ROLAC.API/Services/Security/TinProtector.cs @@ -0,0 +1,22 @@ +using Microsoft.AspNetCore.DataProtection; + +namespace ROLAC.API.Services.Security; + +public class TinProtector : ITinProtector +{ + private readonly IDataProtector _protector; + + public TinProtector(IDataProtectionProvider provider) + => _protector = provider.CreateProtector("Payee1099.Tin"); + + public string Protect(string plaintext) => _protector.Protect(plaintext); + public string Unprotect(string ciphertext) => _protector.Unprotect(ciphertext); + + /// Last four digits of a TIN (ignoring dashes/spaces); null/empty in => null. + public static string? Last4(string? raw) + { + if (string.IsNullOrWhiteSpace(raw)) return null; + var digits = new string(raw.Where(char.IsDigit).ToArray()); + return digits.Length <= 4 ? digits : digits[^4..]; + } +} diff --git a/APP/src/app/app.routes.ts b/APP/src/app/app.routes.ts index 7695999..81a079e 100644 --- a/APP/src/app/app.routes.ts +++ b/APP/src/app/app.routes.ts @@ -22,6 +22,8 @@ import { DisbursementPageComponent } from './features/disbursement/pages/disburs import { CheckRegisterPageComponent } from './features/disbursement/pages/check-register-page/check-register-page.component'; import { ChurchProfilePageComponent } from './features/disbursement/pages/church-profile-page/church-profile-page.component'; import { Form990ReportPageComponent } from './features/finance-report/pages/form990-report-page/form990-report-page.component'; +import { Form1099ReportPageComponent } from './features/finance-report/pages/form1099-report-page/form1099-report-page.component'; +import { Payee1099PageComponent } from './features/payee1099/pages/payee-1099-page/payee-1099-page.component'; import { AttendanceCounterPageComponent } from './features/meal-attendance/pages/attendance-counter-page/attendance-counter-page.component'; import { OfferingEntryMobilePageComponent } from './features/giving/pages/offering-entry-mobile-page/offering-entry-mobile-page.component'; import { SystemLogsPageComponent } from './features/logging/pages/system-logs-page/system-logs-page.component'; @@ -228,6 +230,24 @@ export const routes: Routes = [ title: 'Form 990 — Functional Expenses', titleZh: 'Form 990 功能性費用表', section: 'Finance', }, }, + { + path: 'finance/payee-1099', + component: Payee1099PageComponent, + canActivate: [PermissionGuard], + data: { + permission: { module: PermissionModules.Form1099, action: 'read' }, + title: '1099 Recipients', titleZh: '1099 收款人', section: 'Finance', + }, + }, + { + path: 'finance/form1099-report', + component: Form1099ReportPageComponent, + canActivate: [PermissionGuard], + data: { + permission: { module: PermissionModules.Form1099, action: 'read' }, + title: '1099 Year-End Report', titleZh: '1099 年度報表', section: 'Finance', + }, + }, ] }, diff --git a/APP/src/app/core/models/permission.model.ts b/APP/src/app/core/models/permission.model.ts index 2f9ac40..d8e22d5 100644 --- a/APP/src/app/core/models/permission.model.ts +++ b/APP/src/app/core/models/permission.model.ts @@ -33,6 +33,7 @@ export const PermissionModules = { SystemLogs: 'SystemLogs', AuditLogs: 'AuditLogs', Settings: 'Settings', + Form1099: 'Form1099', } as const; /** A required permission, used in route data and the *appHasPermission directive. */ diff --git a/APP/src/app/features/disbursement/models/disbursement.model.ts b/APP/src/app/features/disbursement/models/disbursement.model.ts index 0ba7aa9..8ea6a2f 100644 --- a/APP/src/app/features/disbursement/models/disbursement.model.ts +++ b/APP/src/app/features/disbursement/models/disbursement.model.ts @@ -48,7 +48,7 @@ export interface ChurchProfileDto { id: number; name: string; nameZh: string | null; phone: string | null; email: string | null; website: string | null; address: string | null; city: string | null; state: string | null; zipCode: string | null; bankName: string | null; - bankAccountNumber: string | null; bankRoutingNumber: string | null; nextCheckNumber: number; + bankAccountNumber: string | null; bankRoutingNumber: string | null; payerEin: string | null; nextCheckNumber: number; aiProvider: string; claudeModel: string | null; claudeApiKeyMasked: string | null; geminiModel: string | null; geminiApiKeyMasked: string | null; diff --git a/APP/src/app/features/disbursement/pages/church-profile-page/church-profile-page.component.html b/APP/src/app/features/disbursement/pages/church-profile-page/church-profile-page.component.html index c2af9ff..2d15893 100644 --- a/APP/src/app/features/disbursement/pages/church-profile-page/church-profile-page.component.html +++ b/APP/src/app/features/disbursement/pages/church-profile-page/church-profile-page.component.html @@ -55,6 +55,10 @@ Routing # / 路由號碼 + - -