From 0e90f193779bde5fbbf1d05b464f13980ef6a92c Mon Sep 17 00:00:00 2001 From: Chris Chen Date: Tue, 23 Jun 2026 19:03:35 -0700 Subject: [PATCH] Add notification entities, DbContext config, and migration Creates MemberChannelBinding, LineBindingCode, MessagingGroup, and NotificationLog entities under ROLAC.API.Entities.Notifications; wires DbSets and fluent config into AppDbContext; generates EF migration AddNotifications creating the four tables. Co-Authored-By: Claude Opus 4.8 --- API/ROLAC.API/Data/AppDbContext.cs | 49 + .../Entities/Notifications/LineBindingCode.cs | 14 + .../Notifications/MemberChannelBinding.cs | 17 + .../Entities/Notifications/MessagingGroup.cs | 12 + .../Entities/Notifications/NotificationLog.cs | 22 + ...0260624020251_AddNotifications.Designer.cs | 1902 +++++++++++++++++ .../20260624020251_AddNotifications.cs | 176 ++ .../Migrations/AppDbContextModelSnapshot.cs | 207 ++ 8 files changed, 2399 insertions(+) create mode 100644 API/ROLAC.API/Entities/Notifications/LineBindingCode.cs create mode 100644 API/ROLAC.API/Entities/Notifications/MemberChannelBinding.cs create mode 100644 API/ROLAC.API/Entities/Notifications/MessagingGroup.cs create mode 100644 API/ROLAC.API/Entities/Notifications/NotificationLog.cs create mode 100644 API/ROLAC.API/Migrations/20260624020251_AddNotifications.Designer.cs create mode 100644 API/ROLAC.API/Migrations/20260624020251_AddNotifications.cs diff --git a/API/ROLAC.API/Data/AppDbContext.cs b/API/ROLAC.API/Data/AppDbContext.cs index 33448a7..af2cd67 100644 --- a/API/ROLAC.API/Data/AppDbContext.cs +++ b/API/ROLAC.API/Data/AppDbContext.cs @@ -2,6 +2,7 @@ using Microsoft.AspNetCore.Identity.EntityFrameworkCore; using Microsoft.EntityFrameworkCore; using ROLAC.API.Data.Logging; using ROLAC.API.Entities; +using ROLAC.API.Entities.Notifications; namespace ROLAC.API.Data; @@ -26,6 +27,11 @@ public class AppDbContext : IdentityDbContext public DbSet MealAttendances => Set(); public DbSet RolePermissions => Set(); + public DbSet MemberChannelBindings => Set(); + public DbSet LineBindingCodes => Set(); + public DbSet MessagingGroups => Set(); + public DbSet NotificationLogs => Set(); + protected override void OnModelCreating(ModelBuilder builder) { base.OnModelCreating(builder); @@ -326,6 +332,49 @@ public class AppDbContext : IdentityDbContext entity.HasIndex(e => new { e.Year, e.Month }).IsUnique(); }); + // ── Notifications (email + Line) ───────────────────────────────────── + builder.Entity(entity => + { + entity.Property(e => e.Channel).HasMaxLength(20).IsRequired(); + entity.Property(e => e.ExternalId).HasMaxLength(100).IsRequired(); + entity.HasIndex(e => new { e.MemberId, e.Channel }).IsUnique(); + entity.HasIndex(e => new { e.Channel, e.ExternalId }).IsUnique(); + entity.HasOne(e => e.Member).WithMany() + .HasForeignKey(e => e.MemberId).OnDelete(DeleteBehavior.Cascade); + }); + + builder.Entity(entity => + { + entity.Property(e => e.Code).HasMaxLength(20).IsRequired(); + entity.HasIndex(e => e.Code); + entity.HasOne(e => e.Member).WithMany() + .HasForeignKey(e => e.MemberId).OnDelete(DeleteBehavior.Cascade); + }); + + builder.Entity(entity => + { + entity.Property(e => e.Channel).HasMaxLength(20).IsRequired(); + entity.Property(e => e.ExternalId).HasMaxLength(100).IsRequired(); + entity.Property(e => e.Name).HasMaxLength(200); + entity.HasIndex(e => new { e.Channel, e.ExternalId }).IsUnique(); + }); + + builder.Entity(entity => + { + entity.Property(e => e.Channel).HasMaxLength(20).IsRequired(); + entity.Property(e => e.TargetType).HasMaxLength(20).IsRequired(); + entity.Property(e => e.TargetExternalId).HasMaxLength(200).IsRequired(); + entity.Property(e => e.Subject).HasMaxLength(300); + entity.Property(e => e.Status).HasMaxLength(20).IsRequired(); + entity.Property(e => e.SentByUserId).HasMaxLength(450).IsRequired(); + entity.HasIndex(e => e.SentAt); + entity.HasIndex(e => e.Channel); + entity.HasOne(e => e.Member).WithMany() + .HasForeignKey(e => e.MemberId).OnDelete(DeleteBehavior.SetNull); + entity.HasOne(e => e.MessagingGroup).WithMany() + .HasForeignKey(e => e.MessagingGroupId).OnDelete(DeleteBehavior.SetNull); + }); + // ── SystemLog / AuditLog (append-only) ─────────────────────────────── // Mapped here for SCHEMA only — there are deliberately no DbSets on this // context, so business code can't write logs through the audited context. diff --git a/API/ROLAC.API/Entities/Notifications/LineBindingCode.cs b/API/ROLAC.API/Entities/Notifications/LineBindingCode.cs new file mode 100644 index 0000000..3e559c3 --- /dev/null +++ b/API/ROLAC.API/Entities/Notifications/LineBindingCode.cs @@ -0,0 +1,14 @@ +using ROLAC.API.Entities; + +namespace ROLAC.API.Entities.Notifications; + +/// A short-lived code a member types to the Line bot to complete account binding. +public class LineBindingCode +{ + public int Id { get; set; } + public string Code { get; set; } = null!; + public int MemberId { get; set; } + public Member? Member { get; set; } + public DateTime ExpiresAt { get; set; } + public DateTime? ConsumedAt { get; set; } // null = unused +} diff --git a/API/ROLAC.API/Entities/Notifications/MemberChannelBinding.cs b/API/ROLAC.API/Entities/Notifications/MemberChannelBinding.cs new file mode 100644 index 0000000..6be24d9 --- /dev/null +++ b/API/ROLAC.API/Entities/Notifications/MemberChannelBinding.cs @@ -0,0 +1,17 @@ +using ROLAC.API.Entities; + +namespace ROLAC.API.Entities.Notifications; + +/// +/// Binds a member to an external channel account (e.g. a Line userId). Separate table so future +/// channels don't require changes to Member. +/// +public class MemberChannelBinding +{ + public int Id { get; set; } + public int MemberId { get; set; } + public Member? Member { get; set; } + public string Channel { get; set; } = null!; // "line" + public string ExternalId { get; set; } = null!; // Line userId + public DateTime BoundAt { get; set; } +} diff --git a/API/ROLAC.API/Entities/Notifications/MessagingGroup.cs b/API/ROLAC.API/Entities/Notifications/MessagingGroup.cs new file mode 100644 index 0000000..08ed417 --- /dev/null +++ b/API/ROLAC.API/Entities/Notifications/MessagingGroup.cs @@ -0,0 +1,12 @@ +namespace ROLAC.API.Entities.Notifications; + +/// A Line group the bot was added to. Named by an admin after the join event. +public class MessagingGroup +{ + public int Id { get; set; } + public string Channel { get; set; } = null!; // "line" + public string ExternalId { get; set; } = null!; // Line groupId + public string? Name { get; set; } + public bool IsActive { get; set; } = true; + public DateTime RegisteredAt { get; set; } +} diff --git a/API/ROLAC.API/Entities/Notifications/NotificationLog.cs b/API/ROLAC.API/Entities/Notifications/NotificationLog.cs new file mode 100644 index 0000000..2bf87b7 --- /dev/null +++ b/API/ROLAC.API/Entities/Notifications/NotificationLog.cs @@ -0,0 +1,22 @@ +using ROLAC.API.Entities; + +namespace ROLAC.API.Entities.Notifications; + +/// An append-only audit row for every email or Line send (success or failure). +public class NotificationLog +{ + public long Id { get; set; } + public string Channel { get; set; } = null!; // "email" | "line" + public string TargetType { get; set; } = null!; // "email" | "user" | "group" + public string TargetExternalId { get; set; } = null!; // email address OR Line id + public string? Subject { get; set; } // email only + public int? MemberId { get; set; } + public Member? Member { get; set; } + public int? MessagingGroupId { get; set; } + public MessagingGroup? MessagingGroup { get; set; } + public string Body { get; set; } = null!; + public string Status { get; set; } = null!; // "sent" | "failed" + public string? Error { get; set; } + public string SentByUserId { get; set; } = null!; + public DateTime SentAt { get; set; } +} diff --git a/API/ROLAC.API/Migrations/20260624020251_AddNotifications.Designer.cs b/API/ROLAC.API/Migrations/20260624020251_AddNotifications.Designer.cs new file mode 100644 index 0000000..ecad867 --- /dev/null +++ b/API/ROLAC.API/Migrations/20260624020251_AddNotifications.Designer.cs @@ -0,0 +1,1902 @@ +// +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("20260624020251_AddNotifications")] + partial class AddNotifications + { + /// + 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("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("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(450) + .HasColumnType("character varying(450)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("NextCheckNumber") + .HasColumnType("integer"); + + 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("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("CategoryGroupId") + .HasColumnType("integer"); + + 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("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("SubCategoryId") + .HasColumnType("integer"); + + 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("CategoryGroupId"); + + b.HasIndex("ExpenseDate"); + + b.HasIndex("MemberId"); + + b.HasIndex("MinistryId"); + + b.HasIndex("Status") + .HasFilter("\"IsDeleted\" = false"); + + b.HasIndex("SubCategoryId"); + + 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("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("ExpenseCategoryGroups"); + }); + + 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("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("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.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("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("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.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.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("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.ExpenseCategoryGroup", "CategoryGroup") + .WithMany() + .HasForeignKey("CategoryGroupId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + 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.ExpenseSubCategory", "SubCategory") + .WithMany() + .HasForeignKey("SubCategoryId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("CategoryGroup"); + + b.Navigation("Member"); + + b.Navigation("Ministry"); + + b.Navigation("SubCategory"); + }); + + modelBuilder.Entity("ROLAC.API.Entities.ExpenseSubCategory", b => + { + b.HasOne("ROLAC.API.Entities.ExpenseCategoryGroup", "Group") + .WithMany("SubCategories") + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + 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.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.AppUser", b => + { + b.Navigation("RefreshTokens"); + }); + + modelBuilder.Entity("ROLAC.API.Entities.Check", b => + { + b.Navigation("Lines"); + }); + + modelBuilder.Entity("ROLAC.API.Entities.ExpenseCategoryGroup", b => + { + b.Navigation("SubCategories"); + }); + + modelBuilder.Entity("ROLAC.API.Entities.OfferingSession", b => + { + b.Navigation("Givings"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/API/ROLAC.API/Migrations/20260624020251_AddNotifications.cs b/API/ROLAC.API/Migrations/20260624020251_AddNotifications.cs new file mode 100644 index 0000000..d678ed3 --- /dev/null +++ b/API/ROLAC.API/Migrations/20260624020251_AddNotifications.cs @@ -0,0 +1,176 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace ROLAC.API.Migrations +{ + /// + public partial class AddNotifications : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "LineBindingCodes", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Code = table.Column(type: "character varying(20)", maxLength: 20, nullable: false), + MemberId = table.Column(type: "integer", nullable: false), + ExpiresAt = table.Column(type: "timestamp with time zone", nullable: false), + ConsumedAt = table.Column(type: "timestamp with time zone", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_LineBindingCodes", x => x.Id); + table.ForeignKey( + name: "FK_LineBindingCodes_Members_MemberId", + column: x => x.MemberId, + principalTable: "Members", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "MemberChannelBindings", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + MemberId = table.Column(type: "integer", nullable: false), + Channel = table.Column(type: "character varying(20)", maxLength: 20, nullable: false), + ExternalId = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), + BoundAt = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_MemberChannelBindings", x => x.Id); + table.ForeignKey( + name: "FK_MemberChannelBindings_Members_MemberId", + column: x => x.MemberId, + principalTable: "Members", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "MessagingGroups", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Channel = table.Column(type: "character varying(20)", maxLength: 20, nullable: false), + ExternalId = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), + Name = table.Column(type: "character varying(200)", maxLength: 200, nullable: true), + IsActive = table.Column(type: "boolean", nullable: false), + RegisteredAt = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_MessagingGroups", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "NotificationLogs", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Channel = table.Column(type: "character varying(20)", maxLength: 20, nullable: false), + TargetType = table.Column(type: "character varying(20)", maxLength: 20, nullable: false), + TargetExternalId = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), + Subject = table.Column(type: "character varying(300)", maxLength: 300, nullable: true), + MemberId = table.Column(type: "integer", nullable: true), + MessagingGroupId = table.Column(type: "integer", nullable: true), + Body = table.Column(type: "text", nullable: false), + Status = table.Column(type: "character varying(20)", maxLength: 20, nullable: false), + Error = table.Column(type: "text", nullable: true), + SentByUserId = table.Column(type: "character varying(450)", maxLength: 450, nullable: false), + SentAt = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_NotificationLogs", x => x.Id); + table.ForeignKey( + name: "FK_NotificationLogs_Members_MemberId", + column: x => x.MemberId, + principalTable: "Members", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + table.ForeignKey( + name: "FK_NotificationLogs_MessagingGroups_MessagingGroupId", + column: x => x.MessagingGroupId, + principalTable: "MessagingGroups", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + }); + + migrationBuilder.CreateIndex( + name: "IX_LineBindingCodes_Code", + table: "LineBindingCodes", + column: "Code"); + + migrationBuilder.CreateIndex( + name: "IX_LineBindingCodes_MemberId", + table: "LineBindingCodes", + column: "MemberId"); + + migrationBuilder.CreateIndex( + name: "IX_MemberChannelBindings_Channel_ExternalId", + table: "MemberChannelBindings", + columns: new[] { "Channel", "ExternalId" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_MemberChannelBindings_MemberId_Channel", + table: "MemberChannelBindings", + columns: new[] { "MemberId", "Channel" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_MessagingGroups_Channel_ExternalId", + table: "MessagingGroups", + columns: new[] { "Channel", "ExternalId" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_NotificationLogs_Channel", + table: "NotificationLogs", + column: "Channel"); + + migrationBuilder.CreateIndex( + name: "IX_NotificationLogs_MemberId", + table: "NotificationLogs", + column: "MemberId"); + + migrationBuilder.CreateIndex( + name: "IX_NotificationLogs_MessagingGroupId", + table: "NotificationLogs", + column: "MessagingGroupId"); + + migrationBuilder.CreateIndex( + name: "IX_NotificationLogs_SentAt", + table: "NotificationLogs", + column: "SentAt"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "LineBindingCodes"); + + migrationBuilder.DropTable( + name: "MemberChannelBindings"); + + migrationBuilder.DropTable( + name: "NotificationLogs"); + + migrationBuilder.DropTable( + name: "MessagingGroups"); + } + } +} diff --git a/API/ROLAC.API/Migrations/AppDbContextModelSnapshot.cs b/API/ROLAC.API/Migrations/AppDbContextModelSnapshot.cs index 4db086e..bd00e1f 100644 --- a/API/ROLAC.API/Migrations/AppDbContextModelSnapshot.cs +++ b/API/ROLAC.API/Migrations/AppDbContextModelSnapshot.cs @@ -1323,6 +1323,174 @@ namespace ROLAC.API.Migrations b.ToTable("MonthlyStatements"); }); + 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") @@ -1645,6 +1813,45 @@ namespace ROLAC.API.Migrations 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.RefreshToken", b => { b.HasOne("ROLAC.API.Entities.AppUser", "User")