diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index ed4926f..ba7e82a 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -25,5 +25,11 @@ jobs:
- name: Build
run: dotnet build Aquiis.sln --no-restore --configuration Release
- - name: Run focused tests
- run: dotnet test Aquiis.SimpleStart.Tests/Aquiis.SimpleStart.Tests.csproj --no-build --configuration Release --verbosity normal
+ - name: Test Core Layer
+ run: dotnet test 6-Tests/Aquiis.Core.Tests/Aquiis.Core.Tests.csproj --no-build --configuration Release --verbosity normal
+
+ - name: Test Application Layer
+ run: dotnet test 6-Tests/Aquiis.Application.Tests/Aquiis.Application.Tests.csproj --no-build --configuration Release --verbosity normal
+
+ - name: Test Shared UI Components (bUnit)
+ run: dotnet test 6-Tests/Aquiis.UI.Shared.Tests/Aquiis.UI.Shared.Tests.csproj --no-build --configuration Release --verbosity normal
diff --git a/.gitignore b/.gitignore
index 3c7ca04..c4e69aa 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,8 +1,8 @@
/* Ignore file for Aquiis projects */
-# Build outputs - ignore bin/obj in project folders
-Aquiis.*/bin/
-Aquiis.*/obj/
+# Build outputs - ignore bin/obj in all project folders
+**/bin/
+**/obj/
# Electron host - track the folder but ignore node_modules
obj/Host/node_modules/
@@ -18,6 +18,8 @@ node_modules/
Data/Backups/**
/Data/Backups/**
+Data/Migrations/**
+/Data/Migrations/**
/Data/app*
diff --git a/0-Aquiis.Core/Aquiis.Core.csproj b/0-Aquiis.Core/Aquiis.Core.csproj
new file mode 100644
index 0000000..08bcc56
--- /dev/null
+++ b/0-Aquiis.Core/Aquiis.Core.csproj
@@ -0,0 +1,10 @@
+
+
+
+ net10.0
+ enable
+ enable
+ 0.2.0
+
+
+
diff --git a/0-Aquiis.Core/Constants/ApplicationConstants.cs b/0-Aquiis.Core/Constants/ApplicationConstants.cs
new file mode 100644
index 0000000..e6f7437
--- /dev/null
+++ b/0-Aquiis.Core/Constants/ApplicationConstants.cs
@@ -0,0 +1,789 @@
+using System.Security.Cryptography.X509Certificates;
+
+namespace Aquiis.Core.Constants
+{
+ public static class ApplicationConstants
+ {
+ ///
+ /// System service account for background jobs and automated processes
+ ///
+ public static class SystemUser
+ {
+ ///
+ /// Well-known GUID for system service account.
+ /// Used by background jobs, scheduled tasks, and automated processes.
+ ///
+ public static readonly string Id = "00000000-0000-0000-0000-000000000001";
+
+ public const string Email = "system@aquiis.local";
+ public const string UserName = "system@aquiis.local"; // UserName = Email in this system
+ public const string DisplayName = "System";
+
+ // Service account details
+ public const string FirstName = "System User";
+ public const string LastName = "Account";
+ }
+
+ // DEPRECATED: Legacy Identity roles - kept for backward compatibility but not used for authorization
+ public static string DefaultSuperAdminRole { get; } = "SuperAdministrator";
+ public static string DefaultAdminRole { get; } = "Administrator";
+ public static string DefaultPropertyManagerRole { get; } = "PropertyManager";
+ public static string DefaultTenantRole { get; } = "Tenant";
+ public static string DefaultUserRole { get; } = "User";
+ public static string DefaultGuestRole { get; } = "Guest";
+
+ ///
+ /// Organization-scoped roles for multi-organization support
+ ///
+ public static class OrganizationRoles
+ {
+ ///
+ /// Owner - Full data sovereignty (create/delete orgs, backup/delete data, all features)
+ ///
+ public const string Owner = "Owner";
+
+ ///
+ /// Administrator - Delegated owner access (all features except org creation/deletion/data management)
+ ///
+ public const string Administrator = "Administrator";
+
+ ///
+ /// PropertyManager - Full property management features (no admin/settings access)
+ ///
+ public const string PropertyManager = "Property Manager";
+
+ ///
+ /// Maintenance - Maintenance requests, work orders, and vendors
+ ///
+ public const string Maintenance = "Maintenance";
+
+ ///
+ /// User - Limited feature access (view-only or basic operations)
+ ///
+ public const string User = "User";
+
+ public static readonly string[] AllRoles = { Owner, Administrator, PropertyManager, User };
+
+ public static bool IsValid(string role) => AllRoles.Contains(role);
+
+ public static bool CanManageUsers(string role) => role == Owner || role == Administrator;
+
+ public static bool CanEditSettings(string role) => role == Owner || role == Administrator;
+
+ public static bool CanManageOrganizations(string role) => role == Owner;
+
+ public static bool CanManageProperties(string role) => role == Owner || role == Administrator || role == PropertyManager;
+
+ public static bool CanManageTenants(string role) => role == Owner || role == Administrator || role == PropertyManager;
+
+ public static bool CanManageLeases(string role) => role == Owner || role == Administrator || role == PropertyManager;
+
+ public static bool CanManageInvoices(string role) => role == Owner || role == Administrator || role == PropertyManager;
+
+ public static bool CanManagePayments(string role) => role == Owner || role == Administrator || role == PropertyManager;
+
+ public static bool CanManageSecurityDeposits(string role) => role == Owner || role == Administrator || role == PropertyManager;
+
+ public static bool CanManageDocuments(string role) => role == Owner || role == Administrator || role == PropertyManager;
+
+ public static bool CanManageMaintenanceRequests(string role) => role == Owner || role == Administrator || role == PropertyManager;
+
+ public static bool CanManageInspections(string role) => role == Owner || role == Administrator || role == PropertyManager;
+
+ public static bool CanManageProspectiveTenants(string role) => role == Owner || role == Administrator || role == PropertyManager;
+
+ public static bool CanManageApplications(string role) => role == Owner || role == Administrator || role == PropertyManager;
+
+ public static bool CanManageTours(string role) => role == Owner || role == Administrator || role == PropertyManager;
+
+ public static bool CanManageChecklists(string role) => role == Owner || role == Administrator || role == PropertyManager;
+
+
+
+ public static bool CanViewRecords(string role) => AllRoles.Contains(role);
+
+ public static bool CanEditRecords(string role) => role == Owner || role == Administrator || role == PropertyManager;
+ }
+
+ public static string DefaultSuperAdminPassword { get; } = "SuperAdmin@123!";
+ public static string DefaultAdminPassword { get; } = "Admin@123!";
+ public static string DefaultPropertyManagerPassword { get; } = "PropertyManager@123!";
+ public static string DefaultTenantPassword { get; } = "Tenant@123!";
+ public static string DefaultUserPassword { get; } = "User@123!";
+ public static string DefaultGuestPassword { get; } = "Guest@123!";
+
+ public static string AdministrationPath { get; } = "/Administration";
+ public static string PropertyManagementPath { get; } = "/PropertyManagement";
+ public static string TenantPortalPath { get; } = "/TenantPortal";
+
+
+ public static string SuperAdminUserName { get; } = "superadmin";
+ public static string SuperAdminEmail { get; } = "superadmin@example.local";
+
+ public static IReadOnlyList DefaultRoles { get; } = new List
+ {
+ DefaultSuperAdminRole,
+ DefaultAdminRole,
+ DefaultPropertyManagerRole,
+ DefaultTenantRole,
+ DefaultUserRole,
+ DefaultGuestRole
+ };
+
+ public static IReadOnlyList DefaultPasswords { get; } = new List
+ {
+ DefaultSuperAdminPassword,
+ DefaultAdminPassword,
+ DefaultPropertyManagerPassword,
+ DefaultTenantPassword,
+ DefaultUserPassword,
+ DefaultGuestPassword
+ };
+
+ public static string[] USStateAbbreviations { get; } = States.Abbreviations();
+ public static string[] USStateNames { get; } = States.Names();
+
+ public static State[] USStates { get; } = States.StatesArray();
+
+ public static class PaymentMethods
+ {
+ public const string OnlinePayment = "Online Payment";
+ public const string DebitCard = "Debit Card";
+ public const string CreditCard = "Credit Card";
+ public const string BankTransfer = "Bank Transfer";
+ public const string CryptoCurrency = "Crypto Currency";
+ public const string Cash = "Cash";
+ public const string Check = "Check";
+ public const string Other = "Other";
+
+ public static IReadOnlyList AllPaymentMethods { get; } = new List
+ {
+ OnlinePayment,
+ DebitCard,
+ CreditCard,
+ BankTransfer,
+ CryptoCurrency,
+ Cash,
+ Check,
+ Other
+ };
+ }
+
+ public static class InvoiceStatuses
+ {
+ public const string Pending = "Pending";
+ public const string PaidPartial = "Paid Partial";
+ public const string Paid = "Paid";
+ public const string Overdue = "Overdue";
+ public const string Cancelled = "Cancelled";
+
+ public static IReadOnlyList AllInvoiceStatuses { get; } = new List
+ {
+ Pending,
+ PaidPartial,
+ Paid,
+ Overdue,
+ Cancelled
+ };
+ }
+
+ public static class PaymentStatuses
+ {
+ public const string Completed = "Completed";
+ public const string Pending = "Pending";
+ public const string Failed = "Failed";
+ public const string Refunded = "Refunded";
+
+ public static IReadOnlyList AllPaymentStatuses { get; } = new List
+ {
+ Completed,
+ Pending,
+ Failed,
+ Refunded
+ };
+ }
+ public static class InspectionTypes
+ {
+ public const string MoveIn = "Move-In";
+ public const string MoveOut = "Move-Out";
+ public const string Routine = "Routine";
+ public const string Maintenance = "Maintenance";
+ public const string Other = "Other";
+
+ public static IReadOnlyList AllInspectionTypes { get; } = new List
+ {
+ MoveIn,
+ MoveOut,
+ Routine,
+ Maintenance,
+ Other
+ };
+ }
+
+ public static class LeaseTypes {
+ public const string FixedTerm = "Fixed-Term";
+ public const string MonthToMonth = "Month-to-Month";
+ public const string Sublease = "Sublease";
+ public const string Other = "Other";
+
+ public static IReadOnlyList AllLeaseTypes { get; } = new List
+ {
+ FixedTerm,
+ MonthToMonth,
+ Sublease,
+ Other
+ };
+
+ }
+
+ public static class LeaseStatuses {
+ public const string Offered = "Offered";
+ public const string Pending = "Pending";
+ public const string Accepted = "Accepted";
+ public const string AcceptedPendingStart = "Accepted - Pending Start";
+ public const string Active = "Active";
+ public const string Declined = "Declined";
+ public const string Renewed = "Renewed";
+ public const string MonthToMonth = "Month-to-Month";
+ public const string NoticeGiven = "Notice Given";
+ public const string Interrupted = "Interrupted";
+ public const string Terminated = "Terminated";
+ public const string Expired = "Expired";
+
+ public static IReadOnlyList RenewalStatuses { get; } = new List
+ {
+ "NotRequired",
+ "Pending",
+ "Offered",
+ "Accepted",
+ "Declined",
+ "Expired"
+ };
+
+ public static IReadOnlyList AllLeaseStatuses { get; } = new List
+ {
+ Offered,
+ Pending,
+ Accepted,
+ AcceptedPendingStart,
+ Active,
+ Declined,
+ Renewed,
+ MonthToMonth,
+ NoticeGiven,
+ Interrupted,
+ Terminated,
+ Expired
+ };
+ }
+
+
+
+ public static class PropertyTypes
+ {
+ public const string House = "House";
+ public const string Apartment = "Apartment";
+ public const string Condo = "Condo";
+ public const string Townhouse = "Townhouse";
+ public const string Duplex = "Duplex";
+ public const string Studio = "Studio";
+ public const string Loft = "Loft";
+ public const string Other = "Other";
+
+ public static IReadOnlyList AllPropertyTypes { get; } = new List
+ {
+ House,
+ Apartment,
+ Condo,
+ Townhouse,
+ Duplex,
+ Studio,
+ Loft,
+ Other
+ };
+
+ }
+
+ public static class PropertyStatuses
+ {
+ public const string Available = "Available";
+ public const string ApplicationPending = "Application Pending";
+ public const string LeasePending = "Lease Pending";
+ public const string MoveInPending = "Accepted - Move-In Pending";
+ public const string Occupied = "Occupied";
+ public const string MoveOutPending = "Move-Out Pending";
+ public const string UnderRenovation = "Under Renovation";
+ public const string OffMarket = "Off Market";
+
+ public static IReadOnlyList OccupiedStatuses { get; } = new List
+ {
+ MoveInPending,
+ Occupied,
+ MoveOutPending
+ };
+ public static IReadOnlyList AllPropertyStatuses { get; } = new List
+ {
+ Available,
+ ApplicationPending,
+ LeasePending,
+ Occupied,
+ UnderRenovation,
+ OffMarket
+ };
+ }
+
+
+
+ public static class MaintenanceRequestTypes
+ {
+
+ public const string Plumbing = "Plumbing";
+ public const string Electrical = "Electrical";
+ public const string HeatingCooling = "Heating/Cooling";
+ public const string Appliance = "Appliance";
+ public const string Structural = "Structural";
+ public const string Landscaping = "Landscaping";
+ public const string PestControl = "Pest Control";
+ public const string Other = "Other";
+
+ public static IReadOnlyList AllMaintenanceRequestTypes { get; } = new List
+ {
+ Plumbing,
+ Electrical,
+ HeatingCooling,
+ Appliance,
+ Structural,
+ Landscaping,
+ PestControl,
+ Other
+ };
+ }
+
+ public static class MaintenanceRequestPriorities
+ {
+ public const string Low = "Low";
+ public const string Medium = "Medium";
+ public const string High = "High";
+ public const string Urgent = "Urgent";
+
+ public static IReadOnlyList AllMaintenanceRequestPriorities { get; } = new List
+ {
+ Low,
+ Medium,
+ High,
+ Urgent
+ };
+ }
+
+ public static class MaintenanceRequestStatuses
+ {
+ public const string Submitted = "Submitted";
+ public const string InProgress = "In Progress";
+ public const string Completed = "Completed";
+ public const string Cancelled = "Cancelled";
+
+ public static IReadOnlyList AllMaintenanceRequestStatuses { get; } = new List
+ {
+ Submitted,
+ InProgress,
+ Completed,
+ Cancelled
+ };
+ }
+
+ public static class TenantStatuses
+ {
+ public const string Prospective = "Prospective";
+ public const string Pending = "Pending";
+ public const string MoveInPending = "Move-In Pending";
+ public const string Active = "Active";
+ public const string MoveOutPending = "Move-Out Pending";
+ public const string Inactive = "Inactive";
+ public const string Evicted = "Evicted";
+
+ public static IReadOnlyList AllTenantStatuses { get; } = new List
+ {
+ Prospective,
+ Pending,
+ MoveInPending,
+ Active,
+ MoveOutPending,
+ Inactive,
+ Evicted
+ };
+
+ }
+
+ public static class DocumentTypes
+ {
+ public const string LeaseApplication = "Lease Application";
+ public const string LeaseAgreement = "Lease Agreement";
+ public const string InspectionReport = "Inspection Report";
+ public const string MaintenanceRecord = "Maintenance Record";
+ public const string Invoice = "Invoice";
+ public const string PaymentReceipt = "Payment Receipt";
+ public const string Other = "Other";
+
+ public static IReadOnlyList AllDocumentTypes { get; } = new List
+ {
+ LeaseApplication,
+ LeaseAgreement,
+ InspectionReport,
+ MaintenanceRecord,
+ Invoice,
+ PaymentReceipt,
+ Other
+ };
+
+ }
+
+ public static class ChecklistTypes
+ {
+ public const string MoveIn = "Move-In";
+ public const string MoveOut = "Move-Out";
+ public const string OpenHouse = "Open House";
+ public const string Tour = "Tour";
+ public const string Custom = "Custom";
+
+ public static IReadOnlyList AllChecklistTypes { get; } = new List
+ {
+ MoveIn,
+ MoveOut,
+ OpenHouse,
+ Tour,
+ Custom
+ };
+ }
+
+ public static class ChecklistStatuses
+ {
+ public const string Draft = "Draft";
+ public const string InProgress = "In Progress";
+ public const string Completed = "Completed";
+
+ public static IReadOnlyList AllChecklistStatuses { get; } = new List
+ {
+ Draft,
+ InProgress,
+ Completed
+ };
+ }
+
+ public static class ProspectiveStatuses
+ {
+ public const string Lead = "Lead";
+ public const string TourScheduled = "Tour Scheduled";
+ public const string Applied = "Applied";
+ public const string Screening = "Screening";
+ public const string Approved = "Approved";
+ public const string Denied = "Denied";
+ public const string Withdrawn = "Withdrawn";
+ public const string LeaseOffered = "Lease Offered";
+ public const string LeaseDeclined = "Lease Declined";
+ public const string ConvertedToTenant = "Converted To Tenant";
+
+ public static IReadOnlyList AllProspectiveStatuses { get; } = new List
+ {
+ Lead,
+ TourScheduled,
+ Applied,
+ Screening,
+ Approved,
+ Denied,
+ Withdrawn,
+ LeaseOffered,
+ LeaseDeclined,
+ ConvertedToTenant
+ };
+ }
+
+ public static class ProspectiveSources
+ {
+ public const string Website = "Website";
+ public const string Referral = "Referral";
+ public const string WalkIn = "Walk-in";
+ public const string Zillow = "Zillow";
+ public const string Apartments = "Apartments.com";
+ public const string SignCall = "Sign Call";
+ public const string SocialMedia = "Social Media";
+ public const string Other = "Other";
+
+ public static IReadOnlyList AllProspectiveSources { get; } = new List
+ {
+ Website,
+ Referral,
+ WalkIn,
+ Zillow,
+ Apartments,
+ SignCall,
+ SocialMedia,
+ Other
+ };
+ }
+
+ public static class TourStatuses
+ {
+ public const string Scheduled = "Scheduled";
+ public const string Completed = "Completed";
+ public const string Cancelled = "Cancelled";
+ public const string NoShow = "NoShow";
+
+ public static IReadOnlyList AllTourStatuses { get; } = new List
+ {
+ Scheduled,
+ Completed,
+ Cancelled,
+ NoShow
+ };
+ }
+
+ public static class TourInterestLevels
+ {
+ public const string VeryInterested = "Very Interested";
+ public const string Interested = "Interested";
+ public const string Neutral = "Neutral";
+ public const string NotInterested = "Not Interested";
+
+ public static IReadOnlyList AllTourInterestLevels { get; } = new List
+ {
+ VeryInterested,
+ Interested,
+ Neutral,
+ NotInterested
+ };
+ }
+
+ public static class ApplicationStatuses
+ {
+ public const string Submitted = "Submitted";
+ public const string UnderReview = "Under Review";
+ public const string Screening = "Screening";
+ public const string Approved = "Approved";
+ public const string Denied = "Denied";
+ public const string Expired = "Expired";
+ public const string Withdrawn = "Withdrawn";
+ public const string LeaseOffered = "Lease Offered";
+ public const string LeaseAccepted = "Lease Accepted";
+ public const string LeaseDeclined = "Lease Declined";
+
+ public static IReadOnlyList AllApplicationStatuses { get; } = new List
+ {
+ Submitted,
+ UnderReview,
+ Screening,
+ Approved,
+ Denied,
+ Expired,
+ Withdrawn,
+ LeaseOffered,
+ LeaseAccepted,
+ LeaseDeclined
+ };
+ }
+
+ public static class ScreeningResults
+ {
+ public const string Pending = "Pending";
+ public const string Passed = "Passed";
+ public const string Failed = "Failed";
+ public const string ConditionalPass = "Conditional Pass";
+
+ public static IReadOnlyList AllScreeningResults { get; } = new List
+ {
+ Pending,
+ Passed,
+ Failed,
+ ConditionalPass
+ };
+ }
+
+ public static class SecurityDepositStatuses
+ {
+ public const string Held = "Held";
+ public const string Released = "Released";
+ public const string Refunded = "Refunded";
+ public const string Forfeited = "Forfeited";
+ public const string PartiallyRefunded = "Partially Refunded";
+
+ public static IReadOnlyList AllSecurityDepositStatuses { get; } = new List
+ {
+ Held,
+ Released,
+ Refunded,
+ Forfeited,
+ PartiallyRefunded
+ };
+ }
+
+ public static class InvestmentPoolStatuses
+ {
+ public const string Open = "Open";
+ public const string Calculated = "Calculated";
+ public const string Distributed = "Distributed";
+ public const string Closed = "Closed";
+
+ public static IReadOnlyList AllInvestmentPoolStatuses { get; } = new List
+ {
+ Open,
+ Calculated,
+ Distributed,
+ Closed
+ };
+ }
+
+ public static class DividendPaymentMethods
+ {
+ public const string Pending = "Pending";
+ public const string LeaseCredit = "Lease Credit";
+ public const string Check = "Check";
+
+ public static IReadOnlyList AllDividendPaymentMethods { get; } = new List
+ {
+ Pending,
+ LeaseCredit,
+ Check
+ };
+ }
+
+ public static class DividendStatuses
+ {
+ public const string Pending = "Pending";
+ public const string ChoiceMade = "Choice Made";
+ public const string Applied = "Applied";
+ public const string Paid = "Paid";
+
+ public static IReadOnlyList AllDividendStatuses { get; } = new List
+ {
+ Pending,
+ ChoiceMade,
+ Applied,
+ Paid
+ };
+ }
+
+ public static class EntityTypes
+ {
+ public const string Property = "Property";
+ public const string Tenant = "Tenant";
+ public const string Lease = "Lease";
+ public const string Invoice = "Invoice";
+ public const string Payment = "Payment";
+ public const string MaintenanceRequest = "MaintenanceRequest";
+ public const string Document = "Document";
+ public const string Inspection = "Inspection";
+ public const string ProspectiveTenant = "ProspectiveTenant";
+ public const string Application = "Application";
+ public const string Tour = "Tour";
+ public const string Checklist = "Checklist";
+ public const string Note = "Note";
+ }
+
+
+
+ }
+ public static class States
+ {
+
+ static List _states = new List(50);
+
+ static States()
+ {
+ _states.Add(new State("AL", "Alabama"));
+ _states.Add(new State("AK", "Alaska"));
+ _states.Add(new State("AZ", "Arizona"));
+ _states.Add(new State("AR", "Arkansas"));
+ _states.Add(new State("CA", "California"));
+ _states.Add(new State("CO", "Colorado"));
+ _states.Add(new State("CT", "Connecticut"));
+ _states.Add(new State("DE", "Delaware"));
+ _states.Add(new State("DC", "District Of Columbia"));
+ _states.Add(new State("FL", "Florida"));
+ _states.Add(new State("GA", "Georgia"));
+ _states.Add(new State("HI", "Hawaii"));
+ _states.Add(new State("ID", "Idaho"));
+ _states.Add(new State("IL", "Illinois"));
+ _states.Add(new State("IN", "Indiana"));
+ _states.Add(new State("IA", "Iowa"));
+ _states.Add(new State("KS", "Kansas"));
+ _states.Add(new State("KY", "Kentucky"));
+ _states.Add(new State("LA", "Louisiana"));
+ _states.Add(new State("ME", "Maine"));
+ _states.Add(new State("MD", "Maryland"));
+ _states.Add(new State("MA", "Massachusetts"));
+ _states.Add(new State("MI", "Michigan"));
+ _states.Add(new State("MN", "Minnesota"));
+ _states.Add(new State("MS", "Mississippi"));
+ _states.Add(new State("MO", "Missouri"));
+ _states.Add(new State("MT", "Montana"));
+ _states.Add(new State("NE", "Nebraska"));
+ _states.Add(new State("NV", "Nevada"));
+ _states.Add(new State("NH", "New Hampshire"));
+ _states.Add(new State("NJ", "New Jersey"));
+ _states.Add(new State("NM", "New Mexico"));
+ _states.Add(new State("NY", "New York"));
+ _states.Add(new State("NC", "North Carolina"));
+ _states.Add(new State("ND", "North Dakota"));
+ _states.Add(new State("OH", "Ohio"));
+ _states.Add(new State("OK", "Oklahoma"));
+ _states.Add(new State("OR", "Oregon"));
+ _states.Add(new State("PA", "Pennsylvania"));
+ _states.Add(new State("RI", "Rhode Island"));
+ _states.Add(new State("SC", "South Carolina"));
+ _states.Add(new State("SD", "South Dakota"));
+ _states.Add(new State("TN", "Tennessee"));
+ _states.Add(new State("TX", "Texas"));
+ _states.Add(new State("UT", "Utah"));
+ _states.Add(new State("VT", "Vermont"));
+ _states.Add(new State("VA", "Virginia"));
+ _states.Add(new State("WA", "Washington"));
+ _states.Add(new State("WV", "West Virginia"));
+ _states.Add(new State("WI", "Wisconsin"));
+ _states.Add(new State("WY", "Wyoming"));
+ }
+
+ public static string[] Abbreviations()
+ {
+ List abbrevList = new List(_states.Count);
+ foreach (var state in _states)
+ {
+ abbrevList.Add(state.Abbreviation);
+ }
+ return abbrevList.ToArray();
+ }
+
+ public static string[] Names()
+ {
+ List nameList = new List(_states.Count);
+ foreach (var state in _states)
+ {
+ nameList.Add(state.Name);
+ }
+ return nameList.ToArray();
+ }
+
+ public static State[] StatesArray()
+ {
+ return _states.ToArray();
+ }
+
+ }
+
+ public class State
+ {
+ public State(string ab, string name)
+ {
+ Name = name;
+ Abbreviation = ab;
+ }
+
+ public string Name { get; set; }
+
+ public string Abbreviation { get; set; }
+
+ public override string ToString()
+ {
+ return string.Format("{0} - {1}", Abbreviation, Name);
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/0-Aquiis.Core/Constants/ApplicationSettings.cs b/0-Aquiis.Core/Constants/ApplicationSettings.cs
new file mode 100644
index 0000000..e15454b
--- /dev/null
+++ b/0-Aquiis.Core/Constants/ApplicationSettings.cs
@@ -0,0 +1,108 @@
+namespace Aquiis.Core.Constants
+{
+ public class ApplicationSettings
+ {
+ public string AppName { get; set; } = string.Empty;
+ public string Version { get; set; } = string.Empty;
+ public string Author { get; set; } = string.Empty;
+ public string Email { get; set; } = string.Empty;
+ public string Repository { get; set; } = string.Empty;
+ public bool SoftDeleteEnabled { get; set; }
+ public string SchemaVersion { get; set; } = "1.0.0";
+ }
+
+ // Property & Tenant Lifecycle Enums
+
+ ///
+ /// Property status in the rental lifecycle
+ ///
+ public enum PropertyStatus
+ {
+ Available, // Ready to market and show
+ ApplicationPending, // One or more applications under review
+ LeasePending, // Application approved, lease offered, awaiting signature
+ Occupied, // Active lease in place
+ UnderRenovation, // Not marketable, undergoing repairs/upgrades
+ OffMarket // Temporarily unavailable
+ }
+
+ ///
+ /// Prospect status through the application journey
+ ///
+ public enum ProspectStatus
+ {
+ Inquiry, // Initial contact/lead
+ Contacted, // Follow-up made
+ TourScheduled, // Tour appointment set
+ Toured, // Tour completed
+ ApplicationSubmitted, // Application submitted, awaiting review
+ UnderReview, // Screening in progress
+ ApplicationApproved, // Approved, lease offer pending
+ ApplicationDenied, // Application rejected
+ LeaseOffered, // Lease document sent for signature
+ LeaseSigned, // Lease accepted and signed
+ LeaseDeclined, // Lease offer declined
+ ConvertedToTenant, // Successfully converted to tenant
+ Inactive // No longer pursuing or expired
+ }
+
+ ///
+ /// Rental application status
+ ///
+ public enum ApplicationStatus
+ {
+ Pending, // Application received, awaiting review
+ UnderReview, // Screening in progress
+ Approved, // Approved for lease
+ Denied, // Application rejected
+ Expired, // Not processed within 30 days
+ Withdrawn // Applicant withdrew
+ }
+
+ ///
+ /// Lease status through its lifecycle
+ ///
+ public enum LeaseStatus
+ {
+ Offered, // Lease generated, awaiting tenant signature
+ Active, // Signed and currently active
+ Expired, // Past end date, not renewed
+ Terminated, // Ended early or declined
+ Renewed, // Superseded by renewal lease
+ MonthToMonth // Converted to month-to-month
+ }
+
+ ///
+ /// Security deposit disposition status
+ ///
+ public enum DepositDispositionStatus
+ {
+ Held, // Currently escrowed
+ PartiallyReturned, // Part returned, part withheld
+ FullyReturned, // Fully returned to tenant
+ Withheld, // Fully withheld for damages/unpaid rent
+ PartiallyWithheld // Same as PartiallyReturned (choose one)
+ }
+
+ ///
+ /// Dividend payment method chosen by tenant
+ ///
+ public enum DividendPaymentMethod
+ {
+ TenantChoice, // Not yet chosen
+ LeaseCredit, // Apply as credit to next invoice
+ Check // Send check to tenant
+ }
+
+ ///
+ /// Dividend payment status
+ ///
+ public enum DividendPaymentStatus
+ {
+ Pending, // Calculated but not yet distributed
+ Applied, // Applied as lease credit
+ CheckIssued, // Check sent to tenant
+ Completed, // Fully processed
+ Forfeited // Tenant did not claim (rare)
+ }
+}
\ No newline at end of file
diff --git a/0-Aquiis.Core/Constants/NotificationConstants.cs b/0-Aquiis.Core/Constants/NotificationConstants.cs
new file mode 100644
index 0000000..6037c1c
--- /dev/null
+++ b/0-Aquiis.Core/Constants/NotificationConstants.cs
@@ -0,0 +1,66 @@
+
+namespace Aquiis.Core.Constants;
+public static class NotificationConstants
+{
+ public static class Types
+ {
+ public const string Info = "Info";
+ public const string Warning = "Warning";
+ public const string Error = "Error";
+ public const string Success = "Success";
+ }
+
+ public static class Categories
+ {
+ public const string Lease = "Lease";
+ public const string Payment = "Payment";
+ public const string Maintenance = "Maintenance";
+ public const string Application = "Application";
+ public const string Property = "Property";
+ public const string Inspection = "Inspection";
+ public const string Document = "Document";
+ public const string System = "System";
+ public const string Security = "Security";
+ public const string Message = "Message";
+ public const string Note = "Note";
+ public const string Report = "Report";
+ public const string CalendarEvent = "Calendar Event";
+ public const string Other = "Other";
+ }
+
+ public static class Templates
+ {
+ // Lease notifications
+ public const string LeaseExpiring90Days = "lease_expiring_90";
+ public const string LeaseExpiring60Days = "lease_expiring_60";
+ public const string LeaseExpiring30Days = "lease_expiring_30";
+ public const string LeaseActivated = "lease_activated";
+ public const string LeaseTerminated = "lease_terminated";
+
+ // Payment notifications
+ public const string PaymentDueReminder = "payment_due_reminder";
+ public const string PaymentReceived = "payment_received";
+ public const string PaymentLate = "payment_late";
+ public const string LateFeeApplied = "late_fee_applied";
+
+ // Maintenance notifications
+ public const string MaintenanceRequestCreated = "maintenance_created";
+ public const string MaintenanceRequestAssigned = "maintenance_assigned";
+ public const string MaintenanceRequestStarted = "maintenance_started";
+ public const string MaintenanceRequestCompleted = "maintenance_completed";
+
+ // Application notifications
+ public const string ApplicationSubmitted = "application_submitted";
+ public const string ApplicationUnderReview = "application_under_review";
+ public const string ApplicationApproved = "application_approved";
+ public const string ApplicationRejected = "application_rejected";
+
+ // Inspection notifications
+ public const string InspectionScheduled = "inspection_scheduled";
+ public const string InspectionCompleted = "inspection_completed";
+
+ // Document notifications
+ public const string DocumentUploaded = "document_uploaded";
+ public const string DocumentExpiring = "document_expiring";
+ }
+}
\ No newline at end of file
diff --git a/0-Aquiis.Core/Entities/ApplicationScreening.cs b/0-Aquiis.Core/Entities/ApplicationScreening.cs
new file mode 100644
index 0000000..1c4a702
--- /dev/null
+++ b/0-Aquiis.Core/Entities/ApplicationScreening.cs
@@ -0,0 +1,68 @@
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+using Aquiis.Core.Validation;
+
+namespace Aquiis.Core.Entities
+{
+ public class ApplicationScreening : BaseModel
+ {
+ [RequiredGuid]
+ [Display(Name = "Organization ID")]
+ public Guid OrganizationId { get; set; } = Guid.Empty;
+
+ [RequiredGuid]
+ [Display(Name = "Rental Application")]
+ public Guid RentalApplicationId { get; set; }
+
+ // Background Check
+ [Display(Name = "Background Check Requested")]
+ public bool BackgroundCheckRequested { get; set; }
+
+ [Display(Name = "Background Check Requested Date")]
+ public DateTime? BackgroundCheckRequestedOn { get; set; }
+
+ [Display(Name = "Background Check Passed")]
+ public bool? BackgroundCheckPassed { get; set; }
+
+ [Display(Name = "Background Check Completed Date")]
+ public DateTime? BackgroundCheckCompletedOn { get; set; }
+
+ [StringLength(1000)]
+ [Display(Name = "Background Check Notes")]
+ public string? BackgroundCheckNotes { get; set; }
+
+ // Credit Check
+ [Display(Name = "Credit Check Requested")]
+ public bool CreditCheckRequested { get; set; }
+
+ [Display(Name = "Credit Check Requested Date")]
+ public DateTime? CreditCheckRequestedOn { get; set; }
+
+ [Display(Name = "Credit Score")]
+ public int? CreditScore { get; set; }
+
+ [Display(Name = "Credit Check Passed")]
+ public bool? CreditCheckPassed { get; set; }
+
+ [Display(Name = "Credit Check Completed Date")]
+ public DateTime? CreditCheckCompletedOn { get; set; }
+
+ [StringLength(1000)]
+ [Display(Name = "Credit Check Notes")]
+ public string? CreditCheckNotes { get; set; }
+
+ // Overall Result
+ [Required]
+ [StringLength(50)]
+ [Display(Name = "Overall Result")]
+ public string OverallResult { get; set; } = string.Empty; // Pending, Passed, Failed, ConditionalPass
+
+ [StringLength(2000)]
+ [Display(Name = "Result Notes")]
+ public string? ResultNotes { get; set; }
+
+ // Navigation properties
+ [ForeignKey(nameof(RentalApplicationId))]
+ public virtual RentalApplication? RentalApplication { get; set; }
+ }
+}
diff --git a/0-Aquiis.Core/Entities/BaseModel.cs b/0-Aquiis.Core/Entities/BaseModel.cs
new file mode 100644
index 0000000..815be2f
--- /dev/null
+++ b/0-Aquiis.Core/Entities/BaseModel.cs
@@ -0,0 +1,43 @@
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+using System.Text.Json.Serialization;
+using Aquiis.Core.Interfaces;
+
+namespace Aquiis.Core.Entities
+{
+ public class BaseModel : IAuditable
+ {
+ [Key]
+ [JsonInclude]
+ [DatabaseGenerated(DatabaseGeneratedOption.None)]
+ public Guid Id { get; set; }
+
+ [Required]
+ [JsonInclude]
+ [DataType(DataType.DateTime)]
+ [Display(Name = "Created On")]
+ public DateTime CreatedOn { get; set; } = DateTime.UtcNow;
+
+ [Required]
+ [JsonInclude]
+ [StringLength(100)]
+ [DataType(DataType.Text)]
+ [Display(Name = "Created By")]
+ public string CreatedBy { get; set; } = string.Empty;
+
+ [JsonInclude]
+ [DataType(DataType.DateTime)]
+ [Display(Name = "Last Modified On")]
+ public DateTime? LastModifiedOn { get; set; }
+
+ [JsonInclude]
+ [StringLength(100)]
+ [DataType(DataType.Text)]
+ [Display(Name = "Last Modified By")]
+ public string? LastModifiedBy { get; set; }
+
+ [JsonInclude]
+ [Display(Name = "Is Deleted?")]
+ public bool IsDeleted { get; set; } = false;
+ }
+}
\ No newline at end of file
diff --git a/0-Aquiis.Core/Entities/CalendarEvent.cs b/0-Aquiis.Core/Entities/CalendarEvent.cs
new file mode 100644
index 0000000..b1c820c
--- /dev/null
+++ b/0-Aquiis.Core/Entities/CalendarEvent.cs
@@ -0,0 +1,78 @@
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+using Aquiis.Core.Validation;
+
+namespace Aquiis.Core.Entities
+{
+ ///
+ /// Represents a calendar event that can be either domain-linked (Tour, Inspection, etc.)
+ /// or a custom user-created event
+ ///
+ public class CalendarEvent : BaseModel
+ {
+ [RequiredGuid]
+ [Display(Name = "Organization ID")]
+ public Guid OrganizationId { get; set; } = Guid.Empty;
+
+ [Required]
+ [StringLength(200)]
+ [Display(Name = "Title")]
+ public string Title { get; set; } = string.Empty;
+
+ [Required]
+ [Display(Name = "Start Date & Time")]
+ public DateTime StartOn { get; set; }
+
+ [Display(Name = "End Date & Time")]
+ public DateTime? EndOn { get; set; }
+
+ [Display(Name = "Duration (Minutes)")]
+ public int DurationMinutes { get; set; }
+
+ [Required]
+ [StringLength(50)]
+ [Display(Name = "Event Type")]
+ public string EventType { get; set; } = string.Empty;
+
+ [StringLength(50)]
+ [Display(Name = "Status")]
+ public string Status { get; set; } = string.Empty;
+
+ [StringLength(2000)]
+ [Display(Name = "Description")]
+ public string? Description { get; set; }
+
+ [Display(Name = "Property")]
+ public Guid? PropertyId { get; set; }
+
+ [StringLength(500)]
+ [Display(Name = "Location")]
+ public string? Location { get; set; }
+
+ [StringLength(20)]
+ [Display(Name = "Color")]
+ public string Color { get; set; } = "#6c757d"; // Default gray
+
+ [StringLength(50)]
+ [Display(Name = "Icon")]
+ public string Icon { get; set; } = "bi-calendar-event";
+
+ // Polymorphic reference to source entity (null for custom events)
+ [Display(Name = "Source Entity ID")]
+ public Guid? SourceEntityId { get; set; }
+
+ [StringLength(100)]
+ [Display(Name = "Source Entity Type")]
+ public string? SourceEntityType { get; set; }
+
+ // Navigation properties
+ [ForeignKey(nameof(PropertyId))]
+ public virtual Property? Property { get; set; }
+
+ ///
+ /// Indicates if this is a custom event (not linked to a domain entity)
+ ///
+ [NotMapped]
+ public bool IsCustomEvent => string.IsNullOrEmpty(SourceEntityType);
+ }
+}
diff --git a/0-Aquiis.Core/Entities/CalendarEventTypes.cs b/0-Aquiis.Core/Entities/CalendarEventTypes.cs
new file mode 100644
index 0000000..6847697
--- /dev/null
+++ b/0-Aquiis.Core/Entities/CalendarEventTypes.cs
@@ -0,0 +1,66 @@
+namespace Aquiis.Core.Entities
+{
+ ///
+ /// Defines calendar event type constants and their visual properties
+ ///
+ public static class CalendarEventTypes
+ {
+ // Event Type Constants
+ public const string Tour = "Tour";
+ public const string Inspection = "Inspection";
+ public const string Maintenance = "Maintenance";
+ public const string LeaseExpiry = "LeaseExpiry";
+ public const string RentDue = "RentDue";
+ public const string Custom = "Custom";
+
+ ///
+ /// Configuration for each event type (color and icon)
+ ///
+ public static readonly Dictionary Config = new()
+ {
+ [Tour] = new EventTypeConfig("#0dcaf0", "bi-calendar-check", "Property Tour"),
+ [Inspection] = new EventTypeConfig("#fd7e14", "bi-clipboard-check", "Property Inspection"),
+ [Maintenance] = new EventTypeConfig("#dc3545", "bi-tools", "Maintenance Request"),
+ [LeaseExpiry] = new EventTypeConfig("#ffc107", "bi-calendar-x", "Lease Expiry"),
+ [RentDue] = new EventTypeConfig("#198754", "bi-cash-coin", "Rent Due"),
+ [Custom] = new EventTypeConfig("#6c757d", "bi-calendar-event", "Custom Event")
+ };
+
+ ///
+ /// Get the color for an event type
+ ///
+ public static string GetColor(string eventType)
+ {
+ return Config.TryGetValue(eventType, out var config) ? config.Color : Config[Custom].Color;
+ }
+
+ ///
+ /// Get the icon for an event type
+ ///
+ public static string GetIcon(string eventType)
+ {
+ return Config.TryGetValue(eventType, out var config) ? config.Icon : Config[Custom].Icon;
+ }
+
+ ///
+ /// Get the display name for an event type
+ ///
+ public static string GetDisplayName(string eventType)
+ {
+ return Config.TryGetValue(eventType, out var config) ? config.DisplayName : eventType;
+ }
+
+ ///
+ /// Get all available event types
+ ///
+ public static List GetAllTypes()
+ {
+ return Config.Keys.ToList();
+ }
+ }
+
+ ///
+ /// Configuration record for event type visual properties
+ ///
+ public record EventTypeConfig(string Color, string Icon, string DisplayName);
+}
diff --git a/0-Aquiis.Core/Entities/CalendarSettings.cs b/0-Aquiis.Core/Entities/CalendarSettings.cs
new file mode 100644
index 0000000..01d76d4
--- /dev/null
+++ b/0-Aquiis.Core/Entities/CalendarSettings.cs
@@ -0,0 +1,17 @@
+using System.ComponentModel.DataAnnotations;
+using Aquiis.Core.Validation;
+
+namespace Aquiis.Core.Entities;
+
+public class CalendarSettings : BaseModel
+{
+ [RequiredGuid]
+ [Display(Name = "Organization ID")]
+ public Guid OrganizationId { get; set; } = Guid.Empty;
+ public string EntityType { get; set; } = string.Empty;
+ public bool AutoCreateEvents { get; set; } = true;
+ public bool ShowOnCalendar { get; set; } = true;
+ public string? DefaultColor { get; set; }
+ public string? DefaultIcon { get; set; }
+ public int DisplayOrder { get; set; }
+}
diff --git a/0-Aquiis.Core/Entities/Checklist.cs b/0-Aquiis.Core/Entities/Checklist.cs
new file mode 100644
index 0000000..ae23e63
--- /dev/null
+++ b/0-Aquiis.Core/Entities/Checklist.cs
@@ -0,0 +1,67 @@
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+using Aquiis.Core.Validation;
+
+namespace Aquiis.Core.Entities
+{
+ public class Checklist : BaseModel
+ {
+ [RequiredGuid]
+ [Display(Name = "Organization ID")]
+ public Guid OrganizationId { get; set; } = Guid.Empty;
+
+ [Display(Name = "Property ID")]
+ public Guid? PropertyId { get; set; }
+
+ [Display(Name = "Lease ID")]
+ public Guid? LeaseId { get; set; }
+
+ [RequiredGuid]
+ [Display(Name = "Checklist Template ID")]
+ public Guid ChecklistTemplateId { get; set; }
+
+ [Required]
+ [StringLength(200)]
+ [Display(Name = "Checklist Name")]
+ public string Name { get; set; } = string.Empty;
+
+ [Required]
+ [StringLength(50)]
+ [Display(Name = "Checklist Type")]
+ public string ChecklistType { get; set; } = string.Empty;
+
+ [Required]
+ [StringLength(50)]
+ [Display(Name = "Status")]
+ public string Status { get; set; } = string.Empty;
+
+ [StringLength(100)]
+ [Display(Name = "Completed By")]
+ public string? CompletedBy { get; set; }
+
+ [Display(Name = "Completed On")]
+ public DateTime? CompletedOn { get; set; }
+
+ [Display(Name = "Document ID")]
+ public Guid? DocumentId { get; set; }
+
+ [StringLength(2000)]
+ [Display(Name = "General Notes")]
+ public string? GeneralNotes { get; set; }
+
+ // Navigation properties
+ [ForeignKey(nameof(PropertyId))]
+ public virtual Property? Property { get; set; }
+
+ [ForeignKey(nameof(LeaseId))]
+ public virtual Lease? Lease { get; set; }
+
+ [ForeignKey(nameof(ChecklistTemplateId))]
+ public virtual ChecklistTemplate? ChecklistTemplate { get; set; }
+
+ [ForeignKey(nameof(DocumentId))]
+ public virtual Document? Document { get; set; }
+
+ public virtual ICollection Items { get; set; } = new List();
+ }
+}
diff --git a/0-Aquiis.Core/Entities/ChecklistItem.cs b/0-Aquiis.Core/Entities/ChecklistItem.cs
new file mode 100644
index 0000000..fbae6bc
--- /dev/null
+++ b/0-Aquiis.Core/Entities/ChecklistItem.cs
@@ -0,0 +1,56 @@
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+using Aquiis.Core.Validation;
+
+namespace Aquiis.Core.Entities
+{
+ public class ChecklistItem : BaseModel
+ {
+
+ [RequiredGuid]
+ [Display(Name = "Organization ID")]
+ public Guid OrganizationId { get; set; } = Guid.Empty;
+
+ [RequiredGuid]
+ [Display(Name = "Checklist ID")]
+ public Guid ChecklistId { get; set; }
+
+ [Required]
+ [StringLength(500)]
+ [Display(Name = "Item Text")]
+ public string ItemText { get; set; } = string.Empty;
+
+ [Required]
+ [Display(Name = "Item Order")]
+ public int ItemOrder { get; set; }
+
+ [StringLength(100)]
+ [Display(Name = "Category Section")]
+ public string? CategorySection { get; set; }
+
+ [Display(Name = "Section Order")]
+ public int SectionOrder { get; set; } = 0;
+
+ [Display(Name = "Requires Value")]
+ public bool RequiresValue { get; set; } = false;
+
+ [StringLength(200)]
+ [Display(Name = "Value")]
+ public string? Value { get; set; }
+
+ [StringLength(1000)]
+ [Display(Name = "Notes")]
+ public string? Notes { get; set; }
+
+ [StringLength(500)]
+ [Display(Name = "Photo URL")]
+ public string? PhotoUrl { get; set; }
+
+ [Display(Name = "Is Checked")]
+ public bool IsChecked { get; set; } = false;
+
+ // Navigation properties
+ [ForeignKey(nameof(ChecklistId))]
+ public virtual Checklist? Checklist { get; set; }
+ }
+}
diff --git a/0-Aquiis.Core/Entities/ChecklistTemplate.cs b/0-Aquiis.Core/Entities/ChecklistTemplate.cs
new file mode 100644
index 0000000..d60e781
--- /dev/null
+++ b/0-Aquiis.Core/Entities/ChecklistTemplate.cs
@@ -0,0 +1,33 @@
+using System.ComponentModel.DataAnnotations;
+using Aquiis.Core.Validation;
+
+namespace Aquiis.Core.Entities
+{
+ public class ChecklistTemplate : BaseModel
+ {
+ [RequiredGuid]
+ [Display(Name = "Organization ID")]
+ public Guid OrganizationId { get; set; } = Guid.Empty;
+
+ [Required]
+ [StringLength(100)]
+ [Display(Name = "Template Name")]
+ public string Name { get; set; } = string.Empty;
+
+ [StringLength(500)]
+ [Display(Name = "Description")]
+ public string? Description { get; set; }
+
+ [Required]
+ [StringLength(50)]
+ [Display(Name = "Category")]
+ public string Category { get; set; } = string.Empty;
+
+ [Display(Name = "Is System Template")]
+ public bool IsSystemTemplate { get; set; } = false;
+
+ // Navigation properties
+ public virtual ICollection Items { get; set; } = new List();
+ public virtual ICollection Checklists { get; set; } = new List();
+ }
+}
diff --git a/0-Aquiis.Core/Entities/ChecklistTemplateItem.cs b/0-Aquiis.Core/Entities/ChecklistTemplateItem.cs
new file mode 100644
index 0000000..6c06555
--- /dev/null
+++ b/0-Aquiis.Core/Entities/ChecklistTemplateItem.cs
@@ -0,0 +1,46 @@
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+using Aquiis.Core.Validation;
+
+namespace Aquiis.Core.Entities
+{
+ public class ChecklistTemplateItem : BaseModel
+ {
+ [RequiredGuid]
+ [Display(Name = "Organization ID")]
+ public Guid OrganizationId { get; set; } = Guid.Empty;
+
+ [RequiredGuid]
+ [Display(Name = "Checklist Template ID")]
+ public Guid ChecklistTemplateId { get; set; }
+
+ [Required]
+ [StringLength(500)]
+ [Display(Name = "Item Text")]
+ public string ItemText { get; set; } = string.Empty;
+
+ [Required]
+ [Display(Name = "Item Order")]
+ public int ItemOrder { get; set; }
+
+ [StringLength(100)]
+ [Display(Name = "Category Section")]
+ public string? CategorySection { get; set; }
+
+ [Display(Name = "Section Order")]
+ public int SectionOrder { get; set; } = 0;
+
+ [Display(Name = "Is Required")]
+ public bool IsRequired { get; set; } = false;
+
+ [Display(Name = "Requires Value")]
+ public bool RequiresValue { get; set; } = false;
+
+ [Display(Name = "Allows Notes")]
+ public bool AllowsNotes { get; set; } = true;
+
+ // Navigation properties
+ [ForeignKey(nameof(ChecklistTemplateId))]
+ public virtual ChecklistTemplate? ChecklistTemplate { get; set; }
+ }
+}
diff --git a/0-Aquiis.Core/Entities/Document.cs b/0-Aquiis.Core/Entities/Document.cs
new file mode 100644
index 0000000..4439541
--- /dev/null
+++ b/0-Aquiis.Core/Entities/Document.cs
@@ -0,0 +1,84 @@
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace Aquiis.Core.Entities {
+
+ public class Document:BaseModel
+ {
+
+ [Required]
+ [StringLength(100)]
+ [Display(Name = "Organization ID")]
+ public Guid OrganizationId { get; set; } = Guid.Empty;
+
+ [Required]
+ [StringLength(255)]
+ public string FileName { get; set; } = string.Empty;
+
+ [Required]
+ [StringLength(10)]
+ public string FileExtension { get; set; } = string.Empty; // .pdf, .jpg, .docx, etc.
+
+ [Required]
+ public byte[] FileData { get; set; } = Array.Empty();
+
+ [StringLength(255)]
+ public string FilePath { get; set; } = string.Empty;
+
+ [StringLength(500)]
+ public string ContentType { get; set; } = string.Empty;
+
+ [Required]
+ [StringLength(100)]
+ public string FileType { get; set; } = string.Empty; // PDF, Image, etc.
+
+ public long FileSize { get; set; }
+
+ [Required]
+ [StringLength(100)]
+ public string DocumentType { get; set; } = string.Empty; // Lease Agreement, Invoice, Receipt, Photo, etc.
+
+ [StringLength(500)]
+ public string Description { get; set; } = string.Empty;
+
+ // Foreign keys - at least one must be set
+ public Guid? PropertyId { get; set; }
+ public Guid? TenantId { get; set; }
+ public Guid? LeaseId { get; set; }
+ public Guid? InvoiceId { get; set; }
+ public Guid? PaymentId { get; set; }
+
+ // Navigation properties
+ [ForeignKey("PropertyId")]
+ public virtual Property? Property { get; set; }
+
+ [ForeignKey("TenantId")]
+ public virtual Tenant? Tenant { get; set; }
+
+ [ForeignKey("LeaseId")]
+ public virtual Lease? Lease { get; set; }
+
+ [ForeignKey("InvoiceId")]
+ public virtual Invoice? Invoice { get; set; }
+
+ [ForeignKey("PaymentId")]
+ public virtual Payment? Payment { get; set; }
+
+ // Computed property
+ public string FileSizeFormatted
+ {
+ get
+ {
+ string[] sizes = { "B", "KB", "MB", "GB" };
+ double len = FileSize;
+ int order = 0;
+ while (len >= 1024 && order < sizes.Length - 1)
+ {
+ order++;
+ len = len / 1024;
+ }
+ return $"{len:0.##} {sizes[order]}";
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/0-Aquiis.Core/Entities/ISchedulableEntity.cs b/0-Aquiis.Core/Entities/ISchedulableEntity.cs
new file mode 100644
index 0000000..d81511a
--- /dev/null
+++ b/0-Aquiis.Core/Entities/ISchedulableEntity.cs
@@ -0,0 +1,64 @@
+namespace Aquiis.Core.Entities
+{
+ ///
+ /// Interface for entities that can be scheduled on the calendar.
+ /// Provides a contract for automatic calendar event creation and synchronization.
+ ///
+ public interface ISchedulableEntity
+ {
+ ///
+ /// Entity ID
+ ///
+ Guid Id { get; set; }
+
+ ///
+ /// Organization ID
+ ///
+ Guid OrganizationId { get; set; }
+
+ ///
+ /// Created By User ID
+ ///
+ string CreatedBy { get; set; }
+
+ ///
+ /// Link to the associated CalendarEvent
+ ///
+ Guid? CalendarEventId { get; set; }
+
+ ///
+ /// Get the title to display on the calendar
+ ///
+ string GetEventTitle();
+
+ ///
+ /// Get the start date/time of the event
+ ///
+ DateTime GetEventStart();
+
+ ///
+ /// Get the duration of the event in minutes
+ ///
+ int GetEventDuration();
+
+ ///
+ /// Get the event type (from CalendarEventTypes constants)
+ ///
+ string GetEventType();
+
+ ///
+ /// Get the associated property ID (if applicable)
+ ///
+ Guid? GetPropertyId();
+
+ ///
+ /// Get the description/details for the event
+ ///
+ string GetEventDescription();
+
+ ///
+ /// Get the current status of the event
+ ///
+ string GetEventStatus();
+ }
+}
diff --git a/0-Aquiis.Core/Entities/IncomeStatement.cs b/0-Aquiis.Core/Entities/IncomeStatement.cs
new file mode 100644
index 0000000..24be02f
--- /dev/null
+++ b/0-Aquiis.Core/Entities/IncomeStatement.cs
@@ -0,0 +1,107 @@
+using System.ComponentModel.DataAnnotations;
+using Aquiis.Core.Validation;
+
+namespace Aquiis.Core.Entities;
+
+///
+/// Income statement for a specific period
+///
+public class IncomeStatement
+{
+ [RequiredGuid]
+ [Display(Name = "Organization ID")]
+ public Guid OrganizationId { get; set; } = Guid.Empty;
+
+ public DateTime StartDate { get; set; }
+ public DateTime EndDate { get; set; }
+ public Guid? PropertyId { get; set; }
+ public string? PropertyName { get; set; }
+
+ // Income
+ public decimal TotalRentIncome { get; set; }
+ public decimal TotalOtherIncome { get; set; }
+ public decimal TotalIncome => TotalRentIncome + TotalOtherIncome;
+
+ // Expenses
+ public decimal MaintenanceExpenses { get; set; }
+ public decimal UtilityExpenses { get; set; }
+ public decimal InsuranceExpenses { get; set; }
+ public decimal TaxExpenses { get; set; }
+ public decimal ManagementFees { get; set; }
+ public decimal OtherExpenses { get; set; }
+ public decimal TotalExpenses => MaintenanceExpenses + UtilityExpenses + InsuranceExpenses +
+ TaxExpenses + ManagementFees + OtherExpenses;
+
+ // Net Income
+ public decimal NetIncome => TotalIncome - TotalExpenses;
+ public decimal ProfitMargin => TotalIncome > 0 ? (NetIncome / TotalIncome) * 100 : 0;
+}
+
+///
+/// Rent roll item showing tenant and payment information
+///
+public class RentRollItem
+{
+ [RequiredGuid]
+ public Guid PropertyId { get; set; }
+ public string PropertyName { get; set; } = string.Empty;
+ public string PropertyAddress { get; set; } = string.Empty;
+ public Guid? TenantId { get; set; }
+ public string? TenantName { get; set; }
+ public string LeaseStatus { get; set; } = string.Empty;
+ public DateTime? LeaseStartDate { get; set; }
+ public DateTime? LeaseEndDate { get; set; }
+ public decimal MonthlyRent { get; set; }
+ public decimal SecurityDeposit { get; set; }
+ public decimal TotalPaid { get; set; }
+ public decimal TotalDue { get; set; }
+ public decimal Balance => TotalDue - TotalPaid;
+ public string PaymentStatus => Balance <= 0 ? "Current" : "Outstanding";
+}
+
+///
+/// Property performance summary
+///
+public class PropertyPerformance
+{
+ [RequiredGuid]
+ public Guid PropertyId { get; set; }
+ public string PropertyName { get; set; } = string.Empty;
+ public string PropertyAddress { get; set; } = string.Empty;
+ public decimal TotalIncome { get; set; }
+ public decimal TotalExpenses { get; set; }
+ public decimal NetIncome => TotalIncome - TotalExpenses;
+ public decimal ROI { get; set; }
+ public int OccupancyDays { get; set; }
+ public int TotalDays { get; set; }
+ public decimal OccupancyRate => TotalDays > 0 ? (decimal)OccupancyDays / TotalDays * 100 : 0;
+}
+
+///
+/// Tax report data
+///
+public class TaxReportData
+{
+ public int Year { get; set; }
+ public Guid? PropertyId { get; set; }
+ public string? PropertyName { get; set; }
+ public decimal TotalRentIncome { get; set; }
+ public decimal TotalExpenses { get; set; }
+ public decimal NetRentalIncome => TotalRentIncome - TotalExpenses;
+ public decimal DepreciationAmount { get; set; }
+ public decimal TaxableIncome => NetRentalIncome - DepreciationAmount;
+
+ // Expense breakdown for Schedule E
+ public decimal Advertising { get; set; }
+ public decimal Auto { get; set; }
+ public decimal Cleaning { get; set; }
+ public decimal Insurance { get; set; }
+ public decimal Legal { get; set; }
+ public decimal Management { get; set; }
+ public decimal MortgageInterest { get; set; }
+ public decimal Repairs { get; set; }
+ public decimal Supplies { get; set; }
+ public decimal Taxes { get; set; }
+ public decimal Utilities { get; set; }
+ public decimal Other { get; set; }
+}
diff --git a/0-Aquiis.Core/Entities/Inspection.cs b/0-Aquiis.Core/Entities/Inspection.cs
new file mode 100644
index 0000000..746d043
--- /dev/null
+++ b/0-Aquiis.Core/Entities/Inspection.cs
@@ -0,0 +1,153 @@
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace Aquiis.Core.Entities
+{
+
+ public class Inspection : BaseModel, ISchedulableEntity
+ {
+ [Required]
+ [StringLength(100)]
+ [Display(Name = "Organization ID")]
+ public Guid OrganizationId { get; set; } = Guid.Empty;
+
+ [Required]
+ public Guid PropertyId { get; set; }
+
+ public Guid? CalendarEventId { get; set; }
+
+ public Guid? LeaseId { get; set; }
+
+ [Required]
+ public DateTime CompletedOn { get; set; } = DateTime.Now;
+
+ [Required]
+ [StringLength(50)]
+ public string InspectionType { get; set; } = "Routine"; // Routine, Move-In, Move-Out, Maintenance
+
+ [StringLength(100)]
+ public string? InspectedBy { get; set; } = string.Empty;
+
+ // Exterior Checklist
+ public bool ExteriorRoofGood { get; set; }
+ public string? ExteriorRoofNotes { get; set; }
+
+ public bool ExteriorGuttersGood { get; set; }
+ public string? ExteriorGuttersNotes { get; set; }
+
+ public bool ExteriorSidingGood { get; set; }
+ public string? ExteriorSidingNotes { get; set; }
+
+ public bool ExteriorWindowsGood { get; set; }
+ public string? ExteriorWindowsNotes { get; set; }
+
+ public bool ExteriorDoorsGood { get; set; }
+ public string? ExteriorDoorsNotes { get; set; }
+
+ public bool ExteriorFoundationGood { get; set; }
+ public string? ExteriorFoundationNotes { get; set; }
+
+ public bool LandscapingGood { get; set; }
+ public string? LandscapingNotes { get; set; }
+
+ // Interior Checklist
+ public bool InteriorWallsGood { get; set; }
+ public string? InteriorWallsNotes { get; set; }
+
+ public bool InteriorCeilingsGood { get; set; }
+ public string? InteriorCeilingsNotes { get; set; }
+
+ public bool InteriorFloorsGood { get; set; }
+ public string? InteriorFloorsNotes { get; set; }
+
+ public bool InteriorDoorsGood { get; set; }
+ public string? InteriorDoorsNotes { get; set; }
+
+ public bool InteriorWindowsGood { get; set; }
+ public string? InteriorWindowsNotes { get; set; }
+
+ // Kitchen
+ public bool KitchenAppliancesGood { get; set; }
+ public string? KitchenAppliancesNotes { get; set; }
+
+ public bool KitchenCabinetsGood { get; set; }
+ public string? KitchenCabinetsNotes { get; set; }
+
+ public bool KitchenCountersGood { get; set; }
+ public string? KitchenCountersNotes { get; set; }
+
+ public bool KitchenSinkPlumbingGood { get; set; }
+ public string? KitchenSinkPlumbingNotes { get; set; }
+
+ // Bathroom
+ public bool BathroomToiletGood { get; set; }
+ public string? BathroomToiletNotes { get; set; }
+
+ public bool BathroomSinkGood { get; set; }
+ public string? BathroomSinkNotes { get; set; }
+
+ public bool BathroomTubShowerGood { get; set; }
+ public string? BathroomTubShowerNotes { get; set; }
+
+ public bool BathroomVentilationGood { get; set; }
+ public string? BathroomVentilationNotes { get; set; }
+
+ // Systems
+ public bool HvacSystemGood { get; set; }
+ public string? HvacSystemNotes { get; set; }
+
+ public bool ElectricalSystemGood { get; set; }
+ public string? ElectricalSystemNotes { get; set; }
+
+ public bool PlumbingSystemGood { get; set; }
+ public string? PlumbingSystemNotes { get; set; }
+
+ public bool SmokeDetectorsGood { get; set; }
+ public string? SmokeDetectorsNotes { get; set; }
+
+ public bool CarbonMonoxideDetectorsGood { get; set; }
+ public string? CarbonMonoxideDetectorsNotes { get; set; }
+
+ // Overall Assessment
+ [Required]
+ [StringLength(20)]
+ public string OverallCondition { get; set; } = "Good"; // Excellent, Good, Fair, Poor
+
+ [StringLength(2000)]
+ public string? GeneralNotes { get; set; }
+
+ [StringLength(2000)]
+ public string? ActionItemsRequired { get; set; }
+
+ // Generated PDF Document
+ public Guid? DocumentId { get; set; }
+
+ // Navigation Properties
+ [ForeignKey("PropertyId")]
+ public Property? Property { get; set; }
+
+ [ForeignKey("LeaseId")]
+ public Lease? Lease { get; set; }
+
+ [ForeignKey("DocumentId")]
+ public Document? Document { get; set; }
+
+ // Audit Fields
+ // SEE BASE MODEL
+
+ // ISchedulableEntity implementation
+ public string GetEventTitle() => $"{InspectionType} Inspection: {Property?.Address ?? "Property"}";
+
+ public DateTime GetEventStart() => CompletedOn;
+
+ public int GetEventDuration() => 60; // Default 1 hour for inspections
+
+ public string GetEventType() => CalendarEventTypes.Inspection;
+
+ public Guid? GetPropertyId() => PropertyId;
+
+ public string GetEventDescription() => $"{InspectionType} - {OverallCondition}";
+
+ public string GetEventStatus() => OverallCondition;
+ }
+}
diff --git a/0-Aquiis.Core/Entities/Invoice.cs b/0-Aquiis.Core/Entities/Invoice.cs
new file mode 100644
index 0000000..ab7da0b
--- /dev/null
+++ b/0-Aquiis.Core/Entities/Invoice.cs
@@ -0,0 +1,78 @@
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace Aquiis.Core.Entities
+{
+ public class Invoice : BaseModel
+ {
+
+ [Required]
+ [StringLength(100)]
+ [Display(Name = "Organization ID")]
+ public Guid OrganizationId { get; set; } = Guid.Empty;
+
+ [Required]
+ public Guid LeaseId { get; set; }
+
+ [Required]
+ [StringLength(50)]
+ public string InvoiceNumber { get; set; } = string.Empty;
+
+ [Required]
+ [DataType(DataType.Date)]
+ public DateTime InvoicedOn { get; set; }
+
+ [Required]
+ [DataType(DataType.Date)]
+ public DateTime DueOn { get; set; }
+
+ [Required]
+ [Column(TypeName = "decimal(18,2)")]
+ public decimal Amount { get; set; }
+
+ [Required]
+ [StringLength(100)]
+ public string Description { get; set; } = string.Empty;
+
+ [StringLength(50)]
+ public string Status { get; set; } = "Pending"; // Pending, Paid, Overdue, Cancelled
+
+ public DateTime? PaidOn { get; set; }
+
+ [Column(TypeName = "decimal(18,2)")]
+ public decimal AmountPaid { get; set; }
+
+ [StringLength(500)]
+ public string Notes { get; set; } = string.Empty;
+
+ // Late Fee Properties
+ [Column(TypeName = "decimal(18,2)")]
+ public decimal? LateFeeAmount { get; set; }
+
+ public bool? LateFeeApplied { get; set; }
+
+ public DateTime? LateFeeAppliedOn { get; set; }
+
+ // Reminder Properties
+ public bool? ReminderSent { get; set; }
+
+ public DateTime? ReminderSentOn { get; set; }
+
+ // Document Tracking
+ public Guid? DocumentId { get; set; }
+
+ // Navigation properties
+ [ForeignKey("LeaseId")]
+ public virtual Lease Lease { get; set; } = null!;
+
+ [ForeignKey("DocumentId")]
+ public virtual Document? Document { get; set; }
+
+ public virtual ICollection Payments { get; set; } = new List();
+
+ // Computed properties
+ public decimal BalanceDue => Amount - AmountPaid;
+ public bool IsOverdue => Status != "Paid" && DueOn < DateTime.Now;
+ public int DaysOverdue => IsOverdue ? (DateTime.Now - DueOn).Days : 0;
+ }
+}
\ No newline at end of file
diff --git a/0-Aquiis.Core/Entities/Lease.cs b/0-Aquiis.Core/Entities/Lease.cs
new file mode 100644
index 0000000..3dc23f3
--- /dev/null
+++ b/0-Aquiis.Core/Entities/Lease.cs
@@ -0,0 +1,113 @@
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+using Aquiis.Core.Validation;
+
+namespace Aquiis.Core.Entities
+{
+
+ public class Lease : BaseModel
+ {
+ [RequiredGuid]
+ [Display(Name = "Organization ID")]
+ public Guid OrganizationId { get; set; } = Guid.Empty;
+
+ [RequiredGuid]
+ public Guid PropertyId { get; set; }
+
+ [RequiredGuid]
+ public Guid TenantId { get; set; }
+
+ // Reference to the lease offer if this lease was created from an accepted offer
+ public Guid? LeaseOfferId { get; set; }
+
+ [Required]
+ [DataType(DataType.Date)]
+ public DateTime StartDate { get; set; }
+
+ [Required]
+ [DataType(DataType.Date)]
+ public DateTime EndDate { get; set; }
+
+ [Required]
+ [Column(TypeName = "decimal(18,2)")]
+ public decimal MonthlyRent { get; set; }
+
+ [Column(TypeName = "decimal(18,2)")]
+ public decimal SecurityDeposit { get; set; }
+
+ [StringLength(50)]
+ public string Status { get; set; } = "Active"; // Active, Pending, Expired, Terminated
+
+ [StringLength(1000)]
+ public string Terms { get; set; } = string.Empty;
+
+ [StringLength(500)]
+ public string Notes { get; set; } = string.Empty;
+
+ // Lease Offer & Acceptance Tracking
+ public DateTime? OfferedOn { get; set; }
+
+ public DateTime? SignedOn { get; set; }
+
+ public DateTime? DeclinedOn { get; set; }
+
+ public DateTime? ExpiresOn { get; set; } // Lease offer expires 30 days from OfferedOn
+
+ // Lease Renewal Tracking
+ public bool? RenewalNotificationSent { get; set; }
+
+ public DateTime? RenewalNotificationSentOn { get; set; }
+
+ public DateTime? RenewalReminderSentOn { get; set; }
+
+ [StringLength(50)]
+ public string? RenewalStatus { get; set; } // NotRequired, Pending, Offered, Accepted, Declined, Expired
+
+ public DateTime? RenewalOfferedOn { get; set; }
+
+ public DateTime? RenewalResponseOn { get; set; }
+
+ [Column(TypeName = "decimal(18,2)")]
+ public decimal? ProposedRenewalRent { get; set; }
+
+ [StringLength(1000)]
+ public string? RenewalNotes { get; set; }
+
+ // Lease Chain Tracking
+ public Guid? PreviousLeaseId { get; set; }
+
+ public int RenewalNumber { get; set; } = 0; // 0 for original, 1 for first renewal, etc.
+
+ // Termination Tracking
+ public DateTime? TerminationNoticedOn { get; set; }
+
+ public DateTime? ExpectedMoveOutDate { get; set; }
+
+ public DateTime? ActualMoveOutDate { get; set; }
+
+ [StringLength(500)]
+ public string? TerminationReason { get; set; }
+
+ // Document Tracking
+ public Guid? DocumentId { get; set; }
+
+ // Navigation properties
+ [ForeignKey("PropertyId")]
+ public virtual Property Property { get; set; } = null!;
+
+ [ForeignKey("TenantId")]
+ public virtual Tenant? Tenant { get; set; }
+
+ [ForeignKey("DocumentId")]
+ public virtual Document? Document { get; set; }
+
+ public virtual ICollection Invoices { get; set; } = new List();
+ public virtual ICollection Documents { get; set; } = new List();
+
+ // Computed properties
+ public bool IsActive => Status == "Active" && DateTime.Now >= StartDate && DateTime.Now <= EndDate;
+ public int DaysRemaining => EndDate > DateTime.Now ? (EndDate - DateTime.Now).Days : 0;
+ public bool IsExpiringSoon => DaysRemaining > 0 && DaysRemaining <= 90;
+ public bool IsExpired => DateTime.Now > EndDate;
+ }
+}
\ No newline at end of file
diff --git a/0-Aquiis.Core/Entities/LeaseOffer.cs b/0-Aquiis.Core/Entities/LeaseOffer.cs
new file mode 100644
index 0000000..05046eb
--- /dev/null
+++ b/0-Aquiis.Core/Entities/LeaseOffer.cs
@@ -0,0 +1,71 @@
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace Aquiis.Core.Entities
+{
+ public class LeaseOffer : BaseModel
+ {
+ [Required]
+ [StringLength(100)]
+ [Display(Name = "Organization ID")]
+ public Guid OrganizationId { get; set; } = Guid.Empty;
+
+ [Required]
+ public Guid RentalApplicationId { get; set; }
+
+ [Required]
+ public Guid PropertyId { get; set; }
+
+ [Required]
+ public Guid ProspectiveTenantId { get; set; }
+
+ [Required]
+ [DataType(DataType.Date)]
+ public DateTime StartDate { get; set; }
+
+ [Required]
+ [DataType(DataType.Date)]
+ public DateTime EndDate { get; set; }
+
+ [Required]
+ [Column(TypeName = "decimal(18,2)")]
+ public decimal MonthlyRent { get; set; }
+
+ [Required]
+ [Column(TypeName = "decimal(18,2)")]
+ public decimal SecurityDeposit { get; set; }
+
+ [Required]
+ [StringLength(2000)]
+ public string Terms { get; set; } = string.Empty;
+
+ [StringLength(1000)]
+ public string Notes { get; set; } = string.Empty;
+
+ [Required]
+ public DateTime OfferedOn { get; set; }
+
+ [Required]
+ public DateTime ExpiresOn { get; set; }
+
+ [StringLength(50)]
+ public string Status { get; set; } = "Pending"; // Pending, Accepted, Declined, Expired, Withdrawn
+
+ public DateTime? RespondedOn { get; set; }
+
+ [StringLength(500)]
+ public string? ResponseNotes { get; set; }
+
+ public Guid? ConvertedLeaseId { get; set; } // Set when offer is accepted and converted to lease
+
+ // Navigation properties
+ [ForeignKey("RentalApplicationId")]
+ public virtual RentalApplication RentalApplication { get; set; } = null!;
+
+ [ForeignKey("PropertyId")]
+ public virtual Property Property { get; set; } = null!;
+
+ [ForeignKey("ProspectiveTenantId")]
+ public virtual ProspectiveTenant ProspectiveTenant { get; set; } = null!;
+ }
+}
diff --git a/0-Aquiis.Core/Entities/MaintenanceRequest.cs b/0-Aquiis.Core/Entities/MaintenanceRequest.cs
new file mode 100644
index 0000000..1d4a508
--- /dev/null
+++ b/0-Aquiis.Core/Entities/MaintenanceRequest.cs
@@ -0,0 +1,145 @@
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+using Aquiis.Core.Validation;
+
+namespace Aquiis.Core.Entities
+{
+ public class MaintenanceRequest : BaseModel, ISchedulableEntity
+ {
+ [RequiredGuid]
+ [Display(Name = "Organization ID")]
+ public Guid OrganizationId { get; set; } = Guid.Empty;
+
+ [RequiredGuid]
+ public Guid PropertyId { get; set; }
+
+ public Guid? CalendarEventId { get; set; }
+
+ public Guid? LeaseId { get; set; }
+
+ [Required]
+ [StringLength(100)]
+ public string Title { get; set; } = string.Empty;
+
+ [Required]
+ [StringLength(2000)]
+ public string Description { get; set; } = string.Empty;
+
+ [Required]
+ [StringLength(50)]
+ public string RequestType { get; set; } = string.Empty; // From ApplicationConstants.MaintenanceRequestTypes
+
+ [Required]
+ [StringLength(20)]
+ public string Priority { get; set; } = "Medium"; // From ApplicationConstants.MaintenanceRequestPriorities
+
+ [Required]
+ [StringLength(20)]
+ public string Status { get; set; } = "Submitted"; // From ApplicationConstants.MaintenanceRequestStatuses
+
+ [StringLength(500)]
+ public string RequestedBy { get; set; } = string.Empty; // Name of person requesting
+
+ [StringLength(100)]
+ public string RequestedByEmail { get; set; } = string.Empty;
+
+ [StringLength(20)]
+ public string RequestedByPhone { get; set; } = string.Empty;
+
+ public DateTime RequestedOn { get; set; } = DateTime.Today;
+
+ public DateTime? ScheduledOn { get; set; }
+
+ public DateTime? CompletedOn { get; set; }
+ [Column(TypeName = "decimal(18,2)")]
+ public decimal EstimatedCost { get; set; }
+
+ [Column(TypeName = "decimal(18,2)")]
+ public decimal ActualCost { get; set; }
+
+ [StringLength(100)]
+ public string AssignedTo { get; set; } = string.Empty; // Contractor or maintenance person
+
+ [StringLength(2000)]
+ public string ResolutionNotes { get; set; } = string.Empty;
+
+ // Navigation properties
+ public virtual Property? Property { get; set; }
+ public virtual Lease? Lease { get; set; }
+
+ // Computed property for days open
+ [NotMapped]
+ public int DaysOpen
+ {
+ get
+ {
+ if (CompletedOn.HasValue)
+ return (CompletedOn.Value.Date - RequestedOn.Date).Days;
+
+ return (DateTime.Today - RequestedOn.Date).Days;
+ }
+ }
+
+ [NotMapped]
+ public bool IsOverdue
+ {
+ get
+ {
+ if (Status == "Completed" || Status == "Cancelled")
+ return false;
+
+ if (!ScheduledOn.HasValue)
+ return false;
+
+ return DateTime.Today > ScheduledOn.Value.Date;
+ }
+ }
+
+ [NotMapped]
+ public string PriorityBadgeClass
+ {
+ get
+ {
+ return Priority switch
+ {
+ "Urgent" => "bg-danger",
+ "High" => "bg-warning",
+ "Medium" => "bg-info",
+ "Low" => "bg-secondary",
+ _ => "bg-secondary"
+ };
+ }
+ }
+
+ [NotMapped]
+ public string StatusBadgeClass
+ {
+ get
+ {
+ return Status switch
+ {
+ "Submitted" => "bg-primary",
+ "In Progress" => "bg-warning",
+ "Completed" => "bg-success",
+ "Cancelled" => "bg-secondary",
+ _ => "bg-secondary"
+ };
+ }
+ }
+
+ // ISchedulableEntity implementation
+ public string GetEventTitle() => $"{RequestType}: {Title}";
+
+ public DateTime GetEventStart() => ScheduledOn ?? RequestedOn;
+
+ public int GetEventDuration() => 120; // Default 2 hours for maintenance
+
+ public string GetEventType() => CalendarEventTypes.Maintenance;
+
+ public Guid? GetPropertyId() => PropertyId;
+
+ public string GetEventDescription() => $"{Property?.Address ?? "Property"} - {Priority} Priority";
+
+ public string GetEventStatus() => Status;
+ }
+}
diff --git a/0-Aquiis.Core/Entities/Note.cs b/0-Aquiis.Core/Entities/Note.cs
new file mode 100644
index 0000000..0575a23
--- /dev/null
+++ b/0-Aquiis.Core/Entities/Note.cs
@@ -0,0 +1,43 @@
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace Aquiis.Core.Entities
+{
+ ///
+ /// Represents a timeline note/comment that can be attached to any entity
+ ///
+ public class Note : BaseModel
+ {
+ [Required]
+ [StringLength(100)]
+ [Display(Name = "Organization ID")]
+ public Guid OrganizationId { get; set; } = Guid.Empty;
+
+ [Required]
+ [StringLength(5000)]
+ [Display(Name = "Content")]
+ public string Content { get; set; } = string.Empty;
+
+ [Required]
+ [StringLength(100)]
+ [Display(Name = "Entity Type")]
+ public string EntityType { get; set; } = string.Empty;
+
+ [Required]
+ [Display(Name = "Entity ID")]
+ public Guid EntityId { get; set; }
+
+ [StringLength(100)]
+ [Display(Name = "User Full Name")]
+ public string? UserFullName { get; set; }
+
+ // CreatedBy (UserId) comes from BaseModel - no navigation property needed in Core
+ // Individual projects can add navigation properties to their ApplicationUser if needed
+
+ // public partial class Note
+ // {
+ // [ForeignKey(nameof(CreatedBy))]
+ // public virtual ApplicationUser? User { get; set; }
+ // }
+ }
+}
diff --git a/0-Aquiis.Core/Entities/Notification.cs b/0-Aquiis.Core/Entities/Notification.cs
new file mode 100644
index 0000000..f24b994
--- /dev/null
+++ b/0-Aquiis.Core/Entities/Notification.cs
@@ -0,0 +1,64 @@
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+using Aquiis.Core.Entities;
+using Aquiis.Core.Validation;
+
+public class Notification : BaseModel
+{
+ [RequiredGuid]
+ public Guid OrganizationId { get; set; }
+
+ [Required]
+ [StringLength(200)]
+ public string Title { get; set; } = string.Empty;
+
+ [Required]
+ [StringLength(2000)]
+ public string Message { get; set; } = string.Empty;
+
+ [Required]
+ [StringLength(50)]
+ public string Type { get; set; } = string.Empty; // Info, Warning, Error, Success
+
+ [Required]
+ [StringLength(50)]
+ public string Category { get; set; } = string.Empty; // Lease, Payment, Maintenance, Application
+
+ [Required]
+ public string RecipientUserId { get; set; } = string.Empty;
+
+ [Required]
+ public DateTime SentOn { get; set; }
+
+ public DateTime? ReadOn { get; set; }
+
+ public bool IsRead { get; set; }
+
+ // Optional entity reference for "view details" link
+ public Guid? RelatedEntityId { get; set; }
+
+ [StringLength(50)]
+ public string? RelatedEntityType { get; set; }
+
+ // Delivery channels
+ public bool SendInApp { get; set; } = true;
+ public bool SendEmail { get; set; }
+ public bool SendSMS { get; set; }
+
+ // Delivery status
+ public bool EmailSent { get; set; }
+ public DateTime? EmailSentOn { get; set; }
+
+ public bool SMSSent { get; set; }
+ public DateTime? SMSSentOn { get; set; }
+
+ [StringLength(500)]
+ public string? EmailError { get; set; }
+
+ [StringLength(500)]
+ public string? SMSError { get; set; }
+
+ // Navigation
+ [ForeignKey(nameof(OrganizationId))]
+ public virtual Organization? Organization { get; set; }
+}
\ No newline at end of file
diff --git a/0-Aquiis.Core/Entities/NotificationPreferences.cs b/0-Aquiis.Core/Entities/NotificationPreferences.cs
new file mode 100644
index 0000000..2dd9626
--- /dev/null
+++ b/0-Aquiis.Core/Entities/NotificationPreferences.cs
@@ -0,0 +1,50 @@
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+using Aquiis.Core.Validation;
+
+namespace Aquiis.Core.Entities;
+public class NotificationPreferences : BaseModel
+{
+ [RequiredGuid]
+ public Guid OrganizationId { get; set; }
+
+ [Required]
+ public string UserId { get; set; } = string.Empty;
+
+ // In-App Notification Preferences
+ public bool EnableInAppNotifications { get; set; } = true;
+
+ // Email Preferences
+ public bool EnableEmailNotifications { get; set; } = true;
+
+ [StringLength(200)]
+ public string? EmailAddress { get; set; }
+
+ public bool EmailLeaseExpiring { get; set; } = true;
+ public bool EmailPaymentDue { get; set; } = true;
+ public bool EmailPaymentReceived { get; set; } = true;
+ public bool EmailApplicationStatusChange { get; set; } = true;
+ public bool EmailMaintenanceUpdate { get; set; } = true;
+ public bool EmailInspectionScheduled { get; set; } = true;
+
+ // SMS Preferences
+ public bool EnableSMSNotifications { get; set; } = false;
+
+ [StringLength(20)]
+ public string? PhoneNumber { get; set; }
+
+ public bool SMSPaymentDue { get; set; } = false;
+ public bool SMSMaintenanceEmergency { get; set; } = true;
+ public bool SMSLeaseExpiringUrgent { get; set; } = false; // 30 days or less
+
+ // Digest Preferences
+ public bool EnableDailyDigest { get; set; } = false;
+ public TimeSpan DailyDigestTime { get; set; } = new TimeSpan(9, 0, 0); // 9 AM
+
+ public bool EnableWeeklyDigest { get; set; } = false;
+ public DayOfWeek WeeklyDigestDay { get; set; } = DayOfWeek.Monday;
+
+ // Navigation
+ [ForeignKey(nameof(OrganizationId))]
+ public virtual Organization? Organization { get; set; }
+}
\ No newline at end of file
diff --git a/0-Aquiis.Core/Entities/OperationResult.cs b/0-Aquiis.Core/Entities/OperationResult.cs
new file mode 100644
index 0000000..d3fa243
--- /dev/null
+++ b/0-Aquiis.Core/Entities/OperationResult.cs
@@ -0,0 +1,24 @@
+namespace Aquiis.Core.Entities
+{
+ public class OperationResult
+ {
+ public bool Success { get; set; }
+ public string Message { get; set; } = string.Empty;
+ public List Errors { get; set; } = new();
+
+ public static OperationResult SuccessResult(string message = "Operation completed successfully")
+ {
+ return new OperationResult { Success = true, Message = message };
+ }
+
+ public static OperationResult FailureResult(string message, List? errors = null)
+ {
+ return new OperationResult
+ {
+ Success = false,
+ Message = message,
+ Errors = errors ?? new List()
+ };
+ }
+ }
+}
\ No newline at end of file
diff --git a/0-Aquiis.Core/Entities/Organization.cs b/0-Aquiis.Core/Entities/Organization.cs
new file mode 100644
index 0000000..d1521e9
--- /dev/null
+++ b/0-Aquiis.Core/Entities/Organization.cs
@@ -0,0 +1,49 @@
+using System.ComponentModel.DataAnnotations;
+using Aquiis.Core.Validation;
+
+namespace Aquiis.Core.Entities
+{
+ public class Organization
+ {
+ [RequiredGuid]
+ [Display(Name = "Organization ID")]
+ public Guid Id { get; set; } = Guid.Empty;
+
+ ///
+ /// UserId of the account owner who created this organization
+ ///
+ public string OwnerId { get; set; } = string.Empty;
+
+ ///
+ /// Full organization name (e.g., "California Properties LLC")
+ ///
+ public string Name { get; set; } = string.Empty;
+
+ ///
+ /// Short display name for UI (e.g., "CA Properties")
+ ///
+ public string? DisplayName { get; set; }
+
+ ///
+ /// US state code (CA, TX, FL, etc.) - determines applicable regulations
+ ///
+ public string? State { get; set; }
+
+ ///
+ /// Active/inactive flag for soft delete
+ ///
+ public bool IsActive { get; set; } = true;
+
+ public string CreatedBy { get; set; } = string.Empty;
+ public DateTime CreatedOn { get; set; } = DateTime.UtcNow;
+ public string? LastModifiedBy { get; set; } = string.Empty;
+ public DateTime? LastModifiedOn { get; set; }
+ public bool IsDeleted { get; set; } = false;
+
+ // Navigation properties
+ public virtual ICollection UserOrganizations { get; set; } = new List();
+ public virtual ICollection Properties { get; set; } = new List();
+ public virtual ICollection Tenants { get; set; } = new List();
+ public virtual ICollection Leases { get; set; } = new List();
+ }
+}
diff --git a/0-Aquiis.Core/Entities/OrganizationEmailSettings.cs b/0-Aquiis.Core/Entities/OrganizationEmailSettings.cs
new file mode 100644
index 0000000..6188ffc
--- /dev/null
+++ b/0-Aquiis.Core/Entities/OrganizationEmailSettings.cs
@@ -0,0 +1,72 @@
+using System;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+using Aquiis.Core.Validation;
+
+namespace Aquiis.Core.Entities
+{
+ ///
+ /// Stores SendGrid email configuration per organization.
+ /// Each organization manages their own SendGrid account.
+ ///
+ public class OrganizationEmailSettings : BaseModel
+ {
+ [RequiredGuid]
+ public Guid OrganizationId { get; set; }
+
+ public string ProviderName { get; set; } = "SMTP";
+
+ public string SmtpServer { get; set; } = string.Empty;
+ public int SmtpPort { get; set; } = 587;
+ public string Username { get; set; } = string.Empty;
+ public string Password { get; set; } = string.Empty;
+ public bool EnableSsl { get; set; } = true;
+
+ // SendGrid Configuration
+ public bool IsEmailEnabled { get; set; }
+
+ ///
+ /// Encrypted SendGrid API key using Data Protection API
+ ///
+ [StringLength(1000)]
+ public string? SendGridApiKeyEncrypted { get; set; }
+
+ [StringLength(200)]
+ [EmailAddress]
+ public string? FromEmail { get; set; }
+
+ [StringLength(200)]
+ public string? FromName { get; set; }
+
+ // Email Usage Tracking (local cache)
+ public int EmailsSentToday { get; set; }
+ public int EmailsSentThisMonth { get; set; }
+ public DateTime? LastEmailSentOn { get; set; }
+ public DateTime? StatsLastUpdatedOn { get; set; }
+ public DateTime? DailyCountResetOn { get; set; }
+ public DateTime? MonthlyCountResetOn { get; set; }
+
+ // SendGrid Account Info (cached from API)
+ public int? DailyLimit { get; set; }
+ public int? MonthlyLimit { get; set; }
+
+ [StringLength(100)]
+ public string? PlanType { get; set; } // Free, Essentials, Pro, etc.
+
+ // Verification Status
+ public bool IsVerified { get; set; }
+ public DateTime? LastVerifiedOn { get; set; }
+
+ ///
+ /// Last error encountered when sending email or verifying API key
+ ///
+ [StringLength(1000)]
+ public string? LastError { get; set; }
+
+ public DateTime? LastErrorOn { get; set; }
+
+ // Navigation
+ [ForeignKey(nameof(OrganizationId))]
+ public virtual Organization? Organization { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/0-Aquiis.Core/Entities/OrganizationSMSSettings.cs b/0-Aquiis.Core/Entities/OrganizationSMSSettings.cs
new file mode 100644
index 0000000..896fed1
--- /dev/null
+++ b/0-Aquiis.Core/Entities/OrganizationSMSSettings.cs
@@ -0,0 +1,68 @@
+using System;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+using Aquiis.Core.Validation;
+
+namespace Aquiis.Core.Entities
+{
+ ///
+ /// Stores Twilio SMS configuration per organization.
+ /// Each organization manages their own Twilio account.
+ ///
+ public class OrganizationSMSSettings : BaseModel
+ {
+ [RequiredGuid]
+ public Guid OrganizationId { get; set; }
+
+ // Twilio Configuration
+ public bool IsSMSEnabled { get; set; }
+
+ [StringLength(100)]
+ public string? ProviderName { get; set; }
+
+ ///
+ /// Encrypted Twilio Account SID using Data Protection API
+ ///
+ [StringLength(1000)]
+ public string? TwilioAccountSidEncrypted { get; set; }
+
+ ///
+ /// Encrypted Twilio Auth Token using Data Protection API
+ ///
+ [StringLength(1000)]
+ public string? TwilioAuthTokenEncrypted { get; set; }
+
+ [StringLength(20)]
+ [Phone]
+ public string? TwilioPhoneNumber { get; set; }
+
+ // SMS Usage Tracking (local cache)
+ public int SMSSentToday { get; set; }
+ public int SMSSentThisMonth { get; set; }
+ public DateTime? LastSMSSentOn { get; set; }
+ public DateTime? StatsLastUpdatedOn { get; set; }
+ public DateTime? DailyCountResetOn { get; set; }
+ public DateTime? MonthlyCountResetOn { get; set; }
+
+ // Twilio Account Info (cached from API)
+ public decimal? AccountBalance { get; set; }
+ public decimal? CostPerSMS { get; set; } // Approximate cost
+
+ [StringLength(100)]
+ public string? AccountType { get; set; } // Trial, Paid
+
+ // Verification Status
+ public bool IsVerified { get; set; }
+ public DateTime? LastVerifiedOn { get; set; }
+
+ ///
+ /// Last error encountered when sending SMS or verifying credentials
+ ///
+ [StringLength(1000)]
+ public string? LastError { get; set; }
+
+ // Navigation
+ [ForeignKey(nameof(OrganizationId))]
+ public virtual Organization? Organization { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/0-Aquiis.Core/Entities/OrganizationSettings.cs b/0-Aquiis.Core/Entities/OrganizationSettings.cs
new file mode 100644
index 0000000..fed6227
--- /dev/null
+++ b/0-Aquiis.Core/Entities/OrganizationSettings.cs
@@ -0,0 +1,130 @@
+using System.ComponentModel;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace Aquiis.Core.Entities
+{
+ ///
+ /// Organization-specific settings for late fees, payment reminders, and other configurable features.
+ /// Each organization can have different policies for their property management operations.
+ ///
+ public class OrganizationSettings : BaseModel
+ {
+
+ [Required]
+ [StringLength(100)]
+ [Display(Name = "Organization ID")]
+ public Guid OrganizationId { get; set; } = Guid.Empty;
+
+ [MaxLength(200)]
+ public string? Name { get; set; }
+
+ #region Late Fee Settings
+
+ [Display(Name = "Enable Late Fees")]
+ public bool LateFeeEnabled { get; set; } = true;
+
+ [Display(Name = "Auto-Apply Late Fees")]
+ public bool LateFeeAutoApply { get; set; } = true;
+
+ [Required]
+ [Range(0, 30)]
+ [Display(Name = "Grace Period (Days)")]
+ public int LateFeeGracePeriodDays { get; set; } = 3;
+
+ [Required]
+ [Range(0, 1)]
+ [Display(Name = "Late Fee Percentage")]
+ public decimal LateFeePercentage { get; set; } = 0.05m;
+
+ [Required]
+ [Range(0, 10000)]
+ [Display(Name = "Maximum Late Fee Amount")]
+ public decimal MaxLateFeeAmount { get; set; } = 50.00m;
+
+ #endregion
+
+ #region Payment Reminder Settings
+
+ [Display(Name = "Enable Payment Reminders")]
+ public bool PaymentReminderEnabled { get; set; } = true;
+
+ [Required]
+ [Range(1, 30)]
+ [Display(Name = "Send Reminder (Days Before Due)")]
+ public int PaymentReminderDaysBefore { get; set; } = 3;
+
+ #endregion
+
+ #region Tour Settings
+
+ [Required]
+ [Range(1, 168)]
+ [Display(Name = "Tour No-Show Grace Period (Hours)")]
+ public int TourNoShowGracePeriodHours { get; set; } = 24;
+
+ #endregion
+
+ #region Application Fee Settings
+
+ [Display(Name = "Enable Application Fees")]
+ public bool ApplicationFeeEnabled { get; set; } = true;
+
+ [Required]
+ [Range(0, 1000)]
+ [Display(Name = "Default Application Fee")]
+ public decimal DefaultApplicationFee { get; set; } = 50.00m;
+
+ [Required]
+ [Range(1, 90)]
+ [Display(Name = "Application Expiration (Days)")]
+ public int ApplicationExpirationDays { get; set; } = 30;
+
+ #endregion
+
+ #region Security Deposit Settings
+
+ [Display(Name = "Enable Security Deposit Investment Pool")]
+ public bool SecurityDepositInvestmentEnabled { get; set; } = true;
+
+ [Required]
+ [Range(0, 1)]
+ [Display(Name = "Organization Share Percentage")]
+ [Column(TypeName = "decimal(18,6)")]
+ public decimal OrganizationSharePercentage { get; set; } = 0.20m; // Default 20%
+
+ [Display(Name = "Auto-Calculate Security Deposit from Rent")]
+ public bool AutoCalculateSecurityDeposit { get; set; } = true;
+
+ [Required]
+ [Range(0.5, 3.0)]
+ [Display(Name = "Security Deposit Multiplier")]
+ [Column(TypeName = "decimal(18,2)")]
+ public decimal SecurityDepositMultiplier { get; set; } = 1.0m; // Default 1x monthly rent
+
+ [Required]
+ [Range(1, 12)]
+ [Display(Name = "Refund Processing Days")]
+ public int RefundProcessingDays { get; set; } = 30; // Days after move-out to process refund
+
+ [Required]
+ [Range(1, 12)]
+ [Display(Name = "Dividend Distribution Month")]
+ public int DividendDistributionMonth { get; set; } = 1; // January = 1
+
+ [Display(Name = "Allow Tenant Choice for Dividend Payment")]
+ public bool AllowTenantDividendChoice { get; set; } = true;
+
+ [Display(Name = "Default Dividend Payment Method")]
+ [StringLength(50)]
+ public string DefaultDividendPaymentMethod { get; set; } = "LeaseCredit"; // LeaseCredit or Check
+
+ #endregion
+
+ // Future settings can be added here as new regions:
+ // - Default lease terms
+ // - Routine inspection intervals
+ // - Document retention policies
+ // - etc.
+ }
+}
diff --git a/0-Aquiis.Core/Entities/Payment.cs b/0-Aquiis.Core/Entities/Payment.cs
new file mode 100644
index 0000000..614580e
--- /dev/null
+++ b/0-Aquiis.Core/Entities/Payment.cs
@@ -0,0 +1,41 @@
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace Aquiis.Core.Entities {
+
+ public class Payment : BaseModel
+ {
+ [Required]
+ [StringLength(100)]
+ [Display(Name = "Organization ID")]
+ public Guid OrganizationId { get; set; } = Guid.Empty;
+
+ [Required]
+ public Guid InvoiceId { get; set; }
+
+ [Required]
+ [DataType(DataType.Date)]
+ public DateTime PaidOn { get; set; }
+
+ [Required]
+ [Column(TypeName = "decimal(18,2)")]
+ public decimal Amount { get; set; }
+
+ [StringLength(50)]
+ public string PaymentMethod { get; set; } = string.Empty; // e.g., Cash, Check, CreditCard, BankTransfer
+
+ [StringLength(1000)]
+ public string Notes { get; set; } = string.Empty;
+
+ // Document Tracking
+ public Guid? DocumentId { get; set; }
+
+ // Navigation properties
+ [ForeignKey("InvoiceId")]
+ public virtual Invoice Invoice { get; set; } = null!;
+
+ [ForeignKey("DocumentId")]
+ public virtual Document? Document { get; set; }
+
+ }
+}
\ No newline at end of file
diff --git a/0-Aquiis.Core/Entities/Property.cs b/0-Aquiis.Core/Entities/Property.cs
new file mode 100644
index 0000000..d6164cb
--- /dev/null
+++ b/0-Aquiis.Core/Entities/Property.cs
@@ -0,0 +1,188 @@
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+using System.Text.Json.Serialization;
+using Aquiis.Core.Constants;
+
+namespace Aquiis.Core.Entities
+{
+ public class Property : BaseModel
+ {
+ [Required]
+ [JsonInclude]
+ [StringLength(100)]
+ [DataType(DataType.Text)]
+ [Display(Name = "Organization ID")]
+ public Guid OrganizationId { get; set; } = Guid.Empty;
+
+ [Required]
+ [JsonInclude]
+ [StringLength(200)]
+ [DataType(DataType.Text)]
+ [Display(Name = "Street Address", Description = "Street address of the property",
+ Prompt = "e.g., 123 Main St", ShortName = "Address")]
+ public string Address { get; set; } = string.Empty;
+
+ [StringLength(50)]
+ [JsonInclude]
+ [DataType(DataType.Text)]
+ [Display(Name = "Unit Number", Description = "Optional unit or apartment number",
+ Prompt = "e.g., Apt 2B, Unit 101", ShortName = "Unit")]
+ public string? UnitNumber { get; set; }
+
+ [StringLength(100)]
+ [JsonInclude]
+ [DataType(DataType.Text)]
+ [Display(Name = "City", Description = "City where the property is located",
+ Prompt = "e.g., Los Angeles, New York, Chicago", ShortName = "City")]
+ public string City { get; set; } = string.Empty;
+
+ [StringLength(50)]
+ [JsonInclude]
+ [DataType(DataType.Text)]
+ [Display(Name = "State", Description = "State or province where the property is located",
+ Prompt = "e.g., CA, NY, TX", ShortName = "State")]
+ public string State { get; set; } = string.Empty;
+
+ [StringLength(10)]
+ [JsonInclude]
+ [RegularExpression(@"^\d{5}(-\d{4})?$", ErrorMessage = "Invalid Zip Code format.")]
+ [DataType(DataType.PostalCode)]
+ [Display(Name = "Postal Code", Description = "Postal code for the property",
+ Prompt = "e.g., 12345 or 12345-6789", ShortName = "ZIP")]
+ [MaxLength(10, ErrorMessage = "Zip Code cannot exceed 10 characters.")]
+ public string ZipCode { get; set; } = string.Empty;
+
+ [Required]
+ [JsonInclude]
+ [StringLength(50)]
+ [DataType(DataType.Text)]
+ [Display(Name = "Property Type", Description = "Type of the property",
+ Prompt = "e.g., House, Apartment, Condo", ShortName = "Type")]
+ public string PropertyType { get; set; } = string.Empty; // House, Apartment, Condo, etc.
+
+ [JsonInclude]
+ [Column(TypeName = "decimal(18,2)")]
+ [DataType(DataType.Currency)]
+ [Display(Name = "Monthly Rent", Description = "Monthly rental amount for the property",
+ Prompt = "e.g., 1200.00", ShortName = "Rent")]
+ public decimal MonthlyRent { get; set; }
+
+ [JsonInclude]
+ [Range(0, int.MaxValue, ErrorMessage = "Bedrooms must be a non-negative number.")]
+ [DataType(DataType.Text)]
+ [Display(Name = "Bedrooms", Description = "Number of Bedrooms",
+ Prompt = "e.g., 3", ShortName = "Beds")]
+ [MaxLength(3, ErrorMessage = "Bedrooms cannot exceed 3 digits.")]
+ public int Bedrooms { get; set; }
+
+
+ [JsonInclude]
+ [Column(TypeName = "decimal(3,1)")]
+ [DataType(DataType.Text)]
+ [MaxLength(3, ErrorMessage = "Bathrooms cannot exceed 3 digits.")]
+ [Display(Name = "Bathrooms", Description = "Number of Bathrooms",
+ Prompt = "e.g., 1.5 for one and a half bathrooms", ShortName = "Baths")]
+ public decimal Bathrooms { get; set; }
+
+
+ [JsonInclude]
+ [Range(0, int.MaxValue, ErrorMessage = "Square Feet must be a non-negative number.")]
+ [DataType(DataType.Text)]
+ [MaxLength(7, ErrorMessage = "Square Feet cannot exceed 7 digits.")]
+ [Display(Name = "Square Feet", Description = "Total square footage of the property",
+ Prompt = "e.g., 1500", ShortName = "Sq. Ft.")]
+ public int SquareFeet { get; set; }
+
+
+ [JsonInclude]
+ [StringLength(1000)]
+ [Display(Name = "Description", Description = "Detailed description of the property",
+ Prompt = "Provide additional details about the property", ShortName = "Desc.")]
+ [DataType(DataType.MultilineText)]
+ [MaxLength(1000, ErrorMessage = "Description cannot exceed 1000 characters.")]
+ public string Description { get; set; } = string.Empty;
+
+ [JsonInclude]
+ [Display(Name = "Is Available?", Description = "Indicates if the property is currently available for lease")]
+ public bool IsAvailable { get; set; } = true;
+
+ [JsonInclude]
+ [StringLength(50)]
+ [Display(Name = "Property Status", Description = "Current status in the rental lifecycle")]
+ public string Status { get; set; } = ApplicationConstants.PropertyStatuses.Available;
+
+ // Inspection tracking
+
+
+ [JsonInclude]
+ public DateTime? LastRoutineInspectionDate { get; set; }
+ [JsonInclude]
+ public DateTime? NextRoutineInspectionDueDate { get; set; }
+ [JsonInclude]
+ public int RoutineInspectionIntervalMonths { get; set; } = 12; // Default to annual inspections
+
+ // Navigation properties
+ public virtual ICollection Leases { get; set; } = new List();
+ public virtual ICollection Documents { get; set; } = new List();
+
+ // Computed property for pending application count
+ [NotMapped]
+ [JsonInclude]
+ public int PendingApplicationCount => 0; // Will be populated when RentalApplications are added
+
+ // Computed property for inspection status
+ [NotMapped]
+ public bool IsInspectionOverdue
+ {
+ get
+ {
+ if (!NextRoutineInspectionDueDate.HasValue)
+ return false;
+
+ return DateTime.Today >= NextRoutineInspectionDueDate.Value.Date;
+ }
+ }
+
+ [NotMapped]
+ public int DaysUntilInspectionDue
+ {
+ get
+ {
+ if (!NextRoutineInspectionDueDate.HasValue)
+ return 0;
+
+ return (NextRoutineInspectionDueDate.Value.Date - DateTime.Today).Days;
+ }
+ }
+
+ [NotMapped]
+ public int DaysOverdue
+ {
+ get
+ {
+ if (!IsInspectionOverdue)
+ return 0;
+
+ return (DateTime.Today - NextRoutineInspectionDueDate!.Value.Date).Days;
+ }
+ }
+
+ [NotMapped]
+ public string InspectionStatus
+ {
+ get
+ {
+ if (!NextRoutineInspectionDueDate.HasValue)
+ return "Not Scheduled";
+
+ if (IsInspectionOverdue)
+ return "Overdue";
+
+ if (DaysUntilInspectionDue <= 30)
+ return "Due Soon";
+
+ return "Scheduled";
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/0-Aquiis.Core/Entities/ProspectiveTenant.cs b/0-Aquiis.Core/Entities/ProspectiveTenant.cs
new file mode 100644
index 0000000..2d72942
--- /dev/null
+++ b/0-Aquiis.Core/Entities/ProspectiveTenant.cs
@@ -0,0 +1,87 @@
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace Aquiis.Core.Entities
+{
+ public class ProspectiveTenant : BaseModel
+ {
+ [Required]
+ [StringLength(100)]
+ [Display(Name = "Organization ID")]
+ public Guid OrganizationId { get; set; } = Guid.Empty;
+
+ [Required]
+ [StringLength(100)]
+ [Display(Name = "First Name")]
+ public string FirstName { get; set; } = string.Empty;
+
+ [Required]
+ [StringLength(100)]
+ [Display(Name = "Last Name")]
+ public string LastName { get; set; } = string.Empty;
+
+ [Required]
+ [StringLength(200)]
+ [EmailAddress]
+ [Display(Name = "Email")]
+ public string Email { get; set; } = string.Empty;
+
+ [Required]
+ [StringLength(20)]
+ [Phone]
+ [Display(Name = "Phone")]
+ public string Phone { get; set; } = string.Empty;
+
+ [DataType(DataType.Date)]
+ [Display(Name = "Date of Birth")]
+ public DateTime? DateOfBirth { get; set; }
+
+ [StringLength(100)]
+ [Display(Name = "Identification Number")]
+ public string? IdentificationNumber { get; set; }
+
+ [StringLength(2)]
+ [Display(Name = "Identification State")]
+ public string? IdentificationState { get; set; }
+
+ [Required]
+ [StringLength(50)]
+ [Display(Name = "Status")]
+ public string Status { get; set; } = string.Empty; // Lead, TourScheduled, Applied, Screening, Approved, Denied, ConvertedToTenant
+
+ [StringLength(100)]
+ [Display(Name = "Source")]
+ public string? Source { get; set; } // Website, Referral, Walk-in, Zillow, etc.
+
+ [StringLength(2000)]
+ [Display(Name = "Notes")]
+ public string? Notes { get; set; }
+
+ [Display(Name = "Interested Property")]
+ public Guid? InterestedPropertyId { get; set; }
+
+ [Display(Name = "Desired Move-In Date")]
+ public DateTime? DesiredMoveInDate { get; set; }
+
+ [Display(Name = "First Contact Date")]
+ public DateTime? FirstContactedOn { get; set; }
+
+
+
+ // Computed Property
+ [NotMapped]
+ public string FullName => $"{FirstName} {LastName}";
+
+ // Navigation properties
+ [ForeignKey(nameof(InterestedPropertyId))]
+ public virtual Property? InterestedProperty { get; set; }
+
+ public virtual ICollection Tours { get; set; } = new List();
+
+ ///
+ /// Collection of all applications submitted by this prospect.
+ /// A prospect may have multiple applications over time, but only one "active" (non-disposed) application.
+ ///
+ public virtual ICollection Applications { get; set; } = new List();
+ }
+}
diff --git a/0-Aquiis.Core/Entities/RentalApplication.cs b/0-Aquiis.Core/Entities/RentalApplication.cs
new file mode 100644
index 0000000..f7feb86
--- /dev/null
+++ b/0-Aquiis.Core/Entities/RentalApplication.cs
@@ -0,0 +1,161 @@
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+using System.Text.Json.Serialization;
+
+namespace Aquiis.Core.Entities
+{
+ public class RentalApplication : BaseModel
+ {
+ [Required]
+ [JsonInclude]
+ [StringLength(100)]
+ [DataType(DataType.Text)]
+ [Display(Name = "Organization ID")]
+ public Guid OrganizationId { get; set; } = Guid.Empty;
+
+ [Required]
+ [Display(Name = "Prospective Tenant")]
+ public Guid ProspectiveTenantId { get; set; }
+
+ [Required]
+ [Display(Name = "Property")]
+ public Guid PropertyId { get; set; }
+
+ [Required]
+ [Display(Name = "Applied On")]
+ public DateTime AppliedOn { get; set; }
+
+ [Required]
+ [StringLength(50)]
+ [Display(Name = "Status")]
+ public string Status { get; set; } = string.Empty; // Submitted, UnderReview, Screening, Approved, Denied
+
+ // Current Address
+ [Required]
+ [StringLength(200)]
+ [Display(Name = "Current Address")]
+ public string CurrentAddress { get; set; } = string.Empty;
+
+ [Required]
+ [StringLength(100)]
+ [Display(Name = "City")]
+ public string CurrentCity { get; set; } = string.Empty;
+
+ [Required]
+ [StringLength(2)]
+ [Display(Name = "State")]
+ public string CurrentState { get; set; } = string.Empty;
+
+ [Required]
+ [StringLength(10)]
+ [Display(Name = "Zip Code")]
+ public string CurrentZipCode { get; set; } = string.Empty;
+
+ [Required]
+ [Display(Name = "Current Rent")]
+ [Column(TypeName = "decimal(18,2)")]
+ public decimal CurrentRent { get; set; }
+
+ [Required]
+ [StringLength(200)]
+ [Display(Name = "Landlord Name")]
+ public string LandlordName { get; set; } = string.Empty;
+
+ [Required]
+ [StringLength(20)]
+ [Phone]
+ [Display(Name = "Landlord Phone")]
+ public string LandlordPhone { get; set; } = string.Empty;
+
+ // Employment
+ [Required]
+ [StringLength(200)]
+ [Display(Name = "Employer Name")]
+ public string EmployerName { get; set; } = string.Empty;
+
+ [Required]
+ [StringLength(100)]
+ [Display(Name = "Job Title")]
+ public string JobTitle { get; set; } = string.Empty;
+
+ [Required]
+ [Display(Name = "Monthly Income")]
+ [Column(TypeName = "decimal(18,2)")]
+ public decimal MonthlyIncome { get; set; }
+
+ [Required]
+ [Display(Name = "Employment Length (Months)")]
+ public int EmploymentLengthMonths { get; set; }
+
+ // References
+ [Required]
+ [StringLength(200)]
+ [Display(Name = "Reference 1 - Name")]
+ public string Reference1Name { get; set; } = string.Empty;
+
+ [Required]
+ [StringLength(20)]
+ [Phone]
+ [Display(Name = "Reference 1 - Phone")]
+ public string Reference1Phone { get; set; } = string.Empty;
+
+ [Required]
+ [StringLength(100)]
+ [Display(Name = "Reference 1 - Relationship")]
+ public string Reference1Relationship { get; set; } = string.Empty;
+
+ [StringLength(200)]
+ [Display(Name = "Reference 2 - Name")]
+ public string? Reference2Name { get; set; }
+
+ [StringLength(20)]
+ [Phone]
+ [Display(Name = "Reference 2 - Phone")]
+ public string? Reference2Phone { get; set; }
+
+ [StringLength(100)]
+ [Display(Name = "Reference 2 - Relationship")]
+ public string? Reference2Relationship { get; set; }
+
+ // Fees
+ [Required]
+ [Display(Name = "Application Fee")]
+ [Column(TypeName = "decimal(18,2)")]
+ public decimal ApplicationFee { get; set; }
+
+ [Display(Name = "Application Fee Paid")]
+ public bool ApplicationFeePaid { get; set; }
+
+ [Display(Name = "Fee Paid On")]
+ public DateTime? ApplicationFeePaidOn { get; set; }
+
+ [StringLength(50)]
+ [Display(Name = "Payment Method")]
+ public string? ApplicationFeePaymentMethod { get; set; }
+
+ [Display(Name = "Expires On")]
+ public DateTime? ExpiresOn { get; set; }
+
+ // Decision
+ [StringLength(1000)]
+ [Display(Name = "Denial Reason")]
+ public string? DenialReason { get; set; }
+
+ [Display(Name = "Decided On")]
+ public DateTime? DecidedOn { get; set; }
+
+ [StringLength(100)]
+ [Display(Name = "Decision By")]
+ public string? DecisionBy { get; set; } // UserId
+
+
+ // Navigation properties
+ [ForeignKey(nameof(ProspectiveTenantId))]
+ public virtual ProspectiveTenant? ProspectiveTenant { get; set; }
+
+ [ForeignKey(nameof(PropertyId))]
+ public virtual Property? Property { get; set; }
+
+ public virtual ApplicationScreening? Screening { get; set; }
+ }
+}
diff --git a/0-Aquiis.Core/Entities/SchemaVersion.cs b/0-Aquiis.Core/Entities/SchemaVersion.cs
new file mode 100644
index 0000000..8470257
--- /dev/null
+++ b/0-Aquiis.Core/Entities/SchemaVersion.cs
@@ -0,0 +1,26 @@
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+using System.Text.Json.Serialization;
+
+namespace Aquiis.Core.Entities
+{
+ ///
+ /// Tracks the database schema version for compatibility validation
+ ///
+ public class SchemaVersion
+ {
+ [Key]
+ [JsonInclude]
+ [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+ public int Id { get; set; }
+
+ [Required]
+ [StringLength(50)]
+ public string Version { get; set; } = string.Empty;
+
+ public DateTime AppliedOn { get; set; } = DateTime.UtcNow;
+
+ [StringLength(500)]
+ public string Description { get; set; } = string.Empty;
+ }
+}
diff --git a/0-Aquiis.Core/Entities/SecurityDeposit.cs b/0-Aquiis.Core/Entities/SecurityDeposit.cs
new file mode 100644
index 0000000..6e645eb
--- /dev/null
+++ b/0-Aquiis.Core/Entities/SecurityDeposit.cs
@@ -0,0 +1,100 @@
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+using System.Text.Json.Serialization;
+
+namespace Aquiis.Core.Entities
+{
+ ///
+ /// Security deposit tracking for each lease with complete lifecycle management.
+ /// Tracks deposit collection, investment pool participation, and refund disposition.
+ ///
+ public class SecurityDeposit : BaseModel
+ {
+ [Required]
+ [JsonInclude]
+ [StringLength(100)]
+ [Display(Name = "Organization ID")]
+ public Guid OrganizationId { get; set; } = Guid.Empty;
+
+ [Required]
+ [JsonInclude]
+ public Guid LeaseId { get; set; }
+
+ [Required]
+ [JsonInclude]
+ public Guid TenantId { get; set; }
+
+ [Required]
+ [Column(TypeName = "decimal(18,2)")]
+ [Range(0.01, double.MaxValue, ErrorMessage = "Deposit amount must be greater than 0")]
+ public decimal Amount { get; set; }
+
+ [Required]
+ public DateTime DateReceived { get; set; } = DateTime.UtcNow;
+
+ [Required]
+ [StringLength(50)]
+ public string PaymentMethod { get; set; } = string.Empty; // Check, Cash, Bank Transfer, etc.
+
+ [StringLength(100)]
+ public string? TransactionReference { get; set; } // Check number, transfer ID, etc.
+
+ [Required]
+ [StringLength(50)]
+ public string Status { get; set; } = "Held"; // Held, Released, Refunded, Forfeited, PartiallyRefunded
+
+ ///
+ /// Tracks whether this deposit is included in the investment pool for dividend calculation.
+ /// Set to true when lease becomes active and deposit is added to pool.
+ ///
+ public bool InInvestmentPool { get; set; } = false;
+
+ ///
+ /// Date when deposit was added to investment pool (typically lease start date).
+ /// Used for pro-rating dividend calculations for mid-year move-ins.
+ ///
+ public DateTime? PoolEntryDate { get; set; }
+
+ ///
+ /// Date when deposit was removed from investment pool (typically lease end date).
+ /// Used to stop dividend accrual.
+ ///
+ public DateTime? PoolExitDate { get; set; }
+
+ // Refund Tracking
+ public DateTime? RefundProcessedDate { get; set; }
+
+ [Column(TypeName = "decimal(18,2)")]
+ public decimal? RefundAmount { get; set; }
+
+ [Column(TypeName = "decimal(18,2)")]
+ public decimal? DeductionsAmount { get; set; }
+
+ [StringLength(1000)]
+ public string? DeductionsReason { get; set; }
+
+ [StringLength(50)]
+ public string? RefundMethod { get; set; } // Check, Bank Transfer, Applied to Balance
+
+ [StringLength(100)]
+ public string? RefundReference { get; set; } // Check number, transfer ID
+
+ [StringLength(500)]
+ public string? Notes { get; set; }
+
+ // Navigation properties
+ [ForeignKey("LeaseId")]
+ public virtual Lease Lease { get; set; } = null!;
+
+ [ForeignKey("TenantId")]
+ public virtual Tenant Tenant { get; set; } = null!;
+
+ public virtual ICollection Dividends { get; set; } = new List();
+
+ // Computed properties
+ public bool IsRefunded => Status == "Refunded" || Status == "PartiallyRefunded";
+ public bool IsActive => Status == "Held" && InInvestmentPool;
+ public decimal TotalDividendsEarned => Dividends.Sum(d => d.DividendAmount);
+ public decimal NetRefundDue => Amount + TotalDividendsEarned - (DeductionsAmount ?? 0);
+ }
+}
diff --git a/0-Aquiis.Core/Entities/SecurityDepositDividend.cs b/0-Aquiis.Core/Entities/SecurityDepositDividend.cs
new file mode 100644
index 0000000..47e418c
--- /dev/null
+++ b/0-Aquiis.Core/Entities/SecurityDepositDividend.cs
@@ -0,0 +1,114 @@
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+using System.Text.Json.Serialization;
+
+namespace Aquiis.Core.Entities
+{
+ ///
+ /// Individual dividend payment tracking for each lease's security deposit.
+ /// Dividends are calculated annually and distributed based on tenant's choice.
+ ///
+ public class SecurityDepositDividend : BaseModel
+ {
+ [Required]
+ [JsonInclude]
+ [StringLength(100)]
+ [Display(Name = "Organization ID")]
+ public Guid OrganizationId { get; set; } = Guid.Empty;
+
+ [Required]
+ public Guid SecurityDepositId { get; set; }
+
+ [Required]
+ public Guid InvestmentPoolId { get; set; }
+
+ [Required]
+ public Guid LeaseId { get; set; }
+
+ [Required]
+ public Guid TenantId { get; set; }
+
+ [Required]
+ public int Year { get; set; }
+
+ ///
+ /// Base dividend amount (TenantShareTotal / ActiveLeaseCount from pool).
+ ///
+ [Required]
+ [Column(TypeName = "decimal(18,2)")]
+ public decimal BaseDividendAmount { get; set; }
+
+ ///
+ /// Pro-ration factor for mid-year move-ins (0.0 to 1.0).
+ /// Example: Moved in July 1 = 0.5 (6 months of 12).
+ ///
+ [Required]
+ [Range(0, 1)]
+ [Column(TypeName = "decimal(18,6)")]
+ public decimal ProrationFactor { get; set; } = 1.0m;
+
+ ///
+ /// Actual dividend amount after pro-ration (BaseDividendAmount * ProrationFactor).
+ /// This is the amount paid to the tenant.
+ ///
+ [Required]
+ [Column(TypeName = "decimal(18,2)")]
+ public decimal DividendAmount { get; set; }
+
+ ///
+ /// Tenant's choice for dividend payment.
+ ///
+ [Required]
+ [StringLength(50)]
+ public string PaymentMethod { get; set; } = "Pending"; // Pending, LeaseCredit, Check
+
+ [Required]
+ [StringLength(50)]
+ public string Status { get; set; } = "Pending"; // Pending, ChoiceMade, Applied, Paid
+
+ ///
+ /// Date when tenant made their payment method choice.
+ ///
+ public DateTime? ChoiceMadeOn { get; set; }
+
+ ///
+ /// Date when dividend was applied as lease credit or check was issued.
+ ///
+ public DateTime? PaymentProcessedOn { get; set; }
+
+ [StringLength(100)]
+ public string? PaymentReference { get; set; } // Check number, invoice ID
+
+ ///
+ /// Mailing address if tenant chose check and has moved out.
+ ///
+ [StringLength(500)]
+ public string? MailingAddress { get; set; }
+
+ ///
+ /// Number of months deposit was in pool during the year (for pro-ration calculation).
+ ///
+ public int MonthsInPool { get; set; } = 12;
+
+ [StringLength(500)]
+ public string? Notes { get; set; }
+
+ // Navigation properties
+ [ForeignKey("SecurityDepositId")]
+ public virtual SecurityDeposit SecurityDeposit { get; set; } = null!;
+
+ [ForeignKey("InvestmentPoolId")]
+ public virtual SecurityDepositInvestmentPool InvestmentPool { get; set; } = null!;
+
+ [ForeignKey("LeaseId")]
+ public virtual Lease Lease { get; set; } = null!;
+
+ [ForeignKey("TenantId")]
+ public virtual Tenant Tenant { get; set; } = null!;
+
+ // Computed properties
+ public bool IsPending => Status == "Pending";
+ public bool IsProcessed => Status == "Applied" || Status == "Paid";
+ public bool TenantHasChosen => !string.IsNullOrEmpty(PaymentMethod) && PaymentMethod != "Pending";
+ }
+}
diff --git a/0-Aquiis.Core/Entities/SecurityDepositInvestmentPool.cs b/0-Aquiis.Core/Entities/SecurityDepositInvestmentPool.cs
new file mode 100644
index 0000000..f5417cb
--- /dev/null
+++ b/0-Aquiis.Core/Entities/SecurityDepositInvestmentPool.cs
@@ -0,0 +1,108 @@
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace Aquiis.Core.Entities
+{
+ ///
+ /// Annual investment pool performance tracking.
+ /// All security deposits are pooled and invested, with annual earnings distributed as dividends.
+ ///
+ public class SecurityDepositInvestmentPool : BaseModel
+ {
+ [Required]
+ public Guid OrganizationId { get; set; } = Guid.Empty;
+
+ [Required]
+ public int Year { get; set; }
+
+ ///
+ /// Total security deposit amount in pool at start of year.
+ ///
+ [Required]
+ [Column(TypeName = "decimal(18,2)")]
+ public decimal StartingBalance { get; set; }
+
+ ///
+ /// Total security deposit amount in pool at end of year.
+ ///
+ [Required]
+ [Column(TypeName = "decimal(18,2)")]
+ public decimal EndingBalance { get; set; }
+
+ ///
+ /// Total investment earnings for the year (can be negative for losses).
+ ///
+ [Required]
+ [Column(TypeName = "decimal(18,2)")]
+ public decimal TotalEarnings { get; set; }
+
+ ///
+ /// Rate of return for the year (as decimal, e.g., 0.05 = 5%).
+ /// Calculated as TotalEarnings / StartingBalance.
+ ///
+ [Column(TypeName = "decimal(18,6)")]
+ public decimal ReturnRate { get; set; }
+
+ ///
+ /// Organization's share percentage (default 20%).
+ /// Configurable per organization via OrganizationSettings.
+ ///
+ [Required]
+ [Range(0, 1)]
+ [Column(TypeName = "decimal(18,6)")]
+ public decimal OrganizationSharePercentage { get; set; } = 0.20m;
+
+ ///
+ /// Amount retained by organization (TotalEarnings * OrganizationSharePercentage).
+ /// Only applies if TotalEarnings > 0 (losses absorbed by organization).
+ ///
+ [Column(TypeName = "decimal(18,2)")]
+ public decimal OrganizationShare { get; set; }
+
+ ///
+ /// Amount available for distribution to tenants (TotalEarnings - OrganizationShare).
+ /// Zero if TotalEarnings <= 0 (no negative dividends).
+ ///
+ [Column(TypeName = "decimal(18,2)")]
+ public decimal TenantShareTotal { get; set; }
+
+ ///
+ /// Number of active leases in the pool for the year.
+ /// Used to calculate per-lease dividend (TenantShareTotal / ActiveLeaseCount).
+ ///
+ [Required]
+ public int ActiveLeaseCount { get; set; }
+
+ ///
+ /// Dividend amount per active lease (TenantShareTotal / ActiveLeaseCount).
+ /// Pro-rated for mid-year move-ins.
+ ///
+ [Column(TypeName = "decimal(18,2)")]
+ public decimal DividendPerLease { get; set; }
+
+ ///
+ /// Date when dividends were calculated.
+ ///
+ public DateTime? DividendsCalculatedOn { get; set; }
+
+ ///
+ /// Date when dividends were distributed to tenants.
+ ///
+ public DateTime? DividendsDistributedOn { get; set; }
+
+ [Required]
+ [StringLength(50)]
+ public string Status { get; set; } = "Open"; // Open, Calculated, Distributed, Closed
+
+ [StringLength(1000)]
+ public string? Notes { get; set; }
+
+ // Navigation properties
+ public virtual ICollection Dividends { get; set; } = new List();
+
+ // Computed properties
+ public bool HasEarnings => TotalEarnings > 0;
+ public bool HasLosses => TotalEarnings < 0;
+ public decimal AbsorbedLosses => TotalEarnings < 0 ? Math.Abs(TotalEarnings) : 0;
+ }
+}
diff --git a/0-Aquiis.Core/Entities/Tenant.cs b/0-Aquiis.Core/Entities/Tenant.cs
new file mode 100644
index 0000000..b71b70c
--- /dev/null
+++ b/0-Aquiis.Core/Entities/Tenant.cs
@@ -0,0 +1,57 @@
+using System.ComponentModel.DataAnnotations;
+using Aquiis.Core.Validation;
+
+namespace Aquiis.Core.Entities {
+
+ public class Tenant : BaseModel
+ {
+
+ [RequiredGuid]
+ public Guid OrganizationId { get; set; } = Guid.Empty;
+
+ [Required]
+ [StringLength(100)]
+ public string FirstName { get; set; } = string.Empty;
+
+ [Required]
+ [StringLength(100)]
+ public string LastName { get; set; } = string.Empty;
+
+ [Required]
+ [StringLength(100)]
+ public string IdentificationNumber { get; set; } = string.Empty;
+
+ [Required]
+ [EmailAddress]
+ [StringLength(255)]
+ public string Email { get; set; } = string.Empty;
+
+ [Phone]
+ [StringLength(20)]
+ public string PhoneNumber { get; set; } = string.Empty;
+
+ [DataType(DataType.Date)]
+ public DateTime? DateOfBirth { get; set; }
+
+ public bool IsActive { get; set; } = true;
+
+ [StringLength(200)]
+ public string EmergencyContactName { get; set; } = string.Empty;
+
+ [Phone]
+ [StringLength(20)]
+ public string? EmergencyContactPhone { get; set; }
+
+ [StringLength(500)]
+ public string Notes { get; set; } = string.Empty;
+
+ // Link back to prospect for audit trail
+ public Guid? ProspectiveTenantId { get; set; }
+
+ // Navigation properties
+ public virtual ICollection Leases { get; set; } = new List();
+
+ // Computed property
+ public string FullName => $"{FirstName} {LastName}";
+ }
+}
\ No newline at end of file
diff --git a/0-Aquiis.Core/Entities/Tour.cs b/0-Aquiis.Core/Entities/Tour.cs
new file mode 100644
index 0000000..948f72d
--- /dev/null
+++ b/0-Aquiis.Core/Entities/Tour.cs
@@ -0,0 +1,75 @@
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+using Aquiis.Core.Validation;
+
+namespace Aquiis.Core.Entities
+{
+ public class Tour : BaseModel, ISchedulableEntity
+ {
+ [RequiredGuid]
+ [Display(Name = "Organization ID")]
+ public Guid OrganizationId { get; set; } = Guid.Empty;
+
+ [RequiredGuid]
+ [Display(Name = "Prospective Tenant")]
+ public Guid ProspectiveTenantId { get; set; }
+
+ [RequiredGuid]
+ [Display(Name = "Property")]
+ public Guid PropertyId { get; set; }
+
+ [Required]
+ [Display(Name = "Scheduled Date & Time")]
+ public DateTime ScheduledOn { get; set; }
+
+ [Display(Name = "Duration (Minutes)")]
+ public int DurationMinutes { get; set; }
+
+ [StringLength(50)]
+ [Display(Name = "Status")]
+ public string Status { get; set; } = string.Empty; // Scheduled, Completed, Cancelled, NoShow
+
+ [StringLength(2000)]
+ [Display(Name = "Feedback")]
+ public string? Feedback { get; set; }
+
+ [StringLength(50)]
+ [Display(Name = "Interest Level")]
+ public string? InterestLevel { get; set; } // VeryInterested, Interested, Neutral, NotInterested
+
+ [StringLength(100)]
+ [Display(Name = "Conducted By")]
+ public string? ConductedBy { get; set; } = string.Empty; // UserId of property manager
+
+ [Display(Name = "Property Tour Checklist")]
+ public Guid? ChecklistId { get; set; } // Links to property tour checklist
+
+ [Display(Name = "Calendar Event")]
+ public Guid? CalendarEventId { get; set; }
+
+ // Navigation properties
+ [ForeignKey(nameof(ProspectiveTenantId))]
+ public virtual ProspectiveTenant? ProspectiveTenant { get; set; }
+
+ [ForeignKey(nameof(PropertyId))]
+ public virtual Property? Property { get; set; }
+
+ [ForeignKey(nameof(ChecklistId))]
+ public virtual Checklist? Checklist { get; set; }
+
+ // ISchedulableEntity implementation
+ public string GetEventTitle() => $"Tour: {ProspectiveTenant?.FullName ?? "Prospect"}";
+
+ public DateTime GetEventStart() => ScheduledOn;
+
+ public int GetEventDuration() => DurationMinutes;
+
+ public string GetEventType() => CalendarEventTypes.Tour;
+
+ public Guid? GetPropertyId() => PropertyId;
+
+ public string GetEventDescription() => Property?.Address ?? string.Empty;
+
+ public string GetEventStatus() => Status;
+ }
+}
diff --git a/0-Aquiis.Core/Entities/UserOrganization.cs b/0-Aquiis.Core/Entities/UserOrganization.cs
new file mode 100644
index 0000000..9789e98
--- /dev/null
+++ b/0-Aquiis.Core/Entities/UserOrganization.cs
@@ -0,0 +1,67 @@
+
+
+using System.ComponentModel.DataAnnotations;
+using Aquiis.Core.Validation;
+
+namespace Aquiis.Core.Entities
+{
+ ///
+ /// Junction table for multi-organization user assignments with role-based permissions
+ ///
+ public class UserOrganization
+ {
+
+ [RequiredGuid]
+ [Display(Name = "UserOrganization ID")]
+ public Guid Id { get; set; } = Guid.NewGuid();
+
+ ///
+ /// The user being granted access
+ ///
+ public string UserId { get; set; } = string.Empty;
+
+ ///
+ /// The organization they're being granted access to
+ ///
+ [RequiredGuid]
+ public Guid OrganizationId { get; set; } = Guid.Empty;
+
+ ///
+ /// Role within this organization: "Owner", "Administrator", "PropertyManager", "User"
+ ///
+ public string Role { get; set; } = string.Empty;
+
+ ///
+ /// UserId of the user who granted this access
+ ///
+ public string GrantedBy { get; set; } = string.Empty;
+
+ ///
+ /// When access was granted
+ ///
+ public DateTime GrantedOn { get; set; }
+
+ ///
+ /// When access was revoked (NULL if still active)
+ ///
+ public DateTime? RevokedOn { get; set; }
+
+ ///
+ /// Active assignment flag
+ ///
+ public bool IsActive { get; set; } = true;
+
+ public string CreatedBy { get; set; } = string.Empty;
+
+ public DateTime CreatedOn { get; set; } = DateTime.UtcNow;
+
+ public string? LastModifiedBy { get; set; } = string.Empty;
+
+ public DateTime? LastModifiedOn { get; set; }
+
+ public bool IsDeleted { get; set; } = false;
+
+ // Navigation properties
+ public virtual Organization Organization { get; set; } = null!;
+ }
+}
diff --git a/0-Aquiis.Core/Entities/WorkflowAuditLog.cs b/0-Aquiis.Core/Entities/WorkflowAuditLog.cs
new file mode 100644
index 0000000..0bf156f
--- /dev/null
+++ b/0-Aquiis.Core/Entities/WorkflowAuditLog.cs
@@ -0,0 +1,57 @@
+
+namespace Aquiis.Core.Entities
+{
+ ///
+ /// Audit log for workflow state transitions.
+ /// Tracks all status changes with context and timestamp.
+ ///
+ public class WorkflowAuditLog : BaseModel
+ {
+ ///
+ /// Type of entity (Application, Lease, MaintenanceRequest, etc.)
+ ///
+ public required string EntityType { get; set; }
+
+ ///
+ /// ID of the entity that transitioned
+ ///
+ public required Guid EntityId { get; set; }
+ ///
+ public string? FromStatus { get; set; }
+
+ ///
+ /// New status after transition
+ ///
+ public required string ToStatus { get; set; }
+
+ ///
+ /// Action that triggered the transition (e.g., "Submit", "Approve", "Deny")
+ ///
+ public required string Action { get; set; }
+
+ ///
+ /// Optional reason/notes for the transition
+ ///
+ public string? Reason { get; set; }
+
+ ///
+ /// User who performed the action (from UserContextService)
+ ///
+ public required string PerformedBy { get; set; }
+
+ ///
+ /// When the action occurred
+ ///
+ public required DateTime PerformedOn { get; set; }
+
+ ///
+ /// Organization context for the workflow action
+ ///
+ public required Guid OrganizationId { get; set; }
+
+ ///
+ /// Additional context data (JSON serialized)
+ ///
+ public string? Metadata { get; set; }
+ }
+}
diff --git a/0-Aquiis.Core/Interfaces/IApplicationDbContext.cs b/0-Aquiis.Core/Interfaces/IApplicationDbContext.cs
new file mode 100644
index 0000000..7938a99
--- /dev/null
+++ b/0-Aquiis.Core/Interfaces/IApplicationDbContext.cs
@@ -0,0 +1,13 @@
+namespace Aquiis.Core.Interfaces;
+
+///
+/// Platform-agnostic interface for the application's database context.
+/// This allows the Application layer to reference the DbContext without knowing Infrastructure details.
+///
+public interface IApplicationDbContext
+{
+ ///
+ /// Saves all changes made in this context to the database.
+ ///
+ Task SaveChangesAsync(CancellationToken cancellationToken = default);
+}
diff --git a/0-Aquiis.Core/Interfaces/IAuditable.cs b/0-Aquiis.Core/Interfaces/IAuditable.cs
new file mode 100644
index 0000000..531305f
--- /dev/null
+++ b/0-Aquiis.Core/Interfaces/IAuditable.cs
@@ -0,0 +1,32 @@
+using System.ComponentModel.DataAnnotations;
+
+namespace Aquiis.Core.Interfaces
+{
+ ///
+ /// Interface for entities that track audit information (creation and modification).
+ /// Entities implementing this interface will have their audit fields automatically
+ /// managed by the BaseService during create and update operations.
+ ///
+ public interface IAuditable
+ {
+ ///
+ /// Date and time when the entity was created (UTC).
+ ///
+ DateTime CreatedOn { get; set; }
+
+ ///
+ /// User ID of the user who created the entity.
+ ///
+ string CreatedBy { get; set; }
+
+ ///
+ /// Date and time when the entity was last modified (UTC).
+ ///
+ DateTime? LastModifiedOn { get; set; }
+
+ ///
+ /// User ID of the user who last modified the entity.
+ ///
+ string? LastModifiedBy { get; set; }
+ }
+}
diff --git a/0-Aquiis.Core/Interfaces/ICalendarEventService.cs b/0-Aquiis.Core/Interfaces/ICalendarEventService.cs
new file mode 100644
index 0000000..d2d4af6
--- /dev/null
+++ b/0-Aquiis.Core/Interfaces/ICalendarEventService.cs
@@ -0,0 +1,21 @@
+using Aquiis.Core.Entities;
+
+namespace Aquiis.Core.Interfaces
+{
+ ///
+ /// Service interface for managing calendar events and synchronizing with schedulable entities
+ ///
+ public interface ICalendarEventService
+ {
+ ///
+ /// Create or update a calendar event from a schedulable entity
+ ///
+ Task CreateOrUpdateEventAsync(T entity)
+ where T : BaseModel, ISchedulableEntity;
+
+ ///
+ /// Delete a calendar event
+ ///
+ Task DeleteEventAsync(Guid? calendarEventId);
+ }
+}
diff --git a/0-Aquiis.Core/Interfaces/IPathService.cs b/0-Aquiis.Core/Interfaces/IPathService.cs
new file mode 100644
index 0000000..f2bc1a6
--- /dev/null
+++ b/0-Aquiis.Core/Interfaces/IPathService.cs
@@ -0,0 +1,28 @@
+namespace Aquiis.Core.Interfaces;
+
+///
+/// Platform-agnostic interface for managing application paths and connection strings.
+/// Implementations exist for Electron, Web, and future mobile platforms.
+///
+public interface IPathService
+{
+ ///
+ /// Gets the connection string for the database, using platform-specific paths.
+ ///
+ Task GetConnectionStringAsync(object configuration);
+
+ ///
+ /// Gets the platform-specific database file path.
+ ///
+ Task GetDatabasePathAsync();
+
+ ///
+ /// Gets the platform-specific user data directory path.
+ ///
+ Task GetUserDataPathAsync();
+
+ ///
+ /// Checks if the application is running in the current platform context.
+ ///
+ bool IsActive { get; }
+}
diff --git a/0-Aquiis.Core/Interfaces/Services/IDatabaseService.cs b/0-Aquiis.Core/Interfaces/Services/IDatabaseService.cs
new file mode 100644
index 0000000..380c5fd
--- /dev/null
+++ b/0-Aquiis.Core/Interfaces/Services/IDatabaseService.cs
@@ -0,0 +1,28 @@
+namespace Aquiis.Core.Interfaces.Services;
+
+///
+/// Service for database initialization and management operations.
+/// Abstracts database access from product layers.
+///
+public interface IDatabaseService
+{
+ ///
+ /// Initialize database (apply pending migrations for both business and identity contexts)
+ ///
+ Task InitializeAsync();
+
+ ///
+ /// Check if database can be connected to
+ ///
+ Task CanConnectAsync();
+
+ ///
+ /// Get count of pending migrations for business context
+ ///
+ Task GetPendingMigrationsCountAsync();
+
+ ///
+ /// Get count of pending migrations for identity context
+ ///
+ Task GetIdentityPendingMigrationsCountAsync();
+}
diff --git a/0-Aquiis.Core/Interfaces/Services/IEmailService.cs b/0-Aquiis.Core/Interfaces/Services/IEmailService.cs
new file mode 100644
index 0000000..f5169cd
--- /dev/null
+++ b/0-Aquiis.Core/Interfaces/Services/IEmailService.cs
@@ -0,0 +1,9 @@
+
+namespace Aquiis.Core.Interfaces.Services;
+public interface IEmailService
+{
+ Task SendEmailAsync(string to, string subject, string body);
+ Task SendEmailAsync(string to, string subject, string body, string? fromName = null);
+ Task SendTemplateEmailAsync(string to, string templateId, Dictionary templateData);
+ Task ValidateEmailAddressAsync(string email);
+}
\ No newline at end of file
diff --git a/0-Aquiis.Core/Interfaces/Services/ISMSService.cs b/0-Aquiis.Core/Interfaces/Services/ISMSService.cs
new file mode 100644
index 0000000..bea70cf
--- /dev/null
+++ b/0-Aquiis.Core/Interfaces/Services/ISMSService.cs
@@ -0,0 +1,7 @@
+
+namespace Aquiis.Core.Interfaces.Services;
+public interface ISMSService
+{
+ Task SendSMSAsync(string phoneNumber, string message);
+ Task ValidatePhoneNumberAsync(string phoneNumber);
+}
\ No newline at end of file
diff --git a/0-Aquiis.Core/Interfaces/Services/IUserContextService.cs b/0-Aquiis.Core/Interfaces/Services/IUserContextService.cs
new file mode 100644
index 0000000..78ad844
--- /dev/null
+++ b/0-Aquiis.Core/Interfaces/Services/IUserContextService.cs
@@ -0,0 +1,33 @@
+namespace Aquiis.Core.Interfaces.Services;
+
+///
+/// Service interface for accessing user context information.
+/// Implementations are project-specific and depend on ApplicationDbContext.
+///
+public interface IUserContextService
+{
+ ///
+ /// Gets the current authenticated user's ID.
+ ///
+ Task GetUserIdAsync();
+
+ ///
+ /// Gets the current user's active organization ID.
+ ///
+ Task GetActiveOrganizationIdAsync();
+
+ ///
+ /// Gets the current authenticated user's full name.
+ ///
+ Task GetUserNameAsync();
+
+ ///
+ /// Gets the current user's email address.
+ ///
+ Task GetUserEmailAsync();
+
+ ///
+ /// Gets the current user's OrganizationId (DEPRECATED: Use GetActiveOrganizationIdAsync).
+ ///
+ Task GetOrganizationIdAsync();
+}
\ No newline at end of file
diff --git a/0-Aquiis.Core/Utilities/CalendarEventRouter.cs b/0-Aquiis.Core/Utilities/CalendarEventRouter.cs
new file mode 100644
index 0000000..05050c5
--- /dev/null
+++ b/0-Aquiis.Core/Utilities/CalendarEventRouter.cs
@@ -0,0 +1,59 @@
+using Aquiis.Core.Entities;
+
+namespace Aquiis.Core.Utilities
+{
+ ///
+ /// Helper class for routing calendar event clicks to appropriate detail pages
+ ///
+ public static class CalendarEventRouter
+ {
+ ///
+ /// Get the route URL for a calendar event based on its source entity type
+ ///
+ /// The calendar event
+ /// The route URL or null if it's a custom event or routing not available
+ public static string? GetRouteForEvent(CalendarEvent evt)
+ {
+ if (!evt.SourceEntityId.HasValue || string.IsNullOrEmpty(evt.SourceEntityType))
+ return null;
+
+ return evt.SourceEntityType switch
+ {
+ nameof(Tour) => $"/PropertyManagement/Tours/Details/{evt.SourceEntityId}",
+ nameof(Inspection) => $"/PropertyManagement/Inspections/View/{evt.SourceEntityId}",
+ nameof(MaintenanceRequest) => $"/PropertyManagement/Maintenance/View/{evt.SourceEntityId}",
+ // Add new schedulable entity routes here as they are created
+ _ => null
+ };
+ }
+
+ ///
+ /// Check if an event is routable (has a valid source entity and route)
+ ///
+ /// The calendar event
+ /// True if the event can be routed to a detail page
+ public static bool IsRoutable(CalendarEvent evt)
+ {
+ return !string.IsNullOrEmpty(GetRouteForEvent(evt));
+ }
+
+ ///
+ /// Get a display label for the event type
+ ///
+ /// The calendar event
+ /// User-friendly label for the event source
+ public static string GetSourceLabel(CalendarEvent evt)
+ {
+ if (evt.IsCustomEvent)
+ return "Custom Event";
+
+ return evt.SourceEntityType switch
+ {
+ nameof(Tour) => "Property Tour",
+ nameof(Inspection) => "Property Inspection",
+ nameof(MaintenanceRequest) => "Maintenance Request",
+ _ => evt.EventType
+ };
+ }
+ }
+}
diff --git a/0-Aquiis.Core/Utilities/SchedulableEntityRegistry.cs b/0-Aquiis.Core/Utilities/SchedulableEntityRegistry.cs
new file mode 100644
index 0000000..7c84f8a
--- /dev/null
+++ b/0-Aquiis.Core/Utilities/SchedulableEntityRegistry.cs
@@ -0,0 +1,87 @@
+using System.Reflection;
+using Aquiis.Core.Entities;
+
+namespace Aquiis.Core.Utilities;
+
+public static class SchedulableEntityRegistry
+{
+ private static List? _entityTypes;
+ private static Dictionary? _entityTypeMap;
+
+ public static List GetSchedulableEntityTypes()
+ {
+ if (_entityTypes == null)
+ {
+ _entityTypes = Assembly.GetExecutingAssembly()
+ .GetTypes()
+ .Where(t => typeof(ISchedulableEntity).IsAssignableFrom(t)
+ && t.IsClass && !t.IsAbstract)
+ .ToList();
+ }
+ return _entityTypes;
+ }
+
+ public static List GetEntityTypeNames()
+ {
+ var types = GetSchedulableEntityTypes();
+ var names = new List();
+
+ foreach (var type in types)
+ {
+ try
+ {
+ // Create a temporary instance to get the event type name
+ var instance = Activator.CreateInstance(type) as ISchedulableEntity;
+ if (instance != null)
+ {
+ var eventType = instance.GetEventType();
+ if (!string.IsNullOrEmpty(eventType) && !names.Contains(eventType))
+ {
+ names.Add(eventType);
+ }
+ }
+ }
+ catch
+ {
+ // If instantiation fails, use the class name as fallback
+ if (!names.Contains(type.Name))
+ {
+ names.Add(type.Name);
+ }
+ }
+ }
+
+ return names;
+ }
+
+ public static Dictionary GetEntityTypeMap()
+ {
+ if (_entityTypeMap == null)
+ {
+ _entityTypeMap = new Dictionary();
+ var types = GetSchedulableEntityTypes();
+
+ foreach (var type in types)
+ {
+ try
+ {
+ var instance = Activator.CreateInstance(type) as ISchedulableEntity;
+ if (instance != null)
+ {
+ var eventType = instance.GetEventType();
+ if (!string.IsNullOrEmpty(eventType) && !_entityTypeMap.ContainsKey(eventType))
+ {
+ _entityTypeMap[eventType] = type;
+ }
+ }
+ }
+ catch
+ {
+ // Skip types that can't be instantiated
+ }
+ }
+ }
+
+ return _entityTypeMap;
+ }
+}
diff --git a/0-Aquiis.Core/Validation/OptionalGuidAttribute.cs b/0-Aquiis.Core/Validation/OptionalGuidAttribute.cs
new file mode 100644
index 0000000..c6d3bef
--- /dev/null
+++ b/0-Aquiis.Core/Validation/OptionalGuidAttribute.cs
@@ -0,0 +1,73 @@
+using System.ComponentModel.DataAnnotations;
+
+namespace Aquiis.Core.Validation;
+
+///
+/// Validates that an optional Guid property, if provided, is not Guid.Empty.
+/// Use this for Guid? properties where null is acceptable but Guid.Empty is not.
+///
+/// Example: LeaseId on MaintenanceRequest - can be null (no lease yet) but shouldn't be Guid.Empty (invalid reference)
+///
+[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, AllowMultiple = false)]
+public class OptionalGuidAttribute : ValidationAttribute
+{
+ ///
+ /// Initializes a new instance of OptionalGuidAttribute with a default error message.
+ ///
+ public OptionalGuidAttribute()
+ : base("The {0} field cannot be empty if provided. Either leave it null or provide a valid value.")
+ {
+ }
+
+ ///
+ /// Initializes a new instance of OptionalGuidAttribute with a custom error message.
+ ///
+ /// The error message to display when validation fails.
+ public OptionalGuidAttribute(string errorMessage)
+ : base(errorMessage)
+ {
+ }
+
+ ///
+ /// Validates that if the value is not null, it must not be Guid.Empty.
+ ///
+ protected override ValidationResult? IsValid(object? value, ValidationContext validationContext)
+ {
+ // Null is acceptable for optional fields
+ if (value == null)
+ {
+ return ValidationResult.Success;
+ }
+
+ // Type check
+ if (value is not Guid guidValue)
+ {
+ return new ValidationResult(
+ $"The {validationContext.DisplayName} field must be a valid Guid or null.",
+ new[] { validationContext.MemberName ?? string.Empty }
+ );
+ }
+
+ // Reject Guid.Empty (if you provide a value, it must be real)
+ if (guidValue == Guid.Empty)
+ {
+ return new ValidationResult(
+ FormatErrorMessage(validationContext.DisplayName),
+ new[] { validationContext.MemberName ?? string.Empty }
+ );
+ }
+
+ return ValidationResult.Success;
+ }
+
+ public override bool IsValid(object? value)
+ {
+ if (value == null)
+ return true;
+
+ if (value is not Guid guidValue)
+ return false;
+
+ return guidValue != Guid.Empty;
+ }
+}
diff --git a/0-Aquiis.Core/Validation/RequiredGuidAttribute.cs b/0-Aquiis.Core/Validation/RequiredGuidAttribute.cs
new file mode 100644
index 0000000..f91835e
--- /dev/null
+++ b/0-Aquiis.Core/Validation/RequiredGuidAttribute.cs
@@ -0,0 +1,83 @@
+using System.ComponentModel.DataAnnotations;
+
+namespace Aquiis.Core.Validation;
+
+///
+/// Validates that a Guid property has a value other than Guid.Empty.
+/// Use this instead of [Required] for non-nullable Guid properties.
+///
+/// Note: For nullable Guid? properties, use [Required] to check for null,
+/// and optionally combine with [RequiredGuid] to also reject Guid.Empty.
+///
+[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, AllowMultiple = false)]
+public class RequiredGuidAttribute : ValidationAttribute
+{
+ ///
+ /// Initializes a new instance of RequiredGuidAttribute with a default error message.
+ ///
+ public RequiredGuidAttribute()
+ : base("The {0} field is required and cannot be empty.")
+ {
+ }
+
+ ///
+ /// Initializes a new instance of RequiredGuidAttribute with a custom error message.
+ ///
+ /// The error message to display when validation fails.
+ public RequiredGuidAttribute(string errorMessage)
+ : base(errorMessage)
+ {
+ }
+
+ ///
+ /// Validates that the value is not null, not Guid.Empty, and is a valid Guid.
+ ///
+ /// The value to validate.
+ /// The context information about the validation operation.
+ /// ValidationResult.Success if valid, otherwise a ValidationResult with error message.
+ protected override ValidationResult? IsValid(object? value, ValidationContext validationContext)
+ {
+ // Null check (for Guid? properties)
+ if (value == null)
+ {
+ return new ValidationResult(
+ FormatErrorMessage(validationContext.DisplayName),
+ new[] { validationContext.MemberName ?? string.Empty }
+ );
+ }
+
+ // Type check
+ if (value is not Guid guidValue)
+ {
+ return new ValidationResult(
+ $"The {validationContext.DisplayName} field must be a valid Guid.",
+ new[] { validationContext.MemberName ?? string.Empty }
+ );
+ }
+
+ // Empty Guid check
+ if (guidValue == Guid.Empty)
+ {
+ return new ValidationResult(
+ FormatErrorMessage(validationContext.DisplayName),
+ new[] { validationContext.MemberName ?? string.Empty }
+ );
+ }
+
+ return ValidationResult.Success;
+ }
+
+ ///
+ /// Simple validation for attribute usage without ValidationContext.
+ ///
+ public override bool IsValid(object? value)
+ {
+ if (value == null)
+ return false;
+
+ if (value is not Guid guidValue)
+ return false;
+
+ return guidValue != Guid.Empty;
+ }
+}
diff --git a/1-Aquiis.Infrastructure/Aquiis.Infrastructure.csproj b/1-Aquiis.Infrastructure/Aquiis.Infrastructure.csproj
new file mode 100644
index 0000000..cd859f5
--- /dev/null
+++ b/1-Aquiis.Infrastructure/Aquiis.Infrastructure.csproj
@@ -0,0 +1,32 @@
+
+
+
+ net10.0
+ enable
+ enable
+ 0.2.0
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ PreserveNewest
+
+
+ PreserveNewest
+
+
+
+
diff --git a/1-Aquiis.Infrastructure/Core/Services/SendGridEmailService.cs b/1-Aquiis.Infrastructure/Core/Services/SendGridEmailService.cs
new file mode 100644
index 0000000..52cffc8
--- /dev/null
+++ b/1-Aquiis.Infrastructure/Core/Services/SendGridEmailService.cs
@@ -0,0 +1,276 @@
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+using Aquiis.Core.Entities;
+using Aquiis.Core.Interfaces.Services;
+using Aquiis.Infrastructure.Data;
+using Aquiis.Infrastructure.Interfaces;
+using Microsoft.AspNetCore.DataProtection;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Logging;
+using SendGrid;
+using SendGrid.Helpers.Mail;
+
+namespace Aquiis.Infrastructure.Core.Services
+{
+ public class SendGridEmailService : IEmailService, IEmailProvider
+ {
+ private readonly ApplicationDbContext _context;
+ private readonly IUserContextService _userContext;
+ private readonly ILogger _logger;
+ private readonly IDataProtectionProvider _dataProtection;
+
+ private const string PROTECTION_PURPOSE = "SendGridApiKey";
+
+ public SendGridEmailService(
+ ApplicationDbContext context,
+ IUserContextService userContext,
+ ILogger logger,
+ IDataProtectionProvider dataProtection)
+ {
+ _context = context;
+ _userContext = userContext;
+ _logger = logger;
+ _dataProtection = dataProtection;
+ }
+
+ public async Task SendEmailAsync(string to, string subject, string body)
+ {
+ var orgId = await _userContext.GetActiveOrganizationIdAsync();
+ if (orgId == null)
+ {
+ _logger.LogWarning("Cannot send email - no active organization");
+ return;
+ }
+
+ var settings = await GetEmailSettingsAsync(orgId.Value);
+
+ if (!settings.IsEmailEnabled || string.IsNullOrEmpty(settings.SendGridApiKeyEncrypted))
+ {
+ _logger.LogInformation("Email disabled for organization {OrgId}", orgId);
+ return; // Graceful degradation - don't throw
+ }
+
+ try
+ {
+ var apiKey = DecryptApiKey(settings.SendGridApiKeyEncrypted);
+ var client = new SendGridClient(apiKey);
+
+ var from = new EmailAddress(settings.FromEmail, settings.FromName);
+ var toAddress = new EmailAddress(to);
+ var msg = MailHelper.CreateSingleEmail(from, toAddress, subject, body, body);
+
+ var response = await client.SendEmailAsync(msg);
+
+ if (response.IsSuccessStatusCode)
+ {
+ _logger.LogInformation("Email sent successfully to {To}", to);
+ await UpdateUsageStatsAsync(settings);
+ }
+ else
+ {
+ var error = await response.Body.ReadAsStringAsync();
+ _logger.LogError("SendGrid error {StatusCode}: {Error}", response.StatusCode, error);
+ settings.LastError = $"HTTP {response.StatusCode}: {error}";
+ await _context.SaveChangesAsync();
+ throw new Exception($"SendGrid returned {response.StatusCode}");
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Failed to send email via SendGrid for org {OrgId}", orgId);
+ settings.LastError = ex.Message;
+ settings.LastErrorOn = DateTime.UtcNow;
+ await _context.SaveChangesAsync();
+ throw;
+ }
+ }
+
+ public async Task SendEmailAsync(string to, string subject, string body, string? fromName = null)
+ {
+ // Override from name if provided
+ var orgId = await _userContext.GetActiveOrganizationIdAsync();
+ var settings = await GetEmailSettingsAsync(orgId!.Value);
+
+ var originalFromName = settings.FromName;
+ if (!string.IsNullOrEmpty(fromName))
+ {
+ settings.FromName = fromName;
+ }
+
+ await SendEmailAsync(to, subject, body);
+
+ settings.FromName = originalFromName;
+ }
+
+ public async Task SendTemplateEmailAsync(string to, string templateId, Dictionary templateData)
+ {
+ var orgId = await _userContext.GetActiveOrganizationIdAsync();
+ var settings = await GetEmailSettingsAsync(orgId!.Value);
+
+ if (!settings.IsEmailEnabled)
+ {
+ _logger.LogInformation("Email disabled for organization {OrgId}", orgId);
+ return;
+ }
+
+ try
+ {
+ var apiKey = DecryptApiKey(settings.SendGridApiKeyEncrypted!);
+ var client = new SendGridClient(apiKey);
+
+ var msg = new SendGridMessage();
+ msg.SetFrom(new EmailAddress(settings.FromEmail, settings.FromName));
+ msg.AddTo(new EmailAddress(to));
+ msg.SetTemplateId(templateId);
+ msg.SetTemplateData(templateData);
+
+ var response = await client.SendEmailAsync(msg);
+
+ if (response.IsSuccessStatusCode)
+ {
+ await UpdateUsageStatsAsync(settings);
+ }
+ else
+ {
+ var error = await response.Body.ReadAsStringAsync();
+ _logger.LogError("SendGrid template error: {Error}", error);
+ throw new Exception(error);
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Failed to send template email via SendGrid");
+ throw;
+ }
+ }
+
+ public async Task ValidateEmailAddressAsync(string email)
+ {
+ await Task.CompletedTask;
+ return !string.IsNullOrWhiteSpace(email) &&
+ email.Contains("@") &&
+ email.Contains(".");
+ }
+
+ public async Task VerifyApiKeyAsync(string apiKey)
+ {
+ try
+ {
+ var client = new SendGridClient(apiKey);
+
+ // Test API key by fetching user profile
+ var response = await client.RequestAsync(
+ method: SendGridClient.Method.GET,
+ urlPath: "user/profile");
+
+ return response.IsSuccessStatusCode;
+ }
+ catch (Exception ex)
+ {
+ _logger.LogWarning(ex, "SendGrid API key verification failed");
+ return false;
+ }
+ }
+
+ public async Task GetSendGridStatsAsync()
+ {
+ var orgId = await _userContext.GetActiveOrganizationIdAsync();
+ var settings = await GetEmailSettingsAsync(orgId!.Value);
+
+ if (!settings.IsEmailEnabled)
+ {
+ return new SendGridStats { IsConfigured = false };
+ }
+
+ // Optionally refresh stats from SendGrid API
+ // await RefreshStatsFromSendGridAsync(settings);
+
+ return new SendGridStats
+ {
+ IsConfigured = true,
+ EmailsSentToday = settings.EmailsSentToday,
+ EmailsSentThisMonth = settings.EmailsSentThisMonth,
+ DailyLimit = settings.DailyLimit ?? 100,
+ MonthlyLimit = settings.MonthlyLimit ?? 40000,
+ LastEmailSentOn = settings.LastEmailSentOn,
+ LastVerifiedOn = settings.LastVerifiedOn,
+ PlanType = settings.PlanType ?? "Free",
+ DailyPercentUsed = settings.DailyLimit.HasValue
+ ? (int)((settings.EmailsSentToday / (double)settings.DailyLimit.Value) * 100)
+ : 0,
+ MonthlyPercentUsed = settings.MonthlyLimit.HasValue
+ ? (int)((settings.EmailsSentThisMonth / (double)settings.MonthlyLimit.Value) * 100)
+ : 0
+ };
+ }
+
+ private async Task GetEmailSettingsAsync(Guid organizationId)
+ {
+ var settings = await _context.OrganizationEmailSettings
+ .FirstOrDefaultAsync(s => s.OrganizationId == organizationId && !s.IsDeleted);
+
+ if (settings == null)
+ {
+ throw new InvalidOperationException(
+ $"Email settings not found for organization {organizationId}");
+ }
+
+ return settings;
+ }
+
+ private async Task UpdateUsageStatsAsync(OrganizationEmailSettings settings)
+ {
+ var now = DateTime.UtcNow;
+ var today = now.Date;
+
+ // Reset daily counter if needed
+ if (settings.DailyCountResetOn?.Date != today)
+ {
+ settings.EmailsSentToday = 0;
+ settings.DailyCountResetOn = today;
+ }
+
+ // Reset monthly counter if needed (first of month)
+ if (settings.MonthlyCountResetOn?.Month != now.Month ||
+ settings.MonthlyCountResetOn?.Year != now.Year)
+ {
+ settings.EmailsSentThisMonth = 0;
+ settings.MonthlyCountResetOn = new DateTime(now.Year, now.Month, 1);
+ }
+
+ settings.EmailsSentToday++;
+ settings.EmailsSentThisMonth++;
+ settings.LastEmailSentOn = now;
+ settings.StatsLastUpdatedOn = now;
+
+ await _context.SaveChangesAsync();
+ }
+
+ public string DecryptApiKey(string encrypted)
+ {
+ var protector = _dataProtection.CreateProtector(PROTECTION_PURPOSE);
+ return protector.Unprotect(encrypted);
+ }
+
+ public string EncryptApiKey(string apiKey)
+ {
+ var protector = _dataProtection.CreateProtector(PROTECTION_PURPOSE);
+ return protector.Protect(apiKey);
+ }
+ }
+
+ public class SendGridStats
+ {
+ public bool IsConfigured { get; set; }
+ public int EmailsSentToday { get; set; }
+ public int EmailsSentThisMonth { get; set; }
+ public int DailyLimit { get; set; }
+ public int MonthlyLimit { get; set; }
+ public int DailyPercentUsed { get; set; }
+ public int MonthlyPercentUsed { get; set; }
+ public DateTime? LastEmailSentOn { get; set; }
+ public DateTime? LastVerifiedOn { get; set; }
+ public string? PlanType { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/1-Aquiis.Infrastructure/Core/Services/TwilioSMSService.cs b/1-Aquiis.Infrastructure/Core/Services/TwilioSMSService.cs
new file mode 100644
index 0000000..28c9c55
--- /dev/null
+++ b/1-Aquiis.Infrastructure/Core/Services/TwilioSMSService.cs
@@ -0,0 +1,244 @@
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+using Aquiis.Core.Entities;
+using Aquiis.Core.Interfaces.Services;
+using Aquiis.Infrastructure.Data;
+using Aquiis.Infrastructure.Interfaces;
+using Microsoft.AspNetCore.DataProtection;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Logging;
+using Twilio;
+using Twilio.Rest.Api.V2010.Account;
+using Twilio.Types;
+
+namespace Aquiis.Infrastructure.Core.Services
+{
+ public class TwilioSMSService : ISMSService, ISMSProvider
+ {
+ private readonly ApplicationDbContext _context;
+ private readonly IUserContextService _userContext;
+ private readonly ILogger _logger;
+ private readonly IDataProtectionProvider _dataProtection;
+
+ private const string ACCOUNT_SID_PURPOSE = "TwilioAccountSid";
+ private const string AUTH_TOKEN_PURPOSE = "TwilioAuthToken";
+
+ public TwilioSMSService(
+ ApplicationDbContext context,
+ IUserContextService userContext,
+ ILogger logger,
+ IDataProtectionProvider dataProtection)
+ {
+ _context = context;
+ _userContext = userContext;
+ _logger = logger;
+ _dataProtection = dataProtection;
+ }
+
+ public async Task SendSMSAsync(string phoneNumber, string message)
+ {
+ var orgId = await _userContext.GetActiveOrganizationIdAsync();
+ if (orgId == null)
+ {
+ _logger.LogWarning("Cannot send SMS - no active organization");
+ return;
+ }
+
+ var settings = await GetSMSSettingsAsync(orgId.Value);
+
+ if (!settings.IsSMSEnabled ||
+ string.IsNullOrEmpty(settings.TwilioAccountSidEncrypted) ||
+ string.IsNullOrEmpty(settings.TwilioAuthTokenEncrypted))
+ {
+ _logger.LogInformation("SMS disabled for organization {OrgId}", orgId);
+ return; // Graceful degradation
+ }
+
+ try
+ {
+ var accountSid = DecryptAccountSid(settings.TwilioAccountSidEncrypted);
+ var authToken = DecryptAuthToken(settings.TwilioAuthTokenEncrypted);
+
+ TwilioClient.Init(accountSid, authToken);
+
+ var messageResource = await MessageResource.CreateAsync(
+ body: message,
+ from: new PhoneNumber(settings.TwilioPhoneNumber),
+ to: new PhoneNumber(phoneNumber));
+
+ if (messageResource.Status == MessageResource.StatusEnum.Queued ||
+ messageResource.Status == MessageResource.StatusEnum.Sent)
+ {
+ _logger.LogInformation("SMS sent successfully to {PhoneNumber}", phoneNumber);
+ await UpdateUsageStatsAsync(settings);
+ }
+ else
+ {
+ _logger.LogError("Twilio SMS status: {Status}", messageResource.Status);
+ settings.LastError = $"Status: {messageResource.Status}";
+ await _context.SaveChangesAsync();
+ throw new Exception($"SMS send failed with status: {messageResource.Status}");
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Failed to send SMS via Twilio for org {OrgId}", orgId);
+ settings.LastError = ex.Message;
+ await _context.SaveChangesAsync();
+ throw;
+ }
+ }
+
+ public async Task ValidatePhoneNumberAsync(string phoneNumber)
+ {
+ // Basic validation
+ var digits = new string(phoneNumber.Where(char.IsDigit).ToArray());
+ return await Task.FromResult(digits.Length >= 10);
+ }
+
+ public async Task VerifyTwilioCredentialsAsync(string accountSid, string authToken, string phoneNumber)
+ {
+ try
+ {
+ TwilioClient.Init(accountSid, authToken);
+
+ // Verify by fetching the incoming phone number
+ var incomingPhoneNumber = await IncomingPhoneNumberResource.ReadAsync(
+ phoneNumber: new PhoneNumber(phoneNumber));
+
+ return incomingPhoneNumber.Any();
+ }
+ catch (Exception ex)
+ {
+ _logger.LogWarning(ex, "Twilio credentials verification failed");
+ return false;
+ }
+ }
+
+ public async Task GetTwilioStatsAsync()
+ {
+ var orgId = await _userContext.GetActiveOrganizationIdAsync();
+ var settings = await GetSMSSettingsAsync(orgId!.Value);
+
+ if (!settings.IsSMSEnabled)
+ {
+ return new TwilioStats { IsConfigured = false };
+ }
+
+ return new TwilioStats
+ {
+ IsConfigured = true,
+ SMSSentToday = settings.SMSSentToday,
+ SMSSentThisMonth = settings.SMSSentThisMonth,
+ AccountBalance = settings.AccountBalance ?? 0,
+ CostPerSMS = settings.CostPerSMS ?? 0.0075m,
+ EstimatedMonthlyCost = settings.SMSSentThisMonth * (settings.CostPerSMS ?? 0.0075m),
+ LastSMSSentOn = settings.LastSMSSentOn,
+ LastVerifiedOn = settings.LastVerifiedOn,
+ AccountType = settings.AccountType ?? "Unknown"
+ };
+ }
+
+ private async Task GetSMSSettingsAsync(Guid organizationId)
+ {
+ var settings = await _context.OrganizationSMSSettings
+ .FirstOrDefaultAsync(s => s.OrganizationId == organizationId && !s.IsDeleted);
+
+ if (settings == null)
+ {
+ throw new InvalidOperationException(
+ $"SMS settings not found for organization {organizationId}");
+ }
+
+ return settings;
+ }
+
+ private async Task UpdateUsageStatsAsync(OrganizationSMSSettings settings)
+ {
+ var now = DateTime.UtcNow;
+ var today = now.Date;
+
+ // Reset daily counter if needed
+ if (settings.DailyCountResetOn?.Date != today)
+ {
+ settings.SMSSentToday = 0;
+ settings.DailyCountResetOn = today;
+ }
+
+ // Reset monthly counter if needed
+ if (settings.MonthlyCountResetOn?.Month != now.Month ||
+ settings.MonthlyCountResetOn?.Year != now.Year)
+ {
+ settings.SMSSentThisMonth = 0;
+ settings.MonthlyCountResetOn = new DateTime(now.Year, now.Month, 1);
+ }
+
+ settings.SMSSentToday++;
+ settings.SMSSentThisMonth++;
+ settings.LastSMSSentOn = now;
+ settings.StatsLastUpdatedOn = now;
+
+ await _context.SaveChangesAsync();
+ }
+
+ private string DecryptAccountSid(string encrypted)
+ {
+ var protector = _dataProtection.CreateProtector(ACCOUNT_SID_PURPOSE);
+ return protector.Unprotect(encrypted);
+ }
+
+ private string DecryptAuthToken(string encrypted)
+ {
+ var protector = _dataProtection.CreateProtector(AUTH_TOKEN_PURPOSE);
+ return protector.Unprotect(encrypted);
+ }
+
+ public string EncryptAccountSid(string accountSid)
+ {
+ var protector = _dataProtection.CreateProtector(ACCOUNT_SID_PURPOSE);
+ return protector.Protect(accountSid);
+ }
+
+ public string EncryptAuthToken(string authToken)
+ {
+ var protector = _dataProtection.CreateProtector(AUTH_TOKEN_PURPOSE);
+ return protector.Protect(authToken);
+ }
+
+ // ISMSProvider implementation
+ public async Task VerifyCredentialsAsync(string accountSid, string authToken)
+ {
+ // Use a dummy phone number for verification - just checking credentials work
+ return await VerifyTwilioCredentialsAsync(accountSid, authToken, string.Empty);
+ }
+
+ public string EncryptCredential(string credential)
+ {
+ // Generic encryption using AUTH_TOKEN_PURPOSE
+ var protector = _dataProtection.CreateProtector(AUTH_TOKEN_PURPOSE);
+ return protector.Protect(credential);
+ }
+
+ public string DecryptCredential(string encryptedCredential)
+ {
+ var protector = _dataProtection.CreateProtector(AUTH_TOKEN_PURPOSE);
+ return protector.Unprotect(encryptedCredential);
+ }
+ }
+
+
+
+ public class TwilioStats
+ {
+ public bool IsConfigured { get; set; }
+ public int SMSSentToday { get; set; }
+ public int SMSSentThisMonth { get; set; }
+ public decimal AccountBalance { get; set; }
+ public decimal CostPerSMS { get; set; }
+ public decimal EstimatedMonthlyCost { get; set; }
+ public DateTime? LastSMSSentOn { get; set; }
+ public DateTime? LastVerifiedOn { get; set; }
+ public string AccountType { get; set; } = string.Empty;
+ }
+}
\ No newline at end of file
diff --git a/1-Aquiis.Infrastructure/Data/ApplicationDbContext.cs b/1-Aquiis.Infrastructure/Data/ApplicationDbContext.cs
new file mode 100644
index 0000000..825dad4
--- /dev/null
+++ b/1-Aquiis.Infrastructure/Data/ApplicationDbContext.cs
@@ -0,0 +1,737 @@
+using Microsoft.EntityFrameworkCore;
+using Aquiis.Core.Entities;
+using Aquiis.Core.Interfaces;
+
+namespace Aquiis.Infrastructure.Data
+{
+ ///
+ /// Main application database context for business entities only.
+ /// Identity management is handled by product-specific contexts.
+ /// Products can extend via partial classes following the Portable Feature pattern.
+ ///
+ public partial class ApplicationDbContext : DbContext, IApplicationDbContext
+ {
+ public ApplicationDbContext(DbContextOptions options)
+ : base(options)
+ {
+ }
+
+ protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
+ {
+ base.OnConfiguring(optionsBuilder);
+
+ // Suppress pending model changes warning - bidirectional Document-Invoice/Payment relationship issue
+ // TODO: Fix the Document-Invoice and Document-Payment bidirectional relationships properly
+ optionsBuilder.ConfigureWarnings(warnings =>
+ warnings.Ignore(Microsoft.EntityFrameworkCore.Diagnostics.RelationalEventId.PendingModelChangesWarning));
+ }
+
+ public DbSet Properties { get; set; }
+ public DbSet Leases { get; set; }
+ public DbSet LeaseOffers { get; set; }
+ public DbSet Tenants { get; set; }
+ public DbSet Invoices { get; set; }
+ public DbSet Payments { get; set; }
+ public DbSet Documents { get; set; }
+ public DbSet Inspections { get; set; }
+ public DbSet MaintenanceRequests { get; set; }
+ public DbSet OrganizationSettings { get; set; }
+ public DbSet SchemaVersions { get; set; }
+ public DbSet ChecklistTemplates { get; set; }
+ public DbSet ChecklistTemplateItems { get; set; }
+ public DbSet Checklists { get; set; }
+ public DbSet ChecklistItems { get; set; }
+ public DbSet ProspectiveTenants { get; set; }
+ public DbSet Tours { get; set; }
+ public DbSet RentalApplications { get; set; }
+ public DbSet ApplicationScreenings { get; set; }
+ public DbSet CalendarEvents { get; set; }
+ public DbSet CalendarSettings { get; set; }
+ public DbSet Notes { get; set; }
+ public DbSet SecurityDeposits { get; set; }
+ public DbSet SecurityDepositInvestmentPools { get; set; }
+ public DbSet SecurityDepositDividends { get; set; }
+
+ // Multi-organization support
+ public DbSet Organizations { get; set; }
+ public DbSet UserOrganizations { get; set; }
+
+ // Workflow audit logging
+ public DbSet WorkflowAuditLogs { get; set; }
+
+
+ // Notification system
+ public DbSet Notifications { get; set; }
+ public DbSet NotificationPreferences { get; set; }
+ public DbSet OrganizationEmailSettings { get; set; }
+ public DbSet OrganizationSMSSettings { get; set; }
+
+ protected override void OnModelCreating(ModelBuilder modelBuilder)
+ {
+ base.OnModelCreating(modelBuilder);
+
+ // Configure Property entity
+ modelBuilder.Entity(entity =>
+ {
+ entity.HasIndex(e => e.Address);
+ entity.Property(e => e.MonthlyRent).HasPrecision(18, 2);
+
+ // Configure relationship with Organization
+ entity.HasOne()
+ .WithMany(o => o.Properties)
+ .HasForeignKey(e => e.OrganizationId)
+ .OnDelete(DeleteBehavior.NoAction);
+ });
+
+ // Configure Tenant entity
+ modelBuilder.Entity(entity =>
+ {
+ entity.HasIndex(e => e.Email).IsUnique();
+ entity.HasIndex(e => e.IdentificationNumber).IsUnique();
+
+ // Configure relationship with Organization
+ entity.HasOne()
+ .WithMany(o => o.Tenants)
+ .HasForeignKey(e => e.OrganizationId)
+ .OnDelete(DeleteBehavior.NoAction);
+ });
+
+ // Configure Lease entity
+ modelBuilder.Entity(entity =>
+ {
+ entity.HasOne(l => l.Property)
+ .WithMany(p => p.Leases)
+ .HasForeignKey(l => l.PropertyId)
+ .OnDelete(DeleteBehavior.Restrict);
+
+ entity.HasOne(l => l.Tenant)
+ .WithMany(t => t.Leases)
+ .HasForeignKey(l => l.TenantId)
+ .OnDelete(DeleteBehavior.Restrict);
+
+ entity.HasOne(l => l.Document)
+ .WithMany()
+ .HasForeignKey(l => l.DocumentId)
+ .OnDelete(DeleteBehavior.SetNull);
+
+ entity.Property(e => e.MonthlyRent).HasPrecision(18, 2);
+ entity.Property(e => e.SecurityDeposit).HasPrecision(18, 2);
+
+ // Configure relationship with Organization
+ entity.HasOne()
+ .WithMany(o => o.Leases)
+ .HasForeignKey(e => e.OrganizationId)
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+
+ // Configure Invoice entity
+ modelBuilder.Entity(entity =>
+ {
+ entity.HasOne(i => i.Lease)
+ .WithMany(l => l.Invoices)
+ .HasForeignKey(i => i.LeaseId)
+ .OnDelete(DeleteBehavior.Restrict);
+
+ entity.HasOne(i => i.Document)
+ .WithMany()
+ .HasForeignKey(i => i.DocumentId)
+ .OnDelete(DeleteBehavior.SetNull);
+
+ entity.HasIndex(e => e.InvoiceNumber).IsUnique();
+ entity.Property(e => e.Amount).HasPrecision(18, 2);
+ entity.Property(e => e.AmountPaid).HasPrecision(18, 2);
+
+ // Configure relationship with User
+ entity.HasOne()
+ .WithMany()
+ .HasForeignKey(e => e.OrganizationId)
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+
+ // Configure Payment entity
+ modelBuilder.Entity(entity =>
+ {
+ entity.HasOne(p => p.Invoice)
+ .WithMany(i => i.Payments)
+ .HasForeignKey(p => p.InvoiceId)
+ .OnDelete(DeleteBehavior.Restrict);
+
+ entity.HasOne(p => p.Document)
+ .WithMany()
+ .HasForeignKey(p => p.DocumentId)
+ .OnDelete(DeleteBehavior.SetNull);
+
+ entity.Property(e => e.Amount).HasPrecision(18, 2);
+
+ // Configure relationship with User
+ entity.HasOne()
+ .WithMany()
+ .HasForeignKey(e => e.OrganizationId)
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+
+ // Configure Document entity
+ modelBuilder.Entity(entity =>
+ {
+ entity.HasOne(d => d.Property)
+ .WithMany(p => p.Documents)
+ .HasForeignKey(d => d.PropertyId)
+ .OnDelete(DeleteBehavior.SetNull);
+
+ entity.HasOne(d => d.Tenant)
+ .WithMany()
+ .HasForeignKey(d => d.TenantId)
+ .OnDelete(DeleteBehavior.SetNull);
+
+ entity.HasOne(d => d.Lease)
+ .WithMany(l => l.Documents)
+ .HasForeignKey(d => d.LeaseId)
+ .OnDelete(DeleteBehavior.SetNull);
+
+ entity.HasOne(d => d.Invoice)
+ .WithMany()
+ .HasForeignKey(d => d.InvoiceId)
+ .OnDelete(DeleteBehavior.SetNull);
+
+ entity.HasOne(d => d.Payment)
+ .WithMany()
+ .HasForeignKey(d => d.PaymentId)
+ .OnDelete(DeleteBehavior.SetNull);
+
+ // FileData is automatically stored as BLOB in SQLite
+ // No need to specify column type
+
+ // Configure relationship with User
+ entity.HasOne()
+ .WithMany()
+ .HasForeignKey(e => e.OrganizationId)
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+
+ // Configure Inspection entity
+ modelBuilder.Entity(entity =>
+ {
+ entity.HasOne(i => i.Property)
+ .WithMany()
+ .HasForeignKey(i => i.PropertyId)
+ .OnDelete(DeleteBehavior.Restrict);
+
+ entity.HasOne(i => i.Lease)
+ .WithMany()
+ .HasForeignKey(i => i.LeaseId)
+ .OnDelete(DeleteBehavior.SetNull);
+
+ entity.HasOne(i => i.Document)
+ .WithMany()
+ .HasForeignKey(i => i.DocumentId)
+ .OnDelete(DeleteBehavior.SetNull);
+
+ entity.HasIndex(e => e.PropertyId);
+ entity.HasIndex(e => e.CompletedOn);
+ });
+
+ // Configure MaintenanceRequest entity
+ modelBuilder.Entity(entity =>
+ {
+ entity.HasOne(m => m.Property)
+ .WithMany()
+ .HasForeignKey(m => m.PropertyId)
+ .OnDelete(DeleteBehavior.Restrict);
+
+ entity.HasOne(m => m.Lease)
+ .WithMany()
+ .HasForeignKey(m => m.LeaseId)
+ .OnDelete(DeleteBehavior.SetNull);
+
+ entity.Property(e => e.EstimatedCost).HasPrecision(18, 2);
+ entity.Property(e => e.ActualCost).HasPrecision(18, 2);
+
+ entity.HasIndex(e => e.PropertyId);
+ entity.HasIndex(e => e.Status);
+ entity.HasIndex(e => e.Priority);
+ entity.HasIndex(e => e.RequestedOn);
+ });
+
+ // Configure OrganizationSettings entity
+ modelBuilder.Entity(entity =>
+ {
+ entity.Property(e => e.OrganizationId).HasConversion();
+ entity.HasIndex(e => e.OrganizationId).IsUnique();
+ entity.Property(e => e.LateFeePercentage).HasPrecision(5, 4);
+ entity.Property(e => e.MaxLateFeeAmount).HasPrecision(18, 2);
+ entity.Property(e => e.DefaultApplicationFee).HasPrecision(18, 2);
+ entity.Property(e => e.OrganizationSharePercentage).HasPrecision(18, 6);
+ entity.Property(e => e.SecurityDepositMultiplier).HasPrecision(18, 2);
+ });
+
+ // Configure ChecklistTemplate entity
+ modelBuilder.Entity(entity =>
+ {
+ entity.HasIndex(e => e.OrganizationId);
+ entity.HasIndex(e => e.Category);
+ });
+
+ // Configure ChecklistTemplateItem entity
+ modelBuilder.Entity(entity =>
+ {
+ entity.HasOne(cti => cti.ChecklistTemplate)
+ .WithMany(ct => ct.Items)
+ .HasForeignKey(cti => cti.ChecklistTemplateId)
+ .OnDelete(DeleteBehavior.Cascade);
+
+ entity.HasIndex(e => e.ChecklistTemplateId);
+ });
+
+ // Configure Checklist entity
+ modelBuilder.Entity(entity =>
+ {
+ entity.HasOne(c => c.Property)
+ .WithMany()
+ .HasForeignKey(c => c.PropertyId)
+ .OnDelete(DeleteBehavior.Restrict);
+
+ entity.HasOne(c => c.Lease)
+ .WithMany()
+ .HasForeignKey(c => c.LeaseId)
+ .OnDelete(DeleteBehavior.SetNull);
+
+ entity.HasOne(c => c.ChecklistTemplate)
+ .WithMany(ct => ct.Checklists)
+ .HasForeignKey(c => c.ChecklistTemplateId)
+ .OnDelete(DeleteBehavior.Restrict);
+
+ entity.HasOne(c => c.Document)
+ .WithMany()
+ .HasForeignKey(c => c.DocumentId)
+ .OnDelete(DeleteBehavior.SetNull);
+
+ entity.HasIndex(e => e.PropertyId);
+ entity.HasIndex(e => e.LeaseId);
+ entity.HasIndex(e => e.ChecklistType);
+ entity.HasIndex(e => e.Status);
+ entity.HasIndex(e => e.CompletedOn);
+ });
+
+ // Configure ChecklistItem entity
+ modelBuilder.Entity(entity =>
+ {
+ entity.HasOne(ci => ci.Checklist)
+ .WithMany(c => c.Items)
+ .HasForeignKey(ci => ci.ChecklistId)
+ .OnDelete(DeleteBehavior.Cascade);
+
+ entity.HasIndex(e => e.ChecklistId);
+ });
+
+ // Configure ProspectiveTenant entity
+ modelBuilder.Entity(entity =>
+ {
+ entity.HasOne(pt => pt.InterestedProperty)
+ .WithMany()
+ .HasForeignKey(pt => pt.InterestedPropertyId)
+ .OnDelete(DeleteBehavior.SetNull);
+
+ entity.HasIndex(e => e.Email);
+ entity.HasIndex(e => e.OrganizationId);
+ entity.HasIndex(e => e.Status);
+ });
+
+ // Configure Tour entity
+ modelBuilder.Entity(entity =>
+ {
+ entity.HasOne(s => s.ProspectiveTenant)
+ .WithMany(pt => pt.Tours)
+ .HasForeignKey(s => s.ProspectiveTenantId)
+ .OnDelete(DeleteBehavior.Restrict);
+
+ entity.HasOne(s => s.Property)
+ .WithMany()
+ .HasForeignKey(s => s.PropertyId)
+ .OnDelete(DeleteBehavior.Restrict);
+
+ entity.HasIndex(e => e.OrganizationId);
+ entity.HasIndex(e => e.ScheduledOn);
+ entity.HasIndex(e => e.Status);
+ });
+
+ // Configure RentalApplication entity
+ // A prospect may have multiple applications over time, but only one "active" application at a time.
+ // Active = not yet disposed (not approved/denied/withdrawn/expired/lease-declined)
+ modelBuilder.Entity(entity =>
+ {
+ entity.HasOne(ra => ra.ProspectiveTenant)
+ .WithMany(pt => pt.Applications)
+ .HasForeignKey(ra => ra.ProspectiveTenantId)
+ .OnDelete(DeleteBehavior.Restrict);
+
+ entity.HasOne(ra => ra.Property)
+ .WithMany()
+ .HasForeignKey(ra => ra.PropertyId)
+ .OnDelete(DeleteBehavior.Restrict);
+
+ entity.Property(e => e.CurrentRent).HasPrecision(18, 2);
+ entity.Property(e => e.MonthlyIncome).HasPrecision(18, 2);
+ entity.Property(e => e.ApplicationFee).HasPrecision(18, 2);
+
+ entity.HasIndex(e => e.OrganizationId);
+ entity.HasIndex(e => e.AppliedOn);
+ entity.HasIndex(e => e.Status);
+ });
+
+ // Configure ApplicationScreening entity
+ modelBuilder.Entity(entity =>
+ {
+ entity.HasOne(asc => asc.RentalApplication)
+ .WithOne(ra => ra.Screening)
+ .HasForeignKey(asc => asc.RentalApplicationId)
+ .OnDelete(DeleteBehavior.Cascade);
+
+ entity.HasIndex(e => e.OrganizationId);
+ entity.HasIndex(e => e.OverallResult);
+ });
+
+ // Configure CalendarEvent entity
+ modelBuilder.Entity(entity =>
+ {
+ entity.HasOne(ce => ce.Property)
+ .WithMany()
+ .HasForeignKey(ce => ce.PropertyId)
+ .OnDelete(DeleteBehavior.SetNull);
+
+ entity.HasIndex(e => e.OrganizationId);
+ entity.HasIndex(e => e.StartOn);
+ entity.HasIndex(e => e.EventType);
+ entity.HasIndex(e => e.SourceEntityId);
+ entity.HasIndex(e => new { e.SourceEntityType, e.SourceEntityId });
+ });
+
+ // Configure CalendarSettings entity
+ modelBuilder.Entity(entity =>
+ {
+ entity.HasIndex(e => e.OrganizationId);
+ entity.HasIndex(e => new { e.OrganizationId, e.EntityType }).IsUnique();
+ });
+
+ // Configure SecurityDeposit entity
+ modelBuilder.Entity(entity =>
+ {
+ entity.HasOne(sd => sd.Lease)
+ .WithMany()
+ .HasForeignKey(sd => sd.LeaseId)
+ .OnDelete(DeleteBehavior.Restrict);
+
+ entity.HasOne(sd => sd.Tenant)
+ .WithMany()
+ .HasForeignKey(sd => sd.TenantId)
+ .OnDelete(DeleteBehavior.Restrict);
+
+ entity.Property(e => e.Amount).HasPrecision(18, 2);
+ entity.Property(e => e.RefundAmount).HasPrecision(18, 2);
+ entity.Property(e => e.DeductionsAmount).HasPrecision(18, 2);
+
+ entity.HasIndex(e => e.LeaseId).IsUnique();
+ entity.HasIndex(e => e.TenantId);
+ entity.HasIndex(e => e.Status);
+ entity.HasIndex(e => e.InInvestmentPool);
+ });
+
+ // Configure SecurityDepositInvestmentPool entity
+ modelBuilder.Entity(entity =>
+ {
+ entity.Property(e => e.StartingBalance).HasPrecision(18, 2);
+ entity.Property(e => e.EndingBalance).HasPrecision(18, 2);
+ entity.Property(e => e.TotalEarnings).HasPrecision(18, 2);
+ entity.Property(e => e.ReturnRate).HasPrecision(18, 6);
+ entity.Property(e => e.OrganizationSharePercentage).HasPrecision(18, 6);
+ entity.Property(e => e.OrganizationShare).HasPrecision(18, 2);
+ entity.Property(e => e.TenantShareTotal).HasPrecision(18, 2);
+ entity.Property(e => e.DividendPerLease).HasPrecision(18, 2);
+
+ entity.HasIndex(e => e.OrganizationId);
+ entity.HasIndex(e => e.Year).IsUnique();
+ entity.HasIndex(e => e.Status);
+ });
+
+ // Configure SecurityDepositDividend entity
+ modelBuilder.Entity(entity =>
+ {
+ entity.HasOne(sdd => sdd.SecurityDeposit)
+ .WithMany(sd => sd.Dividends)
+ .HasForeignKey(sdd => sdd.SecurityDepositId)
+ .OnDelete(DeleteBehavior.Restrict);
+
+ entity.HasOne(sdd => sdd.InvestmentPool)
+ .WithMany(ip => ip.Dividends)
+ .HasForeignKey(sdd => sdd.InvestmentPoolId)
+ .OnDelete(DeleteBehavior.Restrict);
+
+ entity.HasOne(sdd => sdd.Lease)
+ .WithMany()
+ .HasForeignKey(sdd => sdd.LeaseId)
+ .OnDelete(DeleteBehavior.Restrict);
+
+ entity.HasOne(sdd => sdd.Tenant)
+ .WithMany()
+ .HasForeignKey(sdd => sdd.TenantId)
+ .OnDelete(DeleteBehavior.Restrict);
+
+ entity.Property(e => e.BaseDividendAmount).HasPrecision(18, 2);
+ entity.Property(e => e.ProrationFactor).HasPrecision(18, 6);
+ entity.Property(e => e.DividendAmount).HasPrecision(18, 2);
+
+ entity.HasIndex(e => e.SecurityDepositId);
+ entity.HasIndex(e => e.InvestmentPoolId);
+ entity.HasIndex(e => e.LeaseId);
+ entity.HasIndex(e => e.TenantId);
+ entity.HasIndex(e => e.Year);
+ entity.HasIndex(e => e.Status);
+ });
+
+ // Configure Organization entity
+ modelBuilder.Entity(entity =>
+ {
+ entity.HasKey(e => e.Id);
+
+ entity.HasIndex(e => e.OwnerId);
+ entity.HasIndex(e => e.IsActive);
+
+ // OwnerId is a string foreign key to AspNetUsers (managed by SimpleStartDbContext)
+ // No navigation property configured here
+ });
+
+ // Configure UserOrganization entity
+ modelBuilder.Entity(entity =>
+ {
+ entity.HasKey(e => e.Id);
+
+ entity.HasOne(uo => uo.Organization)
+ .WithMany(o => o.UserOrganizations)
+ .HasForeignKey(uo => uo.OrganizationId)
+ .OnDelete(DeleteBehavior.Cascade);
+
+ // UserId and GrantedBy are string foreign keys to AspNetUsers (managed by SimpleStartDbContext)
+ // No navigation properties configured here
+
+ // Unique constraint: one role per user per organization
+ entity.HasIndex(e => new { e.UserId, e.OrganizationId }).IsUnique();
+ entity.HasIndex(e => e.OrganizationId);
+ entity.HasIndex(e => e.Role);
+ entity.HasIndex(e => e.IsActive);
+ });
+
+ // Configure WorkflowAuditLog entity
+ modelBuilder.Entity(entity =>
+ {
+ entity.HasIndex(e => e.OrganizationId);
+ entity.HasIndex(e => e.EntityType);
+ entity.HasIndex(e => e.EntityId);
+ entity.HasIndex(e => new { e.EntityType, e.EntityId });
+ entity.HasIndex(e => e.Action);
+ entity.HasIndex(e => e.PerformedOn);
+ entity.HasIndex(e => e.PerformedBy);
+ });
+
+ // Configure Notification entity
+ modelBuilder.Entity(entity =>
+ {
+ entity.HasKey(e => e.Id);
+
+ entity.HasIndex(e => e.RecipientUserId);
+ entity.HasIndex(e => e.OrganizationId);
+ entity.HasIndex(e => e.SentOn);
+ entity.HasIndex(e => e.IsRead);
+ entity.HasIndex(e => e.Category);
+
+ // Organization relationship
+ entity.HasOne(n => n.Organization)
+ .WithMany()
+ .HasForeignKey(n => n.OrganizationId)
+ .OnDelete(DeleteBehavior.Cascade);
+
+ // RecipientUserId is a string foreign key to AspNetUsers (managed by SimpleStartDbContext)
+ // No navigation property configured here
+ });
+
+ // Configure NotificationPreferences entity
+ modelBuilder.Entity(entity =>
+ {
+ entity.HasKey(e => e.Id);
+
+ entity.HasIndex(e => e.UserId);
+ entity.HasIndex(e => e.OrganizationId);
+
+ // Unique constraint: one preference record per user per organization
+ entity.HasIndex(e => new { e.UserId, e.OrganizationId })
+ .IsUnique();
+
+ // Organization relationship
+ entity.HasOne(np => np.Organization)
+ .WithMany()
+ .HasForeignKey(np => np.OrganizationId)
+ .OnDelete(DeleteBehavior.Cascade);
+
+ // UserId is a string foreign key to AspNetUsers (managed by SimpleStartDbContext)
+ // No navigation property configured here
+ });
+
+ // Configure OrganizationEmailSettings entity
+ modelBuilder.Entity(entity =>
+ {
+ entity.HasKey(e => e.Id);
+
+ entity.HasIndex(e => e.OrganizationId).IsUnique();
+
+ // Organization relationship - one settings record per organization
+ entity.HasOne(es => es.Organization)
+ .WithMany()
+ .HasForeignKey(es => es.OrganizationId)
+ .OnDelete(DeleteBehavior.Cascade);
+
+ });
+
+ // Configure OrganizationSMSSettings entity
+ modelBuilder.Entity(entity =>
+ {
+ entity.HasKey(e => e.Id);
+
+ entity.HasIndex(e => e.OrganizationId).IsUnique();
+
+ // Organization relationship - one settings record per organization
+ entity.HasOne(ss => ss.Organization)
+ .WithMany()
+ .HasForeignKey(ss => ss.OrganizationId)
+ .OnDelete(DeleteBehavior.Cascade);
+
+ // Precision for financial fields
+ entity.Property(e => e.AccountBalance).HasPrecision(18, 2);
+ entity.Property(e => e.CostPerSMS).HasPrecision(18, 4);
+ });
+
+ // Seed System Checklist Templates
+ SeedChecklistTemplates(modelBuilder);
+ }
+
+ private void SeedChecklistTemplates(ModelBuilder modelBuilder)
+ {
+ var systemTimestamp = DateTime.Parse("2025-11-30T00:00:00Z").ToUniversalTime();
+
+ // Fixed GUIDs for system templates (consistent across deployments)
+ var propertyTourTemplateId = Guid.Parse("00000000-0000-0000-0001-000000000001");
+ var moveInTemplateId = Guid.Parse("00000000-0000-0000-0001-000000000002");
+ var moveOutTemplateId = Guid.Parse("00000000-0000-0000-0001-000000000003");
+ var openHouseTemplateId = Guid.Parse("00000000-0000-0000-0001-000000000004");
+
+ // Seed ChecklistTemplates
+ modelBuilder.Entity().HasData(
+ new ChecklistTemplate
+ {
+ Id = propertyTourTemplateId,
+ Name = "Property Tour",
+ Description = "Standard property showing checklist",
+ Category = "Tour",
+ IsSystemTemplate = true,
+ OrganizationId = Guid.Empty,
+ CreatedOn = systemTimestamp,
+ CreatedBy = string.Empty,
+ IsDeleted = false
+ },
+ new ChecklistTemplate
+ {
+ Id = moveInTemplateId,
+ Name = "Move-In",
+ Description = "Move-in inspection checklist",
+ Category = "MoveIn",
+ IsSystemTemplate = true,
+ OrganizationId = Guid.Empty,
+ CreatedOn = systemTimestamp,
+ CreatedBy = string.Empty,
+ IsDeleted = false
+ },
+ new ChecklistTemplate
+ {
+ Id = moveOutTemplateId,
+ Name = "Move-Out",
+ Description = "Move-out inspection checklist",
+ Category = "MoveOut",
+ IsSystemTemplate = true,
+ OrganizationId = Guid.Empty,
+ CreatedOn = systemTimestamp,
+ CreatedBy = string.Empty,
+ IsDeleted = false
+ },
+ new ChecklistTemplate
+ {
+ Id = openHouseTemplateId,
+ Name = "Open House",
+ Description = "Open house event checklist",
+ Category = "Tour",
+ IsSystemTemplate = true,
+ OrganizationId = Guid.Empty,
+ CreatedOn = systemTimestamp,
+ CreatedBy = string.Empty,
+ IsDeleted = false
+ }
+ );
+
+ // Seed Property Tour Checklist Items
+ modelBuilder.Entity().HasData(
+ // Arrival & Introduction (Section 1)
+ new ChecklistTemplateItem { Id = Guid.Parse("00000000-0000-0000-0002-000000000001"), ChecklistTemplateId = propertyTourTemplateId, ItemText = "Greeted prospect and verified appointment", ItemOrder = 1, CategorySection = "Arrival & Introduction", SectionOrder = 1, IsRequired = true, RequiresValue = false, AllowsNotes = true, OrganizationId = Guid.Empty, CreatedOn = systemTimestamp, CreatedBy = string.Empty, IsDeleted = false },
+ new ChecklistTemplateItem { Id = Guid.Parse("00000000-0000-0000-0002-000000000002"), ChecklistTemplateId = propertyTourTemplateId, ItemText = "Reviewed property exterior and curb appeal", ItemOrder = 2, CategorySection = "Arrival & Introduction", SectionOrder = 1, IsRequired = true, RequiresValue = false, AllowsNotes = true, OrganizationId = Guid.Empty, CreatedOn = systemTimestamp, CreatedBy = string.Empty, IsDeleted = false },
+ new ChecklistTemplateItem { Id = Guid.Parse("00000000-0000-0000-0002-000000000003"), ChecklistTemplateId = propertyTourTemplateId, ItemText = "Showed parking area/garage", ItemOrder = 3, CategorySection = "Arrival & Introduction", SectionOrder = 1, IsRequired = true, RequiresValue = false, AllowsNotes = true, OrganizationId = Guid.Empty, CreatedOn = systemTimestamp, CreatedBy = string.Empty, IsDeleted = false },
+
+ // Interior Tour (Section 2)
+ new ChecklistTemplateItem { Id = Guid.Parse("00000000-0000-0000-0002-000000000004"), ChecklistTemplateId = propertyTourTemplateId, ItemText = "Toured living room/common areas", ItemOrder = 4, CategorySection = "Interior Tour", SectionOrder = 2, IsRequired = true, RequiresValue = false, AllowsNotes = true, OrganizationId = Guid.Empty, CreatedOn = systemTimestamp, CreatedBy = string.Empty, IsDeleted = false },
+ new ChecklistTemplateItem { Id = Guid.Parse("00000000-0000-0000-0002-000000000005"), ChecklistTemplateId = propertyTourTemplateId, ItemText = "Showed all bedrooms", ItemOrder = 5, CategorySection = "Interior Tour", SectionOrder = 2, IsRequired = true, RequiresValue = false, AllowsNotes = true, OrganizationId = Guid.Empty, CreatedOn = systemTimestamp, CreatedBy = string.Empty, IsDeleted = false },
+ new ChecklistTemplateItem { Id = Guid.Parse("00000000-0000-0000-0002-000000000006"), ChecklistTemplateId = propertyTourTemplateId, ItemText = "Showed all bathrooms", ItemOrder = 6, CategorySection = "Interior Tour", SectionOrder = 2, IsRequired = true, RequiresValue = false, AllowsNotes = true, OrganizationId = Guid.Empty, CreatedOn = systemTimestamp, CreatedBy = string.Empty, IsDeleted = false },
+
+ // Kitchen & Appliances (Section 3)
+ new ChecklistTemplateItem { Id = Guid.Parse("00000000-0000-0000-0002-000000000007"), ChecklistTemplateId = propertyTourTemplateId, ItemText = "Toured kitchen and demonstrated appliances", ItemOrder = 7, CategorySection = "Kitchen & Appliances", SectionOrder = 3, IsRequired = true, RequiresValue = false, AllowsNotes = true, OrganizationId = Guid.Empty, CreatedOn = systemTimestamp, CreatedBy = string.Empty, IsDeleted = false },
+ new ChecklistTemplateItem { Id = Guid.Parse("00000000-0000-0000-0002-000000000008"), ChecklistTemplateId = propertyTourTemplateId, ItemText = "Explained which appliances are included", ItemOrder = 8, CategorySection = "Kitchen & Appliances", SectionOrder = 3, IsRequired = true, RequiresValue = false, AllowsNotes = true, OrganizationId = Guid.Empty, CreatedOn = systemTimestamp, CreatedBy = string.Empty, IsDeleted = false },
+ // Utilities & Systems (Section 4)
+ new ChecklistTemplateItem { Id = Guid.Parse("00000000-0000-0000-0002-000000000009"), ChecklistTemplateId = propertyTourTemplateId, ItemText = "Explained HVAC system and thermostat controls", ItemOrder = 9, CategorySection = "Utilities & Systems", SectionOrder = 4, IsRequired = true, RequiresValue = false, AllowsNotes = true, OrganizationId = Guid.Empty, CreatedOn = systemTimestamp, CreatedBy = string.Empty, IsDeleted = false },
+ new ChecklistTemplateItem { Id = Guid.Parse("00000000-0000-0000-0002-000000000010"), ChecklistTemplateId = propertyTourTemplateId, ItemText = "Reviewed utility responsibilities (tenant vs landlord)", ItemOrder = 10, CategorySection = "Utilities & Systems", SectionOrder = 4, IsRequired = true, RequiresValue = false, AllowsNotes = true, OrganizationId = Guid.Empty, CreatedOn = systemTimestamp, CreatedBy = string.Empty, IsDeleted = false },
+ new ChecklistTemplateItem { Id = Guid.Parse("00000000-0000-0000-0002-000000000011"), ChecklistTemplateId = propertyTourTemplateId, ItemText = "Showed water heater location", ItemOrder = 11, CategorySection = "Utilities & Systems", SectionOrder = 4, IsRequired = true, RequiresValue = false, AllowsNotes = true, OrganizationId = Guid.Empty, CreatedOn = systemTimestamp, CreatedBy = string.Empty, IsDeleted = false },
+ // Storage & Amenities (Section 5)
+ new ChecklistTemplateItem { Id = Guid.Parse("00000000-0000-0000-0002-000000000012"), ChecklistTemplateId = propertyTourTemplateId, ItemText = "Showed storage areas (closets, attic, basement)", ItemOrder = 12, CategorySection = "Storage & Amenities", SectionOrder = 5, IsRequired = true, RequiresValue = false, AllowsNotes = true, OrganizationId = Guid.Empty, CreatedOn = systemTimestamp, CreatedBy = string.Empty, IsDeleted = false },
+ new ChecklistTemplateItem { Id = Guid.Parse("00000000-0000-0000-0002-000000000013"), ChecklistTemplateId = propertyTourTemplateId, ItemText = "Showed laundry facilities", ItemOrder = 13, CategorySection = "Storage & Amenities", SectionOrder = 5, IsRequired = true, RequiresValue = false, AllowsNotes = true, OrganizationId = Guid.Empty, CreatedOn = systemTimestamp, CreatedBy = string.Empty, IsDeleted = false },
+ new ChecklistTemplateItem { Id = Guid.Parse("00000000-0000-0000-0002-000000000014"), ChecklistTemplateId = propertyTourTemplateId, ItemText = "Showed outdoor space (yard, patio, balcony)", ItemOrder = 14, CategorySection = "Storage & Amenities", SectionOrder = 5, IsRequired = true, RequiresValue = false, AllowsNotes = true, OrganizationId = Guid.Empty, CreatedOn = systemTimestamp, CreatedBy = string.Empty, IsDeleted = false },
+ // Lease Terms (Section 6)
+ new ChecklistTemplateItem { Id = Guid.Parse("00000000-0000-0000-0002-000000000015"), ChecklistTemplateId = propertyTourTemplateId, ItemText = "Discussed monthly rent amount", ItemOrder = 15, CategorySection = "Lease Terms", SectionOrder = 6, IsRequired = true, RequiresValue = false, AllowsNotes = true, OrganizationId = Guid.Empty, CreatedOn = systemTimestamp, CreatedBy = string.Empty, IsDeleted = false },
+ new ChecklistTemplateItem { Id = Guid.Parse("00000000-0000-0000-0002-000000000016"), ChecklistTemplateId = propertyTourTemplateId, ItemText = "Explained security deposit and move-in costs", ItemOrder = 16, CategorySection = "Lease Terms", SectionOrder = 6, IsRequired = true, RequiresValue = false, AllowsNotes = true, OrganizationId = Guid.Empty, CreatedOn = systemTimestamp, CreatedBy = string.Empty, IsDeleted = false },
+ new ChecklistTemplateItem { Id = Guid.Parse("00000000-0000-0000-0002-000000000017"), ChecklistTemplateId = propertyTourTemplateId, ItemText = "Reviewed lease term length and start date", ItemOrder = 17, CategorySection = "Lease Terms", SectionOrder = 6, IsRequired = true, RequiresValue = false, AllowsNotes = true, OrganizationId = Guid.Empty, CreatedOn = systemTimestamp, CreatedBy = string.Empty, IsDeleted = false },
+ new ChecklistTemplateItem { Id = Guid.Parse("00000000-0000-0000-0002-000000000018"), ChecklistTemplateId = propertyTourTemplateId, ItemText = "Explained pet policy", ItemOrder = 18, CategorySection = "Lease Terms", SectionOrder = 6, IsRequired = true, RequiresValue = false, AllowsNotes = true, OrganizationId = Guid.Empty, CreatedOn = systemTimestamp, CreatedBy = string.Empty, IsDeleted = false },
+
+ // Next Steps (Section 7)
+ new ChecklistTemplateItem { Id = Guid.Parse("00000000-0000-0000-0002-000000000019"), ChecklistTemplateId = propertyTourTemplateId, ItemText = "Explained application process and requirements", ItemOrder = 19, CategorySection = "Next Steps", SectionOrder = 7, IsRequired = true, RequiresValue = false, AllowsNotes = true, OrganizationId = Guid.Empty, CreatedOn = systemTimestamp, CreatedBy = string.Empty, IsDeleted = false },
+ new ChecklistTemplateItem { Id = Guid.Parse("00000000-0000-0000-0002-000000000020"), ChecklistTemplateId = propertyTourTemplateId, ItemText = "Reviewed screening process (background, credit check)", ItemOrder = 20, CategorySection = "Next Steps", SectionOrder = 7, IsRequired = true, RequiresValue = false, AllowsNotes = true, OrganizationId = Guid.Empty, CreatedOn = systemTimestamp, CreatedBy = string.Empty, IsDeleted = false },
+ new ChecklistTemplateItem { Id = Guid.Parse("00000000-0000-0000-0002-000000000021"), ChecklistTemplateId = propertyTourTemplateId, ItemText = "Answered all prospect questions", ItemOrder = 21, CategorySection = "Next Steps", SectionOrder = 7, IsRequired = true, RequiresValue = false, AllowsNotes = true, OrganizationId = Guid.Empty, CreatedOn = systemTimestamp, CreatedBy = string.Empty, IsDeleted = false },
+
+ // Assessment (Section 8)
+ new ChecklistTemplateItem { Id = Guid.Parse("00000000-0000-0000-0002-000000000022"), ChecklistTemplateId = propertyTourTemplateId, ItemText = "Prospect Interest Level", ItemOrder = 22, CategorySection = "Assessment", SectionOrder = 8, IsRequired = true, RequiresValue = true, AllowsNotes = true, OrganizationId = Guid.Empty, CreatedOn = systemTimestamp, CreatedBy = string.Empty, IsDeleted = false },
+ new ChecklistTemplateItem { Id = Guid.Parse("00000000-0000-0000-0002-000000000023"), ChecklistTemplateId = propertyTourTemplateId, ItemText = "Overall showing feedback and notes", ItemOrder = 23, CategorySection = "Assessment", SectionOrder = 8, IsRequired = true, RequiresValue = false, AllowsNotes = true, OrganizationId = Guid.Empty, CreatedOn = systemTimestamp, CreatedBy = string.Empty, IsDeleted = false },
+ // Move-In Checklist Items (Placeholders)
+ new ChecklistTemplateItem { Id = Guid.Parse("00000000-0000-0000-0002-000000000024"), ChecklistTemplateId = moveInTemplateId, ItemText = "Document property condition", ItemOrder = 1, CategorySection = "General", SectionOrder = 1, IsRequired = true, RequiresValue = false, AllowsNotes = true, OrganizationId = Guid.Empty, CreatedOn = systemTimestamp, CreatedBy = string.Empty, IsDeleted = false },
+ new ChecklistTemplateItem { Id = Guid.Parse("00000000-0000-0000-0002-000000000025"), ChecklistTemplateId = moveInTemplateId, ItemText = "Collect keys and access codes", ItemOrder = 2, CategorySection = "General", SectionOrder = 1, IsRequired = true, RequiresValue = false, AllowsNotes = true, OrganizationId = Guid.Empty, CreatedOn = systemTimestamp, CreatedBy = string.Empty, IsDeleted = false },
+ new ChecklistTemplateItem { Id = Guid.Parse("00000000-0000-0000-0002-000000000026"), ChecklistTemplateId = moveInTemplateId, ItemText = "Review lease terms with tenant", ItemOrder = 3, CategorySection = "General", SectionOrder = 1, IsRequired = true, RequiresValue = false, AllowsNotes = true, OrganizationId = Guid.Empty, CreatedOn = systemTimestamp, CreatedBy = string.Empty, IsDeleted = false },
+ // Move-Out Checklist Items (Placeholders)
+ new ChecklistTemplateItem { Id = Guid.Parse("00000000-0000-0000-0002-000000000027"), ChecklistTemplateId = moveOutTemplateId, ItemText = "Inspect property condition", ItemOrder = 1, CategorySection = "General", SectionOrder = 1, IsRequired = true, RequiresValue = false, AllowsNotes = true, OrganizationId = Guid.Empty, CreatedOn = systemTimestamp, CreatedBy = string.Empty, IsDeleted = false },
+ new ChecklistTemplateItem { Id = Guid.Parse("00000000-0000-0000-0002-000000000028"), ChecklistTemplateId = moveOutTemplateId, ItemText = "Collect all keys and access devices", ItemOrder = 2, CategorySection = "General", SectionOrder = 1, IsRequired = true, RequiresValue = false, AllowsNotes = true, OrganizationId = Guid.Empty, CreatedOn = systemTimestamp, CreatedBy = string.Empty, IsDeleted = false },
+ new ChecklistTemplateItem { Id = Guid.Parse("00000000-0000-0000-0002-000000000029"), ChecklistTemplateId = moveOutTemplateId, ItemText = "Document damages and needed repairs", ItemOrder = 3, CategorySection = "General", SectionOrder = 1, IsRequired = true, RequiresValue = false, AllowsNotes = true, OrganizationId = Guid.Empty, CreatedOn = systemTimestamp, CreatedBy = string.Empty, IsDeleted = false },
+ // Open House Checklist Items (Placeholders)
+ new ChecklistTemplateItem { Id = Guid.Parse("00000000-0000-0000-0002-000000000030"), ChecklistTemplateId = openHouseTemplateId, ItemText = "Set up signage and directional markers", ItemOrder = 1, CategorySection = "Preparation", SectionOrder = 1, IsRequired = true, RequiresValue = false, AllowsNotes = true, OrganizationId = Guid.Empty, CreatedOn = systemTimestamp, CreatedBy = string.Empty, IsDeleted = false },
+ new ChecklistTemplateItem { Id = Guid.Parse("00000000-0000-0000-0002-000000000031"), ChecklistTemplateId = openHouseTemplateId, ItemText = "Prepare information packets", ItemOrder = 2, CategorySection = "Preparation", SectionOrder = 1, IsRequired = true, RequiresValue = false, AllowsNotes = true, OrganizationId = Guid.Empty, CreatedOn = systemTimestamp, CreatedBy = string.Empty, IsDeleted = false },
+ new ChecklistTemplateItem { Id = Guid.Parse("00000000-0000-0000-0002-000000000032"), ChecklistTemplateId = openHouseTemplateId, ItemText = "Set up visitor sign-in sheet", ItemOrder = 3, CategorySection = "Preparation", SectionOrder = 1, IsRequired = true, RequiresValue = false, AllowsNotes = true, OrganizationId = Guid.Empty, CreatedOn = systemTimestamp, CreatedBy = string.Empty, IsDeleted = false }
+ );
+
+ // Call partial method hook for features to add custom configuration
+ OnModelCreatingPartial(modelBuilder);
+ }
+
+ ///
+ /// Partial method hook for features to add custom model configuration.
+ /// Features can implement this in their partial ApplicationDbContext classes.
+ ///
+ partial void OnModelCreatingPartial(ModelBuilder modelBuilder);
+ }
+}
\ No newline at end of file
diff --git a/1-Aquiis.Infrastructure/Data/Migrations/20260104205822_InitialCreate.Designer.cs b/1-Aquiis.Infrastructure/Data/Migrations/20260104205822_InitialCreate.Designer.cs
new file mode 100644
index 0000000..7cc048d
--- /dev/null
+++ b/1-Aquiis.Infrastructure/Data/Migrations/20260104205822_InitialCreate.Designer.cs
@@ -0,0 +1,4033 @@
+//
+using System;
+using Aquiis.Infrastructure.Data;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+#nullable disable
+
+namespace Aquiis.Infrastructure.Data.Migrations
+{
+ [DbContext(typeof(ApplicationDbContext))]
+ [Migration("20260104205822_InitialCreate")]
+ partial class InitialCreate
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder.HasAnnotation("ProductVersion", "9.0.11");
+
+ modelBuilder.Entity("Aquiis.Core.Entities.ApplicationScreening", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("TEXT");
+
+ b.Property("BackgroundCheckCompletedOn")
+ .HasColumnType("TEXT");
+
+ b.Property("BackgroundCheckNotes")
+ .HasMaxLength(1000)
+ .HasColumnType("TEXT");
+
+ b.Property("BackgroundCheckPassed")
+ .HasColumnType("INTEGER");
+
+ b.Property("BackgroundCheckRequested")
+ .HasColumnType("INTEGER");
+
+ b.Property("BackgroundCheckRequestedOn")
+ .HasColumnType("TEXT");
+
+ b.Property("CreatedBy")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("TEXT");
+
+ b.Property("CreatedOn")
+ .HasColumnType("TEXT");
+
+ b.Property("CreditCheckCompletedOn")
+ .HasColumnType("TEXT");
+
+ b.Property("CreditCheckNotes")
+ .HasMaxLength(1000)
+ .HasColumnType("TEXT");
+
+ b.Property("CreditCheckPassed")
+ .HasColumnType("INTEGER");
+
+ b.Property("CreditCheckRequested")
+ .HasColumnType("INTEGER");
+
+ b.Property("CreditCheckRequestedOn")
+ .HasColumnType("TEXT");
+
+ b.Property("CreditScore")
+ .HasColumnType("INTEGER");
+
+ b.Property("IsDeleted")
+ .HasColumnType("INTEGER");
+
+ b.Property("LastModifiedBy")
+ .HasMaxLength(100)
+ .HasColumnType("TEXT");
+
+ b.Property("LastModifiedOn")
+ .HasColumnType("TEXT");
+
+ b.Property("OrganizationId")
+ .HasColumnType("TEXT");
+
+ b.Property("OverallResult")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("TEXT");
+
+ b.Property("RentalApplicationId")
+ .HasColumnType("TEXT");
+
+ b.Property("ResultNotes")
+ .HasMaxLength(2000)
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("OrganizationId");
+
+ b.HasIndex("OverallResult");
+
+ b.HasIndex("RentalApplicationId")
+ .IsUnique();
+
+ b.ToTable("ApplicationScreenings");
+ });
+
+ modelBuilder.Entity("Aquiis.Core.Entities.CalendarEvent", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("TEXT");
+
+ b.Property("Color")
+ .IsRequired()
+ .HasMaxLength(20)
+ .HasColumnType("TEXT");
+
+ b.Property("CreatedBy")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("TEXT");
+
+ b.Property("CreatedOn")
+ .HasColumnType("TEXT");
+
+ b.Property("Description")
+ .HasMaxLength(2000)
+ .HasColumnType("TEXT");
+
+ b.Property("DurationMinutes")
+ .HasColumnType("INTEGER");
+
+ b.Property("EndOn")
+ .HasColumnType("TEXT");
+
+ b.Property("EventType")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("TEXT");
+
+ b.Property("Icon")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("TEXT");
+
+ b.Property("IsDeleted")
+ .HasColumnType("INTEGER");
+
+ b.Property("LastModifiedBy")
+ .HasMaxLength(100)
+ .HasColumnType("TEXT");
+
+ b.Property("LastModifiedOn")
+ .HasColumnType("TEXT");
+
+ b.Property("Location")
+ .HasMaxLength(500)
+ .HasColumnType("TEXT");
+
+ b.Property("OrganizationId")
+ .HasColumnType("TEXT");
+
+ b.Property("PropertyId")
+ .HasColumnType("TEXT");
+
+ b.Property("SourceEntityId")
+ .HasColumnType("TEXT");
+
+ b.Property("SourceEntityType")
+ .HasMaxLength(100)
+ .HasColumnType("TEXT");
+
+ b.Property("StartOn")
+ .HasColumnType("TEXT");
+
+ b.Property("Status")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("TEXT");
+
+ b.Property